OpenResty इतना खास क्यों है

API7.ai

October 14, 2022

OpenResty (NGINX + Lua)

पिछले लेखों में, आपने OpenResty के दो मुख्य आधारों: NGINX और LuaJIT के बारे में सीखा है, और मुझे यकीन है कि आप OpenResty द्वारा प्रदान किए गए APIs के बारे में सीखने के लिए तैयार हैं।

लेकिन जल्दबाजी न करें। इससे पहले, आपको OpenResty के सिद्धांतों और बुनियादी अवधारणाओं से परिचित होने के लिए थोड़ा और समय बिताने की आवश्यकता है।

सिद्धांत

Diagram1

OpenResty के Master और Worker प्रक्रियाओं दोनों में एक LuaJIT VM होता है, जो एक ही प्रक्रिया के भीतर सभी कोरोटीन्स द्वारा साझा किया जाता है, और जिसमें Lua कोड चलाया जाता है।

और एक ही समय में, प्रत्येक Worker प्रक्रिया केवल एक उपयोगकर्ता के अनुरोधों को संभाल सकती है, जिसका अर्थ है कि केवल एक कोरोटीन चल रही है। आपके मन में एक प्रश्न हो सकता है: चूंकि NGINX C10K (दस हजार समवर्ती) का समर्थन कर सकता है, तो क्या इसे 10,000 अनुरोधों को एक साथ संभालने की आवश्यकता नहीं है?

बिल्कुल नहीं। NGINX epoll का उपयोग करता है ताकि इवेंट्स को ड्राइव किया जा सके और प्रतीक्षा और निष्क्रियता को कम किया जा सके, ताकि यथासंभव CPU संसाधनों का उपयोग उपयोगकर्ता अनुरोधों को संसाधित करने के लिए किया जा सके। आखिरकार, पूरी चीज़ उच्च प्रदर्शन प्राप्त करती है जब व्यक्तिगत अनुरोधों को पर्याप्त रूप से तेज़ी से संसाधित किया जाता है। यदि एक मल्टी-थ्रेडेड मोड का उपयोग किया जाता है ताकि एक अनुरोध एक थ्रेड से मेल खाता हो, तो C10K के साथ, संसाधन आसानी से समाप्त हो सकते हैं।

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

हम निम्नलिखित डायग्राम को देख सकते हैं, जो पूरी प्रक्रिया का वर्णन करता है। lua_yield और lua_resume दोनों Lua द्वारा प्रदान किए गए lua_CFunction का हिस्सा हैं।

Diagram2

दूसरी ओर, यदि Lua कोड में कोई I/O या sleep ऑपरेशन नहीं हैं, जैसे सभी गहन एन्क्रिप्शन और डिक्रिप्शन ऑपरेशन, तो LuaJIT VM Lua कोरोटीन द्वारा कब्जा कर लिया जाएगा जब तक कि पूरा अनुरोध संसाधित नहीं हो जाता।

मैंने आपको इसे और स्पष्ट रूप से समझने में मदद करने के लिए ngx.sleep का सोर्स कोड का एक स्निपेट नीचे प्रदान किया है। यह कोड ngx_http_lua_sleep.c में स्थित है, जिसे आप lua-nginx-module प्रोजेक्ट के src डायरेक्टरी में पा सकते हैं।

ngx_http_lua_sleep.c में, हम sleep फ़ंक्शन का ठोस कार्यान्वयन देख सकते हैं। आपको पहले Lua API ngx.sleep को C फ़ंक्शन ngx_http_lua_ngx_sleep के साथ रजिस्टर करना होगा।

void ngx_http_lua_inject_sleep_api(lua_State *L) { lua_pushcfunction(L, ngx_http_lua_ngx_sleep); lua_setfield(L, -2, "sleep"); }

निम्नलिखित sleep का मुख्य फ़ंक्शन है, और मैंने यहां केवल मुख्य कोड की कुछ पंक्तियां निकाली हैं।

static int ngx_http_lua_ngx_sleep(lua_State *L) { coctx->sleep.handler = ngx_http_lua_sleep_handler; ngx_add_timer(&coctx->sleep, (ngx_msec_t) delay); return lua_yield(L, 0); }

