एक भारी चैट हमारा API कैसे गिरा सकती थी
28 सितंबर को, एक release के लगभग 17 मिनट बाद, हमारे दोनों production API सर्वर एक ही समय पर मेमोरी से बाहर हो गए, फिर restart हुए, और लगभग पाँच मिनट तक load balancer के पास ट्रैफ़िक भेजने के लिए कोई स्वस्थ सर्वर नहीं था। हमारे पहले फ़िक्स ने API को दोगुनी मेमोरी दी। असली कारण उसी दिन बाद में सामने आया: एक खास चैट खोलने पर API ने एक ही request में 53 MB संदेश लोड करने को कहा। अब चैट का इतिहास गिनती और आकार दोनों से सीमित पृष्ठों में लोड होता है, और वह चैट क्रैश के बजाय 5 MB के पहले पृष्ठ से खुलती है।
क्या गिरा
हमारा API दो सर्वरों पर, load balancer के पीछे, हर सर्वर पर एक container के रूप में चलता है। हर container की मेमोरी सीमा 384 MiB थी। आराम की स्थिति में वह 130–140 MiB इस्तेमाल करता है, इसलिए सीमा काफ़ी जगह वाली लगती थी।
दोनों containers एक साथ इस सीमा तक पहुँचे और "JavaScript heap out of memory" के साथ क्रैश हो गए। Heap वह मेमोरी है जो एक Node.js प्रोसेस अपने objects के लिए इस्तेमाल करती है। चूँकि दोनों सर्वर एक साथ विफल हुए, अनुरोध भेजने के लिए कहीं जगह नहीं बची। दो सर्वरों वाले सेटअप के लिए यह सबसे बुरी तरह का outage है: redundancy तभी मदद करता है जब विफलताएँ एक साथ न हों।
पहला फ़िक्स: थोड़ा और जगह
समय से लगा कि कारण deploy है। Release के बाद, deploy से बाधित हुए agent turns दोबारा शुरू होते हैं और उनके transcripts फिर सेव होते हैं, और काम का वह उछाल मेमोरी में बढ़ोतरी का स्पष्ट स्रोत लगा।
तो पहले बदलाव में हमने API की सीमा 384 MiB से बढ़ाकर 768 MiB की। हमने उसे उसी दिन दोनों सर्वरों पर live लागू किया, और deploy configuration में भी commit किया, ताकि अगला deploy चुपचाप 384 वापस न रखे। जो फ़िक्स हाथ से एक सर्वर पर किया जाए और अगला deploy उसे पलट दे, वह उसी outage को वापस लाने का एक आम तरीका है।
ज़्यादा मेमोरी एक उचित सुरक्षा-मार्जिन थी। वह कारण की व्याख्या नहीं थी।
असली कारण: एक request, 71 MB
बाद में उसी दिन हमने एक चैट की प्रति पर क्रैश दोहराया, production की पुरानी मेमोरी सीमा के साथ। वह प्रति बिना किसी deploy के API को अकेले क्रैश कर गई।
जब आप कोई प्रोजेक्ट खोलते हैं, तो web app चैट का इतिहास API से माँगता है। Endpoint नवीनतम 30 संदेश पूरे लौटाता था। ज़्यादातर चैट के लिए 30 संदेश छोटे होते हैं। इस चैट के लिए, नवीनतम 30 संदेश डेटाबेस में 53 MB के और response में 71 MB के थे। एजेंट के कुछ जवाब 4–10 MB के थे, ज़्यादातर sub-agents के transcripts: वे सहायक एजेंट जो मुख्य एजेंट किसी काम के हिस्से पर समानांतर में लगाता है, और जिनका पूरा काम जवाब के साथ सेव होता है।
उस एक response को बनाने में API के पास जितनी heap थी उससे ज़्यादा लगी। और उस प्रोजेक्ट को खोलने या रीफ़्रेश करने पर हर बार request दोबारा जाती थी। एक चैट उस सर्वर को गिरा सकती थी जिस पर वह पहुँचती, और चूँकि पेज रीलोड हो सकता था, वह दोनों तक पहुँच सकती थी।
"30 संदेश" कभी आकार की असली सीमा नहीं थी। वह गिनती की सीमा थी, जो सामान्य चैट के लिए संयोग से छोटी थी।
फ़िक्स: आकार के साथ-साथ गिनती से सीमित पृष्ठ
इतिहास का एक पृष्ठ अब 30 संदेश या लगभग 4 MB, जो भी पहले हो, का होता है।
यह दो चरणों में काम करता है:
- पहले आकार पूछें। API डेटाबेस से नवीनतम संदेशों के IDs और सेव किए गए आकार माँगता है, बिना उनकी सामग्री के। यह क्वेरी संदेश चाहे कितने भी बड़े हों, सस्ती रहती है।
- बस वही लोड करें जो फ़िट हो। नवीनतम से सबसे पुराने की ओर चलते हुए, API तब तक संदेश जोड़ता है जब तक अगला संदेश पृष्ठ को 4 MB से आगे न धकेल दे। फिर केवल वही पंक्तियाँ पूरी लोड होती हैं।
दो नियम इसे सुरक्षित रखते हैं:
- नवीनतम संदेश हमेशा पृष्ठ पर जाता है, उसका आकार जो भी हो। जो पृष्ठ कुछ भी न लौटाए, वह चैट को अटका देगा, पीछे स्क्रॉल करने का कोई रास्ता नहीं बचेगा।
- कुछ भी काटा नहीं जाता। कोई संदेश न छोटा किया जाता है, न छोड़ा जाता है। बाकी चैट "Scroll up for earlier messages" वाले नियंत्रण से लोड होती है, जो पुराने संदेशों के लिए पहले से था, एक बार में एक पृष्ठ।
एक और जगह भी वही आकार मिला। जब आप एक टर्न रोकते हैं, तो API आखिरी कुछ जवाब जाँचता है कि क्या कोई खाली है और साफ़ किया जाना चाहिए। यह आठ पूरे संदेश लोड करके करता था। कई MB के जवाबों वाली चैट पर यह उसी समस्या का छोटा रूप था। अब वह डेटाबेस से एक हाँ-या-ना सवाल पूछता है, कि क्या इस संदेश में कोई सामग्री है, बिना सामग्री लोड किए।
हमने इसे कैसे जाँचा
चैट की प्रति पर, पुरानी 384 MiB सीमा के साथ चलते हुए:
- पहला पृष्ठ 5 MB का है। पहले यही request API को क्रैश करती थी।
- एक साथ पाँच browser tabs में चैट खोलने पर मेमोरी की चोटी 154 MB रही।
- पूरी चैट में पीछे स्क्रॉल करने पर सभी 190 संदेश लौटते हैं, हर एक ठीक एक बार, क्रम में।
पृष्ठ चुनने के तर्क के unit tests भी हैं, जिनमें एक उस चैट के नवीनतम संदेशों के असली आकारों से बना है। पहला पृष्ठ एक संदेश लौटाता है, बताता है कि और हैं, और अगला पृष्ठ उसके बाद वाले संदेश से शुरू होता है।
हमने क्या सीखा
- गिनती आकार की सीमा नहीं है। कोई भी endpoint जो "नवीनतम N" लौटाता है, जबकि वह चीज़ असीमित बढ़ सकती है, उसे byte बजट भी चाहिए। डेटा अंततः उस अंतर को खोज लेगा।
- स्पष्ट संदिग्ध हमेशा कारण नहीं होता। deploy सचमुच extra काम का कारण था, और ज़्यादा मेमोरी लेने लायक थी। पर क्रैश बिना deploy के भी दोहराया गया, और उसे दोहराए बिना हम मेमोरी बढ़ाने पर रुक जाते।
- दो सर्वर एक ही request मिलने पर एक साथ विफल होते हैं। Redundancy एक सर्वर के मरने से बचाता है। वह उस request से नहीं बचाता जो किसी भी सर्वर तक पहुँचे तो उसे मार दे, खासकर वह जिसे उपयोगकर्ता रीफ़्रेश करके स्वाभाविक रूप से दोबारा भेजेगा।
- config में ठीक करें, सिर्फ़ मशीन पर नहीं। मेमोरी बढ़ोतरी उसी दिन deploy configuration में गई, ताकि अगला deploy उसे बनाए रखे।
यह हमारी इस बारे में दूसरी हालिया मेमोरी कहानी है जिसमें एजेंट का आउटपुट शामिल है। पहली थी एक status label जिसने 690 MB strings कतार में डाली। अगर आप Mythex पर sub-agents इस्तेमाल करते हैं, तो वे कैसे काम करते हैं, यह docs में पढ़ सकते हैं।