paint-brush
Idempotency-Key Belirtimi Apache APISIX'te Nasıl Uygulanır?ile@nfrankel
266 okumalar

Idempotency-Key Belirtimi Apache APISIX'te Nasıl Uygulanır?

ile Nicolas Fränkel14m2024/04/11
Read on Terminal Reader

Çok uzun; Okumak

Bu yazıda, bir eklenti aracılığıyla Apache APISIX üzerinde Idempotency-Key başlık spesifikasyonunun basit bir uygulamasını gösterdim. Bu aşamada iyileştirmeye açık: otomatik testler, Redis'i rota bazında yapılandırma yeteneği, etki alanını/yolu isteğin parçası olacak şekilde yapılandırma, tek bir örnek yerine bir Redis kümesi yapılandırma, başka bir K/ kullanma V mağazası vb.
featured image - Idempotency-Key Belirtimi Apache APISIX'te Nasıl Uygulanır?
Nicolas Fränkel HackerNoon profile picture

Geçen hafta IETF Idempotency-Key spesifikasyonunun bir analizini yazdım. Belirtim, yinelenen istekleri önlemeyi amaçlamaktadır. Kısacası amaç, müşterinin istekle birlikte benzersiz bir anahtar göndermesidir:


  • Sunucu anahtarı bilmiyorsa her zamanki gibi devam eder ve ardından yanıtı saklar.


  • Sunucu anahtarı biliyorsa, daha sonraki işlemlere kısa devre yapar ve saklanan yanıtı hemen geri gönderir.


Bu yazıda bunun Apache APISIX ile nasıl uygulanacağı gösterilmektedir.

Genel Bakış

Kodlamaya başlamadan önce birkaç şeyi tanımlamamız gerekiyor. Apache APISIX, eklenti tabanlı bir mimari sunar. Dolayısıyla yukarıdaki mantığı bir eklentide kodlayacağız.


Apache APISIX, nginx'i temel alan OpenResty'yi temel alır. Her bileşen, bileşenler arasında az çok eşlenen aşamaları tanımlar. Aşamalar hakkında daha fazla bilgi için lütfen bu önceki gönderiye bakın.


Son olarak önceliğe karar vereceğiz. Öncelik, APISIX'in bir aşama içindeki eklentileri çalıştırma sırasını tanımlar. Tüm kimlik doğrulama eklentilerinin 2000 ve üzeri aralıkta önceliği olduğundan 1500 karar verdim, ancak önbelleğe alınan yanıtı en kısa sürede geri vermek istiyorum.


Şartname verileri saklamamızı gerektiriyor. APISIX birçok soyutlama sunar ancak depolama bunlardan biri değildir. Bir anahtar-değer deposu gibi görünmesi için idempotency anahtarı aracılığıyla erişime ihtiyacımız var.


Oldukça yaygın olduğu ve istemcinin zaten APISIX dağıtımının bir parçası olduğu için keyfi olarak Redis'i seçtim. Basit Redis'in JSON depolama alanı sunmadığını unutmayın; bu nedenle redis-stack Docker görüntüsünü kullanıyorum.


Yerel altyapı aşağıdaki gibidir:


 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. Statik rota yapılandırması
  2. Gelecekteki eklentimize giden yol
  3. Redis Insights Limanı (GUI). Kendi başına gerekli değildir, ancak geliştirme sırasında hata ayıklama için çok faydalıdır.


APISIX yapılandırması aşağıdaki gibidir:

 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. Statik rota yapılandırması için APISIX'i yapılandırma
  2. Eklentimizin konumunu yapılandırın
  3. Özel eklentilerin açıkça bildirilmesi gerekir. Öncelik yorumu gerekli değildir ancak iyi bir uygulamadır ve sürdürülebilirliği artırır
  4. Tüm rotalarda ortak eklenti yapılandırması
  5. Aşağıya bakınız