जैसा कि आप देख सकते हैं:

  • यहां पहले कॉलबैक फ़ंक्शन ngx_http_lua_sleep_handler जोड़ा जाता है।
  • फिर ngx_add_timer को कॉल करें, जो NGINX द्वारा प्रदान किया गया एक इंटरफ़ेस है, ताकि NGINX के इवेंट लूप में एक टाइमर जोड़ा जा सके।
  • अंत में, lua_yield का उपयोग करके Lua समवर्ती को निलंबित करें, जिससे नियंत्रण NGINX इवेंट लूप को दिया जा सके।

ngx_http_lua_sleep_handler कॉलबैक फ़ंक्शन तब ट्रिगर होता है जब sleep ऑपरेशन पूरा हो जाता है। यह ngx_http_lua_sleep_resume को कॉल करता है और अंत में lua_resume का उपयोग करके Lua कोरोटीन को जगाता है। आप कोड में कॉल के विवरण को स्वयं पुनः प्राप्त कर सकते हैं ताकि मैं यहां विस्तार से न जाऊं।

ngx.sleep सिर्फ सबसे सरल उदाहरण है, लेकिन इसे विश्लेषण करके, आप lua-nginx-module मॉड्यूल के बुनियादी सिद्धांतों को देख सकते हैं।

बुनियादी अवधारणाएं

सिद्धांतों का विश्लेषण करने के बाद, आइए अपनी याददाश्त को ताज़ा करें और OpenResty के दो महत्वपूर्ण अवधारणाओं: चरणों और नॉन-ब्लॉकिंग को याद करें।

OpenResty, NGINX की तरह, चरणों की अवधारणा रखता है, और प्रत्येक चरण का अपना विशिष्ट भूमिका होता है:

  • set_by_lua, जो चर सेट करने के लिए उपयोग किया जाता है।
  • rewrite_by_lua, फॉरवर्डिंग, रीडायरेक्टिंग आदि के लिए।
  • access_by_lua, एक्सेस, अनुमतियां आदि के लिए।
  • content_by_lua, रिटर्न सामग्री उत्पन्न करने के लिए।
  • header_filter_by_lua, प्रतिक्रिया हेडर फ़िल्टर प्रसंस्करण के लिए।
  • body_filter_by_lua, प्रतिक्रिया बॉडी फ़िल्टरिंग के लिए।
  • log_by_lua, लॉगिंग के लिए।

बेशक, यदि आपके कोड का तर्क बहुत जटिल नहीं है, तो इसे rewrite या content चरण में पूरा करना संभव है।

हालांकि, ध्यान दें कि OpenResty के APIs के चरण उपयोग सीमाएं हैं। प्रत्येक API के पास उन चरणों की एक सूची होती है जिनमें इसका उपयोग किया जा सकता है, और यदि आप इसे सीमा से बाहर उपयोग करते हैं तो आपको एक त्रुटि मिलेगी। यह अन्य विकसित भाषाओं से बहुत अलग है।

एक उदाहरण के रूप में, मैं ngx.sleep का उपयोग करूंगा। दस्तावेज़ीकरण से, मुझे पता है कि इसका उपयोग केवल निम्नलिखित संदर्भों में किया जा सकता है और इसमें log चरण शामिल नहीं है।

context: rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*_

और यदि आप इसे नहीं जानते हैं, तो sleep का उपयोग एक log चरण में करें जिसमें यह समर्थित नहीं है:

location / { log_by_lua_block { ngx.sleep(1) } }

NGINX त्रुटि लॉग में, एक error स्तर का संकेत होगा।

[error] 62666#0: *6 failed to run log_by_lua*: log_by_lua(nginx.conf:14):2: API disabled in the context of log_by_lua* stack traceback: [C]: in function 'sleep'

इसलिए, API का उपयोग करने से पहले, हमेशा दस्तावेज़ीकरण को देखें ताकि यह निर्धारित किया जा सके कि क्या इसे आपके कोड के संदर्भ में उपयोग किया जा सकता है।

चरणों की अवधारणा की समीक्षा करने के बाद, आइए नॉन-ब्लॉकिंग की समीक्षा करें। पहले, आइए स्पष्ट करें कि OpenResty द्वारा प्रदान किए गए सभी APIs नॉन-ब्लॉकिंग हैं।

