रूबी में स्मृति क्या फुलाती है?

हम फ्यूज़न में रूबी में एक साधारण बहु-थ्रेडेड HTTP प्रॉक्सी है (DEB और RPM पैकेज वितरित करता है)। मैंने इसे 1.3 जीबी की मेमोरी खपत पर देखा। लेकिन यह एक सांख्यिकीय प्रक्रिया के लिए पागल है ...


प्रश्न: क्या है? उत्तर: रूबी समय के साथ स्मृति का उपयोग करती है!

यह पता चला है कि मैं इस समस्या में अकेला नहीं हूँ। रूबी अनुप्रयोगों में बहुत अधिक मेमोरी का उपयोग किया जा सकता है। लेकिन क्यों? हेरोकू और नैट बर्कोपेक के अनुसार, ब्लोटिंग मुख्य रूप से स्मृति विखंडन और अत्यधिक हीप वितरण के कारण होता है।

बर्कोपेक ने निष्कर्ष निकाला कि दो समाधान हैं:

  1. या तो glibc की तुलना में एक पूरी तरह से अलग मेमोरी एलोकेटर का उपयोग करें - आमतौर पर जेमलॉक , या:
  2. मैजिक वातावरण चर MALLOC_ARENA_MAX=2 सेट करें।

मैं समस्या के वर्णन और प्रस्तावित समाधानों के बारे में चिंतित हूं। यहाँ कुछ गड़बड़ है ... मुझे यकीन नहीं है कि समस्या को पूरी तरह से सही ढंग से वर्णित किया गया है या ये एकमात्र समाधान उपलब्ध हैं। यह भी मुझे गुस्सा दिलाता है कि कई लोग जादुई चांदी के पूल के रूप में जेमलॉक का उल्लेख करते हैं।

जादू सिर्फ एक विज्ञान है जिसे हम अभी तक नहीं समझ पाए हैं । इसलिए मैं पूरी सच्चाई का पता लगाने के लिए एक शोध यात्रा पर गया। यह लेख निम्नलिखित विषयों को कवर करेगा:

  1. स्मृति आवंटन कैसे काम करता है।
  2. यह "विखंडन" और स्मृति का "अत्यधिक वितरण" क्या है जिसके बारे में हर कोई बात कर रहा है?
  3. एक बड़ी मेमोरी खपत का कारण क्या है? क्या स्थिति वही है जो लोग कह रहे हैं, या कुछ और है? (बिगाड़ने: हाँ, वहाँ कुछ और है)।
  4. क्या कोई वैकल्पिक उपाय हैं? (बिगाड़ने: मैं एक पाया)।

नोट: यह लेख केवल लिनक्स के लिए, और केवल बहु-थ्रेड रूबी अनुप्रयोगों के लिए प्रासंगिक है।

सामग्री



रूबी मेमोरी आवंटन: एक परिचय


रूबी ने तीन स्तरों पर मेमोरी आवंटित की, ऊपर से नीचे तक:

  1. रूबी दुभाषिया जो रूबी वस्तुओं का प्रबंधन करता है।
  2. ऑपरेटिंग सिस्टम की मेमोरी एलोकेटर लाइब्रेरी।
  3. कोर।

प्रत्येक स्तर से गुजरते हैं।

गहरे लाल रंग का


इसके किनारे पर, रूबी स्मृति के क्षेत्रों में वस्तुओं का आयोजन करता है जिसे रूबी ढेर पृष्ठ कहा जाता है। इस तरह के ढेर पृष्ठ को उसी आकार के स्लॉट्स में विभाजित किया जाता है, जहां एक वस्तु एक स्लॉट पर रहती है। चाहे वह एक स्ट्रिंग, एक हैश टेबल, एक सरणी, एक वर्ग, या कुछ और हो, यह एक स्लॉट पर कब्जा कर लेता है।



ढेर पृष्ठ पर स्लॉट व्यस्त या मुक्त हो सकते हैं। जब रूबी एक नई वस्तु का चयन करती है, तो वह तुरंत एक मुफ्त स्लॉट पर कब्जा करने की कोशिश करती है। यदि कोई मुफ्त स्लॉट नहीं हैं, तो एक नया हीप पृष्ठ हाइलाइट किया जाएगा।

