paint-brush
Apache APISIX पर Idempotency-Key विनिर्देश को कैसे लागू करेंद्वारा@nfrankel
411 रीडिंग
411 रीडिंग

Apache APISIX पर Idempotency-Key विनिर्देश को कैसे लागू करें

द्वारा Nicolas Fränkel14m2024/04/11
Read on Terminal Reader

बहुत लंबा; पढ़ने के लिए

इस पोस्ट में, मैंने प्लगइन के माध्यम से अपाचे APISIX पर Idempotency-Key हेडर विनिर्देश का एक सरल कार्यान्वयन दिखाया। इस स्तर पर, इसमें सुधार की गुंजाइश है: स्वचालित परीक्षण, प्रति रूट के आधार पर Redis को कॉन्फ़िगर करने की क्षमता, अनुरोध का हिस्सा बनने के लिए डोमेन/पथ को कॉन्फ़िगर करना, एकल इंस्टेंस के बजाय Redis क्लस्टर को कॉन्फ़िगर करना, किसी अन्य K/V स्टोर का उपयोग करना, आदि।
featured image - Apache APISIX पर Idempotency-Key विनिर्देश को कैसे लागू करें
Nicolas Fränkel HackerNoon profile picture

पिछले हफ़्ते, मैंने 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
  1. स्थैतिक मार्ग विन्यास
  2. हमारे भविष्य के प्लगइन का मार्ग
  3. रेडिस इनसाइट्स (GUI) का पोर्ट। वैसे तो यह आवश्यक नहीं है, लेकिन डिबगिंग के लिए विकास के दौरान बहुत उपयोगी है।


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
  1. स्थैतिक रूट कॉन्फ़िगरेशन के लिए APISIX कॉन्फ़िगर करें
  2. हमारे प्लगइन का स्थान कॉन्फ़िगर करें
  3. कस्टम प्लगइन्स को स्पष्ट रूप से घोषित किया जाना चाहिए। प्राथमिकता टिप्पणी आवश्यक नहीं है, लेकिन यह अच्छा अभ्यास है और रखरखाव में सुधार करता है
  4. सभी रूटों पर सामान्य प्लगइन कॉन्फ़िगरेशन
  5. नीचे देखें


अंत में, हम अपना एकल मार्ग घोषित करते हैं:

 routes: - uri: /* plugins: idempotency: ~ #1 upstream: nodes: "httpbin.org:80": 1 #2 #END #3
  1. उस प्लगइन की घोषणा करें जिसे हम बनाने जा रहे हैं।
  2. httpbin एक उपयोगी अपस्ट्रीम है क्योंकि हम विभिन्न URI और विधियों को आज़मा सकते हैं।
  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
  1. कॉन्फ़िगरेशन का आकार परिभाषित करें


  2. जाँचें कि कॉन्फ़िगरेशन वैध है


क्योंकि मैंने प्लगइन में डिफ़ॉल्ट मान परिभाषित किए हैं, इसलिए मैं अपने डॉकर कंपोज़ इंफ्रास्ट्रक्चर के अंदर चलाने के लिए केवल 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
  1. OpenResty Redis मॉड्यूल के new फ़ंक्शन का संदर्भ लें।


  2. उदाहरण पाने के लिए इसे कॉल करें.


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
  1. अनुरोध से idempotency कुंजी निकालें.
  2. कुंजी को उपसर्ग करें ताकि हम संभावित टकराव से बच सकें।
  3. आइडेम्पोटेंसी कुंजी के अंतर्गत Redis में संग्रहीत डेटा सेट प्राप्त करें।
  4. यदि कुंजी नहीं मिलती है, तो उसे बूलियन चिह्न के साथ संग्रहीत करें।
  5. कस्टम उपयोगिता फ़ंक्शन के माध्यम से Lua तालिका में डेटा को रूपांतरित करें।
  6. हेडर को ध्यान में रखते हुए प्रतिक्रिया JSON प्रारूप में संग्रहीत की जाती है।
  7. प्रतिक्रिया का पुनर्निर्माण करें.
  8. क्लाइंट को पुनर्निर्मित प्रतिक्रिया लौटाएँ। 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
  1. अनुरोध से idempotency कुंजी निकालें.


  2. किसी प्रतिक्रिया के विभिन्न तत्वों को Lua तालिका में व्यवस्थित करें।


  3. 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 पुनः उपयोग करने का प्रयास करें। चूंकि हमने अभी तक कोई त्रुटि प्रबंधन लागू नहीं किया है, इसलिए आपको किसी अन्य अनुरोध के लिए कैश्ड प्रतिक्रिया मिलेगी। अब समय आ गया है कि हम अपने खेल को आगे बढ़ाएँ।

त्रुटि पथों का कार्यान्वयन

विनिर्देश कई त्रुटि पथ परिभाषित करता है:


  • Idempotency-कुंजी गायब है.


  • Idempotency-Key पहले से ही उपयोग किया गया है.


  • इस Idempotency-Key के लिए एक अनुरोध लंबित है


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


 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
  1. केवल प्रासंगिक भागों वाली एक तालिका बनाएं।


  2. cjson लाइब्रेरी JSON बनाती है जिसके सदस्य कई कॉल में अलग-अलग तरीके से सॉर्ट किए जा सकते हैं। इसलिए, यह अलग-अलग हैश में परिणामित होता है। core.json.stably_encode उस समस्या को ठीक करता है।


  3. इसे हैश करें।


फिर, अनुरोध प्राप्त करते समय बूलियन संग्रहीत करने के बजाय, हम परिणामी हैश संग्रहीत करते हैं।


 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


अंतिम त्रुटि प्रबंधन इसके ठीक बाद होता है। निम्नलिखित परिदृश्य की कल्पना करें:


  1. एक अनुरोध idempotency कुंजी X के साथ आता है.


  2. प्लगइन फिंगरप्रिंट्स लेता है और हैश को रेडिस में संग्रहीत करता है।


  3. APISIX अनुरोध को अपस्ट्रीम पर अग्रेषित करता है।


  4. डुप्लिकेट अनुरोध समान आइडेम्पोटेंसी कुंजी, X के साथ आता है।


  5. प्लगइन 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 पर प्रकाशित