跳到内容

第 7 卷 / 第 2 章 / 第 2 课

2.2 Deadline、取消、重试与流式背压 ​

RPC 接口正确不代表调用链可靠。调用方必须限制等待,服务端必须停止无价值工作,重试必须尊重幂等性,流式通信必须限制生产速度。

每次调用都携带 deadline ​

deadline 表示调用方愿意等待到何时。它应从端到端用户预算向下传播,而不是每一层重新获得完整超时。

text
入口剩余 900 ms
  → 服务 A 处理 120 ms
  → 调用 B 时剩余约 780 ms
  → B 调用 C 时继续扣除已耗时间

传播时应传递剩余时长,避免把不同机器的绝对时钟直接当作共同截止时间;gRPC 的 deadline 传播会扣除已耗时间,但是否自动传播取决于语言与调用方式。还应留出返回响应和清理资源的预算。

默认没有 deadline 的客户端可能长期占用调用相关资源。过短的 deadline 则会制造取消和重试流量。数值应由延迟分布、负载测试和业务预算决定。

客户端 deadline 到期后,服务端框架可以通知取消,但业务代码仍要协作检查取消信号,并把取消传播给派生任务和外部调用。已经提交的数据库事务不会因客户端取消自动回滚。

Status 是契约的一部分 ​

常见 gRPC 状态的语义应稳定:

  • INVALID_ARGUMENT:参数本身无效;
  • FAILED_PRECONDITION:系统当前状态不允许操作;
  • ABORTED:并发冲突,通常需在更高层重试事务;
  • NOT_FOUND:目标资源不存在;ALREADY_EXISTS:要创建的资源已存在;
  • UNAVAILABLE:暂时不可用,可能适合退避重试;
  • DEADLINE_EXCEEDED:等待截止,写操作结果仍可能未知;
  • UNAUTHENTICATED 与 PERMISSION_DENIED:没有有效身份凭据和权限不足。

不要把全部业务拒绝变成 INTERNAL,也不要让客户端解析英文 message 决定重试。

Retry 只处理可重试语义 ​

安全自动重试通常要求:

  • 操作天然幂等,或服务端持久处理同一幂等键的重复请求;
  • 错误明确表示瞬态;
  • 总 deadline 尚有预算;
  • 尝试次数、退避和随机抖动受限;
  • 由一层负责重试,避免多层 SDK/代理叠加尝试次数。

DEADLINE_EXCEEDED 不证明服务端未执行。业务可以另行提供 GetOperation(idempotency_key) 查询接口,或保存首次结果供重复请求复用;上一课的评分查询接口没有实现这个操作查询 API。

下面是仅针对幂等查询 GetScore 的 gRPC service config。它需要由客户端 channel 的配置机制加载,且每次调用仍要设置 deadline。maxAttempts: 3 包含首次调用,所以最多额外尝试两次,所有尝试和退避共用一次逻辑调用的时间预算。

json
{
  "methodConfig": [{
    "name": [{"service": "atlas.scoring.v1.ScoringService", "method": "GetScore"}],
    "retryPolicy": {
      "maxAttempts": 3,
      "initialBackoff": "0.1s",
      "maxBackoff": "0.4s",
      "backoffMultiplier": 2,
      "retryableStatusCodes": ["UNAVAILABLE"]
    }
  }]
}

一旦响应头到达,gRPC 通常把该 RPC 视为已提交,不再按这套策略重试;流中断后的续传仍归应用协议负责。没有显式重试策略也可能发生框架的透明重试,其前提是框架能够判断请求尚未进入服务端应用处理,不能由此推导写请求可随意重放。

长连接仍会断 ​

流式 RPC 需要设计恢复协议:

  • 每条消息是否有 sequence/version;
  • 客户端从哪个 checkpoint 续传;
  • 重连后如何去重;
  • 历史保留多长;
  • 认证过期如何刷新;
  • 心跳与业务空闲如何区分。

只在内存维护会话且没有恢复 token,网络抖动会把所有长流变成从头开始。

背压和流量控制 ​

HTTP/2/gRPC 提供传输层流量控制,但应用仍需限制业务队列。生产者持续生成而消费者变慢时,应:

  • 限制在途消息和缓冲字节;
  • 等待消费许可或丢弃允许丢弃的数据;
  • 对不可丢数据持久化并从 checkpoint 读取;
  • 设定单条消息和批次上限;
  • 过载时快速拒绝新流。

无界 onNext 循环会把网络背压变成进程内存问题。以每条 64 KiB 的消息为例,只按 1000 条限制队列,原始 payload 就可占约 62.5 MiB,还没算对象开销。计数与字节预算应一起限制。双向流也不能让双方都只写不读;手动流控用错时,两边可能互等窗口,连接很热闹,业务一步没走。

负载均衡与连接生命周期 ​

长连接上的多路 RPC 可能让传统四层负载均衡分布不均。客户端或代理需要感知服务发现、连接老化、实例排空和健康状态。

发布期间先撤出服务发现并启动服务端 graceful shutdown,再等待在途 unary 和 stream 到达明确边界。仅把实例设为 not-ready 不会关闭已经建立的 HTTP/2 连接,也不能保证传播立刻完成。长流应支持迁移或恢复,并设置排空上限;否则 graceful shutdown 可能一直等不到结束。直接终止则可能让大量客户端同时重连。

观测每次尝试与逻辑调用 ​

一次逻辑调用可能包含多次 attempt。指标和 trace 应区分:

  • 逻辑请求成功率;
  • 每次尝试的状态和延迟;
  • retry count 与 retry success;
  • deadline 来源与剩余预算;
  • 流持续时间、消息速率和取消原因;
  • 请求/响应字节和压缩成本。

参考资料 ​

Built with VitePress | Software Systems Atlas