NGINX वर्कर्स के बीच संचार का जादू: सबसे महत्वपूर्ण डेटा संरचनाओं में से एक `shared dict`

API7.ai

October 27, 2022

OpenResty (NGINX + Lua)

जैसा कि हमने पिछले लेख में कहा था, Lua में table एकमात्र डेटा संरचना है। यह shared dict से मेल खाता है, जो OpenResty प्रोग्रामिंग में उपयोग की जाने वाली सबसे महत्वपूर्ण डेटा संरचना है। यह डेटा स्टोरेज, पठन, परमाणु गिनती, और कतार संचालन का समर्थन करता है।

shared dict के आधार पर, आप कैशिंग और कई Worker के बीच संचार, दर सीमित करने, ट्रैफ़िक सांख्यिकी, और अन्य कार्यों को लागू कर सकते हैं। आप shared dict को एक सरल Redis के रूप में उपयोग कर सकते हैं, सिवाय इसके कि shared dict में डेटा स्थायी नहीं होता है, इसलिए आपको संग्रहीत डेटा के नुकसान पर विचार करना चाहिए।

डेटा साझा करने के कई तरीके

OpenResty Lua कोड लिखते समय, आप अनिवार्य रूप से अनुरोध के विभिन्न चरणों में विभिन्न Worker के बीच डेटा साझा करने की स्थिति में आएंगे। आपको Lua और C कोड के बीच डेटा साझा करने की भी आवश्यकता हो सकती है।

इसलिए, shared dict API को औपचारिक रूप से पेश करने से पहले, आइए पहले OpenResty में सामान्य डेटा साझा करने के तरीकों को समझें और वर्तमान स्थिति के अनुसार अधिक उपयुक्त डेटा साझा करने का तरीका चुनना सीखें।

पहला है NGINX में वेरिएबल। यह NGINX C मॉड्यूल के बीच डेटा साझा कर सकता है। स्वाभाविक रूप से, यह C मॉड्यूल और OpenResty द्वारा प्रदान किए गए lua-nginx-module के बीच भी डेटा साझा कर सकता है, जैसा कि निम्नलिखित कोड में है।

