Mesh 帮你重试了,应用自己也在重试,会发生什么?链越深放大越多吗?Envoy 自己有什么保护?写请求被 Mesh 重试安全吗?这篇用 Envoy 1.39.3 和本机的 Python 服务逐项把倍数量出来。
后端失败率 | 应用层重试次数 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 倍。
后端失败前的延迟 | 配置 | 后端收到的请求数 / 客户端请求数 | 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 {}调用方放弃之后,被放弃的尝试后端仍在处理,超时加重试会让后端的并发超过客户端的并发。