小Cの已经记不起来的博客

微服务间调用超时、重试和熔断配置的取舍原则

上个月线上出了个事,一个挺边缘的下游服务抖了几秒钟,结果前台的一堆接口全跟着挂了,看监控请求量直接翻了快十倍,下游本来只是抖一下,硬生生被打死,雪崩了。复盘的时候发现,链路上几乎每个服务都配了“重试 3 次、超时 5 秒”,七八个服务叠在一起,一次失败瞬间就变成几十个请求砸向同一个下游,而熔断又配得特别迟钝,完全没兜住。这件事之后我把手里几个核心服务的超时、重试、熔断配置全部重新捋了一遍,下面是捋出来的一些取舍原则,不敢说多权威,但至少都是踩过坑之后的真实感受。

超时是地基,先把超时定对再谈别的

很多人(包括之前的我)配超时基本靠拍脑袋,5 秒不行就 10 秒,反正觉得“超时总比失败好”。但超时设得太长,下游真挂了的时候,你的线程就在那儿干等,Tomcat 的工作线程很快被占满,然后你自己也挂了,这就是事故里第一张倒下的多米诺骨牌。

我的做法是看下游的 P99 耗时来定 read timeout,比如下游 P99 是 300ms,设 1s 就挺从容,没必要给到 5s。另外 connect timeout 和 read timeout 一定要分开配,建连给个 500ms 足够了,连不上说明对方基本凉了,多等也没意义。

还有一个特别容易忽略的点:链路上的超时要递减,上游的超时必须大于下游的超时加上自己处理的余量。不然就会出现下游 2 秒就放弃了,上游还在那儿傻等 5 秒,中间那 3 秒纯属浪费线程,还占着连接不放。

重试不是救命稻草,配不好就是个流量放大器

重试这东西真的是双刃剑,配好了能救场,配不好比不配还可怕。它的前提条件其实很苛刻:第一,接口得幂等;第二,错误得是“瞬时”的。

先说幂等。查询类接口随便重,写操作就得掂量了,尤其是超时之后重试——你根本不知道请求到底执行没执行,网络只是断了,下游可能已经把款扣了,你一重试就是双倍扣款,这种事故我在别的团队见过,惨得很。所以我的原则是:只对连接失败、502/503 这类明确“没执行到”的错误重试,超时一律不重试(除非你确认接口幂等并且下游扛得住双倍流量),4xx 就更别重了,参数错了你重试一百次结果也一样。

再说次数,1 次就够了,真的。下游抖动个几百毫秒,重试 1 次能救回来;要是重试 3 次,抖动期间你的流量直接变 4 倍,等于大家约好了冲锋。配套还得加退避加抖动(backoff + jitter),不然服务一重启,所有实例同一时刻一起重试,那画面太美。

链路长的话还有个更阴险的问题:每一层都重试一次,一层放大 2 倍,五层下来就是 32 倍。所以尽量只在最靠近用户的那一层重试,中间层老老实实把失败往上抛。

熔断是保命用的,别指望它当常态降级

熔断解决的是“下游已经不行了,别再往它身上倒流量”的问题。它和重试是配套的:重试在前面试着救,熔断在后面兜底,一看不对劲就把门关上。

阈值别配得太敏感。拿滑动窗口统计失败率,窗口太小,一两个请求失败就 open,过几秒又恢复,来回震荡,业务方看到的错误比不熔断还多,那就本末倒置了。我一般用 20 个左右的窗口、50% 失败率触发,open 10 秒后进半开,放 3 个请求进去探路,成功了再全量放行。

另外 open 期间的 fallback 要提前想好,是返回缓存、给个默认值,还是干脆明确报错让用户重试。这个不提前定,等真熔断了就是抓瞎。

三者放一起看,预算要对得上

这三个东西其实就是三个预算问题:

时间预算:上游给一次调用的总时间,必须 ≥ 重试次数 × 单次超时 + 退避间隔 + 自己的处理时间。比如 read timeout 是 2s,重试 1 次,那上游至少得留 4s 多,不然第二次重试还没回来,上游已经判死了,重试等于白做。反过来说,如果 SLA 卡死了 3s 内必须返回,那就得倒推:重试 1 次,单次超时最多 1s 左右,留点余量给退避和自己。

流量预算:重试会把流量放大若干倍,这个倍数必须控制在下游能承受的范围内。有条件的话给重试加个全局预算,比如重试请求占比超过 10% 就不再重试,这样即使下游抖了,也不会被重试风暴直接打死。

心智模型可以简化成一句话:超时决定“等多久”,重试决定“再试几次”,熔断决定“还要不要试”。三个都得围着下游的真实能力来配,拿监控里的 P99 说话,别抄网上那种“通用最佳实践”,那些数字抄来的基本都不贴合你自己的服务。

一个我自己在用、感觉比较稳的起点配置长这样,具体数值照着下游 P99 调:

timeout:
  connect: 500ms
  read: 1s
retry:
  max-attempts: 1
  retry-on: connect-refused, 502, 503   # 超时千万别加进来
  backoff:
    base: 100ms
    jitter: true
  budget: 10%                           # 重试占比超了就不再重试
circuit-breaker:
  sliding-window-size: 20
  failure-rate-threshold: 50
  wait-duration-in-open-state: 10s
  half-open-probes: 3

最后一点碎碎念

这些参数真没有标准答案,跟下游性能、机器规格、流量模型全都有关系,所以别指望一次配好就完事。有条件的话,把这些配置收敛到统一的 RPC 框架或者 service mesh 里(比如 Istio、Spring Cloud 那套),改一处全局生效,别让每个服务自己写一套,这次排查事故的时候,光是对齐十几个服务的重试配置就花了一下午,散装配置查起来是真的要命。

上线前在预发环境压一遍,上线之后盯着 dashboard 看重试率、熔断触发次数和下游 P99,参数是慢慢养出来的。没准儿过几个月流量变了又得调一版,正常,慢慢磨吧...

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放