bigywater.tech
微服务 · 93

服务网格里的弹性策略与应用内的区别,没有 mesh 时在应用里做同样的事

Mesh 帮你重试了,应用自己也在重试,会发生什么?链越深放大越多吗?Envoy 自己有什么保护?写请求被 Mesh 重试安全吗?这篇用 Envoy 1.39.3 和本机的 Python 服务逐项把倍数量出来。

9 倍
一跳上应用重试 2 次加 Mesh 重试 2 次,后端完全挂掉时收到的请求相对客户端请求的倍数
243 倍
三层链上应用和 Envoy 每层都重试 2 次时叶子服务收到的请求倍数
200 / 100
后端不去重时,Envoy retry_on 5xx 让 100 个 POST 请求在后端执行的写入次数

两层重试相乘

实测 · 本机执行输出
  后端失败率 | 应用层重试次数 a | Mesh 层重试次数 m | 后端收到的请求数 / 客户端请求数 | 客户端成功率
  100%  | 0         | 0            | 1.00              | 0.0%  
  100%  | 0         | 2            | 3.00              | 0.0%  
  100%  | 2         | 0            | 3.00              | 0.0%  
  100%  | 2         | 2            | 9.00              | 0.0%  
  50%   | 0         | 0            | 1.00              | 45.0% 
  50%   | 0         | 2            | 1.79              | 86.5% 
  50%   | 2         | 0            | 1.76              | 85.5% 
  50%   | 2         | 2            | 2.14              | 99.5% 
  后端完全挂掉(100%)时,两层重试的放大倍数相乘,每一层单独看都只重试 2 次,合起来是表里最后一行的倍数;后端只有 50% 失败时,重试把成功率拉高,代价是后端多收的请求(见表)

每一层单独看都只重试 2 次,叠在一起是 (1+a)×(1+m) 倍,后端完全挂掉时是 9 倍。

链越深放大越多

实测 · 本机执行输出
  重试放在哪里                  | 叶子 C 收到的请求数 | 叶子 C 收到的请求数 / 客户端请求数 | 客户端成功数(共 50)
  (a)谁都不重试                | 50          | 1.0                  | 0           
  (b)只有客户端重试 2 次          | 150         | 3.0                  | 0           
  (c)每一层应用都重试 2 次         | 1350        | 27.0                 | 0           
  (d)只有两跳的 Envoy 重试 2 次   | 450         | 9.0                  | 0           
  (e)应用和 Envoy 每一层都重试 2 次 | 12150       | 243.0                | 0           
  各层的重试次数相乘:(b)只在入口重试,叶子收到 3.0 倍;(c)三层应用都重试,27.0 倍(3 的三次方);(d)两跳 Envoy 重试,9.0 倍(3 的平方);(e)应用和 Envoy 都重试,243.0 倍(= 3 × 9 × 9)。链越深,同样的 “只重试 2 次” 在叶子上放大得越多

各层的重试次数相乘:只在入口重试叶子 3 倍,三层应用都重试 27 倍,应用与 Envoy 叠加 243 倍。

Envoy 的并发重试上限

实测 · 本机执行输出
  后端失败前的延迟 | 配置                                     | 后端收到的请求数 / 客户端请求数 | upstream_rq_retry(发起的重试) | upstream_rq_retry_overflow(被限制掉的重试)
  0 ms     | (a)默认 max_retries                      | 2.35              | 674                      | 163                                
  0 ms     | (b)max_retries 1024                    | 3.00              | 1000                     | 0                                  
  0 ms     | (c)max_retries 1024 + retry_budget 20% | 2.31              | 654                      | 173                                
  100 ms   | (a)默认 max_retries                      | 1.24              | 122                      | 439                                
  100 ms   | (b)max_retries 1024                    | 3.00              | 1000                     | 0                                  
  100 ms   | (c)max_retries 1024 + retry_budget 20% | 1.25              | 124                      | 438                                
  默认的 max_retries(同时最多 3 个重试在进行)确实在限制重试:后端失败前延迟 0 ms 时,放大倍数是 2.35(上限 3.00,163 个重试被拒绝);延迟 100 ms 时只有 1.24(439 个重试被拒绝)。失败越慢,同时在进行的重试越多,被拦得越多。把 max_retries 调到 1024 就拦不住了(两种延迟下都是 3.00 倍)。retry_budget 20%(下限 min_retry_concurrency 3)在这个负载下的结果与默认的 max_retries 几乎一样(2.31 和 1.25),我的解释是这个负载的并发请求数太小,20% 算出来还不到下限 3,没有单独验证,所以我没有量出 retry_budget 比固定上限多做了什么

