`lua-resty-*` एनकैप्सुलेशन डेवलपर्स को मल्टी-लेवल कैशिंग से मुक्त करता है

API7.ai

December 30, 2022

OpenResty (NGINX + Lua)

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

यह श्रम विभाजन का एक लाभ है, बुनियादी घटक डेवलपर्स लचीली आर्किटेक्चर, अच्छे प्रदर्शन और कोड स्थिरता पर ध्यान केंद्रित करते हैं, बिना ऊपरी व्यावसायिक तर्क की परवाह किए; जबकि एप्लिकेशन इंजीनियर व्यावसायिक कार्यान्वयन और तेजी से पुनरावृत्ति पर अधिक ध्यान देते हैं, और यह आशा करते हैं कि वे निचले स्तर के विभिन्न तकनीकी विवरणों से विचलित न हों। इनके बीच की खाई को रैपर लाइब्रेरीज़ द्वारा भरा जा सकता है।

OpenResty में कैशिंग भी इसी समस्या का सामना करती है। shared dict और lru caches पर्याप्त स्थिर और कुशल हैं, लेकिन इनके साथ कई विवरणों को संभालना पड़ता है। एप्लिकेशन विकास इंजीनियर्स के लिए "अंतिम मील" कुछ उपयोगी एनकैप्सुलेशन के बिना कठिन हो सकती है। यहीं पर समुदाय का महत्व सामने आता है। एक सक्रिय समुदाय खुद से खाई को ढूंढेगा और उसे जल्दी से भरेगा।

lua-resty-memcached-shdict

चलिए कैश एनकैप्सुलेशन पर वापस आते हैं। lua-resty-memcached-shdict एक आधिकारिक OpenResty प्रोजेक्ट है जो shared dict का उपयोग करके memcached के लिए एक परत एनकैप्सुलेशन बनाता है, जो कैश स्टैम्पीड और समाप्त डेटा जैसे विवरणों को संभालता है। यदि आपका कैश्ड डेटा बैकएंड में memcached में संग्रहीत है, तो आप इस लाइब्रेरी का उपयोग कर सकते हैं।

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

यह एनकैप्सुलेशन लाइब्रेरी वही समाधान है जिसका हमने पिछले लेख में उल्लेख किया था। यह lua-resty-lock का उपयोग करके पारस्परिक रूप से अनन्य बनाता है। कैश विफल होने की स्थिति में, केवल एक अनुरोध memcached से डेटा प्राप्त करता है और कैश स्टॉर्म से बचता है। यदि नवीनतम डेटा प्राप्त नहीं होता है, तो पुराना डेटा एंडपॉइंट को वापस कर दिया जाता है।

हालांकि, यह lua-resty लाइब्रेरी, हालांकि एक आधिकारिक OpenResty प्रोजेक्ट है, परिपूर्ण नहीं है:

  1. पहले, इसमें कोई टेस्ट केस कवरेज नहीं है, जिसका अर्थ है कि कोड की गुणवत्ता को लगातार सुनिश्चित नहीं किया जा सकता है।
  2. दूसरा, यह बहुत अधिक इंटरफ़ेस पैरामीटर्स को उजागर करता है, जिसमें 11 आवश्यक और 7 वैकल्पिक पैरामीटर्स हैं।
local memc_fetch, memc_store = shdict_memc.gen_memc_methods{ tag = "my memcached server tag", debug_logger = dlog, warn_logger = warn, error_logger = error_log, locks_shdict_name = "some_lua_shared_dict_name", shdict_set = meta_shdict_set, shdict_get = meta_shdict_get, disable_shdict = false, -- optional, default false memc_host = "127.0.0.1", memc_port = 11211, memc_timeout = 200, -- in ms memc_conn_pool_size = 5, memc_fetch_retries = 2, -- optional, default 1 memc_fetch_retry_delay = 100, -- in ms, optional, default to 100 (ms) memc_conn_max_idle_time = 10 * 1000, -- in ms, for in-pool connections,optional, default to nil memc_store_retries = 2, -- optional, default to 1 memc_store_retry_delay = 100, -- in ms, optional, default to 100 (ms) store_ttl = 1, -- in seconds, optional, default to 0 (i.e., never expires) }

ज्यादातर पैरामीटर्स को "एक नया memcached हैंडलर बनाकर" सरल किया जा सकता है। वर्तमान में सभी पैरामीटर्स को उपयोगकर्ता पर फेंकने का तरीका उपयोगकर्ता-अनुकूल नहीं है, इसलिए मैं इच्छुक डेवलपर्स को इसका अनुकूलन करने के लिए PR योगदान करने के लिए आमंत्रित करता हूं।

