नॉन-ब्लॉकिंग I/O - OpenResty प्रदर्शन को बेहतर बनाने की कुंजी

API7.ai

December 2, 2022

OpenResty (NGINX + Lua)

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

परफॉर्मेंस सुधारना आसान नहीं है। हमें सिस्टम आर्किटेक्चर ऑप्टिमाइज़ेशन, डेटाबेस ऑप्टिमाइज़ेशन, कोड ऑप्टिमाइज़ेशन, परफॉर्मेंस टेस्टिंग, फ्लेम ग्राफ विश्लेषण, और अन्य चरणों पर विचार करना होगा। लेकिन परफॉर्मेंस को कम करना आसान है, और जैसा कि आज के लेख के शीर्षक से पता चलता है, आप केवल कुछ लाइनों का कोड जोड़कर परफॉर्मेंस को 10 गुना या उससे अधिक कम कर सकते हैं। यदि आप OpenResty का उपयोग करके अपना कोड लिख रहे हैं, लेकिन परफॉर्मेंस में सुधार नहीं हुआ है, तो यह संभवतः ब्लॉकिंग I/O के कारण है।

इसलिए, परफॉर्मेंस ऑप्टिमाइज़ेशन के विवरण में जाने से पहले, आइए OpenResty प्रोग्रामिंग में एक महत्वपूर्ण सिद्धांत को देखें: नॉन-ब्लॉकिंग I/O पहले।

हमें बचपन से ही हमारे माता-पिता और शिक्षकों द्वारा सिखाया गया है कि आग से न खेलें और प्लग को न छुएं, ये खतरनाक व्यवहार हैं। OpenResty में भी इसी तरह के खतरनाक व्यवहार मौजूद हैं। यदि आपको अपने कोड में ब्लॉकिंग I/O ऑपरेशन करने होंगे, तो इससे परफॉर्मेंस में भारी गिरावट आएगी, और OpenResty का उपयोग करके एक हाई-परफॉर्मेंस सर्वर बनाने का मूल उद्देश्य विफल हो जाएगा।

हम ब्लॉकिंग I/O ऑपरेशन क्यों नहीं कर सकते?

यह समझना कि कौन से व्यवहार खतरनाक हैं और उनसे बचना, परफॉर्मेंस ऑप्टिमाइज़ेशन का पहला कदम है। आइए पहले यह समीक्षा करें कि ब्लॉकिंग I/O ऑपरेशन OpenResty की परफॉर्मेंस को कैसे प्रभावित कर सकते हैं।

OpenResty उच्च परफॉर्मेंस बनाए रख सकता है क्योंकि यह NGINX की इवेंट हैंडलिंग और Lua के कोरोटीन का उपयोग करता है, इसलिए:

  • जब आपको नेटवर्क I/O जैसे ऑपरेशन का सामना करना पड़ता है जिसमें आपको जारी रखने से पहले वापसी की प्रतीक्षा करनी होती है, तो आप Lua कोरोटीन yield को कॉल करके खुद को लटका देते हैं और फिर NGINX में एक कॉलबैक रजिस्टर करते हैं।
  • I/O ऑपरेशन पूरा होने के बाद (या टाइमआउट या त्रुटि होने पर), NGINX resume को कॉल करके Lua कोरोटीन को जगाता है।

ऐसी प्रक्रिया सुनिश्चित करती है कि OpenResty हमेशा CPU संसाधनों का कुशलतापूर्वक उपयोग करके सभी अनुरोधों को संसाधित कर सके।

इस प्रसंस्करण प्रवाह में, यदि LuaJIT cosocket जैसे नॉन-ब्लॉकिंग I/O तरीके का उपयोग नहीं करता है, बल्कि ब्लॉकिंग I/O फ़ंक्शन का उपयोग करके I/O को संभालता है, तो यह NGINX के इवेंट लूप को नियंत्रण नहीं देता है। इसके परिणामस्वरूप अन्य अनुरोध ब्लॉकिंग I/O इवेंट के प्रसंस्करण समाप्त होने की प्रतीक्षा करते हैं, इससे पहले कि उन्हें प्रतिक्रिया मिले।

