资讯动态

Java 网关的重试风暴实测:把 6 秒故障放大成 34 秒过载的,正是最照顾单个请求的退避

发布时间:2026/10/29 10:59:16 来源:尧图企业网站定制
解决什么问题模型网关、支付回调这类「上游一抖客户端就重试」的场景里哪种重试策略会把局部故障放大成全局过载。本文用一段可运行的离散事件仿真把六种策略放在同一个故障剧本下对拍并在文末给出完整可粘贴的源码。适合谁读写 Spring Cloud Gateway / Resilience4j 封装、自己实现过退避与熔断的 Java 后端。能收获什么一张 0.94x 到 2.48x 的放大倍数对照表一条「交付率 100% 的策略最危险」的反直觉结论一个把中位延迟压到 205 ms、却让 p99 从 7700 ms 涨到 9146 ms 的抖动实验以及一张熔断器七组单变量对照表——同一套熔断代码只改一个 deadline就从「一次都不跳闸」变成「刚好有用」。GitHub 在 2026 年 8 月 20 日发布了 8 月 17 日那次大故障的复盘署名是 CTO Vlad Fedorov。那次中断 7 小时 47 分钟官方定性不是代码或配置变更而是容量失败。复盘里最扎眼的一段写的不是故障本身而是恢复多数服务当天下午就回来了Copilot 却拖到最后因为客户端的重试循环在恢复期不断抬高流量团队必须先把它压下去才敢放量。公开解读把整改方向归纳成两条统一的重试预算以及可变超时。我把这句话当成一个可以量化的问题如果只给 6 秒故障退避、抖动、预算、deadline、熔断这五种机制各自会把系统带去哪里于是写了个离散事件仿真200 个客户端、60 秒虚拟时间、上游容量 240 req/s、第 20~26 秒整体不可用正好 6 秒故障六种策略跑同一个剧本、同一个随机种子。结论先放这里——指数退避把交付率做到 100%同时把过载秒数从 6 秒拉长到 34 秒只有重试预算能把放大倍数压回 1.03x代价是主动放弃 9.2% 的请求而熔断跳不跳闸取决于你有没有别的机制先把失败攒成「连续」。这篇文章的全部数字都在我这台机器上真跑出来代码在第八节你可以改参数复算。一、模型设定与实测环境口径参数都是显式给定的没有隐藏常量便于你改数复算。参数取值说明客户端数200每秒每客户端发 1 个业务请求即稳态 200 req/s上游容量240 req/s同一秒到达数超过它就过载失败故障窗口第 20~26 秒期间所有请求直接失败共 6 秒正常上游时延200 ms交付时延里叠加这个常数随机种子4242固定保证整张表可复算业务请求总数12000200 × 60放大倍数的分母重试时刻归属到达秒 ceil(累计等待 / 1000)关键约定零退避的 S1 会把 4 次尝试全压进同一秒判定口径交付率 最终拿到结果的业务请求占比被 deadline 掐掉、被预算拒发、被熔断快速失败都算未交付实测环境口径。下面这一格是写这篇文章前现跑的不是从上一篇抄的——这张草稿三天前存的时候版本口径那一行写的是 JDK 17.0.20今天在这台机器上重新执行java -version拿到的是 17.0.18。凡是只在上一个工程里出现过小版本号的东西都不要往这一篇里搬。项本机实测值java -versionjava version 17.0.18 2026-01-20 LTSjavac -versionjavac 17.0.18java.version17.0.18java.version.date2026-01-20java.runtime.version17.0.188-LTS-264java.vendorOracle Corporationos.nameWindows 11os.version10.0os.archamd64file.encodingGBKavailableProcessors16两个由实测值直接导出的注意事项①file.encoding GBK所以javac必须显式带-encoding UTF-8否则源码里的中文策略名和注释会编坏表格里的对齐也会乱②整个仿真是单线程纯计算没有并发、没有第三方依赖用到的新语言特性只有 Java 17 的record换任何 JDK 17 结果都一样。策略定义就是一条记录六个字段解释完全部差异public record Cfg(String name, int maxRetry, long baseMs, String jitter, int budgetPct, long deadlineMs, boolean breaker) { } public static final Cfg[] CFGS { new Cfg(S1 固定重试3次, 3, 0, none, 100, 30_000, false), new Cfg(S2 指数退避无抖动, 5, 500, none, 100, 30_000, false), new Cfg(S3 指数退避全抖动, 5, 500, full, 100, 30_000, false), new Cfg(S4 全抖动重试预算20%, 5, 500, full, 20, 30_000, false), new Cfg(S5 预算deadline, 5, 500, full, 20, 3_000, false), new Cfg(S6 预算熔断, 5, 500, full, 20, 3_000, true), };二、结果交付率 100% 的两行最危险 上游第 20~26 秒整体不可用容量 240 req/s200 客户端 × 60 秒seed4242 策略 放大倍数 交付率 峰值QPS 过载秒 交付中位ms p99ms 熔断开/跳闸秒/误伤 ---------------------------------------------------------------------------------------------------- S1 固定重试3次 1.30x 90.0% 800 6 200 200 0/[]/0 S2 指数退避无抖动 2.34x 100.0% 960 34 700 7700 0/[]/0 S3 指数退避全抖动 2.48x 99.1% 1057 32 205 9146 0/[]/0 S4 全抖动重试预算20% 1.03x 90.8% 273 4 200 366 0/[]/0 S5 预算deadline 1.03x 90.6% 269 3 200 200 0/[]/0 S6 预算熔断 0.94x 90.0% 205 0 200 200 1/[21]/0 注交付率按「业务请求最终有没有拿到结果」计被 deadline 掐掉与预算拒发都算未交付。 业务请求总数 12000放大倍数 上游收到的 attempt 数 / 业务请求数。看三组对照就够了。S1 → S2重试次数从 3 增到 5 并加上指数退避交付率从 90.0% 抬到 100.0%看着是纯粹的改进但同一次仿真里峰值到达从 800 涨到 960过载秒数从6 涨到 34p99 从 200 ms 变成 7700 ms。12000 个业务请求里多保住了 1200 个代价是过载时长变成近 6 倍、尾延迟变成 38 倍——这笔账在单个客户端的指标面板上是看不见负的。S3 → S2加抖动几乎没改善任何总量指标放大倍数甚至更高2.48x vs 2.34x过载秒只少了 2 秒交付率还掉了 0.9 个点。它真正做的事是把交付中位从 700 ms 压到 205 ms但 p99 反向从 7700 ms 涨到 9146 ms。S4~S6只有引入重试预算放大倍数才掉到 1.03x、峰值 273 贴着容量、过载 4 秒——代价是交付率从 100.0% 回到 90.8%。这就是本文最想留下的一句话「我的请求成功了」和「系统还活着」是两个指标默认的重试组合只在优化前者。把 S2 与 S3 单拎出来对照抖动的能力边界就很清楚了下表百分比由这两个 Result 现算指标S2 无抖动S3 全抖动变化流量放大倍数2.34x2.48x更差 6%峰值到达量960 req/s1057 req/s更差 10%交付率100.0%99.1%更差 −0.9 pt交付时延中位700 ms205 ms好 -71%交付时延 p997700 ms9146 ms更差 19%过载秒数3432略好 −2抖动改善的是「谁在什么时候受罪」不是「总共受多少罪」而且这次它连尾都没保住。如果你的告警只看成功率与过载时长加抖动前后你会看到一条几乎不动的曲线。中位数改善、p99 反而恶化的原因在数据里能读出来全抖动把一部分重试推到了更晚的秒这些请求因此没有在第 5 次重试时撞上 deadline而是继续挂着最后落进更长的尾部。再单看 deadlineS5 相对 S4 只动了 deadline 一个字段30 秒 → 3 秒交付率从 90.84% 微降到 90.57%0.28 个点但p99 从 366 ms 掉到 200 ms。预算已经把总量掐住了deadline 在这轮剧本里改的主要是尾部的形状它把那些还要继续退避的请求提前判死于是最长的那一段等待直接消失。它仍然必须配——预算防的是「大家一起重试」deadline 防的是「单个请求挂死占住线程」两者防的是不同故障形态。一个必须做的归零对照。把六种策略的budgetPct全部改成 100等于关掉预算重跑S4 变成 2.48x与 S3 的每一项指标逐位相同——放大倍数、交付率、峰值、过载秒、中位、p99 全部一致。这说明把指标压下来的确实是预算不是抖动。同一轮里把 S5 的预算也关掉只留 3 秒 deadline结果是 2.27x / 87.4% / 过载 29 秒 / p99 3041 msdeadline 单独用确实能把尾砍掉一大截S3 的 p99 是 9146 ms但要拿11.7 个点的交付率去换而过载秒数只从 32 秒降到 29 秒。所以顺序是预算优先deadline 补尾。三、恢复窗口谁在敲门上游恢复的那一秒最危险因为所有积压的重试同时醒来。把每秒到达量打印出来每秒到达上游的请求数容量 240故障 20~26 秒 秒 S1 立即重试 S2 无抖动 S3 全抖动 S4 预算20% 过载(任一策略) 19 200 200 200 200 20 800 200 201 200 21 800 400 585 219 22 800 600 758 229 23 800 600 855 235 24 800 800 937 246 25 800 800 1010 255 26 200 800 1005 273 27 200 800 1057 243 28 200 960 1005 212 29 200 960 983 203 恢复后 3 秒第 26~29 秒到达总量S1 600、S2 2560、S3 3067、S4 728 全程 attempt 总量S1 15600、S2 28080、S3 29717、S4 12321业务请求 12000三个细节值得盯。第一抖动让总量变大而不是变小。恢复后 3 秒S3 的 3067 比 S2 的 2560 还多 507 个。它把重试摊到更后面的秒里于是更多请求还留在「会继续重试」的状态里——摊平不等于减量。第二S1 那条 800 的水平线是最诚实的数字。200 个客户端 × 4 次尝试全落在同一秒因为它退避为 020~25 秒每一秒都是 800故障一结束立刻掉回 200。没有退避的重试不会积压它只是当场把容量踩死。第三S4 的曲线几乎贴着容量线走恢复后 3 秒总量 728只有 S2 的 28%。它过载还剩零星 4 秒因为它根本没把积压放出去。四、重试预算的实现要点预算不是「限制重试次数」那是 S1 干的事预算限制的是重试占总请求的比例。if (c.budgetPct() 100 attempt 0) { if (budgetTokens 0) { r.failed; break; } budgetTokens--; } if (attempt c.maxRetry()) { r.failed; break; } if (sec r.perSec.length) sec r.perSec.length - 1; r.perSec[sec]; r.attempts; if (waitMs c.deadlineMs()) { r.failed; break; } // deadline 传播预算桶的补充放在每秒循环末尾一行就够budgetTokens min(budgetMax, budgetTokens budgetMax)其中budgetMax CAPACITY × budgetPct ÷ 100 240 × 20 ÷ 100 48。三个容易写错的点每个都对应一个能观察到的错误现象上限按容量算不要按当前 QPS 算。按CAPACITY * budgetPct / 100才能得到 48 这个常数按实测 QPS 算会在故障期自我收缩越缺越不给重试。本文 S4 的峰值 273 ≈ 240 48 的零头正是这条公式在起作用的直接证据。只有attempt 0才扣令牌。否则第一次请求也会被预算卡住重试预算退化成一个限流器。预算耗尽时直接放弃不要排队。排队等于把压力留在系统里只是换了个时间点爆发——第三节那条恢复窗口的尖峰就是排队的样子。五、熔断的三种命运不跳、刚好、过敏这一节是重跑时改写的。上一版草稿在这里写的是一个负结果——「我配的熔断一次都没开」。今天在这台机器上重跑它开了。差别不在熔断代码在 deadline。为了把这个变量钉死我把熔断单独做成一组七行单变量对照maxRetry5 / base500ms / full jitter全程不变每次只动一个开关。组预算deadline跳闸阈值放大倍数交付率过载秒峰值跳闸发生在误伤数A 基线无熔断20%3s—1.03x90.6%3269——B 基线 熔断20%3s4000.94x90.0%0205[21]0C 去掉预算 熔断关3s4000.94x90.0%0217[21]0D 放宽 deadline 熔断20%30s4001.03x90.8%4273[]0E 去掉预算与 deadline 熔断关30s4002.48x99.1%321057[]0F 阈值提到 2000 熔断20%3s20001.03x90.6%3269[]0G 阈值降到 100 熔断20%3s1000.84x83.3%0201[20, 25]801读这张表只需要三句话。① 熔断单装等于没装。E 组只有熔断无预算、无 deadline阈值 400结果一次都不跳放大 2.48x、过载 32 秒——和完全不配熔断的 S3 一模一样。因为请求一直在成功和失败之间交替consecFail被反复清零永远攒不到 400。② 熔断会不会跳取决于有没有别的机制先把失败攒成「连续」。B 组只比 E 组多了预算和 3 秒 deadline第 [21] 秒就跳了一次放大倍数被压到 0.94x低于 1.00x意味着有业务请求根本没碰到上游过载秒直接归零而且误伤 0。D 组只把 deadline 从 3 秒放宽回 30 秒其它都不动熔断又不跳了。③ 阈值调敏感代价是可以量化的。G 组把阈值从 400 降到 100跳闸 2 次第 [20, 25] 秒过载秒同样是 0但交付率从 90.6% 掉到 83.3%并且有801 个请求在上游那一秒其实还有空位的时候被快速拒绝了。这就是「误伤」的定义熔断打开期间被拒发的请求如果它落到的那一秒不在故障窗口内、且到达量还没打满容量那它本来是可以被服务的。所以熔断的阈值不能按理想流量定也不能按「加了预算之后的流量」一拍脑袋定它得和退避窗口、deadline 一起当成一个整体来调。这一组实验我没法给你推荐值——B 组和 G 组相差 6.7 个点的交付率中间的最优位置取决于你的故障持续时间和业务能容忍多少误伤而这两件事本文的仿真都不掌握。六、落地建议与参数取值层建议默认值为什么是这个值最大重试次数2不是 5S1→S2 那一步说明多出来的重试几乎全变成过载交付率只换到 10 个点退避基数上游 p50 时延的 1~2 倍比上游还快的重试基本必然撞同一个故障S1 的 800 峰值就是零退避的样子抖动看指标决定别当默认本文实测中位 −71%但 p99 19%、放大 6%、交付 −0.9pt重试预算10%~20% 且按容量算本文里唯一把放大倍数压回 1.0x 的机制归零对照已验证客户端 deadline业务可等的时间不是无限它还是熔断能不能跳闸的隐性前提见第五节 D 组熔断阈值和退避、deadline 一起调阈值 400→100 换来 0 过载秒也换来 801 次误伤和 6.7 个点交付率对应到 Spring Cloud Gateway 或 Resilience4j 的写法就是把maxAttempts从默认 3 改成 2、waitDuration按 p50 配、加RetryConfig.intervalFunction做抖动、并在外层挂一个按容量折算的速率闸。注意 Resilience4j 的重试本身不含预算概念预算要用RateLimiter单独限重试流量别指望配置项里有个叫 budget 的开关。预算的适用前提请求必须幂等上面所有数字都默认一件事——被放弃的请求可以安全放弃。读操作、带幂等键的写操作成立不带幂等键的扣款、发消息、创建订单不成立这类请求被预算掐掉代价不是「少一次结果」而是「状态不确定」。所以对非幂等写正确的组合是预算只作用于读与查询类重试写类走「先落本地待发表、后台带幂等键重投」把重试压力从请求链路上摘出去。这一步不在本文仿真范围内我没法给数字但它决定了这张表能不能直接抄进你的网关。为什么仿真规模取 200 × 60 秒以及换个种子还成不成立够触发拥塞反馈环200 req/s 稳态贴着 240 容量任何放大都会立刻撞顶又够小到单轮仿真是毫秒级、能反复改参数对拍。但「换个随机种子结论还在不在」必须实测不能靠说。下面是六个种子各跑一遍的结果seedS2 放大/过载/交付S3 放大/过载/交付S4 放大/过载/交付S3−S2 放大差42422.34x / 34s / 100.0%2.48x / 32s / 99.1%1.03x / 4s / 90.8%0.1412.34x / 34s / 100.0%2.50x / 32s / 99.3%1.03x / 4s / 90.9%0.1672.34x / 34s / 100.0%2.52x / 33s / 99.3%1.03x / 3s / 90.9%0.18992.34x / 34s / 100.0%2.53x / 33s / 99.4%1.03x / 4s / 90.9%0.1920262.34x / 34s / 100.0%2.51x / 32s / 99.3%1.03x / 5s / 90.9%0.173131312.34x / 34s / 100.0%2.52x / 33s / 99.4%1.03x / 4s / 90.8%0.18三点结论S2 的极差是 0.000因为它jitternone根本不消耗随机数这条曲线是纯确定性的你那边跑出来必须一模一样S3 落在 2.48x~2.53x均值 2.508x极差 0.052六个种子里没有一个比 S2 低S3−S2稳定在 0.14~0.19S4 稳定在 1.03x极差 0.002过载秒在 3~5 之间浮动。反过来说这个规模也意味着峰值统计只有几个数据点别把它当分布估计用要拿它做容量规划至少得把客户端数放大两个量级并跑多种子取分位数。七、适用边界与风险提示这是仿真不是线上数据。故障模型是「整段不可用 超容量即失败」的最粗抽象真实上游有部分失败、有排队、有长尾。仿真的价值在于同一剧本下横向比较策略绝对数字不可外推。交付率口径要认准。本文把「被预算主动放弃」和「被熔断快速拒绝」都算作未交付所以 S4 的 90.8% 是含自我克制的如果你的 SLA 只看「最终响应」S4 会显得更难看。两个口径都该看。所有数字依赖那条「重试时刻归属」约定。把ceil(累计等待/1000)换成别的归秒方式S1 的 800 峰值和 6 秒过载都会变。所以第八节的判定标准写的是排序和形状不是具体值。单上游假设。多路由、按模型分池的场景下路由级预算要各算各的否则一个慢模型会吃掉快模型的预算额度。不含成本维度。模型网关的重试还要付 token 钱一次失败请求若已经产生输入 token 消耗重试等于重复计费本文的放大倍数只统计请求数低估了真实成本放大。熔断阈值不给推荐值。第五节说清楚了B 组和 G 组之间那 6.7 个点交付率的取舍取决于本文仿真不掌握的参数。八、复现与判定标准下面是完整的引擎和一个主程序直接落盘就能跑。我把它们全部贴出来是因为这篇文章里每一个数字都能从这两份文件里重新算出来。kb/Support.java——模型与引擎import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; /** * Shared model for the retry-storm experiments. * Every constant that can move a number is explicit here; no hidden magic. */ public final class Support { /* ---------------- scenario ---------------- */ public static final int NCLIENT 200; // clients, 1 business req each per second public static final int DUR 60; // virtual seconds public static final int CAPACITY 240; // req/s the upstream can actually serve public static final int OUT_FROM 20; // outage window [OUT_FROM, OUT_TO) public static final int OUT_TO 26; public static final int UP_LAT 200; // ms, normal upstream service time public static final long SEED 4242L; // fixed every table cell is recomputable public static final int BREAKER_TRIP_FAILS 400; // consecutive failures 400 open public static final int BREAKER_OPEN_SEC 5; // stay open for 5 virtual seconds /* ---------------- strategies ---------------- */ public record Cfg(String name, int maxRetry, long baseMs, String jitter, int budgetPct, long deadlineMs, boolean breaker) { } public static final Cfg[] CFGS { new Cfg(S1 固定重试3次, 3, 0, none, 100, 30_000, false), new Cfg(S2 指数退避无抖动, 5, 500, none, 100, 30_000, false), new Cfg(S3 指数退避全抖动, 5, 500, full, 100, 30_000, false), new Cfg(S4 全抖动重试预算20%, 5, 500, full, 20, 30_000, false), new Cfg(S5 预算deadline, 5, 500, full, 20, 3_000, false), new Cfg(S6 预算熔断, 5, 500, full, 20, 3_000, true), }; /* ---------------- result ---------------- */ public static final class Result { public long attempts, delivered, failed; public long breakerOpen; // 熔断跳闸次数 public long breakerHarmful; // 打开期间被快速拒绝、但其实本可以被服务的请求数 public final ListInteger tripAt new ArrayList(); public final int[] perSec new int[DUR]; public final ListInteger lat new ArrayList(); public Cfg cfg; public double amp() { return (double) attempts / (NCLIENT * DUR); } public double deliv() { return 100.0 * delivered / (NCLIENT * DUR); } public int peak() { int m 0; for (int v : perSec) m Math.max(m, v); return m; } public int overloadSeconds() { int n 0; for (int v : perSec) if (v CAPACITY) n; return n; } } public static Result run(Cfg c, long seed) { return run(c, seed, BREAKER_TRIP_FAILS); } /** * One simulated outage script. * * Convention that decides everything downstream: a request that arrives in second * {code arr} and has already waited {code waitMs} is charged to second * {code arr ceil(waitMs/1000)}, clamped to the last bucket. So a zero backoff * (S1) charges all of its attempts to the same one-second bucket. */ public static Result run(Cfg c, long seed, int tripThreshold) { Random rnd new Random(seed); Result r new Result(); r.cfg c; long budgetMax (long) CAPACITY * c.budgetPct() / 100L; // refill unit long budgetTokens budgetMax; int openUntil -1; int consecFail 0; for (int arr 0; arr DUR; arr) { for (int client 0; client NCLIENT; client) { // one business request int attempt 0; long waitMs 0; int sec arr; boolean done false; while (!done) { // 熔断打开期间直接快速失败不打上游 if (c.breaker() sec openUntil) { // 这个请求连上游都没碰到。它本可以被服务吗 if (!(sec OUT_FROM sec OUT_TO) r.perSec[sec] CAPACITY) r.breakerHarmful; r.failed; done true; break; } // 重试预算令牌不足直接放弃这就是「宁可失败也不放大」 if (c.budgetPct() 100 attempt 0) { if (budgetTokens 0) { r.failed; break; } budgetTokens--; } if (attempt c.maxRetry()) { r.failed; break; } if (sec r.perSec.length) sec r.perSec.length - 1; r.perSec[sec]; r.attempts; if (waitMs c.deadlineMs()) { r.failed; break; } // deadline 传播 boolean outage sec OUT_FROM sec OUT_TO; boolean overload !outage r.perSec[sec] CAPACITY; boolean ok !outage !overload; if (ok) { r.delivered; r.lat.add((int) waitMs UP_LAT); consecFail 0; break; } consecFail; if (c.breaker() consecFail tripThreshold sec openUntil) { r.breakerOpen; r.tripAt.add(sec); openUntil sec BREAKER_OPEN_SEC; } long backoff c.baseMs() attempt; // 500,1000,2000,4000,8000 long delay full.equals(c.jitter()) ? (long) (rnd.nextDouble() * backoff) // full jitter: [0, backoff) : backoff; waitMs delay; attempt; sec (int) Math.min(arr (waitMs 999) / 1000, DUR - 1L); // ceil to whole seconds } } // 预算桶每秒补充一次budgetTokens min(budgetMax, budgetTokens budgetMax) budgetTokens Math.min(budgetMax, budgetTokens budgetMax); } return r; } /* ---------------- small helpers ---------------- */ public static int percentile(ListInteger xs, double p) { if (xs.isEmpty()) return 0; ListInteger c new ArrayList(xs); Collections.sort(c); int idx (int) Math.ceil(p / 100.0 * c.size()) - 1; return c.get(Math.max(0, Math.min(idx, c.size() - 1))); } public static String pad(String s, int w) { int display 0; for (int i 0; i s.length(); i) display (s.charAt(i) 0x2E7F) ? 2 : 1; // CJK 2 columns return s .repeat(Math.max(0, w - display)); } public static Support.Cfg with(Support.Cfg b, int budgetPct) { return new Support.Cfg(b.name(), b.maxRetry(), b.baseMs(), b.jitter(), budgetPct, b.deadlineMs(), b.breaker()); } private Support() { } }demo20/RetryStorm.java——跑六策略对照表/** Six retry strategies, one outage script, one seed. Prints the comparison table. */ public class RetryStorm { public static void main(String[] args) { long seed Support.SEED; int budgetOverride -1; for (String a : args) { if (a.startsWith(--seed)) seed Long.parseLong(a.substring(7)); else if (a.startsWith(--budget)) budgetOverride Integer.parseInt(a.substring(9)); } final long sd seed; System.out.println( 上游第 Support.OUT_FROM ~ Support.OUT_TO 秒整体不可用容量 Support.CAPACITY req/s Support.NCLIENT 客户端 × Support.DUR 秒seed sd (budgetOverride 0 ? budgetPct budgetOverride 全局覆盖 : ) ); System.out.println(); System.out.print(Support.pad(策略, 24) Support.pad(放大倍数, 10) Support.pad(交付率, 9) Support.pad(峰值QPS, 9) Support.pad(过载秒, 8) Support.pad(交付中位ms, 12) Support.pad(p99ms, 10) 熔断开/跳闸秒/误伤); System.out.println(); System.out.println(-.repeat(100)); for (Support.Cfg base : Support.CFGS) { Support.Cfg c budgetOverride 0 ? Support.with(base, budgetOverride) : base; Support.Result r Support.run(c, sd); System.out.print(Support.pad(c.name(), 24) Support.pad(String.format(%.2fx, r.amp()), 10) Support.pad(String.format(%.1f%%, r.deliv()), 9) Support.pad(String.valueOf(r.peak()), 9) Support.pad(String.valueOf(r.overloadSeconds()), 8) Support.pad(String.valueOf(Support.percentile(r.lat, 50)), 12) Support.pad(String.valueOf(Support.percentile(r.lat, 99)), 10) r.breakerOpen / r.tripAt / r.breakerHarmful); System.out.println(); } System.out.println(); System.out.println(注交付率按「业务请求最终有没有拿到结果」计被 deadline 掐掉与预算拒发都算未交付。); System.out.println(业务请求总数 (Support.NCLIENT * Support.DUR) 放大倍数 上游收到的 attempt 数 / 业务请求数。); } }编译与执行Windows 用;分隔 classpathLinux/macOS 换成:cd kb javac -encoding UTF-8 -d out Support.java cd ../demo20 javac -encoding UTF-8 -cp ../kb/out -d out RetryStorm.java Herd.java Sweep.java Breaker.java java -cp ../kb/out;out RetryStorm # 六种策略放大倍数与过载秒数 java -cp ../kb/out;out Herd # 恢复窗口每秒到达量 java -cp ../kb/out;out Sweep # 六个种子 预算归零对照 判定断言 java -cp ../kb/out;out Breaker # 熔断七组单变量对照四条判定对不上就别用这张表S4 的过载秒必须远小于 S2本文 4 vs 34说明过载反馈环在起作用S1 的放大倍数必须低于 S2 和 S3本文 1.30x vs 2.34x / 2.48x——如果你那边不是检查是不是把「重试次数」和「最大重试次数」写混了恢复后 3 秒到达总量必须满足 S4 远小于 S2 与 S3本文 728 vs 2560 / 3067把budgetPct改成 100 重跑S4 应当逐位塌回 S3——这一步是验证「起作用的是预算不是抖动」本文实测两者差值 0.000。Sweep.java里我把前三条写成了自动断言跑完直接打印 PASS/FAIL本次三条全部 PASS第四条是差值对照打印出来的是 0.000。另外提醒一句RetryStorm输出的表格用等宽对齐中文列宽按两格算见Support.pad在file.encodingGBK的终端里直接跑会看到乱码加-Dsun.stdout.encodingUTF-8或者重定向到文件再看。九、结论六秒故障能变成三十四秒过载中间没有任何人做错事只是每个客户端都在「对自己负责」。三句可以记住的判断一、指数退避是局部最优、全局负债。它把交付率买到 100%同时把过载秒数从 6 拉到 34、p99 从 200 ms 拉到 7700 ms。你监控里那个「成功率」变好看了系统活着的时间变短了。二、抖动治同步化不治总量这次连尾都没治住。中位数改善 71%p99 却恶化 19%放大倍数还高了 6%——把「加抖动」当成万能药之前先想清楚你要改善的是哪一个分位。三、真正把放大掐住的是重试预算它的本质是承认「有些请求就该失败」。而熔断不是独立防线它跳不跳闸取决于预算和 deadline 有没有先把失败攒成连续。今晚可以做的两件事把你的maxAttempts从 3 降到 2再给重试单独加一层按容量折算的速率闸然后用第八节这两份文件跑一次你自己的故障剧本改OUT_FROM、CAPACITY、budgetPct三个参数就够了看过载秒数降不降。两个提问想听你的线上经验你的网关有没有给「重试」单独限流还是和正常流量共用一个配额以及你有没有量过一次故障期间上游到达量相对正常时刻的放大倍数——如果你的数字低于 1.3x我很想知道你是怎么做到的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