इसके अलावा, इस एनकैप्सुलेशन लाइब्रेरी के दस्तावेज़ में निम्नलिखित दिशाओं में और अनुकूलन का उल्लेख किया गया है।

  1. lua-resty-lrucache का उपयोग करके Worker-स्तरीय कैश बढ़ाएं, न कि केवल server-स्तरीय shared dict कैश।
  2. ngx.timer का उपयोग करके एसिंक्रोनस कैश अपडेट ऑपरेशन करें।

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

lua-resty-mlcache

अगले, हम OpenResty में आमतौर पर उपयोग की जाने वाली एक कैशिंग एनकैप्सुलेशन का परिचय देते हैं: lua-resty-mlcache, जो shared dict और lua-resty-lrucache का उपयोग करके एक मल्टी-लेयर कैशिंग मैकेनिज्म को लागू करता है। आइए निम्नलिखित दो कोड उदाहरणों में देखें कि यह लाइब्रेरी कैसे उपयोग की जाती है।

local mlcache = require "resty.mlcache" local cache, err = mlcache.new("cache_name", "cache_dict", { lru_size = 500, -- size of the L1 (Lua VM) cache ttl = 3600, -- 1h ttl for hits neg_ttl = 30, -- 30s ttl for misses }) if not cache then error("failed to create mlcache: " .. err) end

आइए पहले कोड को देखें। इस कोड की शुरुआत में mlcache लाइब्रेरी को शामिल किया गया है और इनिशियलाइज़ेशन के लिए पैरामीटर्स सेट किए गए हैं। हम आमतौर पर इस कोड को init चरण में रखते हैं और इसे केवल एक बार करने की आवश्यकता होती है।

दो आवश्यक पैरामीटर्स, कैश नाम और डिक्शनरी नाम के अलावा, तीसरा पैरामीटर एक डिक्शनरी है जिसमें 12 विकल्प हैं जो वैकल्पिक हैं और यदि भरे नहीं जाते हैं तो डिफ़ॉल्ट मानों का उपयोग करते हैं। यह lua-resty-memcached-shdict की तुलना में बहुत अधिक सुंदर है। यदि हमें इंटरफ़ेस डिज़ाइन करना होता, तो यह बेहतर होता कि हम mlcache के दृष्टिकोण को अपनाएं - इंटरफ़ेस को जितना संभव हो सरल रखें, जबकि पर्याप्त लचीलापन बनाए रखें।

यहां दूसरा कोड है, जो अनुरोध प्रसंस्करण के दौरान का तार्किक कोड है।

local function fetch_user(id) return db:query_user(id) end local id = 123 local user , err = cache:get(id , nil , fetch_user , id) if err then ngx.log(ngx.ERR , "failed to fetch user: ", err) return end if user then print(user.id) -- 123 end

जैसा कि आप देख सकते हैं, मल्टी-लेयर कैश छिपा हुआ है, इसलिए आपको कैश प्राप्त करने के लिए mlcache ऑब्जेक्ट का उपयोग करना होगा और कैश समाप्त होने पर कॉलबैक फ़ंक्शन सेट करना होगा। इसके पीछे की जटिल तर्क को पूरी तरह से छिपाया जा सकता है।

आपको यह जानने की उत्सुकता हो सकती है कि यह लाइब्रेरी आंतरिक रूप से कैसे लागू की गई है। आइए अब इस लाइब्रेरी की आर्किटेक्चर और कार्यान्वयन पर एक और नज़र डालें। निम्नलिखित छवि OpenResty Con 2018 में mlcache के लेखक Thibault Charbonnier द्वारा दिए गए एक टॉक का स्लाइड है।

mlcache architecture

जैसा कि आप डायग्राम से देख सकते हैं, mlcache डेटा को तीन परतों में विभाजित करता है, जिन्हें L1, L2 और L3 कहा जाता है।

L1 कैश lua-resty-lrucache है, जहां प्रत्येक Worker का अपना कॉपी होता है, और N Workers के साथ, डेटा के N कॉपी होते हैं, इसलिए डेटा रिडंडेंसी होती है। चूंकि एकल Worker के भीतर lrucache को संचालित करने से लॉक ट्रिगर नहीं होते हैं, इसलिए इसका प्रदर्शन अधिक होता है और यह प्रथम-स्तरीय कैश के रूप में उपयुक्त है।

