This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/gRPC/4. 客户端开发/12-Call Options 与 Context.md
T
2026-05-11 19:02:38 +08:00

12 KiB
Raw Blame History

tags, create time
tags create time
gRPC
Go
Context
Call Options
Metadata
Retry
Deadline
2026-05-11 15:32

Call Options 与 Context

概述

一个 gRPC 调用不只是 method name + params——你有丰富的 options 来配置超时、认证、路由、优先级等行为。而 Context 则是贯穿所有选项的灵魂:取消信号、deadline、元数据全部通过 context 传递。

[!question] Context 应该用 Background 还是 Todo? 在 RPC 场景中,永远用 context.Background() 或从上游继承 context。context.TODO() 表示"还没想好该用什么",不应该出现在生产代码中。

Context 基础

ctx := context.Background()

// 推荐:显式设置 deadline,让下游知道剩余时间
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel() // 不调用会泄漏 timer

Context 的传播链是单向的——每个 With* 函数生成新的 context,原始 context 不会被修改:

flowchart LR
    A["Background"] -->|"WithTimeout"| B["WithTimeout\n5s"]
    B -->|"Append Metadata"| C["AppendToOutgoingContext"]
    C --> D["ClientMethod Call"]

    B -.->|"cancel / deadline"\n到达 | E["RPC 被取消"]
    E --> F["返回 context.DeadlineExceeded"]

    style B fill:#74b9ff,color:#000
    style E fill:#fdcb6e,color:#000
    style F fill:#d63031,color:#fff

如果任何一层调用了 cancel(),整个链条上的 Recv/Send 都会立即感知到。

Timeout / Deadline:由 Context 负责

你可能在其他 gRPC 库中看到过 timeout 这个参数,但在 Go 中统一通过 context 传递:

[!success] 核心原则:超时走 Context,其余走 Call Option 这不是限制,而是设计哲学——gRPC Go 把一切生命周期管理都交给 context。所有选项本质上可以分成两类:

  • Context 相关:取消、超时、元数据、认证凭证
  • Call Option 相关:消息大小限制、压缩算法、重试策略、用户代理
// ⚠️ gRPC 没有 grpc.WithTimeout() 这个 call option!
// 正确做法:用 context 控制超时
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

resp, err := client.GetUser(ctx, req) // context 自带超时信息

Unary Call Options

import "google.golang.org/grpc"

resp, err := client.GetUser(ctx, &pb.GetRequest{Id: "123"},
    grpc.WaitForReady(true),          // 连接排队中时不拒绝,等待建立
    grpc.MaxCallRecvMsgSize(10<<20),  // 本 call 接收上限 10MB
    grpc.UseCompressor(gzip.Name),    // 强制 gzip 压缩请求体
)

每个 call option 的参数都是 func(*callOpts)——这就是 Go 的函数式选项模式。gRPC 内置了约 15 个选项,常用以上几种。

超时 vs Call Option 对比

维度 通过 Context 通过 gRPC Option
取消信号 ✅ ctx.Done() ❌
超时控制 ✅ WithTimeout / WithDeadline ❌
元数据传递 ✅ AppendToOutgoingContext ❌
消息大小限制 ❌ ✅ MaxCallRecvMsgSize
压缩算法 ❌ ✅ UseCompressor
重试次数 ❌ ✅ NumRetries(需 service config)

[!question] 为什么超时不设计成 Call Option? 因为 context 不仅携带超时信息,还携带取消信号、值传递、认证信息等。如果超时散落在各个 call option 里,就无法统一管理整条调用链的生命周期。Go 的做法是:一个 context,所有生命周期管理。

Option 作用
WaitForReady 连接不在 Ready 状态时等待而非直接失败(默认 false)
MaxCallRecvMsgSize 覆盖该次调用的接收上限(默认 4MB)
MaxCallSendMsgSize 覆盖该次调用的发送上限(默认无限制)
UseCompressor 指定压缩算法(gzip / deflate),不指定则由 gRPC 自动协商
FailOnNonTempDialError 非临时 dial 错误立即返回,不再重试连接
NumRetries 显式指定重试次数(需配合 service config 使用)
UserAgent 设置本次调用的 User-Agent header,用于服务端识别客户端
InitialCredentials 首次通信使用的 credentials(与 PerRPCCredentials 配合)