मैं sleep 1-second आवश्यकता को एक उदाहरण के रूप में जारी रखूंगा। यदि आप इसे Lua में लागू करना चाहते हैं, तो आपको यह करना होगा।

function sleep(s) local ntime = os.time() + s repeat until os.time() > ntime end

चूंकि मानक Lua में sleep फ़ंक्शन नहीं है, मैं यहां एक लूप का उपयोग करता हूं ताकि यह निर्धारित किया जा सके कि निर्दिष्ट समय पहुंच गया है या नहीं। यह कार्यान्वयन ब्लॉकिंग है, और sleep चलने के दौरान, Lua कुछ नहीं कर रहा है जबकि अन्य अनुरोध जिन्हें संसाधित करने की आवश्यकता है, बस प्रतीक्षा कर रहे हैं।

हालांकि, यदि हम ngx.sleep(1) पर स्विच करते हैं, तो ऊपर विश्लेषण किए गए सोर्स कोड के अनुसार, OpenResty इस एक सेकंड के दौरान अन्य अनुरोधों (जैसे request B) को संसाधित कर सकता है। वर्तमान अनुरोध (request A) का संदर्भ सहेजा जाएगा और NGINX इवेंट मैकेनिज्म द्वारा जगाया जाएगा और फिर request A पर वापस जाएगा, ताकि CPU हमेशा एक प्राकृतिक कार्य स्थिति में रहे।

चर और जीवन चक्र

इन दो महत्वपूर्ण अवधारणाओं के अलावा, चरों का जीवन चक्र भी OpenResty विकास में गलत होने वाला एक आसान क्षेत्र है।

जैसा कि मैंने पहले कहा था, OpenResty में, मैं अनुशंसा करता हूं कि आप सभी चरों को स्थानीय चर के रूप में घोषित करें और luacheck और lua-releng जैसे टूल्स का उपयोग करें ताकि वैश्विक चरों का पता लगाया जा सके। यह मॉड्यूल के लिए भी समान है, जैसे निम्नलिखित।

local ngx_re = require "ngx.re"

OpenResty में, init_by_lua और init_worker_by_lua दो चरणों को छोड़कर, सभी चरणों के लिए वैश्विक चरों का एक अलग टेबल सेट किया जाता है ताकि प्रसंस्करण के दौरान अन्य अनुरोधों को दूषित होने से बचाया जा सके। इन दो चरणों में भी जहां आप वैश्विक चर परिभाषित कर सकते हैं, आपको ऐसा करने से बचना चाहिए।

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

local _M = {} _M.color = { red = 1, blue = 2, green = 3 } return _M

मैंने hello.lua नामक एक फ़ाइल में एक मॉड्यूल परिभाषित किया है, जिसमें color टेबल शामिल है, और फिर मैंने nginx.conf में निम्नलिखित कॉन्फ़िगरेशन जोड़ा है।

location / { content_by_lua_block { local hello = require "hello" ngx.say(hello.color.green) } }

यह कॉन्फ़िगरेशन content चरण में मॉड्यूल को आवश्यक करेगा और HTTP प्रतिक्रिया बॉडी के रूप में green का मान प्रिंट करेगा।

आप सोच रहे होंगे कि मॉड्यूल चर इतना अद्भुत क्यों है?

मॉड्यूल को एक ही Worker प्रक्रिया में केवल एक बार लोड किया जाएगा; उसके बाद, Worker द्वारा संभाले गए सभी अनुरोध मॉड्यूल में डेटा को साझा करेंगे। हम कहते हैं कि "वैश्विक" डेटा मॉड्यूल में एनकैप्सुलेट करने के लिए उपयुक्त है क्योंकि OpenResty के Worker एक दूसरे से पूरी तरह से अलग होते हैं, इसलिए प्रत्येक Worker मॉड्यूल को स्वतंत्र रूप से लोड करता है, और मॉड्यूल का डेटा Worker के बीच साझा नहीं किया जा सकता है।

जहां तक Worker के बीच साझा किए जाने वाले डेटा को संभालने की बात है, मैं इसे बाद के एक अध्याय के लिए छोड़ दूंगा, ताकि आपको यहां इसे खोदने की आवश्यकता न हो।

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