संक्षेप में, OpenResty प्रोग्रामिंग में, हमें उन फ़ंक्शन कॉल के बारे में विशेष रूप से सावधान रहना चाहिए जो I/O को ब्लॉक कर सकते हैं; अन्यथा, एकल ब्लॉकिंग I/O कोड की एक लाइन पूरी सेवा की परफॉर्मेंस को गिरा सकती है।

नीचे, मैं कुछ सामान्य समस्याओं, कुछ अक्सर गलत तरीके से उपयोग किए जाने वाले ब्लॉकिंग I/O फ़ंक्शन का परिचय दूंगा; आइए यह भी अनुभव करें कि कैसे सबसे आसान तरीके से "गड़बड़" करके आप अपनी सेवा की परफॉर्मेंस को 10 गुना तक कम कर सकते हैं।

बाहरी कमांड निष्पादित करना

कई परिदृश्यों में, डेवलपर्स OpenResty का उपयोग केवल एक वेब सर्वर के रूप में नहीं करते हैं, बल्कि इसे और अधिक व्यावसायिक तर्क प्रदान करते हैं। इस स्थिति में, कुछ ऑपरेशन पूरा करने के लिए बाहरी कमांड और टूल्स को कॉल करना आवश्यक हो सकता है।

उदाहरण के लिए, एक प्रक्रिया को समाप्त करने के लिए।

os.execute("kill -HUP " .. pid)

या अधिक समय लेने वाले ऑपरेशन जैसे फ़ाइलों की प्रतिलिपि बनाना, OpenSSL का उपयोग करके कुंजी उत्पन्न करना, आदि।

os.execute(" cp test.exe /tmp ") os.execute(" openssl genrsa -des3 -out private.pem 2048 ")

सतह पर, os.execute Lua में एक बिल्ट-इन फ़ंक्शन है, और Lua की दुनिया में, यह वास्तव में बाहरी कमांड को कॉल करने का तरीका है। हालांकि, यह याद रखना महत्वपूर्ण है कि Lua एक एम्बेडेड प्रोग्रामिंग भाषा है और अन्य संदर्भों में इसके उपयोग की अलग-अलग सिफारिशें हो सकती हैं।

OpenResty के वातावरण में, os.execute वर्तमान अनुरोध को ब्लॉक करता है। इसलिए, यदि इस कमांड का निष्पादन समय विशेष रूप से कम है, तो प्रभाव बहुत बड़ा नहीं होता है। लेकिन यदि कमांड को निष्पादित करने में सैकड़ों मिलीसेकंड या यहां तक कि सेकंड लगते हैं, तो परफॉर्मेंस में भारी गिरावट आएगी।

हम समस्या को समझते हैं, तो इसे कैसे हल करें? सामान्य तौर पर, दो समाधान हैं।

1. यदि FFI लाइब्रेरी उपलब्ध है, तो हम FFI तरीके को प्राथमिकता देते हैं

उदाहरण के लिए, यदि हमने ऊपर OpenSSL कमांड लाइन का उपयोग करके कुंजी उत्पन्न की है, तो हम इसे OpenSSL C फ़ंक्शन को कॉल करने के लिए FFI का उपयोग करके बायपास कर सकते हैं।

प्रक्रिया को समाप्त करने के लिए, आप OpenResty के साथ आने वाली lua-resty-signal लाइब्रेरी का उपयोग करके इसे नॉन-ब्लॉकिंग तरीके से हल कर सकते हैं। कोड कार्यान्वयन निम्नलिखित है। बेशक, यहां lua-resty-signal भी सिस्टम फ़ंक्शन को कॉल करने के लिए FFI का उपयोग करके हल किया गया है।

local resty_signal = require "resty.signal" local pid = 12345 local ok, err = resty_signal.kill(pid, "KILL")

इसके अलावा, LuaJIT की आधिकारिक वेबसाइट पर एक विशेष पृष्ठ है जो विभिन्न श्रेणियों में विभिन्न FFI बाइंडिंग लाइब्रेरी का परिचय देता है। उदाहरण के लिए, जब छवियों, एन्क्रिप्शन और डिक्रिप्शन जैसे CPU-गहन ऑपरेशन से निपटने की बात आती है, तो आप पहले यह देख सकते हैं कि क्या वहां पहले से ही पैकेज्ड लाइब्रेरी हैं जिनका उपयोग सीधे किया जा सकता है।