location /foo { set $my_var ''; # यह लाइन $my_var को कॉन्फ़िग समय पर बनाने के लिए आवश्यक है content_by_lua_block { ngx.var.my_var = 123; ... } }

हालांकि, NGINX वेरिएबल का उपयोग करके डेटा साझा करना धीमा है क्योंकि इसमें हैश लुकअप और मेमोरी आवंटन शामिल है। इसके अलावा, इस तरीके की सीमा यह है कि इसे केवल स्ट्रिंग्स को स्टोर करने के लिए उपयोग किया जा सकता है और यह जटिल Lua प्रकारों का समर्थन नहीं कर सकता है।

दूसरा है ngx.ctx, जो एक ही अनुरोध के विभिन्न चरणों के बीच डेटा साझा कर सकता है। यह एक सामान्य Lua table है, इसलिए यह तेज़ है और विभिन्न Lua ऑब्जेक्ट्स को स्टोर कर सकता है। इसका जीवनकाल अनुरोध-स्तर का है; जब अनुरोध समाप्त होता है, तो ngx.ctx नष्ट हो जाता है।

निम्नलिखित एक विशिष्ट उपयोग परिदृश्य है जहां हम ngx.ctx का उपयोग करते हैं जैसे कि NGINX वेरिएबल और इसे विभिन्न चरणों में उपयोग करते हैं।

location /test { rewrite_by_lua_block { ngx.ctx.host = ngx.var.host } access_by_lua_block { if (ngx.ctx.host == 'api7.ai') then ngx.ctx.host = 'test.com' end } content_by_lua_block { ngx.say(ngx.ctx.host) } }

इस मामले में, यदि आप curl का उपयोग करके इसे एक्सेस करते हैं।

curl -i 127.0.0.1:8080/test -H 'host:api7.ai'

तो यह test.com प्रिंट करेगा, यह दिखाते हुए कि ngx.ctx विभिन्न चरणों में डेटा साझा कर रहा है। बेशक, आप उपरोक्त उदाहरण को संशोधित करके सरल स्ट्रिंग्स के बजाय table जैसे अधिक जटिल ऑब्जेक्ट्स को सहेज सकते हैं और देख सकते हैं कि यह आपकी अपेक्षाओं को पूरा करता है या नहीं।

हालांकि, यहां एक विशेष नोट यह है कि क्योंकि ngx.ctx का जीवनकाल अनुरोध-स्तर का है, यह मॉड्यूल स्तर पर कैश नहीं करता है। उदाहरण के लिए, मैंने अपनी foo.lua फ़ाइल में इसका उपयोग करने की गलती की।

local ngx_ctx = ngx.ctx local function bar() ngx_ctx.host = 'test.com' end

हमें फ़ंक्शन-स्तर पर कॉल और कैश करना चाहिए।

local ngx = ngx local function bar() ngx_ctx.host = 'test.com' end

ngx.ctx के बारे में और भी कई विवरण हैं, जिन्हें हम बाद में प्रदर्शन अनुकूलन अनुभाग में जारी रखेंगे।

तीसरा तरीका मॉड्यूल-स्तरीय वेरिएबल का उपयोग करके एक ही Worker के भीतर सभी अनुरोधों के बीच डेटा साझा करना है। पिछले NGINX वेरिएबल और ngx.ctx के विपरीत, यह तरीका थोड़ा कम समझ में आता है। लेकिन चिंता न करें, अवधारणा अमूर्त है, और कोड पहले आता है, इसलिए आइए एक उदाहरण देखकर मॉड्यूल-स्तरीय वेरिएबल को समझें।

-- mydata.lua local _M = {} local data = { dog = 3, cat = 4, pig = 5, } function _M.get_age(name) return data[name] end return _M

nginx.conf में कॉन्फ़िगरेशन निम्नलिखित है।

location /lua { content_by_lua_block { local mydata = require "mydata" ngx.say(mydata.get_age("dog")) } }

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

स्वाभाविक रूप से, mydata मॉड्यूल में data वेरिएबल एक मॉड्यूल-स्तरीय वेरिएबल है जो मॉड्यूल के शीर्ष स्तर पर स्थित है, यानी मॉड्यूल की शुरुआत में, और सभी फ़ंक्शन्स के लिए सुलभ है।

इसलिए, आप अनुरोधों के बीच साझा किए जाने वाले डेटा को मॉड्यूल के शीर्ष-स्तरीय वेरिएबल में रख सकते हैं। हालांकि, यह ध्यान रखना आवश्यक है कि हम आमतौर पर इस तरीके का उपयोग केवल पठनीय डेटा को स्टोर करने के लिए करते हैं। यदि लेखन संचालन शामिल हैं, तो आपको बहुत सावधान रहना चाहिए क्योंकि race condition हो सकता है, जो एक पेचीदा बग है जिसे ढूंढना मुश्किल होता है।

हम निम्नलिखित सरल उदाहरण के साथ इसे अनुभव कर सकते हैं।

-- mydata.lua local _M = {} local data = { dog = 3, cat = 4, pig = 5, } function _M.incr_age(name) data[name] = data[name] + 1 return data[name] end return _M

मॉड्यूल में, हम incr_age फ़ंक्शन जोड़ते हैं, जो data टेबल में डेटा को संशोधित करता है।

फिर, कॉलिंग कोड में, हम सबसे महत्वपूर्ण लाइन ngx.sleep(5) जोड़ते हैं, जहां sleep एक yield ऑपरेशन है।

location /lua { content_by_lua_block { local mydata = require "mydata" ngx.say(mydata. incr_age("dog")) ngx.sleep(5) -- yield API ngx.say(mydata. incr_age("dog")) } }

इस sleep कोड (या अन्य नॉन-ब्लॉकिंग IO ऑपरेशन, जैसे Redis एक्सेस, आदि) के बिना, कोई yield ऑपरेशन नहीं होगा, कोई प्रतिस्पर्धा नहीं होगी, और अंतिम आउटपुट अनुक्रमिक होगा।

लेकिन जब हम इस कोड को जोड़ते हैं, तो भले ही यह केवल 5 सेकंड की नींद के भीतर हो, एक अन्य अनुरोध संभवतः mydata.incr_age फ़ंक्शन को कॉल करेगा और वेरिएबल के मान को संशोधित करेगा, इस प्रकार अंतिम आउटपुट संख्याएं असंतत हो जाएंगी। वास्तविक कोड में तर्क इतना सरल नहीं है, और बग को ढूंढना बहुत कठिन होता है।

इसलिए, जब तक आप सुनिश्चित नहीं हैं कि बीच में कोई yield ऑपरेशन नहीं होगा जो NGINX इवेंट लूप को नियंत्रण देगा, मैं अनुशंसा करता हूं कि आप अपने मॉड्यूल-स्तरीय वेरिएबल को केवल पठनीय रखें।

चौथा और अंतिम तरीका shared dict का उपयोग करके डेटा साझा करना है जो कई workers के बीच साझा किया जा सकता है।

यह तरीका एक रेड-ब्लैक ट्री कार्यान्वयन पर आधारित है, जो अच्छा प्रदर्शन करता है। फिर भी, इसकी सीमाएं हैं: आपको NGINX कॉन्फ़िगरेशन फ़ाइल में शेयर्ड मेमोरी का आकार पहले से घोषित करना होगा, और इसे रनटाइम में बदला नहीं जा सकता है:

lua_shared_dict dogs 10m;

shared dict केवल string डेटा को कैश करता है और जटिल Lua डेटा प्रकारों का समर्थन नहीं करता है। इसका मतलब है कि जब मुझे table जैसे जटिल डेटा प्रकारों को स्टोर करने की आवश्यकता होती है, तो मुझे JSON या अन्य तरीकों का उपयोग करके उन्हें सीरियलाइज़ और डीसीरियलाइज़ करना होगा, जो स्वाभाविक रूप से बहुत अधिक प्रदर्शन हानि का कारण बनेगा।

वैसे भी, यहां कोई सिल्वर बुलेट नहीं है, और डेटा साझा करने का कोई सही तरीका नहीं है। आपको अपनी आवश्यकताओं और परिदृश्यों के अनुसार कई तरीकों को संयोजित करना होगा।

Shared dict

हमने ऊपर डेटा साझा करने के बारे में बहुत समय बिताया है, और आप में से कुछ लोग सोच रहे होंगे: ऐसा लगता है कि वे सीधे shared dict से संबंधित नहीं हैं। क्या यह विषय से हटकर नहीं है?

वास्तव में, नहीं। कृपया इस बारे में सोचें: OpenResty में shared dict क्यों है? याद रखें कि डेटा साझा करने के पहले तीन तरीके सभी अनुरोध स्तर या व्यक्तिगत Worker स्तर पर हैं। इसलिए, OpenResty के वर्तमान कार्यान्वयन में, केवल shared dict ही Worker के बीच डेटा साझा कर सकता है, जो Worker के बीच संचार को सक्षम करता है, और यही इसके अस्तित्व का मूल्य है।

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

Shared dict पर वापस आते हुए, जो 20 से अधिक Lua API को सार्वजनिक करता है, सभी परमाणु हैं, इसलिए आपको कई Workers और उच्च समवर्तीता के मामले में प्रतिस्पर्धा की चिंता करने की आवश्यकता नहीं है।

इन API के सभी के लिए विस्तृत आधिकारिक दस्तावेज़ हैं, इसलिए मैं उन सभी पर विस्तार से नहीं जाऊंगा। मैं फिर से जोर देना चाहता हूं कि कोई भी तकनीकी पाठ्यक्रम आधिकारिक दस्तावेज़ को ध्यान से पढ़ने की जगह नहीं ले सकता है। कोई भी इन समय लेने वाले और मूर्खतापूर्ण प्रक्रियाओं को छोड़ नहीं सकता है।

आगे, आइए shared dict API को देखना जारी रखें, जिन्हें तीन श्रेणियों में विभाजित किया जा सकता है: dict पठन/लेखन, कतार संचालन, और प्रबंधन।

Dict पठन/लेखन

आइए पहले dict पठन और लेखन वर्ग को देखें। मूल संस्करण में, केवल dict पठन और लेखन वर्ग के API थे, जो साझा शब्दकोशों की सबसे सामान्य विशेषताएं हैं। यहां सबसे सरल उदाहरण है।

$ resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogs dict:set("Tom", 56) print(dict:get("Tom"))'

set के अलावा, OpenResty चार लेखन विधियां भी प्रदान करता है: safe_set, add, safe_add, और replace। यहां safe उपसर्ग का अर्थ है कि यदि मेमोरी भर गई है, तो LRU के अनुसार पुराने डेटा को हटाने के बजाय, लेखन विफल हो जाएगा और no memory त्रुटि लौटाएगा।

get के अलावा, OpenResty get_stale विधि भी प्रदान करता है, जो get विधि की तुलना में समाप्त हो चुके डेटा के लिए एक अतिरिक्त रिटर्न मान प्रदान करता है।

value, flags, stale = ngx.shared.DICT:get_stale(key)

आप delete विधि को कॉल करके निर्दिष्ट कुंजी को हटा सकते हैं, जो set(key, nil) के बराबर है।

कतार संचालन

कतार संचालन की ओर मुड़ते हुए, यह OpenResty में बाद में जोड़ा गया है और Redis के समान इंटरफ़ेस प्रदान करता है। कतार में प्रत्येक तत्व को ngx_http_lua_shdict_list_node_t द्वारा वर्णित किया जाता है।

typedef struct { ngx_queue_t queue; uint32_t value_len; uint8_t value_type; u_char data[1]; } ngx_http_lua_shdict_list_node_t;

मैंने इन कतारिंग API के PR को लेख में पोस्ट किया है। यदि आप इसके बारे में रुचि रखते हैं, तो आप दस्तावेज़, परीक्षण मामलों, और स्रोत कोड का अनुसरण करके विशिष्ट कार्यान्वयन का विश्लेषण कर सकते हैं।

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

  • lpush``/``rpush का अर्थ है कतार के दोनों छोर पर तत्व जोड़ना।
  • lpop``/``rpop, जो कतार के दोनों छोर से तत्व निकालता है।
  • llen, जो कतार में तत्वों की संख्या लौटाता है।

आइए हम पिछले लेख में चर्चा किए गए एक और उपयोगी उपकरण को न भूलें: परीक्षण मामले। यदि दस्तावेज़ में नहीं है, तो हम आमतौर पर परीक्षण मामले में संबंधित कोड पा सकते हैं। कतार-संबंधित परीक्षण 145-shdict-list.t फ़ाइल में हैं।

=== TEST 1: lpush & lpop --- http_config lua_shared_dict dogs 1m; --- config location = /test { content_by_lua_block { local dogs = ngx.shared.dogs local len, err = dogs:lpush("foo", "bar") if len then ngx.say("push success") else ngx.say("push err: ", err) end local val, err = dogs:llen("foo") ngx.say(val, " ", err) local val, err = dogs:lpop("foo") ngx.say(val, " ", err) local val, err = dogs:llen("foo") ngx.say(val, " ", err) local val, err = dogs:lpop("foo") ngx.say(val, " ", err) } } --- request GET /test --- response_body push success 1 nil bar nil 0 nil nil nil --- no_error_log [error]

प्रबंधन

अंतिम प्रबंधन API भी बाद में जोड़ा गया है और समुदाय में एक लोकप्रिय आवश्यकता है। सबसे विशिष्ट उदाहरणों में से एक शेयर्ड मेमोरी का उपयोग है। उदाहरण के लिए, यदि एक उपयोगकर्ता shared dict के रूप में 100M स्थान का अनुरोध करता है, तो क्या यह 100M पर्याप्त है? इसमें कितनी कुंजियाँ संग्रहीत हैं, और वे कौन सी हैं? ये सभी प्रामाणिक प्रश्न हैं।

इस प्रकार की समस्या के लिए, OpenResty आधिकारिक रूप से उपयोगकर्ताओं को फ्लेम ग्राफ का उपयोग करके इसे हल करने की उम्मीद करता है, यानी एक गैर-आक्रामक तरीके से, कोडबेस को कुशल और साफ-सुथरा रखते हुए, बजाय एक आक्रामक API प्रदान करके सीधे परिणाम लौटाने के।

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

पहला है get_keys(max_count?), जो डिफ़ॉल्ट रूप से केवल पहले 1024 कुंजियाँ लौटाता है; यदि आप max_count को 0 पर सेट करते हैं, तो यह सभी कुंजियाँ लौटाएगा। फिर capacity और free_space आते हैं, जो दोनों lua-resty-core रिपॉजिटरी का हिस्सा हैं, इसलिए आपको उनका उपयोग करने से पहले require करना होगा।

require "resty.core.shdict" local cats = ngx.shared.cats local capacity_bytes = cats:capacity() local free_page_bytes = cats:free_space()

वे शेयर्ड मेमोरी का आकार (lua_shared_dict में कॉन्फ़िगर किया गया आकार) और मुक्त पृष्ठों के बाइट्स की संख्या लौटाते हैं। चूंकि shared dict पृष्ठ द्वारा आवंटित किया जाता है, भले ही free_space 0 लौटाए, आवंटित पृष्ठों में स्थान हो सकता है। इसलिए, इसका रिटर्न मान यह नहीं दर्शाता है कि शेयर्ड मेमोरी कितना उपयोग किया गया है।

सारांश

व्यवहार में, हम अक्सर मल्टी-लेवल कैशिंग का उपयोग करते हैं, और आधिकारिक OpenResty प्रोजेक्ट में भी एक कैशिंग पैकेज है। क्या आप पता लगा सकते हैं कि वे कौन से प्रोजेक्ट हैं? या क्या आप कुछ अन्य lua-resty लाइब्रेरीज जानते हैं जो कैशिंग को एनकैप्सुलेट करती हैं?

आप इस लेख को अपने सहयोगियों और दोस्तों के साथ साझा करने के लिए स्वागत कर रहे हैं ताकि हम एक साथ संवाद और सुधार कर सकें।