Son olarak tek rotamızı ilan ediyoruz:

 routes: - uri: /* plugins: idempotency: ~ #1 upstream: nodes: "httpbin.org:80": 1 #2 #END #3
  1. Oluşturacağımız eklentiyi bildirin.
  2. httpbin, farklı URI'leri ve yöntemleri deneyebildiğimiz için yararlı bir yukarı akıştır.
  3. Statik rota konfigürasyonu için zorunludur!


Bu altyapının oluşmasıyla uygulamaya başlayabiliriz.

Eklentinin Düzenlenmesi

Apache APISIX eklentisinin temelleri oldukça basittir:


 local plugin_name = "idempotency" local _M = { version = 1.0, priority = 1500, schema = {}, name = plugin_name, } return _M


Bir sonraki adım, örneğin Redis ana bilgisayarı ve bağlantı noktasının yapılandırılmasıdır. Yeni başlayanlar için tüm rotalarda tek bir Redis yapılandırması sunacağız. config.yaml dosyasındaki plugin_attr bölümünün arkasındaki fikir budur: ortak yapılandırma. Eklentimizi detaylandıralım:


 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. Yapılandırmanın şeklini tanımlayın


  2. Yapılandırmanın geçerli olup olmadığını kontrol edin


Eklentide varsayılan değerleri tanımladığım için, Docker Compose altyapımda çalıştırmak ve varsayılan bağlantı noktasını kullanmak için yalnızca host redis geçersiz kılabilirim.


Daha sonra Redis istemcisini oluşturmam gerekiyor. Platformun, yeniden yazma/erişim bölümünden sonraki herhangi bir aşamada bağlanmamı engellediğini unutmayın. Bu nedenle onu init() yönteminde oluşturacağım ve sonuna kadar tutacağım.


 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 modülünün new işlevine bakın.


  2. Bir örnek almak için onu arayın.


Redis istemcisi artık eklenti yürütme döngüsünün geri kalanı boyunca redis değişkeninde mevcuttur.

Nominal Yolun Uygulanması

Önceki yazılım mühendisi hayatımda genellikle ilk önce nominal yolu uyguladım. Daha sonra hata durumlarını tek tek yöneterek kodu daha sağlam hale getirdim. Bu şekilde, herhangi bir noktada yayınlamak zorunda kalsam bile uyarılarla birlikte iş değerleri sunmaya devam edeceğim. Ben de bu mini projeye aynı şekilde yaklaşacağım.


Nominal yol üzerindeki sözde algoritma aşağıdakine benzer:


 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


Mantığını yukarıda bahsettiğim aşamaya eşlememiz gerekiyor. Yukarı akış, yeniden yazma ve erişimden önce iki aşama mevcuttur; üç sonra, başlık_filter , body_filter ve log . Erişim aşaması daha önce iş için açık görünüyordu, ancak diğer üç aşama arasında karar vermem gerekiyordu. Body_filter'ı rastgele seçtim, ancak diğer aşamalar için mantıklı argümanları dinlemeye fazlasıyla istekliyim.


Kodu daha okunaklı hale getirmek için günlükleri kaldırdığımı unutmayın. Üretim sorunlarının hata ayıklamasını kolaylaştırmak için hata ve bilgi günlükleri gereklidir.


 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. İdempotency anahtarını istekten çıkarın.
  2. Olası çarpışmaları önlemek için anahtarın önüne ekleyin.
  3. Redis'te depolanan veri kümesini idempotency anahtarı altında alın.
  4. Anahtar bulunamazsa, onu bir boole işaretiyle saklayın.
  5. Özel bir yardımcı program işlevi aracılığıyla bir Lua tablosundaki verileri dönüştürün.
  6. Yanıt, başlıkları hesaba katmak için JSON biçiminde saklanır.
  7. Yanıtı yeniden oluşturun.
  8. Yeniden oluşturulan yanıtı müşteriye geri gönderin. return ifadesine dikkat edin: APISIX daha sonraki yaşam döngüsü aşamalarını atlar.


 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. İdempotency anahtarını istekten çıkarın.


  2. Bir yanıtın farklı unsurlarını bir Lua tablosunda düzenleyin.


  3. JSON kodlu yanıtı bir Redis kümesinde saklayın


Testler beklendiği gibi çalıştığını ortaya koyuyor.


Denemek:

 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


Ayrıca, üçüncü istek için uyumsuzluk anahtarını ( örneğin A yeniden kullanmayı deneyin. Henüz herhangi bir hata yönetimi uygulamadığımız için başka bir istek için önbelleğe alınmış yanıtı alacaksınız. Oyunumuzu geliştirmenin zamanı geldi.

Hata Yollarını Uygulama

Spesifikasyon birkaç hata yolunu tanımlar:


  • Idempotency-Anahtarı eksik.


  • Kimlik Anahtarı zaten kullanılıyor.


  • Bu Kimlik Anahtarı için bekleyen bir istek var


Bunları tek tek uygulayalım. Öncelikle isteğin bir idempotency anahtarı olup olmadığını kontrol edelim. Eklentiyi rota bazında yapılandırabileceğimizi unutmayın; dolayısıyla rota eklentiyi içeriyorsa bunun zorunlu olduğu sonucuna varabiliriz.


 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 -- ...


Anahtar eksikse uygun 400'ü iade etmeniz yeterli. Bu kolaydı.


Mevcut bir anahtarın farklı bir istek için yeniden kullanımının kontrol edilmesi biraz daha karmaşıktır. Öncelikle talebi, daha doğrusu talebi oluşturan şeyin parmak izini saklamamız gerekiyor. İki istek, eğer varsa aynıdır: aynı yöntem, aynı yol, aynı gövde ve aynı başlıklar. Durumunuza bağlı olarak alan adı (ve bağlantı noktası) bunların bir parçası olabilir veya olmayabilir. Basit uygulamam için bunu dışarıda bırakacağım.


Çözülmesi gereken birkaç sorun var. İlk olarak, daha aşina olduğum diğer dillerde olduğu gibi core.request nesnesini hashlemek için mevcut bir API bulamadım, örneğin Java'nın Object.hash() . Nesneyi JSON'da kodlamaya ve dizeyi karma hale getirmeye karar verdim. Ancak mevcut core.request JSON'a dönüştürülemeyen alt öğelere sahiptir. Yukarıda bahsettiğim parçaları çıkarıp tabloyu dönüştürmek zorunda kaldım.


 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. Yalnızca ilgili bölümleri içeren bir tablo oluşturun.


  2. cjson kütüphanesi, üyeleri çeşitli çağrılarda farklı şekilde sıralanabilen JSON üretir. Dolayısıyla farklı karmalarla sonuçlanır. core.json.stably_encode bu sorunu düzeltir.


  3. Vuruldu.


Daha sonra, isteği alırken bir boole depolamak yerine, ortaya çıkan hash'i saklarız.


 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 -- ...


Diğer daldaki idempotency anahtarının altında saklanan hash'i okuyoruz. Eşleşmiyorlarsa ilgili hata koduyla çıkıyoruz:


 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


Nihai hata yönetimi hemen ardından gerçekleşir. Aşağıdaki senaryoyu hayal edin:


  1. Bir istek, idempotency anahtarı X ile birlikte gelir.


  2. Eklenti parmak izlerini alır ve karma değeri Redis'te saklar.


  3. APISIX, isteği yukarı akışa iletir.


  4. Yinelenen bir istek aynı önemsizlik anahtarı X ile birlikte gelir.


  5. Eklenti, Redis'ten gelen verileri okur ve önbelleğe alınmış yanıt bulamaz.


Yukarı akış isteği işlemeyi tamamlamadı; dolayısıyla ilk istek henüz body_filter aşamasına ulaşmadı.


Yukarıdaki kod parçasına aşağıdaki kodu ekliyoruz:


 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


Bu kadar.

Çözüm

Bu yazıda, bir eklenti aracılığıyla Apache APISIX üzerinde Idempotency-Key başlık spesifikasyonunun basit bir uygulamasını gösterdim. Bu aşamada iyileştirmeye açık: otomatik testler, Redis'i rota bazında yapılandırma yeteneği, etki alanını/yolu isteğin parçası olacak şekilde yapılandırma, tek bir örnek yerine bir Redis kümesi yapılandırma, başka bir K/ kullanma V mağazası vb.


Ancak spesifikasyonu uygulamaktadır ve daha üretim düzeyinde bir uygulamaya dönüşme potansiyeline sahiptir.


Bu yazının kaynak kodunun tamamı GitHub'da bulunabilir.


Daha ileri gitmek için:



İlk olarak 7 Nisan 2024'te A Java Geek'te yayınlandı