सीढ़ी-चरण प्रोफ़ाइल का रहस्य हमने हाल ही में , एक सेवा को देखा, जब मेमोरी उपयोग की बात आती है तो कुछ अजीब व्यवहार प्रदर्शित होता है। एक अजीब-सी दिखने वाली मेमोरी प्रोफाइल वह आखिरी चीज है जिसकी मैं रस्ट प्रोग्राम से अपेक्षा करता हूं, लेकिन यहां हम हैं। अपने रस्ट प्रोजेक्ट्स में से एक axum सेवा कुछ समय के लिए "फ्लैट" मेमोरी के साथ चलेगी, फिर अचानक एक नए स्तर पर पहुंच जाएगी। यह पैटर्न घंटों तक दोहराया जाएगा, कभी-कभी लोड के तहत, लेकिन हमेशा नहीं। चिंताजनक बात यह थी कि एक बार जब हमने तेज वृद्धि देखी, तो स्मृति का वापस नीचे गिरना दुर्लभ था। यह ऐसा था मानो स्मृति खो गई हो, या अन्यथा कभी-कभार "लीक" हो गई हो। सामान्य परिस्थितियों में, यह "सीढ़ी-कदम" प्रोफ़ाइल बस अजीब लग रही थी, लेकिन एक बिंदु पर मेमोरी उपयोग असंगत रूप से बढ़ गया। असीमित मेमोरी वृद्धि के कारण सेवाओं को बाहर निकलने के लिए मजबूर होना पड़ सकता है। जब सेवाएँ अचानक बंद हो जाती हैं, तो इससे उपलब्धता कम हो सकती है... जो है। मैं गहराई से जानना चाहता था कि क्या हो रहा है। व्यवसाय के लिए बुरा आम तौर पर जब मैं किसी प्रोग्राम में अप्रत्याशित मेमोरी वृद्धि के बारे में सोचता हूं, तो मैं लीक के बारे में सोचता हूं। फिर भी, यह अलग लग रहा था. रिसाव के साथ, आप विकास का अधिक स्थिर, नियमित पैटर्न देखते हैं। अक्सर यह ऊपर और दाहिनी ओर झुकी हुई एक रेखा की तरह दिखता है। तो, यदि हमारी सेवा लीक नहीं हो रही थी, तो वह क्या कर रही थी? यदि मैं उन स्थितियों की पहचान कर सकता हूं जिनके कारण मेमोरी उपयोग में उछाल आया है, तो शायद मैं जो कुछ भी हो रहा था उसे कम कर सकता हूं। में खुदाई मेरे पास दो ज्वलंत प्रश्न थे: क्या इस व्यवहार को बढ़ावा देने के लिए हमारे कोड में कुछ बदलाव किया गया? अन्यथा, क्या कोई नया ट्रैफ़िक पैटर्न सामने आया? ऐतिहासिक मेट्रिक्स को देखते हुए, मैं लंबी सपाट अवधियों के बीच तेज वृद्धि के समान पैटर्न देख सकता था, लेकिन पहले कभी भी हमने इस तरह की वृद्धि नहीं देखी थी। यह जानने के लिए कि क्या विकास स्वयं नया था ("सीढ़ी-कदम" पैटर्न हमारे लिए होने के बावजूद), मुझे इस व्यवहार को पुन: उत्पन्न करने के लिए एक विश्वसनीय तरीके की आवश्यकता होगी। सामान्य अगर मैं "कदम" को खुद को दिखाने के लिए मजबूर कर सकता हूं, तो मेरे पास स्मृति वृद्धि को रोकने के लिए कदम उठाते समय व्यवहार में बदलाव को सत्यापित करने का एक तरीका होगा। मैं हमारे गिट इतिहास को भी पीछे ले जा सकूंगा और एक ऐसे समय की तलाश कर सकूंगा जब सेवा ने असीमित वृद्धि प्रदर्शित नहीं की थी। अपने लोड परीक्षण चलाते समय मैंने जिन आयामों का उपयोग किया वे थे: सेवा में भेजे गए POST निकायों का आकार. अनुरोध दर (यानी, प्रति सेकंड अनुरोध)। समवर्ती ग्राहक कनेक्शन की संख्या. मेरे लिए जादुई संयोजन था: और । बड़े अनुरोध निकाय उच्च समवर्तीता स्थानीय सिस्टम पर लोड परीक्षण चलाते समय, क्लाइंट और सर्वर दोनों को चलाने के लिए उपलब्ध प्रोसेसर की सीमित संख्या सहित सभी प्रकार के सीमित कारक होते हैं। फिर भी, मैं अपनी स्थानीय मशीन पर मेमोरी में "स्टेयर-स्टेप" को सही परिस्थितियों में देख पा रहा था, यहां तक कि कम समग्र अनुरोध दर पर भी। एक निश्चित आकार के पेलोड का उपयोग करना और बैचों में अनुरोध भेजना, उनके बीच थोड़े समय के आराम के साथ, मैं एक समय में एक कदम, बार-बार सेवा की मेमोरी को बढ़ाने में सक्षम था। मुझे यह दिलचस्प लगा कि समय के साथ मेरी याददाश्त तो बढ़ सकती है, लेकिन आख़िरकार मैं घटते प्रतिफल के बिंदु पर पहुँच जाऊँगा। अंततः, वृद्धि की कुछ सीमा (अभी भी अपेक्षा से कहीं अधिक) होगी। थोड़ा और खेलने पर, मैंने पाया कि अलग-अलग पेलोड आकारों के साथ अनुरोध भेजकर मैं और भी ऊंची सीमा तक पहुंच सकता हूं। एक बार जब मैंने अपने इनपुट की पहचान कर ली, तो मैं हमारे गिट इतिहास के माध्यम से पीछे की ओर काम करने में सक्षम हो गया, अंततः मुझे पता चला कि हमारा उत्पादन डर हमारी ओर से हाल के परिवर्तनों का परिणाम होने की संभावना नहीं है। इस "सीढ़ी-कदम" को ट्रिगर करने के लिए कार्यभार का विवरण स्वयं एप्लिकेशन के लिए विशिष्ट है, हालांकि मैं एक के साथ एक समान ग्राफ बनाने में सक्षम था। खिलौना परियोजना #[derive(serde::Deserialize, Clone)] struct Widget { payload: serde_json::Value, } #[derive(serde::Serialize)] struct WidgetCreateResponse { id: String, size: usize, } async fn create_widget(Json(widget): Json<Widget>) -> Response { ( StatusCode::CREATED, Json(process_widget(widget.clone()).await), ) .into_response() } async fn process_widget(widget: Widget) -> WidgetCreateResponse { let widget_id = uuid::Uuid::new_v4(); let bytes = serde_json::to_vec(&widget.payload).unwrap_or_default(); // An arbitrary sleep to pad the handler latency as a stand-in for a more // complex code path. // Tweak the duration by setting the `SLEEP_MS` env var. tokio::time::sleep(std::time::Duration::from_millis( std::env::var("SLEEP_MS") .as_deref() .unwrap_or("150") .parse() .expect("invalid SLEEP_MS"), )) .await; WidgetCreateResponse { id: widget_id.to_string(), size: bytes.len(), } } यह पता चला कि आपको वहां पहुंचने के लिए ज्यादा कुछ नहीं चाहिए था। मैं JSON बॉडी प्राप्त करने वाले एकल हैंडलर के साथ एक ऐप से एक समान तेज (लेकिन इस मामले में बहुत छोटा) वृद्धि देखने में कामयाब रहा। axum हालाँकि मेरे खिलौना प्रोजेक्ट में स्मृति वृद्धि कहीं भी उतनी नाटकीय नहीं थी जितनी हमने उत्पादन सेवा में देखी थी, यह मेरी जांच के अगले चरण के दौरान तुलना और अंतर करने में मेरी मदद करने के लिए पर्याप्त थी। जब मैंने विभिन्न कार्यभार के साथ प्रयोग किया तो इससे मुझे छोटे कोडबेस का सख्त पुनरावृत्ति लूप प्राप्त करने में भी मदद मिली। मैंने अपने लोड परीक्षण कैसे चलाए, इसके विवरण के लिए देखें। README मैंने कुछ समय वेब पर बग रिपोर्ट या चर्चाएँ खोजने में बिताया जो समान व्यवहार का वर्णन कर सकती हैं। एक शब्द जो बार-बार सामने आया वह था और इस विषय पर थोड़ा और पढ़ने के बाद, ऐसा लगा कि यह जो मैं देख रहा था उसमें फिट हो सकता है। हीप फ्रैग्मेंटेशन ढेर विखंडन क्या है? एक निश्चित उम्र के लोगों को "प्रयुक्त" और "मुक्त" क्षेत्रों को समेकित करने के लिए हार्ड डिस्क पर डीओएस या पर ब्लॉकों को घुमाते हुए देखने का अनुभव हो सकता है। विंडोज़ डीफ़्रैग उपयोगिता को इस पुराने पीसी हार्ड ड्राइव के मामले में, अलग-अलग आकार की फ़ाइलों को डिस्क पर लिखा गया था और बाद में अन्य उपयोग किए गए क्षेत्रों के बीच उपलब्ध स्थान का "छेद" छोड़कर स्थानांतरित या हटा दिया गया था। जैसे ही डिस्क भरने लगती है, आप एक नई फ़ाइल बनाने का प्रयास कर सकते हैं जो उन छोटे क्षेत्रों में से किसी एक में बिल्कुल फिट नहीं बैठती है। ढेर विखंडन परिदृश्य में, इससे आवंटन विफलता हो जाएगी, हालांकि डिस्क विखंडन की विफलता मोड थोड़ा कम कठोर होगा। डिस्क पर, फ़ाइल को छोटे टुकड़ों में विभाजित करने की आवश्यकता होगी जो पहुंच को बहुत कम कुशल बनाता है ( धन्यवाद)। डिस्क ड्राइव का समाधान उन खुले ब्लॉकों को निरंतर स्थानों के साथ फिर से व्यवस्थित करने के लिए ड्राइव को "डीफ़्रैग" (डी-फ़्रैगमेंट) करना है। सुधार के लिए wongarsu कुछ ऐसा ही तब सकता है जब एलोकेटर (आपके प्रोग्राम में मेमोरी आवंटन को प्रबंधित करने के लिए जिम्मेदार चीज़) समय के साथ अलग-अलग आकार के मान जोड़ता और हटाता है। अंतराल जो बहुत छोटे हैं और पूरे ढेर में बिखरे हुए हैं, एक नए मूल्य को समायोजित करने के लिए मेमोरी के नए "ताज़ा" ब्लॉक आवंटित किए जा सकते हैं जो अन्यथा फिट नहीं होंगे। हालाँकि दुर्भाग्य से मेमोरी प्रबंधन कैसे काम करता है, इसके कारण "डीफ़्रैग" संभव नहीं है। हो विखंडन का विशिष्ट कारण कई चीजें हो सकती हैं: के साथ JSON पार्सिंग, में फ्रेमवर्क-स्तर पर कुछ, में कुछ गहरा, या यहां तक कि दिए गए सिस्टम के लिए विशिष्ट आवंटनकर्ता कार्यान्वयन का एक विचित्र रूप भी। यहां तक कि मूल कारण (यदि ऐसी कोई बात है) को जाने बिना भी व्यवहार हमारे वातावरण में देखा जा सकता है और कुछ हद तक नंगे-हड्डियों वाले ऐप में पुनरुत्पादित किया जा सकता है। (अपडेट: अधिक जांच की आवश्यकता है, लेकिन हमें पूरा यकीन है कि यह JSON पार्सिंग है, देखें) serde axum tokio HackerN ews पर हमारी टिप्पणी यदि प्रक्रिया स्मृति के साथ यही हो रहा था, तो इसके बारे में क्या किया जा सकता है? ऐसा लगता है कि विखंडन से बचने के लिए कार्यभार को बदलना कठिन होगा। ऐसा भी लगता है कि विखंडन की घटनाएं कैसे घटित हो रही हैं, इसके लिए कोड में मूल कारण ढूंढने के लिए मेरे प्रोजेक्ट में सभी निर्भरताओं को दूर करना मुश्किल होगा। तो क्या कर सकते हैं? बचाव के लिए Jemalloc समवर्तीता वास्तव में मेरे कार्यक्रम के लिए समस्या का एक हिस्सा थी, और विखंडन से बचना खेल का नाम है। ऐसा लगता है जैसे यह वही हो सकता है जिसकी मुझे आवश्यकता है। jemalloc "विखंडन से बचने और स्केलेबल समवर्ती समर्थन पर जोर देने" के लक्ष्य के रूप में वर्णित किया गया है। jemalloc चूँकि एक एलोकेटर है जो सबसे पहले विखंडन से बचने के लिए अपने रास्ते से हट जाता है, उम्मीद थी कि हमारी सेवा मेमोरी को धीरे-धीरे बढ़ाए बिना लंबे समय तक चलने में सक्षम हो सकती है। jemalloc मेरे प्रोग्राम में इनपुट, या एप्लिकेशन निर्भरता के ढेर को बदलना इतना मामूली नहीं है। हालाँकि, आवंटनकर्ता की अदला-बदली करना मामूली बात है। रीडमी में उदाहरणों के बाद, इसे टेस्ट ड्राइव के लिए लेने के लिए बहुत कम काम की आवश्यकता थी। https://github.com/tikv/jemallocator अपने के लिए, मैंने वैकल्पिक रूप से के लिए डिफ़ॉल्ट एलोकेटर को स्वैप करने और अपने लोड परीक्षणों को फिर से चलाने के लिए एक कार्गो सुविधा जोड़ी। खिलौना प्रोजेक्ट jemalloc मेरे सिम्युलेटेड लोड के दौरान रेजिडेंट मेमोरी को रिकॉर्ड करने से दो अलग-अलग मेमोरी प्रोफाइल दिखाई देते हैं। के बिना, हम परिचित सीढ़ी-चरण प्रोफ़ाइल देखते हैं। के साथ, हम देखते हैं कि परीक्षण चलने पर मेमोरी बार-बार बढ़ती और घटती रहती है। इससे भी महत्वपूर्ण बात यह है कि लोड और निष्क्रिय समय के दौरान के साथ मेमोरी के उपयोग के बीच काफी अंतर होता है, हम "अपनी जमीन नहीं खोते" जैसा कि हमने पहले किया था क्योंकि मेमोरी हमेशा बेसलाइन पर वापस आती है। jemalloc jemalloc jemalloc ऊपर लपेटकर यदि आपको रस्ट सेवा पर "सीढ़ी-कदम" प्रोफ़ाइल दिखाई देती है, तो परीक्षण ड्राइव के लिए लेने पर विचार करें। यदि आपके पास कार्यभार है जो ढेर विखंडन को बढ़ावा देता है, समग्र रूप से बेहतर परिणाम दे सकता है। jemalloc jemalloc अलग से, रेपो में लोड परीक्षण टूल के साथ उपयोग के लिए एक शामिल है। यह देखने के लिए कि एलोकेटर में परिवर्तन मेमोरी प्रोफ़ाइल को कैसे प्रभावित करता है, समवर्तीता, बॉडी आकार (और सेवा में मनमाने ढंग से हैंडलर नींद की अवधि) आदि को बदलने का प्रयास करें। खिलौना प्रोजेक्ट https://github.com/fcsonline/dill benchmark.yml जहां तक वास्तविक दुनिया के प्रभाव की बात है, जब हमने पर स्विच शुरू किया तो आप प्रोफ़ाइल में बदलाव को स्पष्ट रूप से देख सकते हैं। jemalloc जहां सेवा अक्सर लोड की परवाह किए बिना सपाट रेखाएं और बड़े चरण दिखाती थी, अब हम एक अधिक टेढ़ी-मेढ़ी रेखा देखते हैं जो सक्रिय कार्यभार का अधिक बारीकी से अनुसरण करती है। सेवा को अनावश्यक मेमोरी वृद्धि से बचने में मदद करने के अलावा, इस बदलाव ने हमें इस बात की बेहतर जानकारी दी कि हमारी सेवा लोड पर कैसे प्रतिक्रिया करती है, इसलिए कुल मिलाकर, यह एक सकारात्मक परिणाम था। यदि आप रस्ट का उपयोग करके एक मजबूत और स्केलेबल सेवा बनाने में रुचि रखते हैं, तो हम भर्ती कर रहे हैं! अधिक जानकारी के लिए देखें। हमारा करियर पेज इस तरह की अधिक सामग्री के लिए, के नवीनतम अपडेट के लिए हमें , या पर फ़ॉलो करना सुनिश्चित करें, या पर चर्चा में शामिल हों। स्विक्स वेबहुक सेवा ट्विटर जीथब आरएसएस हमारे समुदाय स्लैक यहाँ भी प्रकाशित किया गया।