उदाहरण के लिए, मॉड्यूल चर green का वर्तमान मान 3 है, और आप अपने कोड में एक plus 1 ऑपरेशन करते हैं, तो क्या green का मान अब 4 है? जरूरी नहीं; यह 4, 5, या 6 हो सकता है क्योंकि OpenResty मॉड्यूल चर लिखते समय लॉक नहीं करता है। फिर प्रतिस्पर्धा होती है, और मॉड्यूल चर का मान एक साथ कई अनुरोधों द्वारा अपडेट किया जाता है।

वैश्विक, स्थानीय, और मॉड्यूल चरों के बारे में कहने के बाद, आइए क्रॉस-चरण चरों पर चर्चा करें।

ऐसी स्थितियां होती हैं जहां हमें चरों की आवश्यकता होती है जो चरणों को पार कर सकते हैं और पढ़े और लिखे जा सकते हैं। $host, $scheme आदि जैसे चर, जो NGINX में हमें परिचित हैं, डायनामिक रूप से बनाए नहीं जा सकते हैं भले ही वे क्रॉस-चरण स्थिति को पूरा करते हों, और आपको उन्हें कॉन्फ़िगरेशन फ़ाइल में परिभाषित करना होगा ताकि उनका उपयोग किया जा सके। उदाहरण के लिए, यदि आप निम्नलिखित जैसा कुछ लिखते हैं।

location /foo { set $my_var ; # पहले $my_var चर बनाने की आवश्यकता है content_by_lua_block { ngx.var.my_var = 123 } }

OpenResty इस तरह की समस्या को हल करने के लिए ngx.ctx प्रदान करता है। यह एक Lua टेबल है जिसका उपयोग वर्तमान अनुरोध के साथ जीवनकाल वाले Lua डेटा को संग्रहीत करने के लिए किया जा सकता है। आइए आधिकारिक दस्तावेज़ीकरण से इस उदाहरण को देखें।

location /test { rewrite_by_lua_block { ngx.ctx.foo = 76 } access_by_lua_block { ngx.ctx.foo = ngx.ctx.foo + 3 } content_by_lua_block { ngx.say(ngx.ctx.foo) } }

आप देख सकते हैं कि हमने एक चर foo परिभाषित किया है जो ngx.ctx में संग्रहीत है। यह चर rewrite, access, और content चरणों को पार करता है और अंत में content चरण में मान प्रिंट करता है, जो हमारे अपेक्षित 79 है।

बेशक, ngx.ctx की अपनी सीमाएं हैं।

उदाहरण के लिए, ngx.location.capture के साथ बनाए गए चाइल्ड अनुरोधों का अपना अलग ngx.ctx डेटा होगा, जो पैरेंट अनुरोध के ngx.ctx से स्वतंत्र होगा।

फिर, ngx.exec के साथ बनाए गए आंतरिक रीडायरेक्ट मूल अनुरोध के ngx.ctx को नष्ट कर देते हैं और इसे एक खाली ngx.ctx के साथ पुनः उत्पन्न करते हैं।

इन दोनों सीमाओं के विस्तृत कोड उदाहरण आधिकारिक दस्तावेज़ीकरण में हैं, इसलिए यदि आप रुचि रखते हैं तो आप उन्हें स्वयं देख सकते हैं।

सारांश

अंत में, मैं कुछ और शब्द कहूंगा। हम OpenResty के सिद्धांतों और कुछ महत्वपूर्ण अवधारणाओं को सीख रहे हैं, लेकिन आपको उन्हें याद रखने की आवश्यकता नहीं है। आखिरकार, वे हमेशा समझ में आते हैं और वास्तविक आवश्यकताओं और कोड के साथ जीवंत हो जाते हैं।

मुझे आश्चर्य है कि आप इसे कैसे समझते हैं? टिप्पणी करने और मुझसे चर्चा करने के लिए स्वागत है, और इस लेख को अपने सहयोगियों और दोस्तों के साथ साझा करने के लिए भी स्वागत है। हम एक साथ संवाद करते हैं, एक साथ प्रगति करते हैं।