स्लॉट छोटा है, लगभग 40 बाइट्स। जाहिर है, कुछ ऑब्जेक्ट इसमें फिट नहीं होंगे, उदाहरण के लिए, 1 एमबी लाइनें। फिर रूबी ढेर पृष्ठ के बाहर कहीं और जानकारी संग्रहीत करता है, और स्लॉट में इस बाहरी मेमोरी क्षेत्र के लिए एक संकेतक रखता है।


स्लॉट में फिट नहीं होने वाले डेटा को ढेर पृष्ठ के बाहर संग्रहीत किया जाता है। रूबी स्लॉट में इस बाहरी डेटा के लिए एक पॉइंटर रखता है

रूबी हीप पेज और किसी भी बाहरी मेमोरी क्षेत्र दोनों को सिस्टम मेमोरी एलोकेटर का उपयोग करके आवंटित किया जाता है।

सिस्टम मेमोरी एलोकेटर


ऑपरेटिंग सिस्टम मेमोरी एलोकेटर glibc (C रनटाइम) का हिस्सा है। इसका उपयोग लगभग सभी अनुप्रयोगों द्वारा किया जाता है, न कि केवल रूबी। इसका एक साधारण API है:

  • malloc(size) को कॉल करके मेमोरी आवंटित की जाती है। आप इसे बाइट्स की संख्या देते हैं जिसे आप आवंटित करना चाहते हैं, और यह आवंटन पते या त्रुटि को लौटाता है।
  • आवंटित मेमोरी को free(address) कहकर मुक्त किया जाता है।

रूबी के विपरीत, जहां एक ही आकार के स्लॉट आवंटित किए जाते हैं, मेमोरी एलोकेटर किसी भी आकार की मेमोरी को आवंटित करने के लिए अनुरोध करता है। जैसा कि आप बाद में जानेंगे, यह तथ्य कुछ जटिलताओं को जन्म देता है।

बदले में, मेमोरी आवंटन कर्नेल एपीआई तक पहुंचता है। यह कर्नेल से अपने स्वयं के ग्राहकों के अनुरोध की तुलना में मेमोरी का बहुत बड़ा हिस्सा लेता है, क्योंकि कर्नेल कॉल महंगा है और कर्नेल एपीआई में एक सीमा है: यह केवल 4 KB के गुणकों में मेमोरी आवंटित कर सकता है।


मेमोरी एलोकेटर बड़ी मात्रा में आवंटन करता है - उन्हें सिस्टम हीप्स कहा जाता है - और अनुप्रयोगों से अनुरोधों को पूरा करने के लिए अपनी सामग्री साझा करता है

स्मृति का वह क्षेत्र जिसे कर्नेल से स्मृति आबंटक आवंटित करता है उसे ढेर कहा जाता है। ध्यान दें कि इसका रूबी के ढेर के पन्नों से कोई लेना-देना नहीं है, इसलिए स्पष्टता के लिए हम सिस्टम हीप शब्द का उपयोग करेंगे।

स्मृति आबंटक तब सिस्टम के कुछ हिस्सों को अपने कॉल करने वालों को तब तक असाइन करता है जब तक कि खाली स्थान न हो। इस स्थिति में, स्मृति आबंटक कर्नेल से एक नया सिस्टम हीप आवंटित करता है। यह उसी तरह है जैसे कि रूबी ढेर के पन्नों से वस्तुओं का चयन करती है।


रूबी मेमोरी एलोकेटर से मेमोरी आवंटित करता है, जो बदले में कर्नेल से मेमोरी आवंटित करता है

कोर


कर्नेल केवल 4 KB इकाइयों में मेमोरी आवंटित कर सकता है। इस तरह के 4K ब्लॉक को एक पेज कहा जाता है। रूबी हीप पृष्ठों के साथ भ्रम से बचने के लिए, स्पष्टता के लिए हम सिस्टम पेज (ओएस पेज) शब्द का उपयोग करेंगे।

कारण स्पष्ट करना मुश्किल है, लेकिन यह है कि सभी आधुनिक गुठली कैसे काम करते हैं।

कर्नेल के माध्यम से मेमोरी को आवंटित करने का प्रदर्शन पर महत्वपूर्ण प्रभाव पड़ता है, यही वजह है कि मेमोरी आवंटन कर्नेल कॉल की संख्या को कम करने की कोशिश करते हैं।

