<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Higress on 潘达窝</title><link>https://daidaij.github.io/tags/higress/</link><description>Recent content in Higress on 潘达窝</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><copyright>pandazhangs</copyright><lastBuildDate>Sat, 01 Aug 2026 23:44:36 +0800</lastBuildDate><atom:link href="https://daidaij.github.io/tags/higress/index.xml" rel="self" type="application/rss+xml"/><item><title>Higress 网关 LLM 端点故障临时屏蔽（Endpoint Failover）</title><link>https://daidaij.github.io/p/higress_llm_server_failover_in_router/</link><pubDate>Sat, 01 Aug 2026 23:44:36 +0800</pubDate><guid>https://daidaij.github.io/p/higress_llm_server_failover_in_router/</guid><description>&lt;img src="https://picsum.photos/seed/1ba03f14/800/600" alt="Featured image of post Higress 网关 LLM 端点故障临时屏蔽（Endpoint Failover）" />&lt;h1 id="higress-网关-llm-端点故障临时屏蔽endpoint-failover">Higress 网关 LLM 端点故障临时屏蔽（Endpoint Failover）
&lt;/h1>&lt;hr>
&lt;blockquote>
&lt;p>给 LLM 网关做选端的时候碰到一个绕不开的问题：供应商端点的故障大多是&amp;quot;业务性&amp;quot;的——429 限流、配额耗尽、5xx，连接层完全正常，Envoy Outlier Detection 这种平台健康检查根本发现不了。所以在 ai-load-balancer 选端前加了一层黑名单过滤器：失败端点暂时打入冷宫，惩罚期过了再放回候选池。整个设计最核心的理念是软屏蔽——被屏蔽不等于不可用，只是低优先级。&lt;/p>
&lt;/blockquote>
&lt;h2 id="需求拆解">需求拆解
&lt;/h2>&lt;hr>
&lt;p>LLM 端点故障和普通微服务不一样：故障持续时间从几十秒到几分钟，期间所有请求都挂在超时上；同一个集群里 A 挂了，B、C 还活着，需要快速改道；429 限流是常态而不是异常，但连续 429 说明端点已经过载。&lt;/p>
&lt;p>需求拆出来五条规则：&lt;/p>
&lt;ul>
&lt;li>R1 只有上游真实响应才算失败（网关自己拦的不算）；&lt;/li>
&lt;li>R2 连续失败 ≥ 阈值进屏蔽名单，窗口内不参与选端；&lt;/li>
&lt;li>R3 过滤后候选集为空 → 不覆盖端点，走 Envoy 默认选端；&lt;/li>
&lt;li>R4 候选集非空时按负载策略从候选集选最优；&lt;/li>
&lt;li>R5 TTL到期惰性恢复，可选健康检查加速。&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>R3 是这个设计最反直觉的地方。多数熔断在&amp;quot;全被屏蔽&amp;quot;时会直接吐 503，这里把控制权还给 Envoy 默认路由。我认同这个取舍：网关的职责是转发，不是裁决。宁可流量打到可能已恢复的端点，也不主动拒绝。&lt;/p>
&lt;/blockquote>
&lt;h2 id="核心流程">核心流程
&lt;/h2>&lt;hr>
&lt;p>一条请求走四个阶段：请求头阶段 &lt;code>DisableReroute()&lt;/code> 锁死路由，保留选端控制权；请求体阶段拿到 &lt;code>GetUpstreamHosts()&lt;/code> 的健康端点，过滤屏蔽名单；响应头阶段判定成败；流结束阶段兜底。&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;span class="lnt">7
&lt;/span>&lt;span class="lnt">8
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">请求头 DisableReroute() 锁死路由，保留选端控制权
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">请求体 GetUpstreamHosts() → 过滤健康端点
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → 读屏蔽名单（本地缓存 1s TTL，过期才拉 SharedData）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → candidates = healthy − blocked
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → 空？不覆盖走默认（R3） ｜ 非空？按策略选最优并覆盖
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">响应头 IsResponseFromUpstream() 且命中失败码 → 计数 +1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> → 连续失败 ≥ 阈值 → 写屏蔽名单（blockUntil = now + 60s ± 抖动）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">流结束 正常结束清零计数；异常断流按失败处理
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>选端放在请求体阶段是关键：在建立上游连接之前就把坏端点过滤掉，故障的代价从&amp;quot;超时级&amp;quot;降到&amp;quot;选端级&amp;quot;。失败判定放在响应头阶段，此时请求已结束，不增加时延。&lt;/p>
&lt;h2 id="状态机端点怎么被打入冷宫">状态机：端点怎么被&amp;quot;打入冷宫&amp;quot;
&lt;/h2>&lt;hr>
&lt;p>以 consecutive 模式（阈值 3、屏蔽 60s）为例：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;span class="lnt">6
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">T0 正常，FailCount=0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">T1 503 → FailCount=1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">T2 503 → FailCount=2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">T3 503 → FailCount=3 → 触发！BlockUntil = now + 60s + 随机±6s，计数清零
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">T4~T63 选端时在屏蔽名单里，跳过
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">T63+ 到期，惰性恢复，回到候选池
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>触发判定核心就几行：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">FailCount&lt;/span> &lt;span class="o">&amp;gt;=&lt;/span> &lt;span class="nx">failureThreshold&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nx">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">BlockUntil&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">now&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">blockDurationMs&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nf">jitter&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1">// ← 抖动防同步解封
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="nx">b&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">FailCount&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="c1">// 恢复后从零重新计数
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;blockquote>
&lt;p>触发后清零 FailCount，这个细节我琢磨了一下：不清零的话，历史失败会一直累积，端点永远达不到新阈值，等于屏蔽一次就永久拉黑。但清零也带来盲区——间歇性抖动（偶发 429 后连续成功）永远不会触发，所以设计里还有 window 触发模式（60s 内失败 ≥ 5 次）和 rate 模式（窗口失败率 ≥ 50%）补这个洞。&lt;/p>
&lt;/blockquote>
&lt;p>两个实现细节值得展开：&lt;/p>
&lt;p>&lt;strong>CAS 并发安全&lt;/strong>。wasm 每个 worker 一个 VM，多个 VM 可能同时判定&amp;quot;该屏蔽 A&amp;quot;。屏蔽状态必须走 &lt;code>GetSharedData → 修改 → SetSharedData(cas)&lt;/code> 原子循环，冲突重试上限 10 次，超限降级为本地计数。这套模式在 ai-proxy 的 apiToken failover 里已经跑过一遍，代码直接复用。&lt;/p>
&lt;p>&lt;strong>本地缓存 + TTL&lt;/strong>。热路径上每请求读一次 SharedData 撑不住，每个 VM 缓存屏蔽名单快照（默认 1s TTL），选端时直接用内存副本。代价是屏蔽生效有最多 1s 延迟——性能和生效速度的权衡，对企业场景是合理折中。&lt;/p>
&lt;h2 id="软屏蔽全被屏蔽时不覆盖">软屏蔽：全被屏蔽时不覆盖
&lt;/h2>&lt;hr>
&lt;p>选端逻辑压成一行公式：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">candidates = GetUpstreamHosts().healthy − blocked
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">selected = LoadBalancer.Select(candidates)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>候选集非空 → 按 least_busy / global_least_request / prefix_cache 从候选集里选最优；候选集为空 → 不调 &lt;code>SetUpstreamOverrideHost&lt;/code>，Envoy 默认路由接管，流量仍可能打到被屏蔽端点。&lt;/p>
&lt;p>软屏蔽带来三个收益：全死锁时至少还有流量能过去；刚恢复的端点能立即承载流量；屏蔽判定有误时，流量不会被完全阻断。思路和 Envoy Outlier Detection 的硬剔除（全剔除时也回退）一致，但 wasm 层能叠加业务规则——比如按 model 维度屏蔽：端点 A 的 qwen-max 稳定但 gpt-4 超配额，只屏蔽 gpt-4。&lt;/p>
&lt;blockquote>
&lt;p>一个没想通的地方：候选集为空时流量均匀撒向所有端点（包括坏的），失败率保持高位，直到某个端点恢复。设计没有做&amp;quot;全屏蔽时按最近成功率挑一个最可能活的&amp;quot;这类优化。可能因为收益不确定，不值得为极端场景加复杂度。&lt;/p>
&lt;/blockquote>
&lt;h2 id="失败判定网关的拦截不算数">失败判定：网关的拦截不算数
&lt;/h2>&lt;hr>
&lt;p>R1 是最容易踩坑的地方：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="cl">&lt;span class="k">if&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="nx">wrapper&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">IsResponseFromUpstream&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="c1">// 网关自己拦的不算
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="k">return&lt;/span> &lt;span class="nx">types&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ActionContinue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>waf 拦截的 403、限流返回的 429、管理接口的响应，都是网关自身行为，不是上游真实反应。把这些计入失败，健康端点会被误判成故障。&lt;/p>
&lt;p>设计文档点出一个更隐蔽的问题：插件执行顺序会让 &lt;code>IsResponseFromUpstream()&lt;/code> 不可靠。限流插件如果在 failover 之后执行，限流 429 从 failover 视角看就是&amp;quot;来自上游&amp;quot;。对策是让限流插件注入 &lt;code>X-Rate-Limited: true&lt;/code> 头做二级判定；健康检查子请求也要打标记跳过判定，否则探活请求的 503 会不断延长屏蔽时间，端点永远无法恢复。&lt;/p>
&lt;blockquote>
&lt;p>判定污染在插件体系里很常见：每个插件只看到自己阶段的响应，不知道响应是别人伪造的。用响应头标记做显式契约，比赌执行顺序可靠。&lt;/p>
&lt;/blockquote>
&lt;h2 id="恢复机制不引发二次故障">恢复机制：不引发二次故障
&lt;/h2>&lt;hr>
&lt;p>恢复期最容易出事——所有端点同时解封 → 瞬时过载 → 再屏蔽，形成同步震荡。设计里堆了几层防护：&lt;code>blockUntil&lt;/code> 加随机抖动（±10%）打散解封时刻；健康检查需连续成功 ≥ successThreshold 次防振荡；租约选主只让一个 VM 发探活，最小模型降成本；可选指数退避，连续多次屏蔽时窗口倍增 60s→120s→240s。&lt;/p>
&lt;p>恢复有两条路径：惰性恢复零成本但最慢，等 60s 自然到期；主动健康检查用最小模型发探活请求，连续成功就提前解封，代价是额外计算资源。&lt;/p>
&lt;blockquote>
&lt;p>惰性恢复的实现是&amp;quot;选端时发现过期即清除&amp;quot;，不需要任何定时器，恢复完全被请求驱动。这个设计我喜欢，零开销。&lt;/p>
&lt;/blockquote>
&lt;h2 id="观测设计">观测设计
&lt;/h2>&lt;hr>
&lt;p>需求明确要求&amp;quot;完整支持失败次数与时间点的可观测性&amp;quot;，四路输出：Prometheus 指标（Counter 累计失败/屏蔽/恢复，Gauge 实时状态/剩余时间，Histogram 屏蔽时长）、访问日志（endpoint_selected、endpoint_blocked、endpoint_fail_count）、链路 span 标签、admin 查询接口（&lt;code>/_internal/_higress/endpoint-failover/status&lt;/code> 返回 JSON）。&lt;/p>
&lt;p>数据模型上，每个端点维护：连续失败数、累计失败数、滑动窗口失败事件序列（上限 20 条），外加最近失败/成功/进入屏蔽/到期/恢复五个时间点。剩余屏蔽时间、失败率这类派生指标不存储，查询时现算。&lt;/p>
&lt;p>观测后端可配置。Prometheus 方案是 VM 本地指标，跨实例靠采集端聚合；Redis 方案复用限流插件已有的 RedisClient，ZSet 存事件流（score=时间戳），跨 VM 跨实例天然一致，管理接口直接读 JSON，不用 PromQL。&lt;/p>
&lt;blockquote>
&lt;p>Redis 方案能覆盖 Prometheus 的全部观测语义，差别只在&amp;quot;谁来看&amp;quot;——Prometheus 由采集端拉取，Redis 由查询接口消费。对没有 Prometheus 基础设施的企业，Redis 反而是更省事的选项。&lt;/p>
&lt;/blockquote>
&lt;h2 id="与现有能力的分层">与现有能力的分层
&lt;/h2>&lt;hr>
&lt;p>这个设计不是孤岛，和企业网关已有的能力叠成五层：&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;span class="lnt">5
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">第 1 层：连接/传输健康 Envoy Outlier Detection（平台级，硬剔除） —— 管&amp;#34;连不上&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">第 2 层：业务健康判定 ★ 端点软屏蔽（本设计） —— 管&amp;#34;业务异常/限流&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">第 3 层：请求重试 ai-proxy retryOnFailure（换 token 重试） —— 管&amp;#34;单请求偶发失败&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">第 4 层：认证可用性 ai-proxy apiToken failover（token 隔离） —— 管&amp;#34;key 失效&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">第 5 层：容量保护 ai-token-ratelimit / ai-quota —— 管&amp;#34;超卖&amp;#34;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>本设计管&amp;quot;换端点&amp;quot;（这个后端坏了换那个），ai-proxy 的 failover 管&amp;quot;换 token&amp;quot;（这个 key 没额度了换那个），两层正交可叠加。端点全挂时，ai-proxy 的重试自然兜底。&lt;/p>
&lt;blockquote>
&lt;p>分层的关键是别重复实现平台已有的能力。连接级健康判定交给 Envoy，这里专注 LLM 业务语义——429、配额耗尽、业务错误码，这些平台健康检查永远管不到。&lt;/p>
&lt;/blockquote>
&lt;h2 id="边界情况踩过的坑提前排雷">边界情况：踩过的坑提前排雷
&lt;/h2>&lt;hr>
&lt;p>设计文档列了十几个 Bad Case，挑几个有代表性的：&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Bad Case&lt;/th>
&lt;th>根因&lt;/th>
&lt;th>预防&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>限流 429 误判为上游故障&lt;/td>
&lt;td>&lt;code>IsResponseFromUpstream()&lt;/code> 受插件执行顺序影响&lt;/td>
&lt;td>限流插件注入 &lt;code>X-Rate-Limited&lt;/code> 头&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>健康检查自身触发熔断&lt;/td>
&lt;td>探活请求的 503 进入失败判定，屏蔽无限延长&lt;/td>
&lt;td>健康检查请求打标记，跳过判定&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>单端点集群死锁&lt;/td>
&lt;td>唯一端点被屏蔽后请求全打它，失败又延长屏蔽&lt;/td>
&lt;td>单端点时不延长 BlockUntil&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>熔断误伤无法恢复&lt;/td>
&lt;td>误配 &lt;code>failoverOnStatus&lt;/code> 导致全端点被屏蔽&lt;/td>
&lt;td>Admin 接口 &lt;code>unblock&lt;/code> / &lt;code>unblock-all&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Redis 分区后本地不一致&lt;/td>
&lt;td>各 VM 独立判定，屏蔽状态分裂&lt;/td>
&lt;td>本地模式降阈值 + 短屏蔽时间&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>贯穿始终还有两条原则：一是 fail-open，&lt;code>GetUpstreamHosts&lt;/code> / &lt;code>SetSharedData&lt;/code> 任一出错就记日志走默认路由，熔断自身故障不能阻断流量；二是可回滚，&lt;code>enabled: false&lt;/code> 热更新即全量直通，&lt;code>failoverOnStatus: []&lt;/code> 让失败判定永不触发，已有屏蔽自然到期。&lt;/p>
&lt;h2 id="总结">总结
&lt;/h2>&lt;hr>
&lt;p>把&amp;quot;换端点&amp;quot;做成可观测、可回滚、fail-open 的特性，而不是在 ai-load-balancer 里硬编码一个熔断。但离开箱即用还有距离：本地缓存一致性窗口、CAS 写放大、Redis 故障降级，这些参数需要在自己环境中靠压测和灰度慢慢调。&lt;/p>
&lt;p>如果要上生产，需要多关注三个点：多副本网关下 SharedData 只覆盖单实例，所以靠不住，屏蔽状态全局一致得靠 Redis；window 触发模式的失败事件序列存 SharedData 做 CAS 更新，跨 VM 竞争比单纯计数激烈得多，也许把窗口判定直接交给 Redis 的 &lt;code>ZCount&lt;/code> 才是正解；上线前对 &lt;code>failureThreshold&lt;/code> / &lt;code>blockDurationMs&lt;/code> 做灰度验证，误配 &lt;code>failoverOnStatus&lt;/code> 的代价是全端点被拉黑。甚至可以考虑在上述设计逻辑中为特殊的上游端点配置白名单，可以是从缓存里面拿去，或者是从按配置的正则来匹配&lt;/p></description></item></channel></rss>