গত সপ্তাহে, আমি IETF Idempotency-Key স্পেসিফিকেশনের একটি বিশ্লেষণ লিখেছিলাম। স্পেসিফিকেশনটি সদৃশ অনুরোধগুলি এড়াতে লক্ষ্য করে। সংক্ষেপে, ধারণাটি হল ক্লায়েন্টের অনুরোধের সাথে একটি অনন্য কী পাঠানোর জন্য:
এই পোস্টটি দেখায় কিভাবে এটি Apache APISIX এর সাথে বাস্তবায়ন করতে হয়।
কোডিং শুরু করার আগে, আমাদের কয়েকটি জিনিস সংজ্ঞায়িত করতে হবে। Apache APISIX একটি প্লাগইন-ভিত্তিক আর্কিটেকচার অফার করে। সুতরাং, আমরা একটি প্লাগইনে উপরের লজিক কোড করব।
Apache APISIX OpenResty-এর উপর তৈরি করে, যা nginx-এর উপর তৈরি করে। প্রতিটি উপাদান পর্যায়গুলিকে সংজ্ঞায়িত করে, যা উপাদানগুলি জুড়ে কমবেশি ম্যাপ করে। পর্যায়ক্রমে আরও তথ্যের জন্য, অনুগ্রহ করে এই পূর্ববর্তী পোস্টটি দেখুন।
অবশেষে, আমরা একটি অগ্রাধিকার সিদ্ধান্ত নেব. অগ্রাধিকার সেই ক্রমে সংজ্ঞায়িত করে যেখানে APISIX একটি ফেজের ভিতরে প্লাগইন চালায়। আমি 1500
-এ সিদ্ধান্ত নিয়েছি, কারণ সমস্ত প্রমাণীকরণ প্লাগইনগুলির 2000
এবং আরও বেশি পরিসরে অগ্রাধিকার রয়েছে, কিন্তু আমি যত তাড়াতাড়ি সম্ভব ক্যাশে করা প্রতিক্রিয়া ফেরত দিতে চাই।
স্পেসিফিকেশনের জন্য আমাদের ডেটা সঞ্চয় করতে হবে। APISIX অনেক বিমূর্ততা অফার করে, কিন্তু স্টোরেজ তাদের মধ্যে একটি নয়। আমাদের আইডেমপোটেন্সি কী এর মাধ্যমে অ্যাক্সেস দরকার যাতে এটি একটি কী-মূল্যের দোকানের মতো দেখায়।
আমি নির্বিচারে রেডিস বেছে নিয়েছি, কারণ এটি বেশ বিস্তৃত, এবং ক্লায়েন্ট ইতিমধ্যে APISIX বিতরণের অংশ। মনে রাখবেন যে সাধারণ রেডিস 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
এই পরিকাঠামোর সাথে আমরা বাস্তবায়ন শুরু করতে পারি।
একটি Apache 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
করতে পারি।
এর পরে, আমাকে রেডিস ক্লায়েন্ট তৈরি করতে হবে। নোট করুন যে প্ল্যাটফর্মটি আমাকে পুনর্লিখন/অ্যাক্সেস বিভাগের পরে যেকোনো পর্যায়ে সংযোগ করতে বাধা দেয়। তাই, আমি এটি 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
আমি উপরে উল্লিখিত ধাপে আমাদের যুক্তি ম্যাপ করতে হবে। আপস্ট্রিমের আগে দুটি পর্যায় উপলব্ধ, পুনর্লিখন এবং অ্যাক্সেস ; তিন পরে, হেডার_ফিল্টার , বডি_ফিল্টার এবং লগ । অ্যাক্সেসের পর্যায়টি আগে কাজের জন্য সুস্পষ্ট বলে মনে হয়েছিল, কিন্তু আমাকে অন্য তিনটির মধ্যে খুঁজে বের করতে হবে। আমি এলোমেলোভাবে body_filter বেছে নিয়েছি, কিন্তু আমি অন্যান্য পর্যায়ের জন্য যুক্তিযুক্ত যুক্তি শুনতে ইচ্ছুক।
নোট করুন যে আমি কোডটিকে আরও পঠনযোগ্য করতে লগগুলি সরিয়ে দিয়েছি। ত্রুটি এবং তথ্যগত লগগুলি ডিবাগিং উত্পাদন সমস্যাগুলি সহজ করার জন্য প্রয়োজনীয়।
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 কী বের করুন।
একটি লুয়া টেবিলে প্রতিক্রিয়ার বিভিন্ন উপাদান সাজান।
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 কী আছে। মনে রাখবেন যে আমরা প্রতি-রুটে প্লাগইন কনফিগার করতে পারি, তাই যদি রুটে প্লাগইন অন্তর্ভুক্ত থাকে, তাহলে আমরা উপসংহারে আসতে পারি যে এটি বাধ্যতামূলক।
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 -- ...
আমরা অন্য শাখায় আইডেমপোটেন্সি কী-এর অধীনে সংরক্ষিত হ্যাশটি পড়ি। যদি সেগুলি মেলে না, আমরা প্রাসঙ্গিক ত্রুটি কোড দিয়ে প্রস্থান করি:
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 আপস্ট্রিমে অনুরোধটি ফরোয়ার্ড করে।
একটি ডুপ্লিকেট অনুরোধ একই idempotency কী, X সহ আসে।
প্লাগইনটি রেডিস থেকে ডেটা পড়ে এবং কোনও ক্যাশে করা প্রতিক্রিয়া খুঁজে পায় না।
আপস্ট্রিম অনুরোধটি প্রক্রিয়াকরণ শেষ করেনি; তাই, প্রথম অনুরোধটি এখনও 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
এটাই.
এই পোস্টে, আমি একটি প্লাগইনের মাধ্যমে Apache APISIX-এ Idempotency-Key
হেডার স্পেসিফিকেশনের একটি সহজ বাস্তবায়ন দেখিয়েছি। এই পর্যায়ে, এটির উন্নতির জন্য জায়গা রয়েছে: স্বয়ংক্রিয় পরীক্ষা, প্রতি রুটের ভিত্তিতে রেডিস কনফিগার করার ক্ষমতা, অনুরোধের অংশ হতে ডোমেন/পাথ কনফিগার করুন, একটি একক উদাহরণের পরিবর্তে একটি রেডিস ক্লাস্টার কনফিগার করুন, অন্য K/ ব্যবহার করুন ভি স্টোর, ইত্যাদি
তবুও, এটি স্পেসিফিকেশন বাস্তবায়ন করে এবং আরও উত্পাদন-গ্রেড বাস্তবায়নে বিকশিত হওয়ার সম্ভাবনা রয়েছে।
এই পোস্টের জন্য সম্পূর্ণ সোর্স কোড GitHub এ পাওয়া যাবে।
আরো যেতে:
মূলত 7 এপ্রিল, 2024-এ A Java Geek এ প্রকাশিত