默认 max_retries 为 3,限制同时进行的重试个数,失败越慢拦得越多;调大后就不拦了。

幂等性

实测 · 本机执行输出
  配置                          | 客户端看到 200 | 客户端看到 503 | 后端执行的写入次数(共 100 个请求)
  (a)retry_on 5xx,后端不去重       | 100       | 0         | 200                 
  (b)retry_on connect-failure | 0         | 100       | 100                 
  (c)retry_on 5xx,后端按请求号去重    | 100       | 0         | 100                 
  Envoy 对 POST 也按 retry_on 重试:(a)后端已经写入又返回 503,重试把同一笔写入再执行一次,后端执行了 200 次写入(请求只有 100 个),客户端却看到 100 个成功;(b)只在连不上时重试,没有重复写入,但客户端看到的 503 是 100 个;(c)后端按请求号去重后写入次数回到 100。Mesh 不知道哪些接口是幂等的,对非幂等接口配 retry_on 5xx 要先确认后端能去重

Mesh 不知道哪个接口是幂等的,retry_on 5xx 会把已经写入的 POST 再执行一次,除非后端按请求号去重。

超时加重试

实测 · 本机执行输出
  配置                                                          | 客户端最后看到       | 后端收到的尝试数(共 20 个请求) | 每个请求的尝试数 | 后端同时处理的最大并发
  (a)客户端 1 s、不重试;Envoy 重试 2 次,总超时 10 s                        | {timeout: 20} | 20                 | 1.0      | 8          
  (b)客户端 1 s、应用重试 2 次;Envoy 不重试                               | {timeout: 20} | 60                 | 3.0      | 8          
  (c)客户端 2 s、不重试;Envoy per_try_timeout 0.5 s 重试 2 次,总超时 1.5 s | {504: 20}     | 60                 | 3.0      | 16         
  (a)客户端 1 s 就走了,Envoy 没有继续替它重试(每个请求只有 1.0 次尝试);我的解释是客户端断开让 Envoy 放弃了这个请求,没有单独验证。(b)客户端自己超时后重试:每个请求 3.0 次尝试。(c)Envoy 的 per_try_timeout 把每次尝试限制在 0.5 s,每个请求 3.0 次尝试,最后客户端看到的是 Envoy 的 504。三种配置下后端同时处理的最大并发是 8 / 8 / 16,客户端同时只有 4 个线程,说明被放弃的尝试后端仍在处理(后端是我写的,不会取消);(c)的并发最高。超时只是调用方不等了,不等于后端停止干活;超时加重试会让后端同时处理的请求数超过客户端的并发
说明(哪些是测到的,哪些没有测)
  每个场景只跑了一次(随机失败用固定种子);没有测:Envoy 的退避参数(retry_back_off)对放大的影响、重试预算在高并发慢失败下的长期行为、gRPC 的重试策略、Istio 的 VirtualService 重试默认值(它默认会重试,这里没有验证)、应用层的指数退避加抖动
ERRORS {}

调用方放弃之后,被放弃的尝试后端仍在处理,超时加重试会让后端的并发超过客户端的并发。

分步演示

应用重试与 Mesh 重试
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / Go / nginx / Envoy / etcd / Consul / Nacos / OpenTelemetry / Prometheus(按各篇注明用到哪些及版本);所有服务都是本机自己起的进程,端口 9150 以上,只监听 127.0.0.1,不连任何外部服务,不碰本机已有的实例;故障用 kill -9 进程、让连接断开或在代码里注入延迟与错误来制造;计时用 time.perf_counter,数字每次运行会有小幅抖动。Envoy 1.39.3,route 的 retry_policy 与集群的 circuit_breakers;应用层重试是 Python 里的循环;后端是本机的 Python 服务(固定的失败率、延迟,50% 失败时用固定种子);每种配置只跑了一次,50% 失败的倍数和成功率每次运行略有不同(例如 a=0 m=2 在两次运行里是 1.74 倍和 1.79 倍);第一节开始时没有调 max_retries,结果 m=2 的后端倍数只有 1.05(不是 3),原因是 Envoy 默认的 max_retries 3 限制了并发重试,所以第一、二、四节都先把它调到了 1024
    • 没有验证:Envoy 的退避参数(retry_back_off)对放大的影响;retry_budget 在高并发慢失败下的效果(这个负载下与默认 max_retries 无法区分);gRPC 的重试策略;Istio 的 VirtualService 默认重试(我印象里默认会重试 2 次,没有验证);应用层的指数退避加抖动;客户端断开后 Envoy 放弃重试的机制(只观察到每个请求只有 1.0 次尝试);后端收到取消信号后停止处理的情形(本篇后端不会取消)
    京ICP备2026067502号-1京公网安备11010502063487号