ओपनरेस्टी में डायनामिक रेट-लिमिटिंग

API7.ai

January 6, 2023

OpenResty (NGINX + Lua)

पिछले लेख में, मैंने आपको leaky bucket और token bucket एल्गोरिदम से परिचित कराया, जो बर्स्ट ट्रैफिक को संभालने के लिए आम हैं। साथ ही, हमने NGINX कॉन्फ़िगरेशन का उपयोग करके अनुरोधों की दर सीमित करने का तरीका सीखा। हालांकि, NGINX कॉन्फ़िगरेशन का उपयोग केवल उपयोगिता के स्तर पर है और यह उपयोगी होने से अभी बहुत दूर है।

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

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

यह समझना महत्वपूर्ण है कि प्रौद्योगिकी व्यवसाय की सेवा करती है, और साथ ही, व्यवसाय प्रौद्योगिकी को चलाता है। NGINX के जन्म के समय, कॉन्फ़िगरेशन को गतिशील रूप से समायोजित करने की आवश्यकता बहुत कम थी; यह अधिक रिवर्स प्रॉक्सी, लोड बैलेंसिंग, कम मेमोरी उपयोग, और इसी तरह की अन्य आवश्यकताओं के बारे में था जो NGINX के विकास को चलाती थीं। प्रौद्योगिकी आर्किटेक्चर और कार्यान्वयन के संदर्भ में, कोई भी मोबाइल इंटरनेट, IoT, और माइक्रोसर्विसेज जैसे परिदृश्यों में गतिशील और सूक्ष्म नियंत्रण की मांग के विशाल विस्फोट की भविष्यवाणी नहीं कर सकता था।

OpenResty का Lua स्क्रिप्टिंग का उपयोग NGINX की इस कमी को पूरा करता है, जिससे यह एक प्रभावी पूरक बन जाता है। यही कारण है कि OpenResty NGINX के प्रतिस्थापन के रूप में इतना व्यापक रूप से उपयोग किया जाता है। अगले कुछ लेखों में, मैं आपको OpenResty में और अधिक गतिशील परिदृश्यों और उदाहरणों से परिचित कराऊंगा। आइए देखें कि OpenResty का उपयोग करके गतिशील दर सीमित करने का तरीका कैसे लागू किया जाए।

OpenResty में, हम lua-resty-limit-traffic का उपयोग करने की सलाह देते हैं। इसमें limit-req (अनुरोध दर सीमित करना), limit-count (अनुरोध संख्या सीमित करना), और limit-conn (समवर्ती कनेक्शन सीमित करना) शामिल हैं; और इन तीनों विधियों को संयोजित करने के लिए limit.traffic प्रदान करता है।

अनुरोध दर सीमित करना

आइए limit-req से शुरू करें, जो अनुरोधों की दर को सीमित करने के लिए लीकी बकेट एल्गोरिदम का उपयोग करता है।

पिछले भाग में, हमने इस रेस्टी लाइब्रेरी में लीकी बकेट एल्गोरिदम के मुख्य कार्यान्वयन कोड का संक्षिप्त परिचय दिया, और अब हम इस लाइब्रेरी का उपयोग करने का तरीका सीखेंगे। पहले, निम्नलिखित नमूना कोड को देखें।

resty --shdict='my_limit_req_store 100m' -e 'local limit_req = require "resty.limit.req" local lim, err = limit_req.new("my_limit_req_store", 200, 100) local delay, err = lim:incoming("key", true) if not delay then if err == "rejected" then return ngx.exit(503) end return ngx.exit(500) end if delay >= 0.001 then ngx.sleep(delay) end'

हम जानते हैं कि lua-resty-limit-traffic कुंजियों को संग्रहीत और गिनने के लिए एक shared dict का उपयोग करता है, इसलिए हमें limit-req का उपयोग करने से पहले my_limit_req_store के लिए 100m स्थान घोषित करने की आवश्यकता है। यह limit-conn और limit-count के लिए भी समान है, जिन्हें अलग-अलग shared dict स्थान की आवश्यकता होती है।

