कैश स्टैम्पीड से कैसे बचें?
API7.ai
December 29, 2022
पिछले लेख में, हमने shared dict और lru cache के साथ कुछ उच्च-प्रदर्शन अनुकूलन तकनीकों के बारे में सीखा। हालांकि, हमने एक महत्वपूर्ण मुद्दे को छोड़ दिया था जो आज के लेख का विषय है, "Cache Stampede"।
Cache Stampede क्या है?
आइए एक परिदृश्य की कल्पना करें।
डेटा स्रोत एक MySQL डेटाबेस में है, कैश किया गया डेटा shared dict में है, और टाइमआउट 60 सेकंड है। कैश में डेटा के 60 सेकंड के दौरान, सभी अनुरोध कैश से डेटा प्राप्त कर रहे हैं न कि MySQL से। लेकिन 60 सेकंड के बाद, कैश किया गया डेटा समाप्त हो जाता है। यदि बड़ी संख्या में समवर्ती अनुरोध होते हैं, तो कैश में कोई डेटा नहीं मिल सकता है। फिर डेटा स्रोत की क्वेरी फ़ंक्शन ट्रिगर होगी, और ये सभी अनुरोध MySQL डेटाबेस पर जाएंगे, जो सीधे डेटाबेस सर्वर को ब्लॉक कर देगा या यहां तक कि क्रैश कर देगा।
इस घटना को "Cache Stampede" कहा जा सकता है, और इसे कभी-कभी Dog-Piling के रूप में भी जाना जाता है। पिछले खंडों में दिखाई देने वाले कैश-संबंधित कोड में से किसी में भी इसका उपचार नहीं है। निम्नलिखित एक ऐसे छद्म-कोड का उदाहरण है जिसमें कैश स्टैम्पीड की संभावना है।
local value = get_from_cache(key) if not value then value = query_db(sql) set_to_cache(value, timeout = 60) end return value
छद्म-कोड में लगता है कि तर्क सही है, और यूनिट टेस्ट या एंड-टू-एंड टेस्ट का उपयोग करके आप कैश स्टैम्पीड को ट्रिगर नहीं करेंगे। केवल एक लंबा स्ट्रेस टेस्ट ही समस्या का पता लगाएगा। हर 60 सेकंड में, डेटाबेस में क्वेरीज़ में एक नियमित स्पाइक होगा। लेकिन यदि आप यहां कैश समाप्ति समय को लंबा सेट करते हैं, तो कैश स्टॉर्म समस्या के पता चलने की संभावना कम हो जाती है।
इसे कैसे टालें?
आइए चर्चा को कई अलग-अलग मामलों में विभाजित करें।
1. सक्रिय रूप से कैश को अपडेट करें
उपरोक्त छद्म-कोड में, कैश को निष्क्रिय रूप से अपडेट किया जाता है और केवल तब डेटाबेस पर नया डेटा क्वेरी करने जाता है जब अनुरोध किया जाता है लेकिन कैश विफलता पाई जाती है। इसलिए, कैश को अपडेट करने के तरीके को निष्क्रिय से सक्रिय में बदलने से कैश स्टैम्पीड समस्या को टाला जा सकता है।
OpenResty में, हम इसे इस तरह से लागू कर सकते हैं।
पहले, हम ngx.timer.every का उपयोग करके एक टाइमर टास्क बनाते हैं जो हर मिनट चलता है और MySQL डेटाबेस से नवीनतम डेटा प्राप्त करता है और इसे shared dict में डालता है:
local function query_db(premature, sql) local value = query_db(sql) set_to_cache(value, timeout = 60) end local ok, err = ngx.timer.every(60, query_db, sql)
फिर, अनुरोध को संभालने वाले कोड के तर्क में, हमें MySQL को क्वेरी करने वाले हिस्से को हटाना होगा और केवल shared dict कैश प्राप्त करने वाले कोड को रखना होगा।
local value = get_from_cache(key) return value
उपरोक्त दो छद्म-कोड स्निपेट हमें कैश स्टैम्पीड समस्या को टालने में मदद कर सकते हैं। लेकिन यह दृष्टिकोण पूर्ण नहीं है, प्रत्येक कैश को एक आवधिक कार्य के साथ मेल खाना होगा (OpenResty में टाइमर की संख्या की एक ऊपरी सीमा है), और कैश समाप्ति समय और निर्धारित कार्य के चक्र समय को अच्छी तरह से मेल खाना होगा। यदि इस दौरान कोई गलती होती है, तो अनुरोध खाली डेटा प्राप्त करता रह सकता है।
इसलिए, वास्तविक परियोजनाओं में, हम आमतौर पर कैश स्टैम्पीड समस्या को हल करने के लिए लॉकिंग का उपयोग करते हैं। यहां कुछ अलग-अलग लॉकिंग तरीके दिए गए हैं, आप अपनी आवश्यकताओं के अनुसार अपना चुन सकते हैं।
2. lua-resty-lock
जब लॉक जोड़ने की बात आती है, तो आपको मुश्किल लग सकता है, यह सोचकर कि यह एक भारी ऑपरेशन है, और यदि डेडलॉक हो जाए तो आपको कई अपवादों से निपटना पड़ सकता है।
हम OpenResty में lua-resty-lock लाइब्रेरी का उपयोग करके इस चिंता को कम कर सकते हैं। lua-resty-lock OpenResty की resty लाइब्रेरी है, जो shared dict पर आधारित है और एक नॉन-ब्लॉकिंग लॉक API प्रदान करती है। आइए एक सरल उदाहरण देखें।
resty --shdict='locks 1m' -e 'local resty_lock = require "resty.lock" local lock, err = resty_lock:new("locks") local elapsed, err = lock:lock("my_key") -- query db and update cache local ok, err = lock:unlock() ngx.say("unlock: ", ok)'
चूंकि lua-resty-lock shared dict का उपयोग करके लागू किया गया है, हमें पहले shdict का नाम और आकार घोषित करना होगा और फिर new मेथड का उपयोग करके एक नया lock ऑब्जेक्ट बनाना होगा। उपरोक्त कोड स्निपेट में, हम केवल पहला पैरामीटर, shdict का नाम पास करते हैं। new मेथड में एक दूसरा पैरामीटर होता है, जिसका उपयोग समाप्ति समय, लॉक के लिए टाइमआउट समय और कई अन्य पैरामीटर्स को निर्दिष्ट करने के लिए किया जा सकता है। यहां हम डिफ़ॉल्ट मान रखते हैं। ये पैरामीटर्स डेडलॉक और अन्य अपवादों से बचने के लिए उपयोग किए जाते हैं।
फिर हम lock मेथड को कॉल करके लॉक प्राप्त करने का प्रयास कर सकते हैं। यदि हम लॉक प्राप्त करने में सफल होते हैं, तो हम यह सुनिश्चित कर सकते हैं कि एक ही समय में केवल एक अनुरोध डेटा स्रोत पर डेटा अपडेट करने जाएगा। लेकिन यदि लॉक प्राप्त करने में विफल होता है, जैसे कि प्रीम्प्शन, टाइमआउट आदि के कारण, तो डेटा पुराने कैश से प्राप्त किया जाता है और अनुरोधकर्ता को वापस किया जाता है। यह हमें पिछले पाठ में पेश किए गए get_stale API तक ले जाता है।
local elapsed, err = lock:lock("my_key") # elapsed to nil का मतलब है कि लॉकिंग विफल हो गई। err का रिटर्न वैल्यू timeout, locked में से एक है if not elapsed and err then dict:get_stale("my_key") end
यदि lock सफल होता है, तो डेटाबेस को क्वेरी करना और परिणाम को कैश में अपडेट करना सुरक्षित है, और अंत में हम unlock इंटरफ़ेस को कॉल करके लॉक को रिलीज़ करते हैं।
lua-resty-lock और get_stale को मिलाकर, हमारे पास कैश स्टैम्पीड समस्या का सही समाधान है। lua-resty-lock का दस्तावेज़ीकरण इसे संभालने के लिए एक बहुत ही पूर्ण कोड प्रदान करता है। यदि रुचि हो, तो आप इसे यहां देख सकते हैं।
आइए गहराई से देखें कि lock इंटरफ़ेस लॉकिंग को कैसे लागू करता है। जब हम कुछ दिलचस्प कार्यान्वयन के साथ आते हैं, तो हम हमेशा यह देखना चाहते हैं कि यह स्रोत कोड में कैसे लागू किया गया है, जो ओपन सोर्स के लाभों में से एक है।
local ok, err = dict:add(key, true, exptime) if ok then cdata.key_id = ref_obj(key) self.key = key return 0 end
जैसा कि shared dict के लेख में उल्लेख किया गया है, shared dict के सभी APIs परमाणु ऑपरेशन हैं और प्रतिस्पर्धा के बारे में चिंता करने की आवश्यकता नहीं है। इसलिए लॉक की स्थिति को चिह्नित करने के लिए shared dict का उपयोग करना एक अच्छा विचार है।
lock का उपरोक्त कार्यान्वयन dict:add का उपयोग करके key को सेट करने का प्रयास करता है: यदि key shared memory में मौजूद नहीं है, तो add सफलता वापस करेगा, जो इंगित करता है कि लॉकिंग सफल हुई; अन्य समवर्ती अनुरोध dict:add कोड लाइन के तर्क तक पहुंचने पर विफलता वापस करेंगे, और फिर कोड err जानकारी के आधार पर सीधे वापस लौटने या कई बार पुनः प्रयास करने का चयन कर सकता है।
3. lua-resty-shcache
lua-resty-lock के उपरोक्त कार्यान्वयन में, आपको लॉकिंग, अनलॉकिंग, समाप्त डेटा प्राप्त करने, पुनः प्रयास करने, अपवाद संभालने और अन्य मुद्दों को संभालना होगा, जो अभी भी काफी थकाऊ है।
यहां आपके लिए एक सरल रैपर है: lua-resty-shcache, जो Cloudflare की एक lua-resty लाइब्रेरी है, यह shared dictionaries और बाहरी स्टोरेज के ऊपर एक परत का एनकैप्सुलेशन करती है और सीरियलाइज़ेशन और डी-सीरियलाइज़ेशन के लिए अतिरिक्त फ़ंक्शन प्रदान करती है, ताकि आपको उपरोक्त विवरणों की परवाह न करनी पड़े:
local shcache = require("shcache") local my_cache_table = shcache:new( ngx.shared.cache_dict { external_lookup = lookup, encode = cmsgpack.pack, decode = cmsgpack.decode, }, { positive_ttl = 10, -- cache good data for 10s negative_ttl = 3, -- cache failed lookup for 3s name = 'my_cache', -- "named" cache, useful for debug / report } ) local my_table, from_cache = my_cache_table:load(key)
यह नमूना कोड आधिकारिक उदाहरण से निकाला गया है और सभी विवरणों को छिपा दिया गया है। यह कैश एनकैप्सुलेशन लाइब्रेरी सबसे अच्छा विकल्प नहीं है, लेकिन यह शुरुआती लोगों के लिए अच्छा सीखने का सामग्री है। निम्नलिखित लेख कुछ अन्य बेहतर और अधिक सामान्यतः उपयोग किए जाने वाले एनकैप्सुलेशन का परिचय देगा।
4. NGINX directives
यदि आप OpenResty की lua-resty लाइब्रेरी का उपयोग नहीं कर रहे हैं, तो आप लॉकिंग और समाप्त डेटा प्राप्त करने के लिए NGINX कॉन्फ़िगरेशन directives का भी उपयोग कर सकते हैं: proxy_cache_lock और proxy_cache_use_stale। हालांकि, हम यहां NGINX directive का उपयोग करने की सलाह नहीं देते हैं, क्योंकि यह पर्याप्त लचीला नहीं है, और इसका प्रदर्शन Lua कोड के जितना अच्छा नहीं है।
सारांश
कैश स्टैम्पीड, जैसा कि हमने पहले बार-बार उल्लेख किया है, कोड समीक्षा और परीक्षण के माध्यम से पता लगाना मुश्किल है। इसे हल करने का सबसे अच्छा तरीका है कि आप अपने कोडिंग को सुधारें या एक एनकैप्सुलेशन लाइब्रेरी का उपयोग करें।
एक अंतिम प्रश्न: आप जिन भाषाओं और प्लेटफ़ॉर्म्स से परिचित हैं, उनमें कैश स्टैम्पीड और ऐसे मुद्दों को कैसे संभालते हैं? क्या OpenResty से बेहतर कोई तरीका है? कृपया मुझे टिप्पणियों में साझा करें।