पिछले हफ़्ते, मैंने IETF Idempotency-Key विनिर्देश का विश्लेषण लिखा था। विनिर्देश का उद्देश्य दोहराए गए अनुरोधों से बचना है। संक्षेप में, विचार यह है कि क्लाइंट अनुरोध के साथ एक अद्वितीय कुंजी भेजे:
यह पोस्ट दिखाता है कि इसे Apache APISIX के साथ कैसे कार्यान्वित किया जाए।
कोडिंग शुरू करने से पहले, हमें कुछ चीजें परिभाषित करने की आवश्यकता है। अपाचे APISIX एक प्लगइन-आधारित आर्किटेक्चर प्रदान करता है। इसलिए, हम उपरोक्त तर्क को एक प्लगइन में कोड करेंगे।
अपाचे APISIX ओपनरेस्टी पर आधारित है, जो nginx पर आधारित है। प्रत्येक घटक चरणों को परिभाषित करता है, जो घटकों में कमोबेश मैप होते हैं। चरणों के बारे में अधिक जानकारी के लिए, कृपया यह पिछली पोस्ट देखें।
अंत में, हम प्राथमिकता पर निर्णय लेंगे। प्राथमिकता उस क्रम को परिभाषित करती है जिसमें APISIX किसी चरण के अंदर प्लगइन चलाता है। मैंने 1500
पर निर्णय लिया, क्योंकि सभी प्रमाणीकरण प्लगइन्स की प्राथमिकता 2000
और उससे अधिक रेंज में होती है, लेकिन मैं कैश्ड प्रतिक्रिया को जल्द से जल्द वापस करना चाहता हूँ।
विनिर्देश के अनुसार हमें डेटा संग्रहीत करना आवश्यक है। APISIX कई अमूर्तताएं प्रदान करता है, लेकिन संग्रहण उनमें से एक नहीं है। हमें आइडेम्पोटेंसी कुंजी के माध्यम से एक्सेस की आवश्यकता है ताकि यह एक कुंजी-मूल्य संग्रह की तरह दिखे।
मैंने मनमाने ढंग से Redis को चुना, क्योंकि यह काफी व्यापक है, और क्लाइंट पहले से ही APISIX वितरण का हिस्सा है। ध्यान दें कि सरल Redis JSON संग्रहण प्रदान नहीं करता है; इसलिए, मैं redis-stack
Docker छवि का उपयोग करता हूँ।
स्थानीय बुनियादी ढांचा निम्नलिखित है:
services: apisix: image: apache/apisix:3.9.0-debian volumes: - ./apisix/config.yml:/usr/local/apisix/conf/config.yaml:ro - ./apisix/apisix.yml:/usr/local/apisix/conf/apisix.yaml:ro #1 - ./plugin/src:/opt/apisix/plugins:ro #2 ports: - "9080:9080" redis: image: redis/redis-stack:7.2.0-v9 ports: - "8001:8001" #3
APISIX कॉन्फ़िगरेशन निम्नलिखित है:
deployment: role: data_plane role_data_plane: config_provider: yaml #1 apisix: extra_lua_path: /opt/?.lua #2 plugins: - idempotency # priority: 1500 #3 plugin_attr: #4 idempotency: host: redis #5
अंत में, हम अपना एकल मार्ग घोषित करते हैं:
routes: - uri: /* plugins: idempotency: ~ #1 upstream: nodes: "httpbin.org:80": 1 #2 #END #3
इस बुनियादी ढांचे के तैयार हो जाने पर हम कार्यान्वयन शुरू कर सकते हैं।
अपाचे APISIX प्लगइन की नींव बहुत बुनियादी है:
local plugin_name = "idempotency" local _M = { version = 1.0, priority = 1500, schema = {}, name = plugin_name, } return _M
अगला चरण कॉन्फ़िगरेशन है, उदाहरण के लिए, Redis होस्ट और पोर्ट। शुरुआत के लिए, हम सभी रूट पर एक ही Redis कॉन्फ़िगरेशन प्रदान करेंगे। config.yaml
फ़ाइल में plugin_attr
अनुभाग के पीछे यही विचार है: सामान्य कॉन्फ़िगरेशन। आइए अपने प्लगइन को विस्तार से देखें:
local core = require("apisix.core") local plugin = require("apisix.plugin") local attr_schema = { --1 type = "object", properties = { host = { type = "string", description = "Redis host", default = "localhost", }, port = { type = "integer", description = "Redis port", default = 6379, }, }, } function _M.init() local attr = plugin.plugin_attr(plugin_name) or {} local ok, err = core.schema.check(attr_schema, attr) --2 if not ok then core.log.error("Failed to check the plugin_attr[", plugin_name, "]", ": ", err) return false, err end end
कॉन्फ़िगरेशन का आकार परिभाषित करें
जाँचें कि कॉन्फ़िगरेशन वैध है
क्योंकि मैंने प्लगइन में डिफ़ॉल्ट मान परिभाषित किए हैं, इसलिए मैं अपने डॉकर कंपोज़ इंफ्रास्ट्रक्चर के अंदर चलाने के लिए केवल host
को redis
पर ओवरराइड कर सकता हूं और डिफ़ॉल्ट पोर्ट का उपयोग कर सकता हूं।
इसके बाद, मुझे Redis क्लाइंट बनाना होगा। ध्यान दें कि प्लेटफ़ॉर्म मुझे रीराइट/एक्सेस सेक्शन के बाद किसी भी चरण में कनेक्ट होने से रोकता है। इसलिए, मैं इसे init()
विधि में बनाऊँगा और अंत तक रखूँगा।
local redis_new = require("resty.redis").new --1 function _M.init() -- ... redis = redis_new() --2 redis:set_timeout(1000) local ok, err = redis:connect(attr.host, attr.port) if not ok then core.log.error("Failed to connect to Redis: ", err) return false, err end end
OpenResty Redis मॉड्यूल के new
फ़ंक्शन का संदर्भ लें।
उदाहरण पाने के लिए इसे कॉल करें.
Redis क्लाइंट अब शेष प्लगइन निष्पादन चक्र के दौरान redis
वेरिएबल में उपलब्ध है।
अपने पिछले सॉफ़्टवेयर इंजीनियर जीवन में, मैंने आमतौर पर पहले नाममात्र पथ को लागू किया। उसके बाद, मैंने त्रुटि मामलों को अलग-अलग प्रबंधित करके कोड को और अधिक मज़बूत बनाया। इस तरह, अगर मुझे किसी भी बिंदु पर रिलीज़ करना पड़ा, तो मैं अभी भी व्यावसायिक मूल्य प्रदान करूँगा - चेतावनियों के साथ। मैं इस मिनी-प्रोजेक्ट को उसी तरह से देखूँगा।
नाममात्र पथ पर छद्म एल्गोरिथ्म निम्नलिखित जैसा दिखता है:
DO extract idempotency key from request DO look up value from Redis IF value doesn't exist DO set key in Redis with empty value ELSE RETURN cached response DO forward to upstream DO store response in Redis RETURN response
हमें तर्क को उस चरण से मैप करना होगा जिसका मैंने ऊपर उल्लेख किया है। अपस्ट्रीम से पहले दो चरण उपलब्ध हैं, रीराइट और एक्सेस ; उसके बाद तीन, हेडर_फ़िल्टर , बॉडी_फ़िल्टर और लॉग । एक्सेस चरण पहले काम के लिए स्पष्ट लग रहा था, लेकिन मुझे तीन अन्य के बीच का पता लगाना था। मैंने बेतरतीब ढंग से बॉडी_फ़िल्टर चुना, लेकिन मैं अन्य चरणों के लिए समझदार तर्क सुनने के लिए तैयार हूँ।
ध्यान दें कि मैंने कोड को अधिक पठनीय बनाने के लिए लॉग हटा दिए हैं। डिबगिंग उत्पादन समस्याओं को आसान बनाने के लिए त्रुटि और सूचनात्मक लॉग आवश्यक हैं।
function _M.access(conf, ctx) local idempotency_key = core.request.header(ctx, "Idempotency-Key") --1 local redis_key = "idempotency#" .. idempotency_key --2 local resp, err = redis:hgetall(redis_key) --3 if not resp then return end if next(resp) == nil then --4 local resp, err = redis:hset(redis_key, "request", true ) --4 if not resp then return end else local data = normalize_hgetall_result(resp) --5 local response = core.json.decode(data["response"]) --6 local body = response["body"] --7 local status_code = response["status"] --7 local headers = response["headers"] for k, v in pairs(headers) do --7 core.response.set_header(k, v) end return core.response.exit(status_code, body) --8 end end
return
स्टेटमेंट पर ध्यान दें: APISIX बाद के जीवनचक्र चरणों को छोड़ देता है।
function _M.body_filter(conf, ctx) local idempotency_key = core.request.header(ctx, "Idempotency-Key") --1 local redis_key = "idempotency#" .. idempotency_key if core.response then local response = { --2 status = ngx.status, body = core.response.hold_body_chunk(ctx, true), headers = ngx.resp.get_headers() } local redis_key = "idempotency#" .. redis_key local resp, err = red:set(redis_key, "response", core.json.encode(response)) --3 if not resp then return end end end
अनुरोध से idempotency कुंजी निकालें.
किसी प्रतिक्रिया के विभिन्न तत्वों को Lua तालिका में व्यवस्थित करें।
JSON-एन्कोडेड प्रतिक्रिया को Redis सेट में संग्रहीत करें
परीक्षणों से पता चला कि यह अपेक्षा के अनुरूप काम करता है।
कोशिश करना:
curl -i -X POST -H 'Idempotency-Key: A' localhost:9080/response-headers\?freeform=hello curl -i -H 'Idempotency-Key: B' localhost:9080/status/250 curl -i -H 'Idempotency-Key: C' -H 'foo: bar' localhost:9080/status/250
इसके अलावा, तीसरे अनुरोध के लिए एक बेमेल आइडेम्पोटेंसी कुंजी, जैसे कि A
पुनः उपयोग करने का प्रयास करें। चूंकि हमने अभी तक कोई त्रुटि प्रबंधन लागू नहीं किया है, इसलिए आपको किसी अन्य अनुरोध के लिए कैश्ड प्रतिक्रिया मिलेगी। अब समय आ गया है कि हम अपने खेल को आगे बढ़ाएँ।
विनिर्देश कई त्रुटि पथ परिभाषित करता है:
आइए इन्हें एक-एक करके लागू करें। सबसे पहले, आइए जाँचें कि अनुरोध में एक आइडेम्पोटेंसी कुंजी है। ध्यान दें कि हम प्लगइन को प्रति-रूट के आधार पर कॉन्फ़िगर कर सकते हैं, इसलिए यदि रूट में प्लगइन शामिल है, तो हम यह निष्कर्ष निकाल सकते हैं कि यह अनिवार्य है।
function _M.access(conf, ctx) local idempotency_key = core.request.header(ctx, "Idempotency-Key") if not idempotency_key then return core.response.exit(400, "This operation is idempotent and it requires correct usage of Idempotency Key") end -- ...
अगर चाबी गुम हो तो उचित 400 लौटा दें। यह आसान था।
किसी भिन्न अनुरोध के लिए किसी मौजूदा कुंजी के पुनः उपयोग की जाँच करना थोड़ा अधिक जटिल है। हमें सबसे पहले अनुरोध को संग्रहीत करने की आवश्यकता है, या अधिक सटीक रूप से, अनुरोध का गठन करने वाले फिंगरप्रिंट को संग्रहीत करने की आवश्यकता है। दो अनुरोध समान हैं यदि उनमें: समान विधि, समान पथ, समान बॉडी और समान हेडर हैं। आपकी स्थिति के आधार पर, डोमेन (और पोर्ट) उनका हिस्सा हो सकता है या नहीं भी हो सकता है। मेरे सरल कार्यान्वयन के लिए, मैं इसे छोड़ दूंगा।
हल करने के लिए कई समस्याएँ हैं। सबसे पहले, मुझे core.request
ऑब्जेक्ट को हैश करने के लिए कोई मौजूदा API नहीं मिला, जैसा कि अन्य भाषाओं में है, जिनसे मैं अधिक परिचित हूँ, जैसे कि , Java का Object.hash()
। मैंने JSON में ऑब्जेक्ट को एनकोड करने और स्ट्रिंग को हैश करने का निर्णय लिया। हालाँकि, मौजूदा core.request
में ऐसे उप-तत्व हैं जिन्हें JSON में परिवर्तित नहीं किया जा सकता है। मुझे ऊपर बताए गए भागों को निकालना था और तालिका को परिवर्तित करना था।
local function hash_request(request, ctx) local request = { --1 method = core.request.get_method(), uri = ctx.var.request_uri, headers = core.request.headers(), body = core.request.get_body() } local json = core.json.stably_encode(request) --2 return ngx.encode_base64(json) --3 end
केवल प्रासंगिक भागों वाली एक तालिका बनाएं।
cjson
लाइब्रेरी JSON बनाती है जिसके सदस्य कई कॉल में अलग-अलग तरीके से सॉर्ट किए जा सकते हैं। इसलिए, यह अलग-अलग हैश में परिणामित होता है। core.json.stably_encode
उस समस्या को ठीक करता है।
इसे हैश करें।
फिर, अनुरोध प्राप्त करते समय बूलियन संग्रहीत करने के बजाय, हम परिणामी हैश संग्रहीत करते हैं।
local hash = hash_request(core.request, ctx) if next(resp) == nil then core.log.warn("No key found in Redis for Idempotency-Key, set it: ", redis_key) local resp, err = redis:hset(redis_key, "request", hash) if not resp then core.log.error("Failed to set data in Redis: ", err) return end then -- ...
हम दूसरी शाखा पर idempotency कुंजी के अंतर्गत संग्रहीत हैश को पढ़ते हैं। यदि वे मेल नहीं खाते हैं, तो हम संबंधित त्रुटि कोड के साथ बाहर निकलते हैं:
local data = normalize_hgetall_result(resp) local stored_hash = data["request"] if hash ~= stored_hash then return core.response.exit(422, "This operation is idempotent and it requires correct usage of Idempotency Key. Idempotency Key MUST not be reused across different payloads of this operation.") end
अंतिम त्रुटि प्रबंधन इसके ठीक बाद होता है। निम्नलिखित परिदृश्य की कल्पना करें:
एक अनुरोध idempotency कुंजी X के साथ आता है.
प्लगइन फिंगरप्रिंट्स लेता है और हैश को रेडिस में संग्रहीत करता है।
APISIX अनुरोध को अपस्ट्रीम पर अग्रेषित करता है।
डुप्लिकेट अनुरोध समान आइडेम्पोटेंसी कुंजी, X के साथ आता है।
प्लगइन Redis से डेटा पढ़ता है और कोई कैश्ड प्रतिक्रिया नहीं पाता है।
अपस्ट्रीम ने अनुरोध का प्रसंस्करण पूरा नहीं किया; इसलिए, पहला अनुरोध अभी तक body_filter
चरण तक नहीं पहुंचा है।
हम उपरोक्त स्निपेट में निम्नलिखित कोड जोड़ते हैं:
if not data["response"] then return core.response.exit(409, " request with the same Idempotency-Key for the same operation is being processed or is outstanding.") end
इतना ही।
इस पोस्ट में, मैंने प्लगइन के माध्यम से अपाचे APISIX पर Idempotency-Key
हेडर विनिर्देश का एक सरल कार्यान्वयन दिखाया। इस स्तर पर, इसमें सुधार की गुंजाइश है: स्वचालित परीक्षण, प्रति रूट के आधार पर Redis को कॉन्फ़िगर करने की क्षमता, अनुरोध का हिस्सा बनने के लिए डोमेन/पथ को कॉन्फ़िगर करना, एकल इंस्टेंस के बजाय Redis क्लस्टर को कॉन्फ़िगर करना, किसी अन्य K/V स्टोर का उपयोग करना, आदि।
फिर भी, यह विनिर्देश को कार्यान्वित करता है और इसमें अधिक उत्पादन-स्तर के कार्यान्वयन के रूप में विकसित होने की क्षमता है।
इस पोस्ट का पूरा स्रोत कोड GitHub पर पाया जा सकता है।
आगे जाने के लिए:
मूल रूप से 7 अप्रैल, 2024 को A Java Geek पर प्रकाशित