--- tags: [gRPC, Go, Context, Call Options, Metadata, Retry, Deadline] create time: 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 基础 ```go ctx := context.Background() // 推荐:显式设置 deadline,让下游知道剩余时间 ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() // 不调用会泄漏 timer ``` Context 的传播链是单向的——每个 `With*` 函数生成新的 context,原始 context 不会被修改: ```mermaid 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 相关**:消息大小限制、压缩算法、重试策略、用户代理 ```go // ⚠️ 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 ```go 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 等上下文信息: ```go // 注入 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 中声明: ```go 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 会导致重复写入。 ```mermaid 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 被取消时,整个调用链的反应如下: ```mermaid 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。 ```go // 第一层 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 创建时就确定了,不能在过程中动态修改**: ```go 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 控制生命周期