[!tip] 两个常用的遗漏选项

  • InitialGzip(true):仅在本次调用中启用 gzip 压缩请求体(无需全局配置 compressor)。
  • ReturnRawServerStats():开启后获取原始服务器遥测数据(用于监控和埋点)。

[!warning] MaxCallRecvMsgSize 的层级关系 Dial-level 设的是全局上限,call-level 设的是本次上限。两者取最小值生效。如果服务端发送的消息超过了你客户端的限制,你会收到 received message larger than max 错误。

Metadata 注入与读取

Metadata 是 key-value 对,用于透传 token、trace-id、region 等上下文信息:

// 注入 outgoing metadata
ctx = metadata.AppendToOutgoingContext(ctx,
    "authorization", "Bearer "+token,
    "x-trace-id", traceID,
    "x-region", "cn-east",
)

// 发起 call,同时接收 Header 和 Trailer
resp, md, err := client.SecureMethod(ctx, req,
    grpc.Header(&headerMD),   // RPC 开始时的响应头
    grpc.Trailer(&trailerMD), // RPC 结束时的尾随元数据
)
if err != nil {
    // gRPC 错误详情实际上在 trailer 中
    if cerr, ok := status.FromError(err); ok {
        log.Println("Code:", cerr.Code(), "Details:", trailerMD.Get("grpc-status-details"))
    }
}

Metadata 关键注意事项

[!important] Trailing Metadata 是获取错误详情的唯一途径 gRPC 的错误信息(包括 protobuf Any 类型的详情)是通过 Trailer 传递的。如果你调用服务端接口失败,必须通过 grpc.Trailer() option 接收才能拿到完整错误信息。

字段 方向 常见用途
Content-Type Outgoing + Incoming application/grpc
authorization Outgoing Bearer Token / API Key
x-trace-id Outgoing + Incoming 分布式链路追踪
x-rate-limit-remaining Incoming (Header) 限流计数
grpc-status-details Incoming (Trailer) 结构化错误详情

[!tip] Metadata 的键名规范 gRPC metadata 的 key 统一使用小写——因为 HTTP/2 header 本身不区分大小写,gRPC 库会自动将驼峰转换为小写存储。

Retry Policy 实战

gRPC Go 内置的重试机制需要在 service config 中声明:

config := `{
  "methodConfig": [{
    "name": [{"service": "user.v1.UserService", "method": "GetUser"}],
    "retryPolicy": {
      "maxAttempts": 3,
      "initialBackoff": "0.1s",
      "maxBackoff": "1s",
      "backoffMultiplier": 2,
      "retryableStatusCodes": ["UNAVAILABLE"]
    }
  }]
}`

conn, _ := grpc.Dial(addr, grpc.WithDefaultServiceConfig(config))

重试策略的匹配规则:

  1. 优先匹配 "service":"X","method":"Y"(最精确)
  2. 其次匹配 "service":"X"(服务级)
  3. 最后匹配 "{}" (全局兜底,即没有 name 字段时生效)

⚠️ 幂等性警告:只有 GET 类操作才应该交给自动重试。Write 操作(Create/Update/Delete)必须自行判断是否幂等,因为 retry 会导致重复写入。

flowchart TB
    A["发送请求"] --> B{"收到响应"}
    B -->|"OK 200"| C["返回结果"]
    B -->|"UNAVAILABLE /\nDEADLINE_EXCEEDED"| D{"已达最大\n重试次数?"}
    B -->|"其他错误码"| H["直接返回错误"]

    D -->|"否"| E["等待 Backoff 时间"]
    D -->|"是"| F["返回最终错误"]

    E --> G{"Context 已取消?"}
    G -->|"是"| I["提前退出:\ncontext.Canceled"]
    G -->|"否"| A

    style C fill:#00D866,color:#000
    style F fill:#d63031,color:#fff
    style I fill:#fdcb6e,color:#000
    style H fill:#e17055,color:#fff

