标签: Envoy

  • 深入 API 网关限流与熔断:从 Token Bucket 突发击穿看 Envoy 熔断器状态抖动排查实战

    结论先行:网关层单一使用 Token Bucket 限流,极易因 burst(突发)参数配置过大导致下游在流量毛刺下被击穿。某次排查发现,瞬间高并发耗尽令牌桶后直接透传,打挂了后端服务,进而引发 Envoy v1.27.0 熔断器(Outlier Detection)频繁弹射健康节点,触发 Panic 路由机制导致全局雪崩。核心解法:引入 Leaky Bucket 平滑流量,并精确调优 Envoy 驱逐窗口与熔断阈值。

    案发现场:P99 飙升与诡异的 503 UO 报错

    排查某核心交易链路问题时,监控大盘显示网关入口 QPS 平稳维持在 3000 左右,但 P99 延迟却在某些瞬间飙升至 4000ms 以上。紧接着,下游服务开始大面积报警,CPU 使用率出现锯齿状波动,Load Average 瞬间飙升至宿主机核心数的 3 倍。

    抓取入口网关与 Sidecar 的日志,发现海量的 503 报错。提取关键的 Envoy (v1.27.0) Access Log:

    {
      "response_code": "503",
      "response_flags": "UO",
      "upstream_cluster": "outbound|8080||order-svc",
      "duration": "2",
      "upstream_service_time": null
    }
    

    注意这里的 Response Flag UO (Upstream Overflow)。在 Envoy 的语义中,UO 意味着请求不仅没有到达后端应用代码,甚至连连接池都没建起来,直接被 Envoy 的 Circuit Breaker 拦截了。但进一步看,日志中还夹杂着大量 UC (Upstream Connection Termination) 和 503 URX (Upstream Retry Limit Exceeded)。

    这就很有意思了:流量大盘是平稳的,网关层配置了 5000 QPS 的全局限流,按理说后端集群(20个 Pod,单 Pod 容量 300 QPS)完全吃得消,为什么会被打出 UOUC

    为什么 Token Bucket 算法无法应对瞬间毛刺流量?

    排查网关层的分布式限流实现,发现业务研发基于 Redis + Lua 实现了一个标准的 Token Bucket(令牌桶)算法。核心 Lua 脚本片段如下:

    -- KEYS[1]: rate_limit_key
    -- ARGV[1]: capacity (桶容量)
    -- ARGV[2]: rate (每秒生成令牌数)
    -- ARGV[3]: current_timestamp (当前时间戳)
    local capacity = tonumber(ARGV[1])
    local rate = tonumber(ARGV[2])
    local now = tonumber(ARGV[3])
    
    local last_time = tonumber(redis.call('hget', KEYS[1], 'last_time') or '0')
    local current_tokens = tonumber(redis.call('hget', KEYS[1], 'tokens') or capacity)
    
    -- 计算这期间生成的令牌
    local delta_tokens = math.floor((now - last_time) * rate)
    local tokens = math.min(capacity, current_tokens + delta_tokens)
    
    if tokens > 0 then
        redis.call('hset', KEYS[1], 'tokens', tokens - 1)
        redis.call('hset', KEYS[1], 'last_time', now)
        return 1 -- 放行
    else
        return 0 -- 限流
    end
    

    当时的配置是:capacity = 2000rate = 1000。 这就是典型的防御盲区。Token Bucket 的核心特性是允许突发流量(Burst)。如果系统在过去 2 秒内极其空闲,桶里积攒了 2000 个令牌。此时一个瞬间的流量毛刺(Microburst)打过来,这 2000 个请求会在 10 毫秒 内全部被网关放行,直接砸向后端。

    对于后端来说,这不是 1000 QPS,这是瞬时 2000 / 0.01s = 200,000 QPS 的冲击。 微服务的连接池瞬间被打满,TCP Accept Queue 溢出,导致部分请求超时(产生 504/503)。

    如果是 Leaky Bucket(漏桶) 算法,由于其恒定速率流出的特性(类似 Nginx 的 limit_req 且不带 nodelay),这 2000 个请求会被强制在队列中排队,以绝对平滑的 1000 QPS 速率向后端转发,起到真正的削峰填谷作用。

    Envoy 熔断器(Outlier Detection)的雪崩效应

    流量毛刺击穿网关后,真正的灾难在 Envoy 代理层爆发。微服务由于瞬时过载,部分 Pod 开始返回 5xx 错误或连接超时。Envoy 的 Outlier Detection(异常点检测)机制被触发。

    当时配置的 DestinationRule 如下:

    apiVersion: networking.istio.io/v1alpha3
    kind: DestinationRule
    metadata:
      name: order-svc-dr
    spec:
      host: order-svc
      trafficPolicy:
        connectionPool:
          http:
            http1MaxPendingRequests: 1024
            maxRequestsPerConnection: 100
        outlierDetection:
          consecutive5xxErrors: 3
          interval: 10s
          baseEjectionTime: 30s
          maxEjectionPercent: 50
    

    当突发流量导致某几个 Pod 连续返回 3 个 5xx 时,Envoy 毫不犹豫地将它们拉黑(Eject)30 秒。 随着被拉黑的 Pod 越来越多(很快达到了 maxEjectionPercent: 50% 的上限),剩余 50% 的 Pod 必须承受全部流量,瞬间雪崩。

    更致命的是,当 Envoy 发现健康后端节点比例低于 Panic Threshold(默认 50%)时,会触发恐慌路由(Panic Routing)。Envoy 会认为:“既然健康检查机制可能出错了,那我就无视驱逐状态,把流量均匀分发给所有节点”。 于是,处于假死状态的 Pod 再次迎来海量流量,彻底 OOM,Envoy 连接池爆满,最终向上游网关抛出开篇看到的 503 UO503 UC

    体系化修复与架构加固

    为了彻底根治这种“毛刺流量 -> 网关击穿 -> 熔断驱逐 -> 恐慌路由 -> 全局雪崩”的连环雷,我们从网关层和 Mesh 层做了以下防御性调整:

    1. 网关层:平滑限流(Leaky Bucket 变体)替代纯令牌桶

    废弃了原有的自研 Lua 纯令牌桶,在 Nginx/OpenResty 入口层启用基于共享内存的严格限流。即使保留一定的并发度,也必须通过 delay 参数强制平滑:

    # 定义 1000r/s 的速率,桶容量为 500
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=1000r/s;
    
    # burst=500 允许一定突发,但 delay=200 表示超过 200 的突发流量将被严格按速率排队延迟,拒绝瞬间砸穿后端
    limit_req zone=api_limit burst=500 delay=200;
    

    2. Mesh 层:压制重试风暴与调优熔断参数

    调整 Envoy 的 Outlier Detection 与连接池控制,防止误杀:

    1. 放大连续错误阈值:将 consecutive5xxErrors 从 3 调整为 15。在高并发微服务中,3 个连续 5xx 极易被网络抖动误触发。

    2. 细化驱逐条件:启用 splitExternalLocalOriginErrors,明确区分应用自身抛出的 5xx(如 500 业务报错)和本地网络/Envoy 产生的 5xx(如 503 连接超时)。只对真正的网络连接异常进行物理节点驱逐。

    3. 调整恐慌阈值:在 Envoy Cluster 配置中,通过 EnvoyFilter 将 Panic Threshold 从 50% 降低至 20%(如果 80% 节点都挂了,再开启无差别盲发请求)。

    # EnvoyFilter 局部核心配置片段
    name: envoy.filters.network.http_connection_manager
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
      common_http_protocol_options:
        idle_timeout: 60s
      route_config:
        virtual_hosts:
        - name: order_route
          routes:
          - match: { prefix: "/" }
            route:
              cluster: outbound|8080||order-svc
              max_stream_duration:
                max_stream_duration: 3s # 强制设置全局绝对超时
    

    常见问题 (FAQ)

    Q1:Token Bucket 和 Leaky Bucket 在真实网关选型时究竟怎么选? 面对对外网关(防刷、防爬),首选 Leaky Bucket(或者带强排队机制的令牌桶变体),这能把刺猬一样的流量彻底削平。面对内部微服务间的限流(RPC 调用),由于内部流量更可控且对 RT 敏感,通常使用 Token Bucket 以容忍短时间的并发调用,但必须严格限制 burst 上限,burst 绝对不能超过目标服务连接池容量的 1/3。

    Q2:分布式限流用 Redis + Lua 有什么性能隐患? 最大的隐患是单点网络瓶颈和 Redis CPU 阻塞。Lua 脚本在 Redis 中是单线程原子执行的,如果网关单机并发极高,所有请求都在等待 Redis 响应,会导致网关 Worker 进程被严重阻塞。对于 10万+ QPS 的限流,千万别用纯 Redis 强一致限流,必须退化为本地内存限流为主,Redis 异步同步配额为辅的架构(类似 Sentinel 的集群限流机制)。

    Q3:Envoy 的 Circuit Breaker 和 Outlier Detection 有什么本质区别? 这是个极度容易混淆的概念。Envoy 的 Circuit Breaker 本质上是“连接池限制”,比如 max_requests: 1000,超出了直接本地决断拦截返回 503(抛出 UO)。它防御的是“我(Client)发出的并发太多了”。 而 Outlier Detection 才是传统意义上的熔断(类似 Netflix Hystrix),它通过统计后端节点返回 5xx 的频率,将坏节点剔除出负载均衡池。它防御的是“他(Server)坏了,我不要再把请求发给他”。排查时必须严格区分这两种动作产生的不同报错标识。

  • 异地多活网关雪崩实战:5 秒配置同步延迟引发的跨城路由死环与 Envoy 线程耗尽

    近期排查了一起极其典型的“异地多活”架构翻车事故。某业务在做全链路压测与流量切流演练时,双中心网关集群在 10 秒内接连雪崩,P99 延迟从 15ms 直接飙升至网关超时上限(10s),最终导致两个可用区同时瘫痪。

    结论先行:这不是什么深奥的底层 Bug,而是一个极其低级的架构设计缺陷。控制面在下发“单元化路由规则(UID -> AZ)”时存在跨城同步延迟。在这短短 5 秒的数据不一致窗口期内,AZ-A 认为请求该去 AZ-B,而 AZ-B 还在使用旧规则认为请求该回 AZ-A。网关层完全没有做防环处理(Loop Detection),导致请求在两地专线间无限次 Ping-Pong 转发,瞬间打爆了 Envoy 的连接池和跨城专线带宽。

    伪多活架构的遮羞布,就这样被区区 5 秒的延迟扯得粉碎。

    故障现场:从 P99 飙升到全局 502

    排查过程中,监控面板的异动非常诡异:

    1. 外部流量未突增:入口 QPS 正常,没有遭受 DDoS 攻击。

    2. 专线带宽被打满:两地机房之间的 10G 专线监控显示,出入带宽在几秒内呈直线上升至 100%。

    3. 网关层资源枯竭:Envoy 节点的 CPU Load Average 飙升至 80+,envoy_cluster_upstream_rq_pending_overflow 指标疯狂报错。

    4. 后端业务毫无波澜:底层的微服务和 DB 监控一片祥和,甚至 QPS 还下降了——因为流量全死在网关了。

    直接拉取 Envoy 的 Access Log,发现令人窒息的现象,同一个 x-request-id 在同一秒内出现了数百次日志打印:

    {"time": "...", "x-request-id": "a7b2c9-...", "upstream_cluster": "outbound|80||gateway-az-b", "response_code": "504"}
    {"time": "...", "x-request-id": "a7b2c9-...", "upstream_cluster": "outbound|80||gateway-az-b", "response_code": "504"}
    

    而在 AZ-B 的网关日志里,同样的 Request ID 正在被疯狂转发回 AZ-A。

    根因拆解:分布式的“阿喀琉斯之踵”

    该业务号称实现了“异地双活”,其实质是经典的单元化架构(Cell-based Architecture)。路由规则按用户 UID 取模或查表分配:UID_Range_1 在 AZ-A 闭环,UID_Range_2 在 AZ-B 闭环。如果用户访问错了机房,入口网关会负责将其 Proxy 到正确的机房。

    演练时,运维执行了 UID 搬迁操作:将某一批 UID 从 AZ-A 迁移至 AZ-B。 正常的迁移状态机应该是:禁止写入 -> 数据同步 -> 变更路由规则 -> 开放写入

    问题出在路由规则下发环节。全局控制面(Global Control Plane)将新的路由表通过 xDS 下发给两地的 Envoy 集群。 由于跨城网络抖动和底层配置中心的同步机制,AZ-A 的网关瞬间收到了新规则,而 AZ-B 的网关存在约 5 秒的同步延迟。

    这 5 秒内,逻辑变成了这样:

    1. 用户流量进入 AZ-A。

    2. AZ-A 网关查最新路由表:“该 UID 已迁至 AZ-B”,于是将请求通过专线转发给 AZ-B 的网关。

    3. 请求抵达 AZ-B。AZ-B 网关查旧路由表:“该 UID 属于 AZ-A”,于是将请求再转发回 AZ-A 的网关。

    4. AZ-A 再次收到请求,再次转发给 AZ-B……

    一次简单的 HTTP 请求,在没有 Max-Forwards 限制的情况下,变成了跨城专线上的死循环。几千个这样的请求,瞬间裂变成数百万次的内部 RPC 调用,直接击穿 Envoy 的 max_connectionsmax_pending_requests 限制,导致网关假死,进而引发全量业务 502。

    为什么犯错不可原谅?

    真正的多活,不仅是画在 PPT 上的两套对等集群,而是骨子里对分布式系统“弱一致性”的敬畏。 CAP 定理早就告诉我们,跨越 WAN 网络的节点,绝对不可能实现原子的状态变更。只要存在时间差,就一定会出现路由视角的不一致。

    在架构设计时,不假设“配置下发绝对同时生效”,而是假设“一定会出现路由环路并进行兜底拦截”,这叫防御性编程。花了几百万拉跨城专线,却连一个最基础的 Hop Limit 都不加,这种架构翻车纯属人祸。

    止血与防御性修复

    当时在现场的紧急止血操作非常粗暴:直接切断了 AZ-A 到 AZ-B 的专线路由转发(牺牲跨城纠错能力,强行阻断环路),网关雪崩立刻停止。随后紧急排查控制面同步组件并修复延迟。

    彻底的修复方案(防环机制落地):

    1. 网关层强制拦截:引入 Max-Forwards 机制 无论使用 Nginx 还是 Envoy,在进行跨机房流量 Proxy 时,必须注入并校验自定义 Header(如 X-Multi-Active-Hop)。在 Envoy 中,可以通过原生机制或极简的 Lua Filter 实现:

    # Envoy Lua Filter 防环片段
    name: envoy.filters.http.lua
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
      inline_code: |
        function envoy_on_request(request_handle)
          local hop_count = tonumber(request_handle:headers():get("x-multi-active-hop") or "0")
          if hop_count >= 2 then
            request_handle:respond({[":status"] = "508"}, "Loop Detected in Multi-Active Routing")
            return
          end
          request_handle:headers():replace("x-multi-active-hop", tostring(hop_count + 1))
        end
    

    2. 路由变更状态机:平滑过渡 不要做“一刀切”的路由变更。UID 迁移的路由切换必须存在中间态(Transit State)。 当 UID 正在迁移时,路由状态设为 MIGRATING,此时新旧机房的网关对该 UID 的请求应统一 Hold 住(挂起等待)或降级处理,直到两端均确认收到最新配置(ACK)后,再将状态切为 COMMITTED 放行。

    3. 隔离爆炸半径 为跨城 Proxy 流量配置独立的 Cluster 和 Connection Pool,绝对不能与处理本地域内流量的线程池混用。这样即使专线打满或跨城目标假死,本地域内的“正确流量”依然不受影响。

    同类问题速查(排查清单)

    1. 跨城/跨 AZ 路由环路检测:检查所有跨域转发是否携带并校验了 X-Forwarded-ForMax-Forwards 或自定义跳数 Header,超过阈值(通常为1或2)必须立即丢弃并返回 508 (Loop Detected)。

    2. 配置中心弱一致性容灾:检查下发控制面(etcd / Consul / 自研 xDS 服务)在脑裂或跨城延迟 > 10s 的情况下,Data Plane 是否能优雅降级,还是会触发雪崩逻辑。

    3. 隔离与限流(Bulkhead):检查网关对于“跨城纠错流量”是否配置了独立的连接池(Connection Pool)和并发数限制(Circuit Breaker),防止小比例的纠错流量耗尽全局 Worker 资源。

    4. UID 状态机原子性:在单元化架构中,检查 UID 归属地切换是否有明确的“过渡态”,严防因配置生效时间差导致的“两地都不认”或“两地互相抛”的脏读问题。

  • 深度剖析:Istio xDS 全量推送引发的 Envoy 503 与 CPU 激增——从 RDS 延迟到 Delta xDS 调优实战

    结论先行:在规模超 1000 Pod 的 Istio 集群中,默认的全局服务可见性会导致严重的 xDS 广播风暴。当某个服务发生重部署时,全量 RDS/EDS 推送会打满 Envoy 主线程 CPU,引发 Worker 线程 RCU 锁竞争与饥饿,进而导致高频核心接口出现 P99 毛刺和 503 UC。破局核心在于:强制配置 Sidecar CR 切断全局依赖、开启 Delta xDS(Istio 1.18+ 默认支持但不完全,需显式调优),并合理绑核控制 Envoy 并发度。

    故障现场:毫无征兆的 503 UC 与 P99 剧震

    排查过程中,核心交易链路的网关(Envoy)频繁上报少量 503 Service Unavailable,同时 Prometheus 监控显示该时段核心接口的 P99 延迟从 15ms 突增到 300ms 以上。

    拉取业务 Pod 的 Envoy 访问日志,看到大量如下报错:

    [202X-XX-XXT14:32:01.123Z] "POST /api/v1/trade/order HTTP/1.1" 503 - upstream_reset_before_response_started{connection_termination} - "-" 150 0 120 - "-" "Go-http-client/1.1" "x-request-id" "10.2.3.4:8080"
    

    响应标志是 upstream_reset_before_response_started{connection_termination}(即 503 UC)。通常这代表 Upstream 断开了连接。但检查目标业务 Pod 状态,毫无重启,CPU/Memory 水位极低,Listen 队列也没有溢出(netstat -s | grep overflow 为 0)。

    进一步关联监控,发现每次 503 爆发的时间点,都伴随着集群内另外一个毫不相干的数据处理服务(Data-Worker)的批量发布。且在发布期间,Envoy 容器的 container_cpu_usage_seconds_total 速率飙升,Pilot(Istiod)的 pilot_xds_push_time 指标触及 5 秒。

    为什么一次无关服务的 Pod 变动会引发全局的 503 报错?

    Istio 默认的控制面下发策略是“全局可见(Global Visibility)”。这意味着集群里任何一个 Service 或 Endpoint(Pod IP)的变动,Istiod 都会全量计算一次 xDS(LDS/RDS/CDS/EDS),并推送到网格内的每一个 Envoy 实例。

    这里有两个致命的性能瓶颈:

    1. SotW (State of the World) 协议的全量 JSON 解析开销 在未完全启用 Delta xDS 增量下发的版本下,Envoy 与 Istiod 交互走的是 SotW 协议。即便只有一个无关紧要的 Pod 发生变化,Istiod 也会把包含数万个 Endpoints 的 EDS 列表打包下发。Envoy 的 Main 线程收到后,需要反序列化庞大的 protobuf/JSON。如果你的集群有 5000 个 Pod,这就是一次 MB 级别的解析。

    2. Envoy 的单主线程与 RCU (Read-Copy-Update) 锁风暴 Envoy 的架构是单 Main 线程 + 多 Worker 线程。xDS 的接收、解析和配置转换全在 Main 线程完成。一旦配置树更新,Main 线程需要通过 RCU 机制将新配置同步给所有 Worker 线程。 当超大体积的 RDS/EDS 更新到来时:

    • Main 线程 CPU 飙升至 100%。

    • Worker 线程被强制更新配置,由于 RCU 锁更新粒度过大,Worker 线程在处理 Epoll 事件循环时被阻塞(Event Loop Delay)。

    • 恰好此时有高并发流量打进来,Worker 线程处理不过来,导致 Upstream 连接 Keepalive 超时或握手失败,最终抛出 503 UC

    可以通过 istioctl 检查 Envoy 内部卡顿的配置积压:

    # 检查同步状态,如果 SYNCED 比例在发布时急剧下降,说明主线程已卡死
    istioctl proxy-status
    
    # 抓取 Envoy 的性能分析数据(需开启 admin 端口暴露)
    curl -X POST http://localhost:15000/cpuprofiler?enable=y
    

    流量治理与底层优化实战

    针对上述底层机制,我们必须对控制面和数据面进行三道防线改造。(以下配置基于 Istio 1.18.2 和 Envoy 1.26 环境)

    防线一:强管控 Sidecar CR,斩断无效 xDS 推送

    绝不应该让业务侧默认接收所有 Service 变更。必须通过 Sidecar CR 限制 Egress 范围,这是治本之策。

    apiVersion: networking.istio.io/v1beta1
    kind: Sidecar
    metadata:
      name: default-sidecar
      namespace: trade-system # 作用于特定命名空间
    spec:
      egress:
      - hosts:
        - "./*"                   # 允许访问本命名空间的所有服务
        - "istio-system/*"        # 必须放行控制面,否则无法通信
        - "user-center/user-svc"  # 精确声明跨命名空间的外部依赖
    

    优化效果:执行后,通过 istioctl pc clusters | wc -l 观察,Envoy 维护的 Cluster 数量从 3000+ 断崖式下降到不到 50 个。无关服务的发布再也无法触发该 namespace 的 xDS 推送。

    防线二:开启 Delta xDS 增量更新

    SotW 是历史遗留产物,必须在 Istiod 端全面启用 Delta xDS,让控制面只推送变更的 Diff 数据,彻底解放 Envoy Main 线程的解析压力。

    修改 istiod 的 Deployment,在环境变量中注入:

    env:
      # 开启 Delta xDS(部分高版本已默认开启,但仍建议显式声明)
      - name: PILOT_ENABLE_DELTA_XDS
        value: "true"
      # 针对 EDS 的深度优化,仅对发生变动的 Cluster 发送 Endpoint 增量
      - name: PILOT_ENABLE_EDS_DEBOUNCE
        value: "true"
    

    避坑指南:开启 Delta xDS 后,Istiod 需要在内存中为每个 Envoy 代理维护状态缓存(State cache)。这会导致 Istiod 的内存消耗增加约 20%-30%,实施前务必调大 istiod 的 Memory Requests/Limits。

    防线三:Envoy 并发度与系统内核参数调优

    Istio 注入的 Envoy 默认 concurrency 设为 2(即 2 个 Worker 线程)。在高并发场景下,如果被 xDS 阻塞,2 个线程很快会全军覆没。需要结合 Pod 的实际 CPU limits 进行动态绑核。

    在业务 Deployment 的 Pod Annotations 中显式调优:

    template:
      metadata:
        annotations:
          # 将并发度调至 4(建议设为 Pod CPU Limit 的整数值)
          proxy.istio.io/config: '{"concurrency": 4}'
          # 避免连接断开时的 local port 耗尽
          sidecar.istio.io/proxyCPULimit: "4"
    

    配合宿主机的内核参数,解决 Envoy 在高频新建/断开连接时带来的 TIME_WAIT 积压:

    # 在 Pod securityContext 中配置 sysctl (或通过 initContainer)
    sysctl -w net.ipv4.tcp_tw_reuse=1
    sysctl -w net.ipv4.ip_local_port_range="1024 65535"
    

    常见问题 (FAQ)

    Q1:配置了严格的 Sidecar Egress 后,为什么业务主动调用某些外部域名(ExternalName)直接返回 502/NR? A:配置 Sidecar CR 后,Envoy 会丢弃所有未声明的流量。如果有调用外部公网接口的需求,必须配套配置 ServiceEntry 并在 Sidecar 的 hosts 中放行。或者在全局网格配置中将 outboundTrafficPolicy.mode 设置为 ALLOW_ANY(但不推荐,会破坏零信任边界),最佳实践是严格声明 ServiceEntry

    Q2:如何准确监控 Envoy 的 xDS 处理延迟是否成为瓶颈? A:不要只看 Pilot 的下发时间。真正反映 Envoy 卡顿的是 Envoy 自身暴露的 envoy_server_initialization_time_ms 以及控制面的 pilot_xds_push_time 配合 pilot_proxy_convergence_time。当 convergence_time(收敛时间)大于 2 秒时,数据面就已经处于高危状态。

    Q3:开启 Delta xDS 后,发现极少部分流量路由到了已经下线的 Pod,导致偶发 503,怎么排查? A:这通常是 K8s EndpointSlice 延迟更新与 Envoy Delta 缓存不一致导致的边界 Case。如果你的 Envoy 版本低于 1.25,建议检查社区关于 Delta EDS 乱序的 Issue。临时缓解方案是开启 Envoy 侧的重试机制,在 VirtualService 中配置 retries: { attempts: 3, retryOn: "connect-failure,refused-stream,503" },让 Envoy 自动 Failover 到健康节点。