恢复的瞬间,最危险
下游故障恢复的那一刻,往往是最危险的时刻。积压的请求、堆积的重试、等急了不停刷新页面的用户,全在门口排着队——门一开,洪水同时灌进来,刚缓过来的下游二次躺平。重试本是好意,但在下游眼里,无节制的重试就是一次 DDoS。
三重放大
算一笔账,看重试怎么把流量放大。第一重积压放大:10 秒故障期间积压的请求,在恢复瞬间集中涌入,瞬时流量数倍于常态。第二重重试放大:上游失败重试 3 次,同一笔业务最多打出 4 个请求,放大 4 倍。第三重层级放大:调用链三层、每层都重试 3 次,最坏 4×4×4 = 64 个请求,只为同一笔业务。
再叠加用户行为:页面超时,用户手动刷新,又来一轮。10 倍常态流量被放大成上百倍瞬时洪峰——多少「恢复即雪崩」的事故,都是这笔账算出来的。
退避与抖动
第一招是指数退避:第 1 次等 1s、第 2 次等 2s、第 3 次等 4s,重试越来越稀疏,给下游喘息。但光退避还不够——同一时刻失败的所有请求,退避后的重试点完全相同,部队齐步过桥,桥照样塌。所以要再加随机抖动,把重试时间点打散:
long backoff = (long) (baseMs * Math.pow(2, attempt)); // 1s, 2s, 4s...
long jitter = ThreadLocalRandom.current().nextLong(backoff / 2, backoff);
Thread.sleep(jitter); // 指数退避 + 随机抖动,别让所有人同一瞬间重试只重试「值得重试」的
重试之前的前提审查,比重试本身更重要:幂等的才重试——写操作没做幂等保护(分布式事务篇讲过的唯一索引、去重表),重试就是重复下单;可重试的错才重试——超时、503、连接拒绝可以再试,参数错误、余额不足试一万次也是失败;次数封顶——2~3 次足够,重试是补救不是主力,彻底失败还有降级兜底。
重试预算与熔断联动
更精细的做法是重试预算(gRPC 的思路):给整个服务设一个重试额度,比如「重试请求数不超过活跃请求的 10%」——正常时段偶发失败随便重试;下游大面积失败时额度瞬间耗尽,后续请求直连直返,重试自动熄火。再和熔断联动:熔断器 OPEN 期间禁止重试(都快速失败了还试什么),半开态的探测请求走独立通道,不占重试预算。
顺带一提对冲请求(hedging):不等失败就提前发第二个请求,谁先返回用谁——专治长尾延迟,代价是双倍流量,只配给核心接口用。
退避、抖动、预算、联动,纪律都立好了。但回头看看,所有阈值都是人拍的——流量涨了阈值偏小,扩容了阈值偏大。有没有让系统按实时负载自己调阈值的办法?下篇聊自适应保护。
评论 (0)