NGINX ctx और error_page के बारे में एक Coredump कहानी
Wei Liu
September 16, 2022
प्रोडक्शन वातावरण में कोरडंप्स इंजीनियरों के लिए डरावने होते हैं, खासकर जब क्रैश रात के बीच में होते हैं। मैंने इसे एक दशक से अधिक समय पहले स्वयं अनुभव किया था। इस "भूत" बग को ढूंढने की कोशिश कर रहे थे जो केवल रात में दिखाई देता था, पूरी टीम दोपहर में घर सोने चली गई और रात 12 बजे कंपनी वापस आई ताकि क्रैश का इंतजार कर सकें और प्रत्येक सेवा को एक-एक करके जांच सकें। कल्पना कीजिए एक अंधेरी रात और एक ऑफिस बिल्डिंग जिसमें केवल कुछ वर्कस्टेशन जले हुए हैं, डेटा और लॉग्स के लिए लगातार धड़कते मॉनिटर को देख रहे हैं – एक तीव्र और रोमांचक अनुभव। हम तीन रातें रुके थे इससे पहले कि बग को ढूंढकर इसे हल कर सकें, जो एक अविस्मरणीय अनुभव था।
हमें नहीं पता था कि हम इसे एक बार फिर अनुभव करेंगे। बस इस बार यह हमारी अपनी सेवा नहीं थी, बल्कि क्लाइंट की ऑनलाइन सेवा में कोरडंप हुआ था। यह हमारे अपने नियंत्रणीय वातावरण से अधिक चुनौतीपूर्ण था:
- त्रुटि केवल उपयोगकर्ता के प्रोडक्शन वातावरण में होती थी, और उपयोगकर्ता इसे अपने प्री-प्रोडक्शन वातावरण और टेस्टिंग वातावरण में भी पुनरुत्पादित नहीं कर सकता था।
- उपयोगकर्ता ने ओपन सोर्स Apache APISIX के ऊपर कस्टम कोड जोड़ा था, जो अलग से NDA पर हस्ताक्षर करने तक दिखाई नहीं देता था।
- डिबग लॉग्स जोड़ने की अनुमति नहीं थी।
- त्रुटि केवल रात 3 बजे होती थी, इसलिए इसे तुरंत संभालने के लिए क्लाइंट से अतिरिक्त संसाधनों की आवश्यकता थी।
संक्षेप में, यह समस्या पुनरुत्पादित करने में असमर्थ थी, कोई पूर्ण स्रोत कोड नहीं था, और परीक्षण करने के लिए कोई वातावरण नहीं था। हालांकि, हमें त्रुटि का पता लगाना था, इसे पुनरुत्पादित करने का तरीका ढूंढना था और अंततः एक समाधान देना था। इस प्रक्रिया के दौरान, हमें कई चुनौतियों का सामना करना पड़ा और उनसे बहुत कुछ सीखा। यहां हम डिबगिंग प्रक्रिया के दौरान सामने आए कुछ रोचक बिंदुओं को पोस्ट कर रहे हैं। मुझे आशा है कि ये लोगों को NGINX और APISIX के लिए डिबगिंग करते समय कुछ मार्गदर्शन प्रदान करेंगे।
समस्या का विवरण
उपयोगकर्ता ने APISIX को 2.4.1 संस्करण से 2.13.1 संस्करण में अपग्रेड करने के बाद, चक्रीय रूप से कोरडंप्स होने लगे, जिसमें निम्नलिखित त्रुटि संदेश था:

