LabHub
学习 学习路径 课程

微服务架构

网络调用不是函数调用

在 LabHub 中继续学习

一句话总结

同步调用必须默认明确三件事:超时时间、重试条件,以及绝不重试的条件。

概念图: 从外向内逐渐缩短 · 超时 ×(重试次数 + 1)必须小于外层超时。 · 干脆不开始工作 · 失败持续发生时停止尝试本身。

为什么需要了解这些

单体应用内部的函数调用只有一种失败模式:抛出异常或返回值。网络调用却有五种失败模式:连接失败、连接成功但没有响应、响应缓慢、只收到一部分响应,以及最棘手的一种——对方已经完成处理,只有响应丢失。

最后一种之所以棘手,是因为调用方无法区分“没有成功”和“已经成功但自己不知道”。这种不可区分性,正是后续幂等性与 Saga 的起点。

工作原理

先看超时。许多 HTTP 客户端没有默认超时,没有超时实际上就是无限等待。无限等待的危险在于线程或连接会被占住。下游每个请求慢 5 秒,上游连接池会首先耗尽,随后连与该下游完全无关的请求也会失败。这是级联故障最常见的起点。

超时值不应靠猜测,而应从对方的 p99 开始。如果下游 p99 为 120ms,300~500ms 左右通常较为合理。设置为 p99 的十倍,几乎等于没有超时。

重试有明确条件。只有临时错误值得重试,例如连接失败、超时、503、429。400、401、404 等确定性错误,无论发送多少次都会得到相同结果。而且,重试必须同时采用指数退避和抖动。固定间隔重试会制造惊群效应。

有一个数字值得记住。订单服务有 20 个实例,每个实例每秒处理 100 个请求,如果把 maxAttempts 设为 5,下游开始抖动时,支付服务每秒最多会收到 10,000 个请求:100 × 20 × 5。重试会放大负载。

换成 gRPC 后,序列化会更快,也支持流式传输,但上述三个问题依然存在。不过,截止时间在 gRPC 中是一等协议概念,并且能够跨越调用链传播,这一点优于 REST。

生产现场中的常见情况

调用链为 A → B → C 时,一个常见错误是把每层超时都设为 3 秒。A 最多等待 3 秒,B 却会等待 C 3 秒;即使 A 已经超时,B 与 C 仍会继续工作,为注定被丢弃的结果消耗资源。超时应外层较长、内层较短。若能通过请求头或截止时间把剩余预算继续向下传递,效果会更好。

各层超时应当不同

请求经过多个层级时,超时必须从外向内逐渐缩短。 否则,外层会先放弃,内层却继续生成无人等待的响应。

브라우저        30s
  게이트웨이    10s     ← 바깥보다 짧다
    API         6s
      결제 서비스 3s
        DB       1s

如果每一层都配置重试,耗时会相乘。API 的超时为 3 秒、重试 2 次时, 最坏情况需要 9 秒,几乎用尽网关的 10 秒预算。超时 ×(重试次数 + 1)必须小于外层超时。

还可以通过请求头传播期限(deadline propagation)。gRPC 原生支持,HTTP 则需要自行定义 X-Request-Deadline 等请求头。剩余时间接近 0 时,干脆不开始工作更节省资源。

断路器的开启条件

仅靠重试无法挽救已经崩溃的下游,反而会增加负载。断路器会在失败持续发生时停止尝试本身。

닫힘(정상) ──실패율 임계 초과──> 열림(즉시 실패)
    ↑                              │
    └──성공──  반열림(몇 개만 보내 봄) <──일정 시간 뒤──┘

阈值不应只看失败数量,而应结合失败比例与最小样本数。“10 次中失败 5 次”有意义,“2 次中失败 1 次”则可能只是偶然。

# 예: 20건 이상일 때, 실패율 50% 넘으면 30초 열림
minimumNumberOfCalls: 20
failureRateThreshold: 50
waitDurationInOpenState: 30s
permittedNumberOfCallsInHalfOpenState: 3

断路器开启后返回什么才是设计:可以返回缓存中的旧值、降级响应,或明确的错误。毫无计划地直接返回 500,与没有断路器几乎没有区别。

在响应中表示部分失败

调用多个服务生成一个页面时,不应因为其中一个失败就让整个请求失败。更好的方式是返回明确说明缺失内容的响应

{
  "order": {"id": 1043, "amount": 52000},
  "recommendations": null,
  "degraded": ["recommendations"]
}

页面只隐藏推荐区域,其他部分仍正常显示。要做到这一点,必须提前区分必需项与可选项:订单信息是必需项,推荐是可选项。没有这种区分,所有调用都会变成必需调用,整体可用性就会不断相乘下降。

下一次实验要做什么

面对故意变慢、故意失败的下游,设置超时,实现指数退避与抖动,确保 4xx 不会被重试,最后计算重试预算。