← 中文资源

简体中文版本

gRPC 客户端截止时间控制

为每次 gRPC 调用设置独立 deadline 并随元数据向下游传播,按状态码分类错误:超时不可重试,暂不可用可重试。

阅读英文原文

示例说明

没有截止时间的 RPC 调用是延时炸弹:下游卡住时上游调用栈整条挂起。gRPC 的 deadline 以绝对时间戳随调用传播,链路上的每一跳都自动收窄剩余时间,不会出现外层超时 10 秒而内层还在跑 30 秒的错配。错误码分类则把超时与暂不可用区分开——前者重试必然再超时,后者值得换节点重试。

技术要点

  • deadline 是绝对时间戳且随调用传播
  • DEADLINE_EXCEEDED 语义上不可重试
  • 错误分类驱动上层重试决策

代码解读

callWithDeadline 把 deadline 挂在调用选项里,回调按状态码分派:超时映射为不可重试错误,UNAVAILABLE 映射为可重试错误,其余归为通用失败。Promise 包装把回调式 API 转为可 await 的现代形式,调用方拿到的错误对象自带重试提示字段。

使用建议

deadline 预算要在调用链入口规划,逐层递减。重试配合退避与节点切换才有效。生产环境监控超时占比,持续偏高说明容量不足。

代码示例

import * as grpc from '@grpc/grpc-js';
import { echo } from './generated/echo_grpc_pb';

const client = new echo(
  'api.example.com:443',
  grpc.credentials.createSsl(),
);

function callWithDeadline(payload: string, timeoutMs: number) {
  return new Promise<string>((resolve, reject) => {
    // 每次调用独立截止时间,随调用元数据传播到下游
    const callOptions = { deadline: Date.now() + timeoutMs };

    client.echo(
      { payload } as never,
      new grpc.Metadata(),
      callOptions,
      (err: grpc.ServiceError | null, response?: { payload: string }) => {
        if (err) {
          if (err.code === grpc.status.DEADLINE_EXCEEDED) {
            reject(Object.assign(new Error('deadline exceeded'), { retryable: false }));
            return;
          }
          if (err.code === grpc.status.UNAVAILABLE) {
            reject(Object.assign(new Error('unavailable'), { retryable: true }));
            return;
          }
          reject(new Error('rpc failed: ' + err.details));
          return;
        }
        resolve(response?.payload ?? '');
      },
    );
  });
}

try {
  const echoed = await callWithDeadline('ping', 2_000);
  console.log('echo ok:', echoed.length);
} catch (err) {
  const { message, retryable } = err as Error & { retryable?: boolean };
  console.log('failed:', message, 'retryable:', retryable ?? false);
}