退避算法(Backoff)计算示例

假设配置 initialBackoff: 100ms, maxBackoff: 1s, backoffMultiplier: 2:

重试轮次 实际等待时间
第 1 次 100ms
第 2 次 200ms
第 3 次 400ms
第 4 次及以上 1000ms(被 maxBackoff 截断)

[!tip] 不要手动实现指数退避 服务配置会自动处理退避计算。如果需要在代码中做类似逻辑,直接使用 context 的 WithTimeout/WithDeadline 配合循环,或者使用 golang.org/x/time/rate 包。

Context 取消传播

当客户端 context 被取消时,整个调用链的反应如下:

flowchart TB
    A["context.Background()"] -->|"WithTimeout"| B["5秒 Timeout"]
    B --> C["AppendToOutgoingContext MD"]
    C --> D["ClientMethod Call"]

    subgraph Server["服务端"]
        E["Handler ctx.Done()"]
        F["清理资源 / 中止计算"]
    end

    B -.->|"cancel / deadline\ndelivered"| E
    E --> F

    D -.->|"RPC cancelled"| G["返回\ncontext.Canceled"]

    style B fill:#74b9ff,color:#000
    style F fill:#00D866,color:#000
    style E fill:#fdcb6e,color:#000
    style G fill:#d63031,color:#fff
  1. 服务端 handler 的 ctx.Done() channel 会关闭
  2. 服务端应主动停止计算、释放资源
  3. 客户端下一次 Recv 会收到 context.Canceled 错误

[!warning] Cancel 是单向通知 Client 调用 cancel() 后,服务端可能仍在继续处理已接收的消息。gRPC 没有双向联动机制——如果需要强制服务端在 client 取消时也退出,应在服务端 handler 中监听 ctx.Done() 并主动 return。

Deadline vs Timeout

方法 参数类型 语义 适用场景
WithTimeout time.Duration 相对当前时间的 duration 简单场景
WithDeadline time.Time 绝对时间点 跨调用链追踪

推荐使用 WithDeadline 的原因:当你把一个 context 传给下游 RPC 时,你可以从 context 中提取 deadline,计算出剩余时间,作为下一级 timeout。这样整条链路可以共享同一个总 deadline。

// 第一层
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(5*time.Second))
defer cancel()

// 传递给下游时减去缓冲时间
if deadline, ok := ctx.Deadline(); ok {
    remaining := time.Until(deadline) - 500*time.Millisecond
    ctx = context.WithDeadline(ctx, time.Now().Add(remaining))
}

流式调用的 Option 差异

流式(Streaming)的 Call Options 和 Unary 基本一致,但有一个关键区别——Option 在 Stream 创建时就确定了,不能在过程中动态修改:

stream, err := client.FullDuplex(ctx,
    grpc.MaxCallRecvMsgSize(10<<20),  // 影响整个 stream 生命周期
    grpc.UseCompressor(gzip.Name),
)
if err != nil {
    return err
}
defer func() { _ = stream.CloseSend() }() // 关闭 send half

// 后续收发共享同一套配置
for {
    req := <-reqChan
    if err := stream.Send(req); err != nil { break }
    resp, err := stream.Recv() // 超出 MaxCallRecvMsgSize 会失败
    if err != nil { break }
    _ = resp
}

流式调用的特殊注意事项

关注点 Unary Server Stream Client Stream Full Duplex
超时控制 context context context context
消息大小 单次限制 每帧独立限制 每帧独立限制 每帧独立限制
Cancel 时机 返回后立即 Recv 循环结束后 Send 循环结束后 全部完成后
必须关闭 ❌ CloseAndRecv() SendMsg 后自动关半双工 CloseAndSend()

[!tip] 流式调用的 Cancel 泄漏问题 如果服务端仍在发送数据而客户端 context 已取消,Recv goroutine 可能不会被回收。确保每个 Recv goroutine 都监听 ctx.Done() 并在收到取消信号后退出。

关联笔记

  • 11-Client 连接与 Dial — Dial-level 配置是 Call Options 的前置基础
  • 13-Streaming Client — Stream 调用同样依赖 Context 控制生命周期