मेमोरी उपयोग की परिभाषा


इस प्रकार, मेमोरी को कई स्तरों पर आवंटित किया जाता है, और प्रत्येक स्तर को वास्तव में आवश्यकता से अधिक मेमोरी आवंटित करता है। रूबी हीप पृष्ठों में मुफ्त स्लॉट्स, साथ ही सिस्टम हीप हो सकते हैं। इसलिए, प्रश्न का उत्तर "कितनी मेमोरी का उपयोग किया जाता है?" पूरी तरह से आप किस स्तर पर पूछते हैं!

top या ps जैसे उपकरण कर्नेल के नजरिए से मेमोरी का उपयोग दिखाते हैं। इसका मतलब है कि उच्च स्तर के संगीत को स्मृति को कर्नेल बिंदु से मुक्त करने के लिए काम करना चाहिए। जैसा कि आप बाद में जानेंगे, यह जितना लगता है उससे कहीं ज्यादा कठिन है।

विखंडन क्या है?


मेमोरी विखंडन का मतलब है कि मेमोरी आवंटन बेतरतीब ढंग से बिखरे हुए हैं। यह दिलचस्प समस्या पैदा कर सकता है।

रूबी स्तर का विखंडन


रूबी कचरा संग्रह पर विचार करें। एक वस्तु के लिए कचरा संग्रह का मतलब है रूबी हीप पृष्ठ स्लॉट को मुफ्त में चिह्नित करना, जिससे इसका पुन: उपयोग किया जा सके। यदि रूबी हीप के पूरे पृष्ठ में केवल नि: शुल्क स्लॉट होते हैं, तो इसके पूरे पृष्ठ को मेमोरी एलोकेटर (और, संभवतः, कर्नेल पर वापस) मुक्त किया जा सकता है।



लेकिन क्या होता है अगर सभी स्लॉट मुफ्त नहीं होते हैं? क्या होगा अगर हमारे पास रूबी हीप के कई पृष्ठ हैं, और कचरा संग्रहकर्ता विभिन्न स्थानों में वस्तुओं को मुक्त करता है, ताकि अंत में कई मुफ्त स्लॉट हों, लेकिन विभिन्न पृष्ठों पर? इस स्थिति में, रूबी के पास वस्तुओं को रखने के लिए नि: शुल्क स्लॉट हैं, लेकिन मेमोरी आवंटनकर्ता और कर्नेल मेमोरी को आवंटित करना जारी रखेंगे!

मेमोरी आवंटन विखंडन


मेमोरी एलोकेटर में एक समान लेकिन पूरी तरह से अलग समस्या है। उसे पूरे सिस्टम को तुरंत साफ करने की आवश्यकता नहीं है। सैद्धांतिक रूप से, यह किसी भी एकल सिस्टम पेज को मुक्त कर सकता है। लेकिन चूंकि स्मृति आबंटक मनमाने आकार के मेमोरी आवंटन से संबंधित है, इसलिए सिस्टम पेज पर कई आवंटन हो सकते हैं। यह सिस्टम पेज को तब तक फ्री नहीं कर सकता है जब तक कि सभी चयन मुक्त नहीं हो जाते।



इस बारे में सोचें कि क्या होगा यदि हमारे पास 3 केबी आवंटन है, साथ ही 2 केबी आवंटन भी है, जो दो सिस्टम पृष्ठों में विभाजित है। यदि आप पहले 3 KB को मुक्त करते हैं, तो दोनों सिस्टम पेज आंशिक रूप से कब्जे में रहेंगे और उन्हें मुक्त नहीं किया जा सकता है।



इसलिए, यदि परिस्थितियां विफल होती हैं, तो सिस्टम पृष्ठों पर बहुत सारी खाली जगह होगी, लेकिन उन्हें पूरी तरह से मुक्त नहीं किया जाएगा।

इससे भी बदतर: क्या होगा यदि बहुत सारे खाली स्थान हैं, लेकिन उनमें से एक भी बड़ा नहीं है जो एक नए आवंटन अनुरोध को पूरा करने के लिए है? मेमोरी एलोकेटर को एक पूरी नई सिस्टम हीप आवंटित करनी होगी।

क्या रूबी हीप पृष्ठ विखंडन के कारण मेमोरी ब्लोट है?