L2 कैश एक shared dict है। सभी Workers कैश्ड डेटा की एक ही कॉपी साझा करते हैं और यदि L1 कैश हिट नहीं होता है, तो L2 कैश को क्वेरी किया जाएगा। ngx.shared.DICT एक API प्रदान करता है जो स्पिनलॉक का उपयोग करके ऑपरेशन की परमाणुता सुनिश्चित करता है, इसलिए हमें यहां रेस कंडीशन की चिंता करने की आवश्यकता नहीं है।

L3 वह स्थिति है जब L2 कैश भी हिट नहीं होता है, और कॉलबैक फ़ंक्शन को निष्पादित करने की आवश्यकता होती है ताकि डेटा स्रोत, जैसे कि एक बाहरी डेटाबेस, से डेटा क्वेरी किया जा सके और फिर इसे L2 में कैश किया जा सके। यहां, कैश स्टॉर्म से बचने के लिए, यह lua-resty-lock का उपयोग करता है ताकि केवल एक Worker डेटा स्रोत से डेटा प्राप्त करे।

एक अनुरोध के दृष्टिकोण से:

  • पहले, यह Worker के भीतर L1 कैश को क्वेरी करेगा और यदि L1 हिट होता है, तो सीधे वापस कर देगा।
  • यदि L1 हिट नहीं होता है या कैश विफल हो जाता है, तो यह Workers के बीच L2 कैश को क्वेरी करेगा। यदि L2 हिट होता है, तो यह वापस कर देगा और परिणाम को L1 में कैश करेगा।
  • यदि L2 भी मिस हो जाता है या कैश अमान्य हो जाता है, तो एक कॉलबैक फ़ंक्शन को कॉल किया जाएगा ताकि डेटा स्रोत से डेटा खोजा जा सके और इसे L2 कैश में लिखा जा सके, जो L3 डेटा लेयर का कार्य है।

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

हालांकि, mlcache को परिपूर्ण रूप से लागू किया गया है, फिर भी एक दर्द बिंदु है - डेटा का सीरियलाइज़ेशन और डीसीरियलाइज़ेशन। यह mlcache की समस्या नहीं है, बल्कि lrucache और shared dict के बीच का अंतर है, जिसका हमने बार-बार उल्लेख किया है। lrucache में, हम विभिन्न Lua डेटा प्रकारों को संग्रहीत कर सकते हैं, जिसमें table शामिल है; लेकिन shared dict में, हम केवल स्ट्रिंग्स को संग्रहीत कर सकते हैं।

L1, lrucache कैश, उस डेटा की परत है जिसे उपयोगकर्ता छूता है, और हम इसमें विभिन्न प्रकार के डेटा को कैश करना चाहते हैं, जिसमें string, table, cdata, आदि शामिल हैं। समस्या यह है कि L2 केवल स्ट्रिंग्स को संग्रहीत कर सकता है, और जब डेटा L2 से L1 तक बढ़ाया जाता है, तो हमें स्ट्रिंग्स से उन डेटा प्रकारों में एक परत रूपांतरण करने की आवश्यकता होती है जिन्हें हम सीधे उपयोगकर्ता को दे सकते हैं।

सौभाग्य से, mlcache ने इस स्थिति को ध्यान में रखा है और new और get इंटरफ़ेस में वैकल्पिक फ़ंक्शन l1_serializer प्रदान किया है, जो विशेष रूप से L2 से L1 तक डेटा प्रसंस्करण को संभालने के लिए डिज़ाइन किया गया है। हम निम्नलिखित नमूना कोड देख सकते हैं, जिसे मैंने अपने टेस्ट केस सेट से निकाला है।

local mlcache = require "resty.mlcache" local cache, err = mlcache.new("my_mlcache", "cache_shm", { l1_serializer = function(i) return i + 2 end, }) local function callback() return 123456 end local data = assert(cache:get("number", nil, callback)) assert(data == 123458)

मुझे इसे जल्दी से समझाएं। इस मामले में, कॉलबैक फ़ंक्शन संख्या 123456 वापस करता है; new में, हमने जो l1_serializer फ़ंक्शन सेट किया है, वह आने वाली संख्या में 2 जोड़ देगा, जो L1 कैश सेट करने से पहले 123458 बन जाता है। ऐसे सीरियलाइज़ेशन फ़ंक्शन के साथ, डेटा L1 और L2 के बीच रूपांतरण करते समय अधिक लचीला हो सकता है।

सारांश

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

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