limit_req.new("my_limit_req_store", 200, 100)

उपरोक्त कोड लाइन सबसे महत्वपूर्ण कोड लाइनों में से एक है। इसका मतलब है कि my_limit_req_store नामक एक shared dict का उपयोग सांख्यिकी संग्रहीत करने के लिए किया जाता है, और प्रति सेकंड दर 200 पर सेट की जाती है, ताकि यदि यह 200 से अधिक हो लेकिन 300 से कम हो (यह मान 200 + 100 से गणना की जाती है), तो इसे कतारबद्ध किया जाएगा, और यदि यह 300 से अधिक हो, तो इसे अस्वीकार कर दिया जाएगा।

सेटअप पूरा होने के बाद, हमें क्लाइंट से अनुरोध को संसाधित करना होगा। lim: incoming("key", true) यहां यह कार्य करता है। incoming के दो पैरामीटर हैं, जिन्हें हमें विस्तार से पढ़ने की आवश्यकता है।

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

local province = get_ province(ngx.var.binary_remote_addr) local channel = ngx.req.get_headers()["channel"] local key = province .. channel lim:incoming(key, true)

बेशक, आप कुंजी के अर्थ और incoming को कॉल करने की शर्तों को अनुकूलित भी कर सकते हैं, ताकि आपको दर सीमित करने का बहुत लचीला प्रभाव मिल सके।

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

आप सोच रहे होंगे कि यह पैरामीटर क्यों मौजूद है। एक परिदृश्य पर विचार करें जहां आप दो अलग-अलग limit-req इंस्टेंस को अलग-अलग कुंजियों के साथ सेट करते हैं, एक कुंजी होस्टनाम हो और दूसरी कुंजी क्लाइंट का IP पता हो। फिर, जब एक क्लाइंट अनुरोध संसाधित किया जाता है, तो इन दोनों इंस्टेंस के incoming विधियों को क्रम में कॉल किया जाता है, जैसा कि निम्नलिखित छद्म-कोड में दर्शाया गया है।

local limiter_one, err = limit_req.new("my_limit_req_store", 200, 100) local limiter_two, err = limit_req.new("my_limit_req_store", 20, 10) limiter_one :incoming(ngx.var.host, true) limiter_two:incoming(ngx.var.binary_remote_addr, true)

यदि उपयोगकर्ता का अनुरोध limiter_one के थ्रेसहोल्ड डिटेक्शन को पास करता है लेकिन limiter_two के डिटेक्शन द्वारा अस्वीकार कर दिया जाता है, तो limiter_one:incoming फ़ंक्शन कॉल को एक अभ्यास माना जाना चाहिए और हमें इसे गिनने की आवश्यकता नहीं है।

इस मामले में, उपरोक्त कोड लॉजिक पर्याप्त रूप से कठोर नहीं है। हमें सभी लिमिटर्स का पूर्व-अभ्यास करने की आवश्यकता है ताकि यदि कोई लिमिटर थ्रेसहोल्ड ट्रिगर हो जो क्लाइंट अनुरोध को अस्वीकार कर सकता है, तो यह सीधे वापस लौट सकता है।

for i = 1, n do local lim = limiters[i] local delay, err = lim:incoming(keys[i], i == n) if not delay then return nil, err end end

यही incoming फ़ंक्शन का दूसरा तर्क है। यह कोड limit.traffic मॉड्यूल का मुख्य कोड है, जिसका उपयोग कई दर लिमिटर्स को संयोजित करने के लिए किया जाता है।

अनुरोध संख्या सीमित करना

आइए limit.count पर एक नज़र डालें, एक लाइब्रेरी जो अनुरोधों की संख्या को सीमित करती है। यह GitHub API Rate Limiting की तरह काम करती है, जो एक निश्चित समय विंडो में उपयोगकर्ता अनुरोधों की संख्या को सीमित करती है। हमेशा की तरह, आइए एक नमूना कोड से शुरू करें।