यह संभावना है कि रूबी में विखंडन स्मृति अति प्रयोग का कारण बन रहा है। यदि हां, तो दोनों में से कौन सा टुकड़ा अधिक हानिकारक है? यह है ...

  1. रूबी हीप पेज विखंडन? या
  2. स्मृति आवंटन विखंडन?

पहला विकल्प चेक करने के लिए काफी सरल है। रूबी दो एपीआई प्रदान करती है: ObjectSpace.memsize_of_all और GC.stat । इस जानकारी के लिए धन्यवाद, आप सभी मेमोरी की गणना कर सकते हैं जो रूबी को आवंटन से प्राप्त हुई थी।



ObjectSpace.memsize_of_all सभी सक्रिय रूबी वस्तुओं द्वारा कब्जा की गई मेमोरी को वापस करता है। यही है, उनके स्लॉट और किसी भी बाहरी डेटा में सभी स्थान। उपरोक्त आरेख में, यह सभी नीले और नारंगी वस्तुओं का आकार है।

GC.stat सभी नि: शुल्क स्लॉट्स के आकार का पता लगाने की अनुमति देता है, अर्थात् ऊपर चित्रण में पूरे ग्रे क्षेत्र। यहाँ एल्गोरिथ्म है:

 GC.stat[:heap_free_slots] * GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] 

उन्हें सारांशित करने के लिए, यह रूबी के बारे में जानने वाली सभी स्मृति है, और इसमें रूबी के ढेर के पृष्ठ भी शामिल हैं। यदि, कर्नेल के दृष्टिकोण से, स्मृति का उपयोग अधिक है, तो शेष स्मृति रूबी के नियंत्रण से बाहर कहीं जाती है, उदाहरण के लिए, तीसरे पक्ष के पुस्तकालयों या विखंडन के लिए।

मैंने एक साधारण परीक्षण कार्यक्रम लिखा था जो थ्रेड्स का एक गुच्छा बनाता है, जिनमें से प्रत्येक एक लूप में लाइनों का चयन करता है। यहाँ थोड़ी देर के बाद परिणाम है:



यह ... बस ... पागल!

परिणाम से पता चलता है कि उपयोग की गई स्मृति की कुल मात्रा पर रूबी का इतना कमजोर प्रभाव है, इससे कोई फर्क नहीं पड़ता कि रूबी के ढेर के पृष्ठ खंडित हैं या नहीं।

अपराधी को कहीं और देखना होगा। कम से कम अब हम जानते हैं कि रूबी को दोष नहीं देना है।

मेमोरी आवंटन विखंडन अध्ययन


एक और संभावित संदेह एक मेमोरी एलोकेटर है। अंत में, नैट बर्कोपेक और हरोकू ने देखा कि मेमोरी एलोकेटर के साथ उपद्रव (या तो जेमलॉक के लिए पूर्ण प्रतिस्थापन या मैजिक एनवायरनमेंट चर MALLOC_ARENA_MAX=2 ) मेमोरी के उपयोग को काफी कम कर देता है।

आइए पहले देखें कि MALLOC_ARENA_MAX=2 क्या करता है और यह क्यों मदद करता है। फिर हम वितरक स्तर पर विखंडन की जांच करते हैं।

अत्यधिक मेमोरी आवंटन और ग्लिबैक


मल्टीथ्रेडिंग के कारण MALLOC_ARENA_MAX=2 मदद करता है। जब एक साथ कई थ्रेड्स एक ही सिस्टम हीप से मेमोरी आवंटित करने का प्रयास करते हैं, तो वे एक्सेस के लिए लड़ते हैं। एक समय में केवल एक धागा मेमोरी प्राप्त कर सकता है, जो बहु-थ्रेडेड मेमोरी आवंटन के प्रदर्शन को कम करता है।


एक समय में केवल एक धागा सिस्टम हीप के साथ काम कर सकता है। बहु-थ्रेडेड कार्यों में, एक संघर्ष उत्पन्न होता है और, परिणामस्वरूप, प्रदर्शन कम हो जाता है

ऐसे मामले के लिए मेमोरी एलोकेटर में ऑप्टिमाइज़ेशन होता है। वह कई सिस्टम हीप्स बनाने की कोशिश करता है और उन्हें विभिन्न थ्रेड्स को सौंपता है। ज्यादातर समय एक धागा केवल अपने ही ढेर के साथ काम करता है, अन्य धागे के साथ संघर्ष से बचता है।