कोरडंप संदेश से, हम देख सकते हैं कि सेगमेंटेशन फॉल्ट lua-var-nginx-module में होता है। और यहां मेमोरी में संबंधित डेटा (आंशिक) है:
#0 0x00000000005714d4 in ngx_http_lua_var_ffi_scheme (r=0x570d700, scheme=0x7fd2c241a760) at /tmp/vua-openresty/openresty-1.19.3.1-build/openresty-1.19.3.1/../lua-var-nginx-module/src/ngx_http_lua_var_module.c:152 152 /tmp/vua-openresty/openresty-1.19.3.1-build/openresty-1.19.3.1/../lua-var-nginx-module/src/ngx_http_lua_var_module.c: No such file or directory. (gdb) print *r $1 = {signature = 0, connection = 0x0, ctx = 0x15, main_conf = 0x5adf690, srv_conf = 0x0, loc_conf = 0x0, read_event_handler = 0xe, write_event_handler = 0x5adf697, cache = 0x2, upstream = 0x589c15, upstream_states = 0x0, pool = 0x0, header_in = 0xffffffffffffffff}
हम देख सकते हैं कि मेमोरी में डेटा में समस्या है।
विश्लेषण से पहले कुछ विचार
सेगमेंटेशन फॉल्ट आमतौर पर दो परिदृश्यों में होता है:
- मेमोरी में एक अमान्य पते को पढ़ना/लिखना, जैसे कि एक सरणी सूचकांक को सीमा से बाहर एक्सेस करना, सेगमेंटेशन फॉल्ट तुरंत होता है।
- एक अमान्य लिखना करना लेकिन एक मान्य पते को संशोधित करना, इसलिए यह तुरंत सेगमेंटेशन फॉल्ट उत्पन्न नहीं करता है। प्रोग्राम के चलने के दौरान, यह दोषपूर्ण पते में डेटा को एक्सेस करने का प्रयास करता है और सेगमेंटेशन फॉल्ट उत्पन्न करता है। उदाहरण के लिए, जब एक पॉइंटर का मान गलती से संशोधित हो जाता है, बाद में जब हम इस पॉइंटर को एक्सेस करते हैं, तो यह सेगमेंटेशन फॉल्ट को ट्रिगर कर सकता है।
पहले परिदृश्य के लिए, यदि हम कॉल स्टैक की जांच करें तो समस्या को आसानी से ढूंढा जा सकता है।
दूसरे परिदृश्य के लिए, चूंकि यह स्पष्ट नहीं है कि त्रुटि पहली बार में कहां होती है, त्रुटि उत्पन्न करने वाला कोड और सेगमेंटेशन फॉल्ट को ट्रिगर करने वाला कोड एक ही स्थान पर नहीं हो सकते हैं, यहां तक कि पूरी तरह से असंबंधित हो सकते हैं, जिससे इसे ढूंढना मुश्किल हो जाता है। हम केवल क्रैश से जुड़े अधिक संदर्भ जानकारी एकत्र कर सकते हैं, जैसे:
- वर्तमान APISIX की कॉन्फ़िगरेशन विवरण।
- वर्तमान अनुरोध का प्रसंस्करण चरण।
- वर्तमान अनुरोध का विवरण।
- समवर्ती कनेक्शनों की संख्या।
- त्रुटि लॉग।
इस जानकारी के माध्यम से, हम कोड की समीक्षा करके बग को ढूंढने और समस्या को पुनरुत्पादित करने का तरीका ढूंढने का प्रयास करेंगे।
विश्लेषण
1. त्रुटियों का संदर्भ जांच
सावधानीपूर्वक विश्लेषण के बाद, हमने पाया कि त्रुटि आमतौर पर रात 3 बजे से 4 बजे के बीच होती है, जिसमें निम्नलिखित कोरडंप त्रुटि संदेश होता है:

अंततः, हमने महसूस किया कि त्रुटि लॉग उपयोगकर्ता के ऑपरेशन से जुड़ा हुआ है। कुछ कारणों से, इस समय सभी अपस्ट्रीम संदेश साफ हो जाते हैं। इसलिए, हमने संदेह किया कि समस्या अपस्ट्रीम साफ/रीसेट ऑपरेशन से जुड़ी है, और कोरडंप त्रुटि वापसी के बाद एक अपवाद शाखा में प्रवेश करने के कारण हो सकता है।
2. Lua का कॉल स्टैक प्राप्त करें
चूंकि GDB Lua के कॉल स्टैक को ट्रेस नहीं कर सकता है, हम त्रुटि उत्पन्न करने वाले कॉल का पता नहीं लगा सके। इसलिए, पहली चीज जो हमें करने की आवश्यकता थी वह Lua का कॉल स्टैक प्राप्त करना था; हमारे पास कॉल स्टैक प्राप्त करने के लिए दो तरीके थे:
- lua-var-nginx-module लाइब्रेरी में कोड जोड़कर कॉल स्टैक प्रिंट करें
(print(debug.traceback(...))। नकारात्मक पक्ष यह है कि यह बहुत सारे त्रुटि लॉग उत्पन्न करेगा, जो प्रोडक्शन वातावरण पर नकारात्मक प्रभाव डालेगा। - API7.ai द्वारा बनाए रखा गया openresty-gdb-utils का उपयोग करें, यह टूलकिट GDB के DSL और पायथन एक्सटेंशन का उपयोग करके Lua के कॉल स्टैक का विश्लेषण करने की क्षमता को बढ़ाता है। ध्यान दें, आपको कंपाइल समय में डिबग प्रतीक को चालू करने की आवश्यकता होगी। यह APISIX में डिफ़ॉल्ट रूप से शामिल होता है।
फिर, हमें निम्नलिखित कॉल स्टैक मिला।


Lua और C के कॉल स्टैक को मिलाकर, हम देख सकते हैं कि कोरडंप इसलिए हुआ क्योंकि उपयोगकर्ता ने Prometheus प्लगइन में ngx.ctx.api_ctx.var.scheme को कॉल किया था। हालांकि, क्रैश का कारण अभी भी स्पष्ट नहीं था, हमें इसे और विश्लेषण करने की आवश्यकता थी।
3. कैश ने समस्या उत्पन्न की
त्रुटि तब हुई जब ngx.ctx.api_ctx.var से एक वेरिएबल को एक्सेस करते समय ऊपर वर्णित lua-var-nginx-module में एक कॉल किया गया। दक्षता बढ़ाने के लिए, यह वर्तमान अनुरोध को कैश करता है। हमें याद आया कि त्रुटि तब हुई जब अनुरोध बॉडी का मान एक अपवाद था, इसलिए कैश किए गए अनुरोध की वैधता भी संदिग्ध थी। इस विचार को सत्यापित करने के लिए, हमने प्रोग्राम को कैश की गई मेमोरी लेने से रोक दिया, बल्कि हर बार अनुरोध को फिर से प्राप्त किया।
else --val = get_var(key, t._request) val = get_var(key, nil) <============ t._request को nil में बदलें end
उपयोगकर्ता ने इस संशोधित संस्करण के साथ पूरी रात परीक्षण किया, और त्रुटि फिर से नहीं हुई; हमने सत्यापित किया कि अनुरोध बॉडी में समस्या थी।
4. ngx.ctx में त्रुटि
कैश की गई अनुरोध बॉडी ngx.ctx में सहेजी गई थी, और ngx.ctx को संशोधित करने वाला एकमात्र स्थान apisix/init.lua में था।
function _M.http_header_filter_phase() if ngx_var.ctx_ref ~= '' then -- टेबल लीक को रोकने के लिए local stash_ctx = fetch_ctx() -- आंतरिक रीडायरेक्ट, इसलिए हमें ctx को लागू करना चाहिए if ngx_var.from_error_page == "true" then ngx.ctx = stash_ctx <================= यहां end end core.response.set_header("Server", ver_header) local up_status = get_var("upstream_status") if up_status then set_resp_upstream_status(up_status) end
हमें यहां ngx.ctx को पुनर्स्थापित करने की आवश्यकता क्यों थी? क्योंकि ngx.ctx Lua रजिस्ट्री में संग्रहीत है, संबंधित ngx.ctx को रजिस्ट्री इंडेक्स के माध्यम से पाया जा सकता है। और इंडेक्स NGINX अनुरोध संरचना के ctx सदस्य में संग्रहीत था। हर बार जब एक आंतरिक रीडायरेक्ट किया जाता है, NGINX ctx को साफ कर देता है, और इस प्रकार इंडेक्स नहीं मिल सकता है।
इस समस्या को हल करने के लिए, APISIX ने एक टेबल बनाई, टेबल ने रीडायरेक्ट करने से पहले वर्तमान ngx.ctx को सहेजा, और फिर ngx.var का उपयोग करके इस टेबल में ngx.ctx के इंडेक्स को रिकॉर्ड किया, जब भी इसे पुनर्स्थापित करने की आवश्यकता होती है, हम सीधे टेबल से ngx.ctx प्राप्त कर सकते हैं।
उपरोक्त चित्र में कोड केवल तब ट्रिगर होता है जब उपयोगकर्ता ने error_page निर्देशों को कॉन्फ़िगर किया है, और आंतरिक रीडायरेक्शन के बाद एक त्रुटि होने के बाद।
और हमें स्पष्ट रूप से पता था कि उपयोगकर्ता ने संस्करण अपग्रेड के बाद error_page निर्देश को चालू किया था, जो ऊपर वर्णित अपस्ट्रीम परिवर्तन से होने वाली त्रुटि के साथ जुड़ा हुआ था। सभी चीजें जुड़ी हुई लग रही थीं। क्या यह वास्तव में ngx.ctx था जिसने त्रुटि प्रसंस्करण प्रक्रिया के दौरान बग बनाया था?
5. ngx.ctx में बग क्यों है
प्रश्न के साथ, हमने ngx.ctx से संबंधित बैकअप कोड की जांच की, और हमें और अधिक अजीब समस्याएं मिलीं। set_upstream में विफल होने के बाद, यह कभी भी ngx.ctx को पुनर्स्थापित करने का कदम नहीं उठाता था, क्योंकि यह जल्दी बाहर निकल जाता था, यह ngx.ctx का बैकअप नहीं लेता था, और यह पुनर्स्थापना प्रक्रिया में भी नहीं जाता था।

यह स्पष्ट रूप से एक बग था! खैर, यह इसलिए है क्योंकि ngx.ctx को रीडायरेक्ट करने के बाद पुनर्स्थापित करने की आवश्यकता थी। और यह प्रक्रिया ngx.ctx को कैसे पुनर्स्थापित करती है? ngx.ctx में पहली बार में क्या गलत हुआ था? अब बहुत सारी अनिश्चितताएं थीं, हमें और अधिक डेटा एकत्र करने की आवश्यकता थी।
चर्चा के बाद, हमने प्रोडक्शन पर एक दूसरा लॉग जोड़ा, और हमने पाया कि, जब आंतरिक रीडायरेक्शन के बाद त्रुटि होती है, तो ngx.ctx पुनर्स्थापना प्रक्रिया से नहीं गुजरता है, बल्कि यह सीधे कोरडंप का कारण बनता है!
यह एक चकित करने वाली घटना थी क्योंकि यदि ngx.ctx रीडायरेक्शन के बाद पुनर्स्थापित नहीं होता है, तो ngx.ctx खाली/शून्य होगा। और खाली मानों की जांच करने वाला कोड था; यह प्लगइन में कोरडंप कोड को ट्रिगर नहीं करना चाहिए था।
local function common_phase(phase_name) local api_ctx = ngx.ctx.api_ctx if not api_ctx then <============ यहां return end plugin.run_global_rules(api_ctx, api_ctx.global_rules, phase_name)
तो, क्या यह error_page आंतरिक रीडायरेक्शन था जिसने ngx.ctx को समस्या में डाल दिया?
6.प्रारंभिक निष्कर्ष
विश्लेषण और जांच के बाद, हमें त्रुटिपूर्ण प्रक्रिया की मूल समझ मिली।
set_upstream विफल हो गया और error_page के त्रुटि प्रसंस्करण चरण में रीडायरेक्ट हो गया, और अपेक्षित खाली ngx.ctx में एक अप्रत्याशित मान था। और APISIX के बग के कारण, यह रीडायरेक्ट करने से पहले ngx.ctx को पुनर्स्थापित नहीं करता था, जिससे ngx.ctx में गंदा डेटा हो गया जब एक्सेस किया गया, इस प्रकार कोरडंप का कारण बना। इस समस्या को हल करने के लिए, हमें केवल रीडायरेक्ट करने के बाद ngx.ctx को पुनर्स्थापित करने की आवश्यकता थी; इस कमिट के विस्तृत कार्यान्वयन के लिए लेख के अंत में संदर्भ दिया गया है।
हालांकि हमने बग के लिए एक समाधान प्रदान किया, हम अभी भी बग की पूरी तर्क को समझने में असमर्थ थे। क्योंकि उपयोगकर्ता ने ngx.ctx के तर्क को संशोधित नहीं किया था, ओपन-सोर्स संस्करण को पुनरुत्पादित करने में सक्षम होना चाहिए। उपयोगकर्ता के प्रोडक्शन वातावरण और डिप्लॉयमेंट प्रक्रिया को प्रभावित करने से बचने के लिए, हमने मूल कारण की जांच जारी रखने का निर्णय लिया।
7.सफल पुनरुत्पादन
जो हम जानते थे, उससे हम समस्या को पुनरुत्पादित करने में असमर्थ थे। विश्लेषण के बाद, हमने निम्नलिखित दो स्थानों की जांच करने का निर्णय लिया:
- क्या आंतरिक रीडायरेक्शन में प्रसंस्करण के लिए विशेष शाखाएं थीं?
- क्या ngx.ctx में प्रसंस्करण के लिए विशेष शाखाएं थीं?
पहले बिंदु के लिए, NGINX के प्रत्येक फिल्टर मॉड्यूल के प्रसंस्करण के दौरान, यह कुछ अपवाद शाखाएं हो सकती हैं जो अनुरोध बॉडी के ctx सदस्यों को प्रभावित करती हैं, जिससे ngx.ctx प्रभावित होता है। हमने उपयोगकर्ता के मॉड्यूल को NGINX में संकलित करने की जांच की संभावित शाखा अपवादों के लिए लेकिन कोई समान समस्या नहीं मिली।
दूसरे बिंदु के लिए, जांच के बाद, हमने पाया कि SSL हैंडशेक के दौरान, ngx.ctx पुन: उपयोग से समस्या होती है। यदि HTTP चरण ngx.ctx प्राप्त नहीं कर सकता है, तो यह SSL चरण से इसे प्राप्त करने का प्रयास करेगा। SSL चरण का अपना ngx.ctx होता है, और यह ngx.ctx के लिए समस्याएं पैदा कर सकता है।
उपरोक्त खोजों के आधार पर, हमने निम्नलिखित शर्तों को डिजाइन किया, और समस्या को सफलतापूर्वक पुनरुत्पादित करने में सक्षम थे:
- उपयोगकर्ता के समान APISIX संस्करण
- SSL प्रमाणपत्र चालू करें
- एक आंतरिक त्रुटि बनाएं जो error_page को ट्रिगर करे (जैसे कि जब कोई अपस्ट्रीम नहीं हो तो अनुरोध करें)
- लंबा कनेक्शन
- Prometheus प्लगइन (उपयोगकर्ता के लिए प्लगइन में कोरडंप हुआ)
हमने अंततः बिना ईंटों के ईंट बनाई। अनुमान और सत्यापन के माध्यम से, हमने अंततः इस बग की पूरी प्रक्रिया को समझ लिया। एक बार पूर्ण पुनरुत्पादन हो जाने के बाद, समस्या को हल करना अब मुश्किल नहीं था, और अंतिम प्रक्रिया को यहां विस्तार से वर्णित नहीं किया जाएगा। मुझे विश्वास है कि हर किसी को सटीक रूप से पुनरुत्पादित करने वाली समस्याओं का बहुत अनुभव है।
सारांश
इस समस्या का मूल कारण: क्योंकि error_page में प्रवेश करने के बाद ngx.ctx को पुनर्स्थापित नहीं किया गया था, लंबे कनेक्शन पर सभी अनुरोध SSL चरण में एक ही ngx.ctx का पुन: उपयोग करते हैं। अंततः, ngx.ctx में गंदा डेटा हो गया और कोरडंप उत्पन्न हुआ।
अनुवर्ती में, हमने इस समस्या के लिए एक संबंधित समाधान प्रस्तावित किया: यह सुनिश्चित करना कि आंतरिक रीडायरेक्शन के बाद ngx.ctx को सामान्य रूप से पुनर्स्थापित किया जा सके। विशिष्ट PR के लिए कृपया संदर्भ लें।
यदि आपका APISIX error_page जैसे आंतरिक रीडायरेक्शन से संबंधित कार्यों का उपयोग करता है, तो कृपया जल्द से जल्द नवीनतम संस्करण में अपग्रेड करें।
प्रोग्राम लिखना और डिबगिंग करना वैज्ञानिक रूप से कठोर कार्य हैं और इसमें कोई अस्पष्टता नहीं हो सकती है। एक समस्या को हल करने के लिए, हमें जानकारी एकत्र करने, सभी संभावित सुरागों को ट्रैक करने, कॉल स्टैक का विश्लेषण करने, और फिर त्रुटि को पुनरुत्पादित करने के लिए संदर्भ को समझने की आवश्यकता होती है। अंततः, समस्या हल हो जाती है।