paint-brush
Cách triển khai Đặc tả khóa Idempotency trên Apache APISIXtừ tác giả@nfrankel
337 lượt đọc
337 lượt đọc

Cách triển khai Đặc tả khóa Idempotency trên Apache APISIX

từ tác giả Nicolas Fränkel14m2024/04/11
Read on Terminal Reader

dài quá đọc không nổi

Trong bài đăng này, tôi đã trình bày cách triển khai đơn giản đặc tả tiêu đề Idempotency-Key trên Apache APISIX thông qua một plugin. Ở giai đoạn này, nó còn chỗ để cải thiện: kiểm tra tự động, khả năng định cấu hình Redis trên cơ sở từng tuyến, định cấu hình miền/đường dẫn trở thành một phần của yêu cầu, định cấu hình cụm Redis thay vì một phiên bản duy nhất, sử dụng K/ khác Cửa hàng V, v.v.
featured image - Cách triển khai Đặc tả khóa Idempotency trên Apache APISIX
Nicolas Fränkel HackerNoon profile picture

Tuần trước, tôi đã viết một bài phân tích về đặc tả IETF Idempotency-Key . Thông số kỹ thuật nhằm mục đích tránh các yêu cầu trùng lặp. Nói tóm lại, ý tưởng là khách hàng gửi một khóa duy nhất cùng với yêu cầu:


  • Nếu máy chủ không biết khóa, nó sẽ tiến hành như bình thường và sau đó lưu trữ phản hồi.


  • Nếu máy chủ biết khóa, nó sẽ ngắt mạch mọi quá trình xử lý tiếp theo và ngay lập tức trả về phản hồi đã lưu trữ.


Bài đăng này cho thấy cách triển khai nó với Apache APISIX .

Tổng quan

Trước khi bắt đầu viết mã, chúng ta cần xác định một số điều. Apache APISIX cung cấp kiến trúc dựa trên plugin. Do đó, chúng tôi sẽ mã hóa logic trên trong một plugin.


Apache APISIX được xây dựng dựa trên OpenResty, được xây dựng dựa trên nginx. Mỗi thành phần xác định các giai đoạn, ánh xạ ít nhiều qua các thành phần. Để biết thêm thông tin về các giai đoạn, vui lòng xem bài viết trước này .


Cuối cùng, chúng ta sẽ quyết định mức độ ưu tiên. Mức độ ưu tiên xác định thứ tự APISIX chạy các plugin bên trong một giai đoạn . Tôi đã quyết định 1500 , vì tất cả các plugin xác thực đều có mức độ ưu tiên trong phạm vi 2000 trở lên, nhưng tôi muốn trả về phản hồi được lưu trong bộ nhớ cache càng sớm càng tốt.


Đặc tả yêu cầu chúng ta lưu trữ dữ liệu. APISIX cung cấp nhiều khái niệm trừu tượng, nhưng lưu trữ không phải là một trong số đó. Chúng tôi cần quyền truy cập thông qua khóa bình thường để nó trông giống như một kho lưu trữ khóa-giá trị.


Tôi đã tùy ý chọn Redis vì nó khá phổ biến ứng dụng khách đã là một phần của bản phân phối APISIX. Lưu ý rằng Redis đơn giản không cung cấp bộ lưu trữ JSON; do đó, tôi sử dụng hình ảnh Docker redis-stack .


Cơ sở hạ tầng địa phương như sau:


 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. Cấu hình tuyến tĩnh
  2. Đường dẫn đến plugin tương lai của chúng tôi
  3. Cổng thông tin chi tiết về Redis (GUI). Bản chất không cần thiết nhưng rất hữu ích trong quá trình phát triển để gỡ lỗi.


Cấu hình APISIX như sau:

 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. Định cấu hình APISIX cho cấu hình tuyến tĩnh
  2. Định cấu hình vị trí plugin của chúng tôi
  3. Các plugin tùy chỉnh cần phải được khai báo rõ ràng. Nhận xét ưu tiên là không bắt buộc nhưng là cách thực hành tốt và cải thiện khả năng bảo trì
  4. Cấu hình plugin phổ biến trên tất cả các tuyến
  5. Xem bên dưới