वास्तव में, इस तरह से आवंटित सिस्टम हीप्स की अधिकतम संख्या डिफ़ॉल्ट रूप से वर्चुअल प्रोसेसर की संख्या के बराबर 8. से गुणा होती है। यानी, दो हाइपर-थ्रेड वाले दोहरे कोर सिस्टम में, प्रत्येक 2 * 2 * 8 = 32 सिस्टम ही पैदा करता है! इसे ही मैं अत्यधिक वितरण कहता हूं।

डिफ़ॉल्ट गुणक इतना बड़ा क्यों है? क्योंकि मेमोरी एलोकेटर का प्रमुख डेवलपर Red Hat है। उनके ग्राहक शक्तिशाली सर्वर वाली बड़ी कंपनियां हैं और एक टन रैम है। उपरोक्त अनुकूलन आपको मेमोरी उपयोग में महत्वपूर्ण वृद्धि के कारण औसत मल्टीथ्रेडिंग प्रदर्शन को 10% तक बढ़ाने की अनुमति देता है। Red Hat ग्राहकों के लिए, यह एक अच्छा समझौता है। बाकी के लिए - शायद ही।

अपने ब्लॉग और हरोकू लेख में नैट का दावा है कि सिस्टम की संख्या में वृद्धि से विखंडन बढ़ता है, और आधिकारिक प्रलेखन का हवाला दिया जाता है। MALLOC_ARENA_MAX चर मल्टीथ्रेडिंग के लिए आवंटित सिस्टम ढेर की अधिकतम संख्या को कम करता है। इस तर्क से, यह विखंडन को कम करता है।

सिस्टम के दृश्य का ढेर


क्या नैट और हरोकू का कथन सही है कि सिस्टम की संख्या बढ़ने से विखंडन बढ़ता है? वास्तव में, क्या मेमोरी एलोकेटर स्तर पर विखंडन के साथ कोई समस्या है? मैं इनमें से किसी भी धारणा को नहीं लेना चाहता था, इसलिए मैंने अध्ययन शुरू किया।

दुर्भाग्य से, सिस्टम हीप्स को विज़ुअलाइज़ करने के लिए कोई उपकरण नहीं हैं, इसलिए मैंने खुद ऐसे विज़ुअलाइज़र को लिखा

सबसे पहले, आपको किसी तरह सिस्टम हीप्स की वितरण योजना को संरक्षित करने की आवश्यकता है। मैंने मेमोरी एलोकेटर के स्रोत का अध्ययन किया और देखा कि यह आंतरिक रूप से मेमोरी का प्रतिनिधित्व कैसे करता है। फिर उन्होंने एक पुस्तकालय लिखा जो इन डेटा संरचनाओं पर निर्भर करता है और स्कीमा को एक फ़ाइल में लिखता है। अंत में, उन्होंने एक उपकरण लिखा जो ऐसी फाइल को इनपुट के रूप में लेता है और विज़ुअलाइज़ेशन को HTML और PNG छवियों ( स्रोत कोड ) के रूप में संकलित करता है।



यहां एक विशिष्ट सिस्टम हीप को देखने का एक उदाहरण है (कई और अधिक हैं)। इस विज़ुअलाइज़ेशन में छोटे ब्लॉक सिस्टम पृष्ठों का प्रतिनिधित्व करते हैं।

  • लाल क्षेत्रों में मेमोरी सेल का उपयोग किया जाता है।
  • ग्रिड मुक्त क्षेत्र हैं जो कोर में वापस नहीं आते हैं।
  • श्वेत क्षेत्रों को नाभिक के लिए मुक्त किया जाता है।

विज़ुअलाइज़ेशन से निम्नलिखित निष्कर्ष निकाले जा सकते हैं:

  1. कुछ विखंडन है। लाल धब्बे स्मृति से बिखरे हुए हैं, और कुछ सिस्टम पृष्ठ केवल आधे लाल हैं।
  2. मेरे आश्चर्य के लिए, अधिकांश सिस्टम हीप में पूरी तरह से मुक्त सिस्टम पेज (ग्रे) की एक महत्वपूर्ण मात्रा होती है!

और फिर यह मुझ पर dawned:

हालाँकि विखंडन एक समस्या बनी हुई है, यह बात नहीं है!

बल्कि, समस्या बहुत ग्रे है: यह मेमोरी एलोकेटर मेमोरी को कर्नेल में वापस नहीं भेजता है !

स्मृति आबंटक के स्रोत कोड का पुन: अध्ययन करने के बाद, यह पता चला कि डिफ़ॉल्ट रूप से यह सिस्टम के ढेर के अंत में केवल कर्नेल में सिस्टम पेज भेजता है, और यहां तक ​​कि शायद ही कभी ऐसा करता है । संभवतः, इस तरह के एक एल्गोरिथ्म को प्रदर्शन कारणों से लागू किया जाता है।

जादू की चाल: खतना


सौभाग्य से, मुझे एक चाल मिली। एक प्रोग्रामिंग इंटरफ़ेस है जो कर्नेल के लिए न केवल अंतिम, बल्कि सभी प्रासंगिक सिस्टम पेजों के लिए मेमोरी एलोकेटर जारी करने के लिए मजबूर करेगा। इसे Malloc_trim कहा जाता है।

मुझे इस फ़ंक्शन के बारे में पता था, लेकिन मुझे नहीं लगा कि यह उपयोगी था, क्योंकि मैनुअल निम्नलिखित कहता है:

Malloc_trim () फ़ंक्शन ढेर के शीर्ष पर मुक्त मेमोरी को मुक्त करने की कोशिश करता है।

मैनुअल गलत है! स्रोत कोड का विश्लेषण कहता है कि कार्यक्रम सभी प्रासंगिक सिस्टम पृष्ठों को मुक्त करता है, न कि केवल शीर्ष।

यदि यह कार्य कचरा संग्रहण के दौरान कहा जाता है तो क्या होगा? मैंने gc_start से gc_start फ़ंक्शन में malloc_trim() कॉल करने के लिए रूबी 2.6 स्रोत कोड को संशोधित किया है:

 gc_prof_timer_start(objspace); { gc_marks(objspace, do_full_mark); // BEGIN MODIFICATION if (do_full_mark) { malloc_trim(0); } // END MODIFICATION } gc_prof_timer_stop(objspace); 

और यहाँ परीक्षण के परिणाम हैं:



कितना बड़ा अंतर है! एक साधारण पैच ने लगभग MALLOC_ARENA_MAX=2 को मेमोरी की खपत कम कर दी।

यहाँ यह दृश्य में कैसा दिखता है:



हम कई सफेद क्षेत्रों को देखते हैं जो सिस्टम पेजों से मेल खाते हैं।

निष्कर्ष


यह पता चला कि विखंडन, मूल रूप से, इससे कोई लेना-देना नहीं था। डीफ़्रैग्मेन्टेशन अभी भी उपयोगी है, लेकिन मुख्य समस्या यह है कि मेमोरी आवंटनकर्ता को कर्नेल को वापस खाली करना पसंद नहीं है।

सौभाग्य से, समाधान बहुत सरल निकला। मुख्य कारण मूल कारण को खोजना था।

विज़ुअलाइज़र स्रोत कोड


स्रोत कोड

प्रदर्शन के बारे में क्या?


प्रदर्शन मुख्य चिंताओं में से एक रहा। malloc_trim() कॉलिंग मुफ्त में नहीं की जा सकती, लेकिन कोड के अनुसार, एल्गोरिथ्म रैखिक समय में काम करता है। इसलिए मैंने नूह गिब्स की ओर रुख किया, जिन्होंने रेल रूबी बेंच बेंचमार्क लॉन्च किया। मेरे आश्चर्य करने के लिए, पैच ने प्रदर्शन में थोड़ी वृद्धि की





इसने मेरे दिमाग को उड़ा दिया। प्रभाव समझ से बाहर है, लेकिन खबर अच्छी है।

अधिक परीक्षण की आवश्यकता है।


इस अध्ययन के हिस्से के रूप में, केवल सीमित संख्या में मामलों को सत्यापित किया गया है। यह ज्ञात नहीं है कि अन्य कार्यभार पर क्या प्रभाव पड़ता है। यदि आप परीक्षण में मदद करना चाहते हैं, तो कृपया मुझसे संपर्क करें

Source: https://habr.com/ru/post/hi444482/


All Articles