LabHub
学习 学习路径 课程

Envoy 内部结构

重试把故障放大的那一刻

在 LabHub 中继续学习

一句话总结

重试可以掩盖临时故障。但后端过载时继续重试,会把负载放大两三倍,最终让故障彻底爆发。因此,重试必须始终设置上限和预算。

概念图: 临时故障 · 27 倍 · 立即失败 · Circuit Breaker

为什么需要这些知识

某个后端变慢 → timeout → 重试 → 负载翻倍 → 变得更慢 → 再次重试 → ……

这种反馈循环称为重试风暴(retry storm)。Service Mesh 为每个服务配置 sidecar;在三层调用链中,如果每一层都重试 3 次,最末端服务会承受27 倍请求。重试会相乘。

有三种防护机制,各自解决不同问题。

机制 观察什么 执行什么
重试预算 重试占总请求的比例 超过一定比例后停止重试
Circuit Breaker 并发连接数与等待请求数 超过限制时立即失败,快速终止
异常值检测 各 endpoint 的连续失败 暂时移除有问题的 endpoint

它是如何运作的

Circuit Breaker 与名称给人的印象不同,与其说是“断路器”,不如说是一组上限

# DestinationRule
trafficPolicy:
  connectionPool:
    tcp: { maxConnections: 100 }
    http:
      http1MaxPendingRequests: 20     # 커넥션을 기다리는 요청 상한
      maxRequestsPerConnection: 100

超过上限的请求不会等待,而是立即收到 503。这看起来很残酷,却是正确选择——让请求等待,会连客户端线程也一起占住,使故障向上游扩散。对整个系统而言,快速失败更好。

异常值检测用于从负载均衡池中移除有问题的 endpoint。

outlierDetection:
  consecutive5xxErrors: 5
  interval: 10s
  baseEjectionTime: 30s
  maxEjectionPercent: 50      # 절반 넘게는 절대 빼지 않는다

maxEjectionPercent 非常重要。如果没有它,在全部后端短暂恶化时,系统可能移除所有 endpoint,导致整个服务彻底中断。检测机制反而制造了故障。

常见误解

“启用重试总是有好处。” 对非幂等请求,例如通过 POST 发起支付或订单,设置重试会制造重复。Envoy 的 retryOn 默认值只包含安全条件,但一旦加入 5xx,服务器处理途中失败的请求也会再次发送。

没有 timeout 的重试。 重试 3 次,每次 timeout 为 30 秒,最坏情况会等待 90 秒。用户早已离开,连接却一直被占用。必须在重试之外同时设定整体时间上限timeout)。

Retry budget 这道安全防线

启用重试时,必须同时设置预算(retry budget)。规定“重试最多只能占总请求的百分之多少”,即使下游崩溃,也能避免重试流量失控。

# Envoy — 클러스터 단위 재시도 예산
circuit_breakers:
  thresholds:
    - priority: DEFAULT
      max_retries: 3          # 동시에 진행 중인 재시도 상한
retry_policy:
  retry_on: 5xx,reset,connect-failure
  num_retries: 2
  per_try_timeout: 1s
  retry_back_off:
    base_interval: 0.025s
    max_interval: 1s

max_retries并发重试数量上限,能够从物理上阻止重试风暴。如果没有它,只设置 num_retries: 3,下游一变慢,每个请求就会变成四个,负载直接增长四倍。这相当于向正在崩溃的服务发送四倍流量。

per_try_timeout 同样重要。没有它时,第一次尝试可能耗完整体 timeout,不再剩余重试时间。必须满足整体 timeout ≥ per_try ×(重试次数 + 1)

哪些情况可以重试,哪些不能

条件 是否重试 原因
连接失败、TCP reset 服务器甚至没有收到请求
503、504 通常在处理前就被拒绝
500 ⚠️ 可能已在处理过程中失败
timeout ⚠️ 服务器可能已经完成处理
4xx 再次发送仍会得到相同结果

timeout 重试最危险。支付请求在 5 秒时 timeout,但服务器在第 6 秒完成,重试就会导致重复支付。因此,非幂等请求应从 retry_on 中排除 timeout,或者必须使用幂等键。

Envoy 可以通过 x-envoy-retry-on header 为每个请求分别指定策略,因此查询可以积极重试,写入操作则保持保守。

通过异常值检测移除故障实例

只靠重试无法解决某个特定实例出现问题的情况,因为重试仍可能发往同一个故障实例。异常值检测(outlier detection)会将其移除。

outlier_detection:
  consecutive_5xx: 5              # 연속 5번 5xx 면
  base_ejection_time: 30s         # 30초 뺀다
  max_ejection_percent: 50        # 다만 절반 이상은 못 뺀다
  interval: 10s

max_ejection_percent 是安全防线。当所有实例同时出问题,例如共同依赖的数据库故障时,如果全部移除,服务就会完全中断。保留一半实例总比彻底崩溃好。

生产环境中真正重要的事

故障调查中,这三类机制对应的信号并不相同。

# 서킷 브레이커에 걸렸나 — 대기열 넘침
envoy_cluster_upstream_rq_pending_overflow

# 이상치 감지가 엔드포인트를 뺐나
envoy_cluster_outlier_detection_ejections_active

# 재시도가 얼마나 도나
envoy_cluster_upstream_rq_retry / envoy_cluster_upstream_rq_total

最后一个比例超过 5% 时,重试已经成为负载的一部分。此时增加重试,无异于火上浇油。