From 5012047b2dbedb1b8c4d2b19716dbde806180776 Mon Sep 17 00:00:00 2001 From: tianhao53 Date: Thu, 30 Jul 2026 13:24:54 +0200 Subject: [PATCH 1/2] feat: add error ratio-based circuit breaking policy to api-breaker plugin Add a new `unhealthy-ratio` policy to the api-breaker plugin, using a sliding time window to trip the breaker based on error rate rather than raw consecutive failure counts. This is more resilient to traffic-volume swings than the existing `unhealthy-count` policy, which remains the default and is unchanged for backward compatibility. The breaker now models CLOSED / OPEN / HALF_OPEN states explicitly under the ratio policy: it opens once error rate and minimum request volume thresholds are met within the window, waits out `max_breaker_sec`, then allows a limited number of half-open test calls before fully closing (on sufficient success rate) or reopening (on any failure). This revives apache/apisix#12765, which implemented and addressed all review feedback from @Baoyuantop and @moonming but was auto-closed for inactivity before a final merge decision. Rebased onto current master; no functional changes from the last reviewed revision of that PR. Co-authored-by: tianhao53 --- apisix/plugins/api-breaker.lua | 512 ++++++++++++-- docs/en/latest/plugins/api-breaker.md | 83 ++- docs/zh/latest/plugins/api-breaker.md | 103 ++- t/lib/server.lua | 8 +- t/plugin/api-breaker.t | 56 +- t/plugin/api-breaker2.t | 928 ++++++++++++++++++++++++++ 6 files changed, 1622 insertions(+), 68 deletions(-) create mode 100644 t/plugin/api-breaker2.t diff --git a/apisix/plugins/api-breaker.lua b/apisix/plugins/api-breaker.lua index 440f0ee60940..9b02bf3c0adb 100644 --- a/apisix/plugins/api-breaker.lua +++ b/apisix/plugins/api-breaker.lua @@ -28,6 +28,10 @@ if not shared_buffer then error("failed to get ngx.shared dict when load plugin " .. plugin_name) end +-- Circuit breaker states (only for ratio policy) +local CLOSED = 0 +local OPEN = 1 +local HALF_OPEN = 2 local schema = { type = "object", @@ -61,70 +65,200 @@ local schema = { type = "integer", minimum = 3, default = 300, + description = "Circuit breaker duration in seconds " .. + "(applies to both count and ratio policies)" }, - unhealthy = { - type = "object", - properties = { - http_statuses = { - type = "array", - minItems = 1, - items = { + policy = { + type = "string", + enum = { "unhealthy-count", "unhealthy-ratio" }, + default = "unhealthy-count", + } + }, + required = { "break_response_code" }, + ["if"] = { + properties = { + policy = { + enum = { "unhealthy-count" }, + }, + }, + }, + ["then"] = { + properties = { + unhealthy = { + type = "object", + properties = { + http_statuses = { + type = "array", + minItems = 1, + items = { + type = "integer", + minimum = 500, + maximum = 599, + }, + uniqueItems = true, + default = { 500 } + }, + failures = { type = "integer", - minimum = 500, - maximum = 599, + minimum = 1, + default = 3, + } + }, + default = { http_statuses = { 500 }, failures = 3 } + }, + healthy = { + type = "object", + properties = { + http_statuses = { + type = "array", + minItems = 1, + items = { + type = "integer", + minimum = 200, + maximum = 499, + }, + uniqueItems = true, + default = { 200 } }, - uniqueItems = true, - default = {500} + successes = { + type = "integer", + minimum = 1, + default = 3, + } + }, + default = { http_statuses = { 200 }, successes = 3 } + } + } + }, + ["else"] = { + ["if"] = { + properties = { + policy = { + enum = { "unhealthy-ratio" }, }, - failures = { - type = "integer", - minimum = 1, - default = 3, - } }, - default = {http_statuses = {500}, failures = 3} }, - healthy = { - type = "object", + ["then"] = { properties = { - http_statuses = { - type = "array", - minItems = 1, - items = { - type = "integer", - minimum = 200, - maximum = 499, + unhealthy = { + type = "object", + properties = { + http_statuses = { + type = "array", + minItems = 1, + items = { + type = "integer", + minimum = 500, + maximum = 599, + }, + uniqueItems = true, + default = { 500 } + }, + error_ratio = { + type = "number", + minimum = 0, + maximum = 1, + default = 0.5, + description = "Failure rate threshold to trigger circuit breaker" + }, + min_request_threshold = { + type = "integer", + minimum = 1, + default = 10, + description = "Minimum number of calls before " .. + "circuit breaker can be triggered" + }, + sliding_window_size = { + type = "integer", + minimum = 10, + maximum = 3600, + default = 300, + description = "Size of the sliding window in seconds" + }, + half_open_max_calls = { + type = "integer", + minimum = 1, + maximum = 20, + default = 3, + description = "Number of permitted calls when " .. + "circuit breaker is half-open" + } }, - uniqueItems = true, - default = {200} + default = { + http_statuses = { 500 }, + error_ratio = 0.5, + min_request_threshold = 10, + sliding_window_size = 300, + half_open_max_calls = 3 + } }, - successes = { - type = "integer", - minimum = 1, - default = 3, + healthy = { + type = "object", + properties = { + http_statuses = { + type = "array", + minItems = 1, + items = { + type = "integer", + minimum = 200, + maximum = 499, + }, + uniqueItems = true, + default = { 200 } + }, + success_ratio = { + type = "number", + minimum = 0, + maximum = 1, + default = 0.6, + description = "Success rate threshold to close circuit breaker " .. + "from half-open state" + } + }, + default = { http_statuses = { 200 }, success_ratio = 0.6 } } - }, - default = {http_statuses = {200}, successes = 3} + } } - }, - required = {"break_response_code"}, + } } - +-- Key generation functions (based on latest APISIX version) local function gen_healthy_key(ctx) return "healthy-" .. core.request.get_host(ctx) .. ctx.var.uri end - local function gen_unhealthy_key(ctx) return "unhealthy-" .. core.request.get_host(ctx) .. ctx.var.uri end - local function gen_lasttime_key(ctx) return "unhealthy-lasttime" .. core.request.get_host(ctx) .. ctx.var.uri end +-- New key generation functions for ratio policy +local function gen_state_key(ctx) + return "cb-state-" .. core.request.get_host(ctx) .. ctx.var.uri +end + +local function gen_total_requests_key(ctx) + return "cb-total-" .. core.request.get_host(ctx) .. ctx.var.uri +end + +local function gen_window_start_time_key(ctx) + return "cb-window-" .. core.request.get_host(ctx) .. ctx.var.uri +end + +local function gen_last_state_change_key(ctx) + return "cb-last-change-" .. core.request.get_host(ctx) .. ctx.var.uri +end + +local function gen_half_open_calls_key(ctx) + return "cb-half-open-calls-" .. core.request.get_host(ctx) .. ctx.var.uri +end + +local function gen_half_open_success_key(ctx) + return "cb-half-open-success-" .. core.request.get_host(ctx) .. ctx.var.uri +end local _M = { version = 0.1, @@ -133,13 +267,67 @@ local _M = { schema = schema, } - function _M.check_schema(conf) return core.schema.check(schema, conf) end +-- Circuit breaker state management functions +local function get_circuit_breaker_state(ctx) + local state_key = gen_state_key(ctx) + local state, err = shared_buffer:get(state_key) + if err then + core.log.warn("failed to get circuit breaker state: ", err) + return CLOSED + end + return state or CLOSED +end + +local function set_circuit_breaker_state(ctx, state) + local state_key = gen_state_key(ctx) + local last_change_key = gen_last_state_change_key(ctx) + local current_time = ngx.time() -function _M.access(conf, ctx) + shared_buffer:set(state_key, state) + shared_buffer:set(last_change_key, current_time) + + core.log.info("Circuit breaker state changed to: ", state, " at: ", current_time) +end + +-- Sliding window management +local function reset_sliding_window(ctx, current_time, window_size) + local window_start_key = gen_window_start_time_key(ctx) + local total_requests_key = gen_total_requests_key(ctx) + local unhealthy_key = gen_unhealthy_key(ctx) + + shared_buffer:set(window_start_key, current_time) + shared_buffer:set(total_requests_key, 0) + shared_buffer:set(unhealthy_key, 0) + + -- Only reset sliding window statistics; circuit breaker state is managed separately + -- Do NOT reset circuit breaker state here to maintain proper OPEN/HALF_OPEN behavior + + core.log.info("Sliding window reset at: ", current_time, " window size: ", window_size, "s") +end + +local function check_and_reset_window(ctx, conf) + local current_time = ngx.time() + local window_start_key = gen_window_start_time_key(ctx) + local window_start_time, err = shared_buffer:get(window_start_key) + + if err then + core.log.warn("failed to get window start time: ", err) + return + end + + local window_size = conf.unhealthy.sliding_window_size or 300 + + if not window_start_time or (current_time - window_start_time) >= window_size then + reset_sliding_window(ctx, current_time, window_size) + end +end + +-- Count-based circuit breaker (based on latest APISIX version) +local function count_based_access(conf, ctx) local unhealthy_key = gen_unhealthy_key(ctx) -- unhealthy counts local unhealthy_count, err = shared_buffer:get(unhealthy_key) @@ -195,8 +383,145 @@ function _M.access(conf, ctx) return end +-- Ratio-based circuit breaker +local function ratio_based_access(conf, ctx) + -- Check and reset sliding window first to ensure consistent state + check_and_reset_window(ctx, conf) -function _M.log(conf, ctx) + local current_state = get_circuit_breaker_state(ctx) + local current_time = ngx.time() + + -- Handle OPEN state + if current_state == OPEN then + local last_change_key = gen_last_state_change_key(ctx) + local last_change_time, err = shared_buffer:get(last_change_key) + if err then + core.log.warn("failed to get last change time: ", err) + return conf.break_response_code or 503, + conf.break_response_body or "Service temporarily unavailable" + end + + local wait_duration = conf.max_breaker_sec or 300 + if last_change_time and (current_time - last_change_time) >= wait_duration then + -- Use atomic operation to ensure only one request transitions to HALF_OPEN + local transition_key = "cb-transition-" .. core.request.get_host(ctx) .. ctx.var.uri + local transition_success + transition_success, err = shared_buffer:add(transition_key, 1, 1) + + if err then + core.log.warn("failed to add transition lock: ", err) + end + + if transition_success then + -- Transition to HALF_OPEN + set_circuit_breaker_state(ctx, HALF_OPEN) + -- Reset half-open counters + shared_buffer:set(gen_half_open_calls_key(ctx), 0) + shared_buffer:set(gen_half_open_success_key(ctx), 0) + core.log.info("Circuit breaker transitioned from OPEN to HALF_OPEN") + + -- Clean up transition lock + shared_buffer:delete(transition_key) + return -- Allow this request to pass + else + -- Another request is already transitioning, reject this one + return conf.break_response_code or 503, + conf.break_response_body or "Service temporarily unavailable" + end + else + -- Still in OPEN state, reject request + if conf.break_response_body then + if conf.break_response_headers then + for _, value in ipairs(conf.break_response_headers) do + local val = core.utils.resolve_var(value.value, ctx.var) + core.response.add_header(value.key, val) + end + end + return conf.break_response_code or 503, + conf.break_response_body or "Service temporarily unavailable" + else + return conf.break_response_code or 503 + end + end + end + + -- Handle HALF_OPEN state + if current_state == HALF_OPEN then + local half_open_calls_key = gen_half_open_calls_key(ctx) + local half_open_calls, err = shared_buffer:incr(half_open_calls_key, 1, 0) + if err then + core.log.warn("failed to increment half-open calls: ", err) + end + + local permitted_calls = conf.unhealthy.half_open_max_calls or 3 + if half_open_calls > permitted_calls then + -- Too many calls in half-open state, reject + return conf.break_response_code or 503, + conf.break_response_body or "Service temporarily unavailable" + end + + -- Allow request to pass for evaluation + return + end + + -- CLOSED state - check if we should transition to OPEN + local total_requests_key = gen_total_requests_key(ctx) + local unhealthy_key = gen_unhealthy_key(ctx) + + local total_requests, err = shared_buffer:get(total_requests_key) + if err then + core.log.warn("failed to get total requests: ", err) + return + end + + local unhealthy_count, err = shared_buffer:get(unhealthy_key) + if err then + core.log.warn("failed to get unhealthy count: ", err) + return + end + + if total_requests and unhealthy_count and total_requests > 0 then + local minimum_calls = conf.unhealthy.min_request_threshold or 10 + local failure_threshold = conf.unhealthy.error_ratio or 0.5 + + if total_requests >= minimum_calls then + local failure_rate = unhealthy_count / total_requests + -- Use precise comparison to avoid floating point issues + -- Round to 4 decimal places (0.0001 precision) for reliable comparison + -- This handles floating point precision issues in Lua + local rounded_failure_rate = math.floor(failure_rate * 10000 + 0.5) / 10000 + local rounded_threshold = math.floor(failure_threshold * 10000 + 0.5) / 10000 + + core.log.info("Circuit breaker check - total: ", total_requests, + " failures: ", unhealthy_count, + " rate: ", rounded_failure_rate, + " threshold: ", rounded_threshold) + + if rounded_failure_rate >= rounded_threshold then + -- Transition to OPEN state + set_circuit_breaker_state(ctx, OPEN) + core.log.warn("Circuit breaker OPENED - failure rate: ", rounded_failure_rate, + " >= threshold: ", rounded_threshold) + return conf.break_response_code or 503, + conf.break_response_body or "Service temporarily unavailable" + end + end + end + + return +end + +function _M.access(conf, ctx) + if conf.policy == "unhealthy-ratio" then + return ratio_based_access(conf, ctx) + else + -- Default to count-based (unhealthy-count) + return count_based_access(conf, ctx) + end +end + +-- Count-based logging (based on latest APISIX version) +local function count_based_log(conf, ctx) local unhealthy_key = gen_unhealthy_key(ctx) local healthy_key = gen_healthy_key(ctx) local upstream_status = core.response.get_upstream_status(ctx) @@ -264,4 +589,107 @@ function _M.log(conf, ctx) return end +-- Ratio-based logging +local function ratio_based_log(conf, ctx) + local upstream_status = core.response.get_upstream_status(ctx) + if not upstream_status then + return + end + + local current_state = get_circuit_breaker_state(ctx) + + -- Increment total request counter + local total_requests_key = gen_total_requests_key(ctx) + local total_requests, err = shared_buffer:incr(total_requests_key, 1, 0) + if err then + core.log.warn("failed to increment total requests: ", err) + end + + -- Handle response based on status + local is_failure = core.table.array_find(conf.unhealthy.http_statuses, upstream_status) + local is_success = not is_failure and + core.table.array_find(conf.healthy.http_statuses, upstream_status) + + if is_failure then + -- Increment failure counter + local unhealthy_key = gen_unhealthy_key(ctx) + local unhealthy_count, err = shared_buffer:incr(unhealthy_key, 1, 0) + if err then + core.log.warn("failed to increment unhealthy count: ", err) + end + + core.log.info("Request failed - status: ", upstream_status, + " total: ", total_requests, + " failures: ", unhealthy_count) + + -- If in HALF_OPEN state and got a failure, immediately go back to OPEN + if current_state == HALF_OPEN then + set_circuit_breaker_state(ctx, OPEN) + core.log.warn("Circuit breaker returned to OPEN state due to failure in HALF_OPEN") + -- Clean up half-open counters + shared_buffer:delete(gen_half_open_calls_key(ctx)) + shared_buffer:delete(gen_half_open_success_key(ctx)) + end + elseif is_success then + core.log.info("Request succeeded - status: ", upstream_status, " total: ", total_requests) + + -- Handle HALF_OPEN state success + if current_state == HALF_OPEN then + local half_open_success_key = gen_half_open_success_key(ctx) + local success_count, err = shared_buffer:incr(half_open_success_key, 1, 0) + if err then + core.log.warn("failed to increment half-open success count: ", err) + end + + local half_open_calls_key = gen_half_open_calls_key(ctx) + local total_calls, err = shared_buffer:get(half_open_calls_key) + if err then + core.log.warn("failed to get half-open calls count: ", err) + return + end + + local permitted_calls = conf.unhealthy.half_open_max_calls or 3 + if total_calls and total_calls >= permitted_calls then + -- Check success rate threshold + local success_ratio = 0.6 -- Default value + if conf.healthy and conf.healthy.success_ratio then + success_ratio = conf.healthy.success_ratio + end + + local success_rate = success_count / total_calls + if success_rate >= success_ratio then + -- Transition back to CLOSED state + set_circuit_breaker_state(ctx, CLOSED) + core.log.info("Circuit breaker transitioned from HALF_OPEN to CLOSED - " .. + "success rate: ", success_rate, " >= threshold: ", success_ratio) + + -- Clean up all counters for fresh start + shared_buffer:delete(gen_half_open_calls_key(ctx)) + shared_buffer:delete(gen_half_open_success_key(ctx)) + shared_buffer:delete(gen_unhealthy_key(ctx)) + shared_buffer:delete(gen_total_requests_key(ctx)) + shared_buffer:delete(gen_window_start_time_key(ctx)) + else + -- Success rate too low, return to OPEN state + set_circuit_breaker_state(ctx, OPEN) + core.log.warn("Circuit breaker returned to OPEN state - success rate: ", + success_rate, " < threshold: ", success_ratio) + -- Clean up half-open counters + shared_buffer:delete(gen_half_open_calls_key(ctx)) + shared_buffer:delete(gen_half_open_success_key(ctx)) + end + end + end + end +end + +function _M.log(conf, ctx) + if conf.policy == "unhealthy-ratio" then + ratio_based_log(conf, ctx) + else + -- Default to count-based (unhealthy-count) + count_based_log(conf, ctx) + end +end + return _M diff --git a/docs/en/latest/plugins/api-breaker.md b/docs/en/latest/plugins/api-breaker.md index 3f2384323aff..a9ef94a55b7e 100644 --- a/docs/en/latest/plugins/api-breaker.md +++ b/docs/en/latest/plugins/api-breaker.md @@ -30,14 +30,31 @@ description: This document describes the information about the Apache APISIX api The `api-breaker` Plugin implements circuit breaker functionality to protect Upstream services. +This plugin supports two circuit breaker policies: + +- **Failure count-based circuit breaking (`unhealthy-count`)**: Triggers circuit breaker when consecutive failure count reaches the threshold +- **Error ratio-based circuit breaking (`unhealthy-ratio`)**: Triggers circuit breaker when error rate within a sliding time window reaches the threshold + :::note +**Failure count-based circuit breaking (`unhealthy-count`)**: + Whenever the Upstream service responds with a status code from the configured `unhealthy.http_statuses` list for the configured `unhealthy.failures` number of times, the Upstream service will be considered unhealthy. The request is then retried in 2, 4, 8, 16 ... seconds until the `max_breaker_sec`. In an unhealthy state, if the Upstream service responds with a status code from the configured list `healthy.http_statuses` for `healthy.successes` times, the service is considered healthy again. +**Error ratio-based circuit breaking (`unhealthy-ratio`)**: + +This policy is based on sliding time window statistics for error rate. When the total number of requests reaches `min_request_threshold` and the error rate exceeds `error_ratio` within the `sliding_window_size` time window, the circuit breaker enters the open state for `max_breaker_sec` seconds. + +The circuit breaker has three states: + +- **CLOSED**: Normal request forwarding +- **OPEN**: Directly returns circuit breaker response without forwarding requests +- **HALF_OPEN**: Allows a limited number of requests to test if the service has recovered + ::: ## Attributes @@ -47,15 +64,35 @@ In an unhealthy state, if the Upstream service responds with a status code from | break_response_code | integer | True | | [200, ..., 599] | HTTP error code to return when Upstream is unhealthy. | | break_response_body | string | False | | | Body of the response message to return when Upstream is unhealthy. | | break_response_headers | array[object] | False | | [{"key":"header_name","value":"can contain Nginx $var"}] | Headers of the response message to return when Upstream is unhealthy. Can only be configured when the `break_response_body` attribute is configured. The values can contain APISIX variables. For example, we can use `{"key":"X-Client-Addr","value":"$remote_addr:$remote_port"}`. | -| max_breaker_sec | integer | False | 300 | >=3 | Maximum time in seconds for circuit breaking. | +| max_breaker_sec | integer | False | 300 | >=3 | Maximum time in seconds for circuit breaking. Applies to both circuit breaker policies. | +| policy | string | False | "unhealthy-count" | ["unhealthy-count", "unhealthy-ratio"] | Circuit breaker policy. `unhealthy-count` for failure count-based circuit breaking, `unhealthy-ratio` for error ratio-based circuit breaking. | + +### Failure count-based circuit breaking (policy = "unhealthy-count") + +| Name | Type | Required | Default | Valid values | Description | +|-------------------------|----------------|----------|---------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | unhealthy.http_statuses | array[integer] | False | [500] | [500, ..., 599] | Status codes of Upstream to be considered unhealthy. | | unhealthy.failures | integer | False | 3 | >=1 | Number of failures within a certain period of time for the Upstream service to be considered unhealthy. | | healthy.http_statuses | array[integer] | False | [200] | [200, ..., 499] | Status codes of Upstream to be considered healthy. | | healthy.successes | integer | False | 3 | >=1 | Number of consecutive healthy requests for the Upstream service to be considered healthy. | +### Error ratio-based circuit breaking (policy = "unhealthy-ratio") + +| Name | Type | Required | Default | Valid values | Description | +|-------------------------|----------------|----------|---------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| unhealthy.http_statuses | array[integer] | False | [500] | [500, ..., 599] | Status codes of Upstream to be considered unhealthy. | +| unhealthy.error_ratio | number | False | 0.5 | [0, 1] | Error rate threshold to trigger circuit breaker. For example, 0.5 means circuit breaker triggers when error rate reaches 50%. | +| unhealthy.min_request_threshold | integer | False | 10 | >=1 | Minimum number of requests required within the sliding window to trigger circuit breaker. Circuit breaker will only evaluate error rate when request count reaches this threshold. | +| unhealthy.sliding_window_size | integer | False | 300 | [10, 3600] | Size of the sliding window in seconds. The time range used to calculate error rate. | +| unhealthy.half_open_max_calls | integer | False | 3 | [1, 20] | Number of permitted calls when circuit breaker is in half-open state. Used to test if the service has recovered. | +| healthy.http_statuses | array[integer] | False | [200] | [200, ..., 499] | Status codes of Upstream to be considered healthy. | +| healthy.success_ratio | number | False | 0.6 | [0, 1] | Success rate threshold to close circuit breaker from half-open state. For example, 0.6 means circuit breaker closes when success rate reaches 60%. | + ## Enable Plugin -The example below shows how you can configure the Plugin on a specific Route: +### Failure count-based circuit breaking example + +The example below shows how you can configure the Plugin with failure count-based circuit breaking policy on a specific Route: :::note You can fetch the `admin_key` from `config.yaml` and save to an environment variable with the following command: @@ -73,6 +110,7 @@ curl "http://127.0.0.1:9180/apisix/admin/routes/1" \ "plugins": { "api-breaker": { "break_response_code": 502, + "policy": "unhealthy-count", "unhealthy": { "http_statuses": [500, 503], "failures": 3 @@ -95,6 +133,47 @@ curl "http://127.0.0.1:9180/apisix/admin/routes/1" \ In this configuration, a response code of `500` or `503` three times within a certain period of time triggers the unhealthy status of the Upstream service. A response code of `200` restores its healthy status. +### Error ratio-based circuit breaking example + +The example below shows how to enable error ratio-based circuit breaking policy. This configuration triggers circuit breaker when the request count reaches 10 and error rate exceeds 50% within a 5-minute sliding window: + +```shell +curl "http://127.0.0.1:9180/apisix/admin/routes/2" \ +-H "X-API-KEY: $admin_key" -X PUT -d ' +{ + "plugins": { + "api-breaker": { + "break_response_code": 503, + "break_response_body": "Service temporarily unavailable due to high error rate", + "break_response_headers": [ + {"key": "X-Circuit-Breaker", "value": "open"}, + {"key": "Retry-After", "value": "60"} + ], + "policy": "unhealthy-ratio", + "max_breaker_sec": 60, + "unhealthy": { + "http_statuses": [500, 502, 503, 504], + "error_ratio": 0.5, + "min_request_threshold": 10, + "sliding_window_size": 300, + "half_open_max_calls": 3 + }, + "healthy": { + "http_statuses": [200, 201, 202], + "success_ratio": 0.6 + } + } + }, + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "uri": "/api" +}' +``` + ## Example usage Once you have configured the Plugin as shown above, you can test it out by sending a request. diff --git a/docs/zh/latest/plugins/api-breaker.md b/docs/zh/latest/plugins/api-breaker.md index 5183f84763e7..9aa987de2ec1 100644 --- a/docs/zh/latest/plugins/api-breaker.md +++ b/docs/zh/latest/plugins/api-breaker.md @@ -6,7 +6,6 @@ keywords: - API Breaker description: 本文介绍了 Apache APISIX api-breaker 插件的相关操作,你可以使用此插件的 API 熔断机制来保护上游业务服务。 --- -