Tuần trước, tôi đã viết một về . 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: bài phân tích đặc tả IETF Idempotency-Key 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 . Tôi đã quyết định , vì tất cả các plugin xác thực đều có mức độ ưu tiên trong phạm vi 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. bên trong một giai đoạn 1500 2000 Đặ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 . và 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 Cấu hình tuyến tĩnh Đường dẫn đến plugin tương lai của chúng tôi Cổng thông tin chi tiết về Redis (GUI). không cần thiết nhưng rất hữu ích trong quá trình phát triển để gỡ lỗi. Bản chất 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 Định cấu hình APISIX cho cấu hình tuyến tĩnh Định cấu hình vị trí plugin của chúng tôi 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ì Cấu hình plugin phổ biến trên tất cả các tuyến 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 Khai báo plugin mà chúng ta sẽ tạo. 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. 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, 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 trong tệp : cấu hình chung. Hãy bổ sung thêm plugin của chúng tôi: ví dụ: plugin_attr config.yaml 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 Xác định hình dạng của cấu hình 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 đè thành để 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. host redis 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 và giữ nó cho đến hết. 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 Tham khảo chức năng của mô-đun OpenResty Redis. new Gọi nó để lấy một ví dụ. Ứng dụng khách Redis hiện có sẵn trong biến trong suốt phần còn lại của chu kỳ thực thi plugin. redis 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, và ; ba sau, , và . Giai đoạn 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 , 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. viết lại truy cập header_filter body_filter log truy cập body_filter 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 Trích xuất khóa bình thường từ yêu cầu. Tiền tố khóa để chúng tôi tránh xung đột có thể xảy ra. Lấy tập dữ liệu được lưu trữ trong Redis dưới khóa idempotency. Nếu không tìm thấy khóa, hãy lưu nó bằng dấu boolean. Chuyển đổi dữ liệu trong bảng Lua thông qua chức năng tiện ích tùy chỉnh. Phản hồi được lưu trữ ở định dạng JSON để tính đến các tiêu đề. Xây dựng lại phản hồi. 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 : APISIX bỏ qua các giai đoạn sau của vòng đời. return 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 Trích xuất khóa bình thường từ yêu cầu. Sắp xếp các thành phần khác nhau của phản hồi trong bảng Lua. 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, : 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. ví dụ A 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 giống như trong các ngôn ngữ khác mà tôi quen thuộc hơn, : 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, 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. core.request ví dụ Object.hash() core.request 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 Tạo một bảng chỉ có các phần có liên quan. Thư viện 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. khắc phục vấn đề đó. cjson core.json.stably_encode Đã 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: Một yêu cầu đi kèm với khóa bình thường X. Dấu vân tay của plugin và lưu trữ hàm băm trong Redis. APISIX chuyển tiếp yêu cầu lên thượng nguồn. 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. 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 đề 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. Idempotency-Key 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: Trường tiêu đề HTTP khóa Idempotency Sửa các yêu cầu API trùng lặp Phát triển plugin - Trang web APISIX Làm cách nào để xây dựng Plugin Apache APISIX từ 0 đến 1? Được xuất bản lần đầu tại vào ngày 7 tháng 4 năm 2024 A Java Geek