2. ngx.pipe पर आधारित lua-resty-shell लाइब्रेरी का उपयोग करें

जैसा कि पहले वर्णित किया गया है, आप अपने कमांड को shell.run में चला सकते हैं, एक नॉन-ब्लॉकिंग I/O ऑपरेशन।

$ resty -e 'local shell = require "resty.shell" local ok, stdout, stderr, reason, status = shell.run([[echo "hello, world"]]) ngx.say(stdout) '

डिस्क I/O

आइए डिस्क I/O को संभालने के परिदृश्य को देखें। एक सर्वर-साइड एप्लिकेशन में, स्थानीय कॉन्फ़िगरेशन फ़ाइल को पढ़ना एक सामान्य ऑपरेशन है, जैसे निम्नलिखित कोड।

local path = "/conf/apisix.conf" local file = io.open(path, "rb") local content = file:read("*a") file:close()

यह कोड io.open का उपयोग करके एक निश्चित फ़ाइल की सामग्री प्राप्त करता है। हालांकि, यह एक ब्लॉकिंग I/O ऑपरेशन है, लेकिन यह न भूलें कि वास्तविक परिदृश्य में चीजों पर विचार करना होगा। इसलिए यदि आप इसे init और init worker में कॉल करते हैं, तो यह एक बार का कार्य है जो किसी भी क्लाइंट अनुरोध को प्रभावित नहीं करता है और पूरी तरह से स्वीकार्य है।

बेशक, यदि प्रत्येक उपयोगकर्ता अनुरोध डिस्क पर पढ़ने या लिखने को ट्रिगर करता है, तो यह अस्वीकार्य हो जाता है। उस समय, आपको समाधान पर गंभीरता से विचार करने की आवश्यकता है।

सबसे पहले, हम lua-io-nginx-module का उपयोग कर सकते हैं, एक तृतीय-पक्ष C मॉड्यूल। यह OpenResty के लिए एक नॉन-ब्लॉकिंग I/O Lua API प्रदान करता है, लेकिन आप इसे cosocket की तरह मनमाने ढंग से उपयोग नहीं कर सकते। क्योंकि डिस्क I/O खपत बिना किसी कारण के गायब नहीं होती है, यह सिर्फ करने का एक अलग तरीका है।

यह दृष्टिकोण काम करता है क्योंकि lua-io-nginx-module NGINX थ्रेड पूलिंग का लाभ उठाता है ताकि डिस्क I/O ऑपरेशन को मुख्य थ्रेड से दूसरे थ्रेड में स्थानांतरित किया जा सके, ताकि मुख्य थ्रेड डिस्क I/O ऑपरेशन द्वारा ब्लॉक न हो।

इस लाइब्रेरी का उपयोग करते समय आपको NGINX को पुनः संकलित करने की आवश्यकता होती है क्योंकि यह एक C मॉड्यूल है। यह Lua की I/O लाइब्रेरी के समान तरीके से उपयोग किया जाता है।

local ngx_io = require "ngx.io" local path = "/conf/apisix.conf" local file, err = ngx_io.open(path, "rb") local data, err = file: read("*a") file:close()

दूसरा, एक आर्किटेक्चरल ट्विक का प्रयास करें। क्या हम इस प्रकार के डिस्क I/O के लिए अपना तरीका बदल सकते हैं और स्थानीय डिस्क पर पढ़ने और लिखने से रोक सकते हैं?

मैं आपको एक उदाहरण दूंगा ताकि आप इससे सीख सकें। कई साल पहले, मैं एक प्रोजेक्ट पर काम कर रहा था जिसमें सांख्यिकी और समस्या निवारण के लिए स्थानीय डिस्क पर लॉगिंग की आवश्यकता थी।

उस समय, डेवलपर्स ने इन लॉग्स को लिखने के लिए ngx.log का उपयोग किया, जैसे निम्नलिखित।

ngx.log(ngx.WARN, "info")