Cuối cùng, chúng tôi khai báo lộ trình duy nhất của mình:

 routes: - uri: /* plugins: idempotency: ~ #1 upstream: nodes: "httpbin.org:80": 1 #2 #END #3
  1. Khai báo plugin mà chúng ta sẽ tạo.
  2. httpbin là một thượng nguồn hữu ích vì chúng ta có thể thử các URI và phương thức khác nhau.
  3. Bắt buộc phải cấu hình các tuyến đường tĩnh!


Với cơ sở hạ tầng này đã có, chúng tôi có thể bắt đầu triển khai.

Bố trí Plugin

Nền tảng của plugin Apache APISIX khá cơ bản:


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


Bước tiếp theo là cấu hình, ví dụ: máy chủ và cổng Redis. Để bắt đầu, chúng tôi sẽ cung cấp một cấu hình Redis duy nhất trên tất cả các tuyến. Đó là ý tưởng đằng sau phần plugin_attr trong tệp config.yaml : cấu hình chung. Hãy bổ sung thêm plugin của chúng tôi:


 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. Xác định hình dạng của cấu hình


  2. Kiểm tra cấu hình có hợp lệ không


Vì tôi đã xác định các giá trị mặc định trong plugin nên tôi chỉ có thể ghi đè host thành redis để chạy bên trong cơ sở hạ tầng Docker Compose của mình và sử dụng cổng mặc định.


Tiếp theo, tôi cần tạo ứng dụng khách Redis. Lưu ý rằng nền tảng ngăn tôi kết nối trong bất kỳ giai đoạn nào sau phần viết lại/truy cập. Do đó, tôi sẽ tạo nó trong phương thức init() và giữ nó cho đến hết.


 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. Tham khảo chức năng new của mô-đun OpenResty Redis.


  2. Gọi nó để lấy một ví dụ.


Ứng dụng khách Redis hiện có sẵn trong biến redis trong suốt phần còn lại của chu kỳ thực thi plugin.

Thực hiện đường dẫn danh nghĩa

Trong cuộc đời kỹ sư phần mềm trước đây của tôi, tôi thường triển khai đường dẫn danh nghĩa trước tiên. Sau đó, tôi làm cho mã trở nên mạnh mẽ hơn bằng cách quản lý từng trường hợp lỗi riêng lẻ. Bằng cách này, nếu tôi phải phát hành vào bất kỳ thời điểm nào, tôi vẫn sẽ mang lại các giá trị kinh doanh - kèm theo cảnh báo. Tôi sẽ tiếp cận dự án nhỏ này theo cách tương tự.


Thuật toán giả trên đường dẫn danh nghĩa trông như sau:


 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


Chúng ta cần ánh xạ logic đến giai đoạn tôi đã đề cập ở trên. Hai giai đoạn có sẵn trước khi ngược dòng, viết lạitruy cập ; ba sau, header_filter , body_filterlog . Giai đoạn truy cập trước đây có vẻ rõ ràng đối với công việc, nhưng tôi cần phải tìm ra giữa ba giai đoạn còn lại. Tôi đã chọn ngẫu nhiên body_filter , nhưng tôi sẵn sàng lắng nghe những lập luận hợp lý cho các giai đoạn khác.


Lưu ý rằng tôi đã xóa nhật ký để mã dễ đọc hơn. Nhật ký lỗi và thông tin là cần thiết để dễ dàng gỡ lỗi các vấn đề sản xuất.


 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. Trích xuất khóa bình thường từ yêu cầu.
  2. Tiền tố khóa để chúng tôi tránh xung đột có thể xảy ra.
  3. Lấy tập dữ liệu được lưu trữ trong Redis dưới khóa idempotency.
  4. Nếu không tìm thấy khóa, hãy lưu nó bằng dấu boolean.
  5. Chuyển đổi dữ liệu trong bảng Lua thông qua chức năng tiện ích tùy chỉnh.
  6. Phản hồi được lưu trữ ở định dạng JSON để tính đến các tiêu đề.
  7. Xây dựng lại phản hồi.
  8. Trả lại phản hồi được xây dựng lại cho khách hàng. Lưu ý câu lệnh return : APISIX bỏ qua các giai đoạn sau của vòng đời.


 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. Trích xuất khóa bình thường từ yêu cầu.


  2. Sắp xếp các thành phần khác nhau của phản hồi trong bảng Lua.


  3. Lưu trữ phản hồi được mã hóa JSON trong bộ Redis


Các thử nghiệm cho thấy nó hoạt động như mong đợi.


Thử:

 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


Ngoài ra, hãy thử sử dụng lại khóa bình thường không khớp, ví dụ : A cho yêu cầu thứ ba. Vì chúng tôi chưa triển khai bất kỳ hoạt động quản lý lỗi nào nên bạn sẽ nhận được phản hồi được lưu trong bộ nhớ đệm cho một yêu cầu khác. Đã đến lúc bắt đầu trò chơi của chúng ta.

Triển khai đường dẫn lỗi

Thông số kỹ thuật xác định một số đường dẫn lỗi:


  • Idempotency-Key bị thiếu.


  • Idempotency-Key đã được sử dụng.


  • Một yêu cầu chưa được xử lý đối với Khóa Idempotency này


Hãy thực hiện từng cái một. Trước tiên, hãy kiểm tra xem yêu cầu có khóa tạm thời hay không. Lưu ý rằng chúng tôi có thể định cấu hình plugin trên cơ sở từng tuyến đường, vì vậy nếu tuyến đường đó bao gồm plugin thì chúng tôi có thể kết luận rằng đó là điều bắt buộc.


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


Chỉ cần trả lại 400 thích hợp nếu thiếu chìa khóa. Cái đó thật dễ dàng.


Việc kiểm tra việc sử dụng lại khóa hiện có cho một yêu cầu khác có liên quan nhiều hơn một chút. Trước tiên, chúng ta cần lưu trữ yêu cầu, hay chính xác hơn là dấu vân tay của những gì cấu thành một yêu cầu. Hai yêu cầu giống nhau nếu chúng có: cùng một phương thức, cùng một đường dẫn, cùng một nội dung và cùng một tiêu đề. Tùy thuộc vào tình huống của bạn, tên miền (và cổng) có thể là một phần của chúng hoặc không. Để thực hiện đơn giản, tôi sẽ bỏ nó đi.


Có một số vấn đề cần giải quyết. Đầu tiên, tôi không tìm thấy API hiện có để băm đối tượng core.request giống như trong các ngôn ngữ khác mà tôi quen thuộc hơn, ví dụ : Object.hash() của Java. Tôi quyết định mã hóa đối tượng bằng JSON và băm chuỗi. Tuy nhiên, core.request hiện tại có các phần tử phụ không thể chuyển đổi thành JSON. Tôi đã phải trích xuất các phần được đề cập ở trên và chuyển đổi bảng.


 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. Tạo một bảng chỉ có các phần có liên quan.


  2. Thư viện cjson tạo ra JSON mà các thành viên của nó có thể được sắp xếp khác nhau qua một số lệnh gọi. Do đó, nó dẫn đến các giá trị băm khác nhau. core.json.stably_encode khắc phục vấn đề đó.


  3. Đã trúng.


Sau đó, thay vì lưu trữ boolean khi nhận được yêu cầu, chúng tôi lưu trữ hàm băm kết quả.


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


Chúng tôi đọc hàm băm được lưu trữ dưới khóa bình thường trên nhánh khác. Nếu chúng không khớp, chúng tôi sẽ thoát với mã lỗi liên quan:


 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


Việc quản lý lỗi cuối cùng diễn ra ngay sau đó. Hãy tưởng tượng kịch bản sau:


  1. Một yêu cầu đi kèm với khóa bình thường X.


  2. Dấu vân tay của plugin và lưu trữ hàm băm trong Redis.


  3. APISIX chuyển tiếp yêu cầu lên thượng nguồn.


  4. Một yêu cầu trùng lặp đi kèm với cùng một khóa tạm thời, X.


  5. Plugin đọc dữ liệu từ Redis và không tìm thấy phản hồi nào được lưu trong bộ nhớ đệm.


Thượng nguồn chưa xử lý xong yêu cầu; do đó, yêu cầu đầu tiên vẫn chưa đến giai đoạn body_filter .


Chúng tôi nối thêm đoạn mã sau vào đoạn mã trên:


 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


Đó là nó.

Phần kết luận

Trong bài đăng này, tôi đã trình bày cách triển khai đơn giản đặc tả tiêu đề Idempotency-Key trên Apache APISIX thông qua một plugin. Ở giai đoạn này, nó còn chỗ để cải thiện: kiểm tra tự động, khả năng định cấu hình Redis trên cơ sở từng tuyến, định cấu hình miền/đường dẫn trở thành một phần của yêu cầu, định cấu hình cụm Redis thay vì một phiên bản duy nhất, sử dụng K/ khác Cửa hàng V, v.v.


Tuy nhiên, nó thực hiện đặc tả kỹ thuật và có tiềm năng phát triển thành một triển khai ở cấp độ sản xuất cao hơn.


Mã nguồn hoàn chỉnh cho bài đăng này có thể được tìm thấy trên GitHub .


Để đi xa hơn:



Được xuất bản lần đầu tại A Java Geek vào ngày 7 tháng 4 năm 2024