local limit_count = require "resty.limit.count" local lim, err = limit_count.new("my_limit_count_store", 5000, 3600) local key = ngx.req.get_headers()["Authorization"] local delay, remaining = lim:incoming(key, true)

आप देख सकते हैं कि limit.count और limit.req का उपयोग समान रूप से किया जाता है। हम nginx.conf में एक shared dict को परिभाषित करके शुरू करते हैं।

lua_shared_dict my_limit_count_store 100m;

फिर एक लिमिटर ऑब्जेक्ट को new करते हैं, और अंत में incoming फ़ंक्शन का उपयोग करके इसे निर्धारित और संसाधित करते हैं।

हालांकि, अंतर यह है कि limit-count में incoming फ़ंक्शन का दूसरा रिटर्न मान शेष कॉल्स का प्रतिनिधित्व करता है, और हम इसके अनुसार प्रतिक्रिया हेडर में फ़ील्ड जोड़ सकते हैं ताकि क्लाइंट को बेहतर संकेत मिल सके।

ngx.header["X-RateLimit-Limit"] = "5000" ngx.header["X-RateLimit-Remaining"] = remaining

समवर्ती कनेक्शन संख्या सीमित करना

limit.conn समवर्ती कनेक्शनों की संख्या को सीमित करने के लिए एक लाइब्रेरी है। यह पहले बताई गई दो लाइब्रेरी से इस मायने में अलग है कि इसमें एक विशेष leaving API है, जिसका मैं यहां संक्षेप में वर्णन करूंगा।

अनुरोध दर और अनुरोधों की संख्या को सीमित करना, जैसा कि ऊपर बताया गया है, सीधे access चरण में किया जा सकता है। समवर्ती कनेक्शनों की संख्या को सीमित करने के विपरीत, जिसके लिए न केवल access चरण में थ्रेसहोल्ड को पार करने का निर्धारण करने की आवश्यकता होती है, बल्कि log चरण में leaving API को कॉल करने की भी आवश्यकता होती है।

log_by_lua_block { local latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay local key = ctx.limit_conn_key local conn, err = lim:leaving(key, latency) }

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

local conn, err = dict:incr(key, -1)

दर लिमिटर्स का संयोजन

यह इन तीनों विधियों का परिचय समाप्त करता है। अंत में, आइए देखें कि limit.rate, limit.conn और limit.count को कैसे संयोजित किया जाए। यहां हमें limit.traffic में combine फ़ंक्शन का उपयोग करने की आवश्यकता है।

local lim1, err = limit_req.new("my_req_store", 300, 200) local lim2, err = limit_req.new("my_req_store", 200, 100) local lim3, err = limit_conn.new("my_conn_store", 1000, 1000, 0.5) local limiters = {lim1, lim2, lim3} local host = ngx.var.host local client = ngx.var.binary_remote_addr local keys = {host, client, client} local delay, err = limit_traffic.combine(limiters, keys, states)

यह कोड आपके द्वारा अभी प्राप्त ज्ञान के साथ समझने में आसान होना चाहिए। combine फ़ंक्शन का मुख्य कोड, जिसका हमने limit.rate के विश्लेषण में पहले ही उल्लेख किया है, मुख्य रूप से drill फ़ंक्शन और uncommit फ़ंक्शन की मदद से कार्यान्वित किया जाता है। यह संयोजन आपको अधिक जटिल व्यावसायिक आवश्यकताओं को प्राप्त करने के लिए कई लिमिटर्स के लिए अलग-अलग थ्रेसहोल्ड और कुंजियां सेट करने की अनुमति देता है।

सारांश

न केवल limit.traffic आज बताए गए तीन दर लिमिटर्स का समर्थन करता है, बल्कि जब तक दर लिमिटर में incoming और uncommit API हो, तब तक इसे limit.traffic के combine फ़ंक्शन द्वारा प्रबंधित किया जा सकता है।

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