यह कोड OpenResty द्वारा प्रदान किए गए Lua API को कॉल करता है, और ऐसा लगता है कि कोई समस्या नहीं है। हालांकि, नुकसान यह है कि आप इसे बहुत बार कॉल नहीं कर सकते। पहले, ngx.log स्वयं एक महंगा फ़ंक्शन कॉल है; दूसरा, बफर के साथ भी, बड़े और लगातार डिस्क लेखन परफॉर्मेंस को गंभीर रूप से प्रभावित कर सकते हैं।

तो हम इसे कैसे हल करें? आइए मूल आवश्यकता पर वापस जाएं - सांख्यिकी, समस्या निवारण, और स्थानीय डिस्क पर लॉग लिखना लक्ष्य तक पहुंचने का एक साधन होगा।

इसलिए आप लॉग्स को एक दूरस्थ लॉगिंग सर्वर पर भेज सकते हैं ताकि cosocket का उपयोग करके नॉन-ब्लॉकिंग नेटवर्क संचार किया जा सके; यानी ब्लॉकिंग डिस्क I/O को लॉगिंग सेवा पर फेंक दें ताकि बाहरी सेवा को ब्लॉक न किया जाए। आप इसे करने के लिए lua-resty-logger-socket का उपयोग कर सकते हैं।

local logger = require "resty.logger.socket" if not logger.initted() then local ok, err = logger.init{ host = 'xxx', port = 1234, flush_limit = 1234, drop_limit = 5678, } local msg = "foo" local bytes, err = logger.log(msg)

जैसा कि आपने देखा होगा, उपरोक्त दोनों तरीके समान हैं: यदि ब्लॉकिंग I/O अपरिहार्य है, तो मुख्य वर्कर थ्रेड को ब्लॉक न करें; इसे अन्य थ्रेड या सेवाओं पर फेंक दें।

luasocket

अंत में, आइए luasocket के बारे में बात करते हैं, एक Lua बिल्ट-इन लाइब्रेरी जिसे डेवलपर्स द्वारा आसानी से उपयोग किया जाता है और अक्सर OpenResty द्वारा प्रदान किए गए cosocket के साथ भ्रमित किया जाता है। luasocket नेटवर्क संचार कार्य कर सकता है, लेकिन इसमें नॉन-ब्लॉकिंग का लाभ नहीं है। परिणामस्वरूप, यदि आप luasocket का उपयोग करते हैं, तो परफॉर्मेंस में भारी गिरावट आती है।

हालांकि, luasocket के भी अपने अनूठे उपयोग के परिदृश्य हैं। उदाहरण के लिए, मुझे नहीं पता कि आपको याद है कि cosocket कई चरणों में उपलब्ध नहीं है, और हम आमतौर पर इसे ngx.timer का उपयोग करके बायपास कर सकते हैं। इसके अलावा, आप init_by_lua* और init_worker_by_lua* जैसे एक-बार के चरणों में cosocket कार्यों के लिए luasocket का उपयोग कर सकते हैं। OpenResty और Lua के बीच समानताओं और अंतरों के बारे में जितना अधिक आप जानते हैं, उतने ही दिलचस्प समाधान आप पाएंगे।

इसके अलावा, lua-resty-socket एक ओपन-सोर्स लाइब्रेरी का द्वितीयक रैपर है जो luasocket और cosocket को संगत बनाता है। यह सामग्री और अधिक अध्ययन के योग्य है। यदि आप अभी भी रुचि रखते हैं, तो मैंने आपके लिए सामग्री तैयार की है ताकि आप अधिक सीख सकें।

सारांश

सामान्य तौर पर, OpenResty में, ब्लॉकिंग I/O ऑपरेशन के प्रकारों और उनके समाधानों को पहचानना अच्छे परफॉर्मेंस ऑप्टिमाइज़ेशन की नींव है। तो, क्या आपने वास्तविक विकास में इसी तरह के ब्लॉकिंग I/O ऑपरेशन का सामना किया है? आप उन्हें कैसे ढूंढते और हल करते हैं? कृपया मुझे टिप्पणियों में अपना अनुभव साझा करें, और इस लेख को साझा करने के लिए स्वतंत्र महसूस करें।