Q先生的世界

面朝大海,春暖花开

gRPC深入详解

只要系统里出现了“服务调用”这件事,大家迟早会遇到两个问题:

  1. 怎么定义接口,才能让不同语言、不同团队、不同服务之间稳定协作
  2. 怎么把一次远程调用,尽可能做得像一次本地函数调用,但又不至于把网络的不可靠性藏得一干二净

gRPC 就是为了解决这类问题而生的。

很多人对 gRPC 的第一印象通常是:

  1. 基于 HTTP/2
  2. 使用 Protocol Buffers
  3. 性能好
  4. 适合微服务

这些都没错,但也都不够。

如果只是记住“gRPC 比 REST 快”,你在真正做系统设计、超时治理、流式处理、负载均衡、排障时,还是会一头雾水。因为 gRPC 真正重要的地方,不只是“快”,而是它在接口契约、传输模型、超时传播、流控和错误语义上提供了一整套比较完整的工程方案。

这篇文章想把这些问题讲透。读完之后,你至少应该能回答下面这些问题:

  1. gRPC 到底解决了什么问题,不解决什么问题
  2. 为什么它要建立在 HTTP/2 之上,而不是 HTTP/1.1
  3. 四种 RPC 模式分别适合什么场景
  4. deadlinetimeoutcontext cancel 到底是什么关系
  5. 为什么有时明明网络没问题,gRPC 还是会卡住
  6. 生产环境里应该如何做服务发现、负载均衡、健康检查和优雅下线
  7. 出现 UnavailableDeadlineExceededCanceled 时,应该先怀疑哪里

1. 先说本质:gRPC 是什么

gRPC 全称是 Google Remote Procedure Call

它本质上是一套 RPC 框架。所谓 RPC,就是让你调用远程服务时,代码形态尽量接近本地函数调用。

比如你希望写出这样的代码:

resp, err := userClient.GetUser(ctx, &GetUserRequest{Id: 1001})

从调用方视角看,这就像本地方法调用;但在底层,实际上发生的是:

  1. 调用参数被序列化
  2. 请求被编码成网络报文
  3. 通过连接发送到远端
  4. 服务端解码、执行业务逻辑
  5. 返回结果再被编码回客户端
  6. 客户端反序列化成语言内对象

所以 RPC 解决的核心问题,不是“发 HTTP 请求”,而是:

  1. 如何定义接口契约
  2. 如何跨进程、跨机器传递参数和返回值
  3. 如何生成客户端/服务端样板代码
  4. 如何处理超时、重试、取消、认证、流控等调用治理问题

一句话说,gRPC 是“面向服务调用”的基础设施,不只是一个传输协议库。

2. gRPC 为什么会流行

在 gRPC 之前,大家也不是不能做服务间调用。

最常见的方式包括:

  1. HTTP + JSON
  2. 各语言自己的 RPC 框架
  3. 基于消息队列的异步通信

那为什么后来很多微服务体系转向 gRPC?

因为它在几个关键点上踩中了工程实践的痛点。

2.1 契约先行

gRPC 以 .proto 为中心定义接口和消息模型。

这意味着:

  1. 接口是明确的
  2. 字段类型是明确的
  3. 服务名、方法名、请求响应结构都是明确的
  4. 可以自动生成多语言代码

相比“文档写在 Confluence,接口靠口头约定”的模式,这种方式稳定得多。

2.2 二进制序列化更紧凑

Protocol Buffers 相比 JSON 更节省带宽,编码解码也通常更高效。

这并不意味着“JSON 就很差”,而是说:

  1. 内部服务调用频繁
  2. 请求量很大
  3. 对延迟和 CPU 比较敏感

这些场景下,Protobuf 的优势会比较明显。

2.3 HTTP/2 带来的连接复用和多路复用

gRPC 默认跑在 HTTP/2 上,这是它工程体验明显优于很多旧 RPC 框架的关键原因之一。

后面我们会详细讲,但先给结论:

  1. 一条 TCP 连接上可以并发多个请求
  2. 不需要像 HTTP/1.1 那样靠大量连接堆吞吐
  3. 天然支持双向流
  4. Header 压缩、二进制分帧更适合高频内部调用

2.4 一整套“调用治理”能力

gRPC 并不只是把请求发出去,它还定义了一整套常见治理语义:

  1. deadline / timeout
  2. cancellation
  3. metadata
  4. status code
  5. interceptor
  6. health checking
  7. load balancing
  8. reflection

这使得它更像一个“服务调用平台协议”,而不是薄薄一层网络封装。


3. 但别神化它:gRPC 不解决什么

很多技术选型出问题,不是因为方案太差,而是因为预期太高。

gRPC 也一样。

它不解决这些问题:

  1. 它不让网络变可靠。远程调用仍然会超时、抖动、丢包、断连。
  2. 它不自动带来正确的服务边界。微服务拆错了,换成 gRPC 也救不了。
  3. 它不等于更简单的调试体验。二进制协议、HTTP/2、多层代理,排障往往比 HTTP + JSON 更复杂。
  4. 它不适合所有对外场景。浏览器兼容性、网关支持、第三方开放接口生态,都让 REST/JSON 仍然长期存在。
  5. 它不天然避免版本兼容问题。Proto 有演进规则,但你仍然可能把接口升级做崩。

所以对 gRPC 最准确的期待应该是:

它能让“内部服务调用”这件事更规范、更高效、更容易治理,但前提是你知道分布式系统里哪些问题永远不会消失。


4. gRPC 的核心组成

从工程视角看,gRPC 可以拆成四层:

  1. IDL 层:用 .proto 定义服务和消息
  2. 序列化层:默认使用 Protocol Buffers
  3. 传输层:通常基于 HTTP/2
  4. 运行时层:stub、interceptor、resolver、LB、deadline、状态码等

这四层最好分开理解,否则容易把一些问题归因错地方。

例如:

  1. 字段兼容性问题,通常属于 Proto 设计问题
  2. 并发卡住,可能是 HTTP/2 流控或连接问题
  3. 调用超时,可能是 deadline 设计或链路上某一跳阻塞
  4. 请求打到错误实例,可能是 resolver / load balancer / service mesh 问题

5. 从一个 proto 文件开始理解 gRPC

先看一个最小可用的例子:

syntax = "proto3";

package user.v1;

option go_package = "example.com/api/user/v1;userv1";

service UserService {
    rpc GetUser(GetUserRequest) returns (GetUserResponse);
    rpc ListUsers(ListUsersRequest) returns (stream User);
    rpc UploadAvatar(stream UploadAvatarRequest) returns (UploadAvatarResponse);
    rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}

message GetUserRequest {
    int64 id = 1;
}

message GetUserResponse {
    User user = 1;
}

message ListUsersRequest {
    int32 page_size = 1;
}

message UploadAvatarRequest {
    int64 user_id = 1;
    bytes chunk = 2;
}

message UploadAvatarResponse {
    string url = 1;
}

message ChatMessage {
    string from = 1;
    string text = 2;
}

message User {
    int64 id = 1;
    string name = 2;
}

这个文件里已经包含了 gRPC 最重要的两个概念:

  1. service:定义可调用的方法
  2. message:定义请求和响应的数据结构

其中每个字段后面的编号,比如 id = 1,不是装饰品,而是 Protobuf 编码里的稳定字段号。这个编号一旦对外发布,就应该被当成接口契约的一部分认真维护。

5.1 契约先行意味着什么

一旦你写了 proto 并发布给其他服务使用,就意味着你实际上承诺了几件事:

  1. 字段语义要稳定
  2. 编号不能随便复用
  3. 删除字段要谨慎
  4. 新增字段要考虑老客户端兼容性

Proto 的兼容性后面会单独讲,这里先记住一句最重要的话:

对 gRPC 来说,.proto 文件就是接口法律文本。


6. 四种 RPC 模式

很多人第一次接触 gRPC,只用了最简单的“请求一次、返回一次”。但 gRPC 真正和传统 HTTP API 拉开差距的地方,是它把流式通信也变成了一等公民。

gRPC 支持四种方法类型。

6.1 Unary RPC

最常见,和普通函数调用最像。

rpc GetUser(GetUserRequest) returns (GetUserResponse);

特点:

  1. 一次请求
  2. 一次响应
  3. 适合绝大部分查、改、创建接口

6.2 Server Streaming RPC

客户端发一个请求,服务端连续返回多个消息。

rpc ListUsers(ListUsersRequest) returns (stream User);

适合:

  1. 批量扫描
  2. 日志/事件订阅
  3. 大结果集分批返回

相比一次性返回大数组,这种方式内存压力更小,也更容易边生成边消费。

6.3 Client Streaming RPC

客户端不断发消息,服务端最终汇总返回一个响应。

rpc UploadAvatar(stream UploadAvatarRequest) returns (UploadAvatarResponse);

适合:

  1. 文件上传
  2. 分片传输
  3. 批量写入

6.4 Bidirectional Streaming RPC

客户端和服务端都可以持续发送消息,彼此独立。

rpc Chat(stream ChatMessage) returns (stream ChatMessage);

适合:

  1. 聊天
  2. 实时协同
  3. 代理/隧道类场景
  4. 持续控制平面通信

6.5 四种模式的本质区别

它们底层都建立在同样的连接与帧模型上,区别主要在于:

  1. 谁能发多条消息
  2. 谁先结束发送
  3. 双方生命周期如何协调

这也是 gRPC 比传统 REST 更像“通用调用框架”的原因之一。


7. 为什么 gRPC 依赖 HTTP/2

如果只从“传数据”角度看,HTTP/1.1 似乎也不是不能用。但 gRPC 想做的事情,不只是“请求-响应”,而是高频、低开销、可流式、可治理的服务调用。

这时 HTTP/2 的几个特性就变得非常关键。

7.1 二进制分帧

HTTP/2 把通信拆成更底层的 frame,多个 frame 组成 stream,多个 stream 共享同一条 TCP 连接。

这带来两个直接好处:

  1. 协议处理更适合机器,不用像文本协议那样做大量字符串解析
  2. 可以在一条连接里并发承载多个请求

7.2 多路复用

HTTP/1.1 虽然有 keep-alive,但请求并发能力很有限,容易出现队头阻塞,很多实现最终要靠连接池顶住并发。

HTTP/2 则允许多个 stream 在一条连接上并发传输。

这意味着:

  1. 连接数量通常更少
  2. TLS 握手成本更低
  3. 长连接利用率更高
  4. 流式调用更自然

7.3 Header 压缩

HTTP/2 使用 HPACK 压缩头部。对高频内部调用来说,这能显著减少重复 header 带来的开销。

7.4 流控

HTTP/2 在连接级和 stream 级都有 flow control。

这个能力非常重要,因为流式调用如果没有背压机制,就很容易出现:

  1. 发送端拼命发
  2. 接收端处理不过来
  3. 内存暴涨
  4. 整个连接被拖慢

流控的存在,让“发送速度”和“消费速度”之间能形成一种受控关系。


8. 一次 gRPC 调用在底层到底发生了什么

以最简单的 Unary 调用为例,整个过程大致如下:

sequenceDiagram
    participant C as Client
    participant S as Server

    C->>S: HTTP/2 HEADERS (path=/user.v1.UserService/GetUser)
    C->>S: DATA (protobuf request)
    C->>S: END_STREAM

    S->>S: decode protobuf + execute handler

    S->>C: HTTP/2 HEADERS (:status=200, content-type=application/grpc)
    S->>C: DATA (protobuf response)
    S->>C: HEADERS (grpc-status=0)

这个过程里有几个很容易被忽略的点。

8.1 gRPC 方法名体现在 HTTP/2 path 上

通常会类似这样:

/user.v1.UserService/GetUser

也就是说,HTTP/2 仍然是承载层,gRPC 不是“脱离 HTTP 的另一套世界”。

8.2 真正的业务状态不主要靠 HTTP status 表达

gRPC 正常情况下,HTTP 状态经常仍然是 200,而真正的调用成功失败由 grpc-statusgrpc-message 来表达。

这点非常重要。

比如:

  1. HTTP 200 不代表业务成功
  2. HTTP 503 往往表示中间代理或传输层问题
  3. gRPC Unavailable 更接近“这次调用没有成功建立/维持有效服务能力”

如果你还用“只看 HTTP 状态码”的思路排障,会很容易看偏。

8.3 响应末尾的 trailers 很关键

gRPC 经常把最终状态放在 HTTP/2 trailers 里。

所以如果中间代理不支持 trailers,或者对 HTTP/2 / gRPC 支持不完整,就可能导致非常诡异的问题。

这也是为什么“随便拿一个七层代理转 gRPC”经常会翻车。


9. Protocol Buffers 为什么适合 gRPC

Protobuf 之所以被选作 gRPC 默认序列化协议,不只是因为它“更快”,而是因为它在接口契约和演进上更适合服务调用。

9.1 强类型

字段是显式类型的:

  1. int32
  2. int64
  3. string
  4. bool
  5. bytes
  6. 嵌套 message
  7. repeated
  8. map

这让跨语言生成代码更自然,避免了大量 JSON 场景里“看起来是字符串,其实想表达时间/数字/枚举”的暧昧状态。

9.2 向前向后兼容能力更强

Proto 并不是完全免疫兼容问题,但只要遵守规则,升级通常比手写 JSON 协议更稳。

最常见的规则包括:

  1. 可以新增字段
  2. 不要修改已发布字段的编号
  3. 不要随便改变字段含义
  4. 删除字段后最好保留 reserved
  5. 枚举值要谨慎扩展

例如:

message User {
    reserved 3;
    reserved "email";

    int64 id = 1;
    string name = 2;
}

这样可以避免后来有人误把旧字段号或旧字段名重新用于别的含义。

9.3 不要把 Proto 当数据库表

这是一个常见误区。

很多团队会把 Proto 设计成 ORM 映射,直接把数据库所有字段原封不动暴露出去。结果往往是:

  1. 接口语义被存储模型绑死
  2. 一点表结构调整就引发大面积兼容问题
  3. 一个 message 同时承担存储模型、传输模型、展示模型三种职责

更合理的做法是:

Proto 应该首先表达接口语义,而不是数据库物理结构。


10. gRPC 的 Metadata、状态码与错误模型

10.1 Metadata 是什么

Metadata 类似 HTTP header,但语义更偏 RPC 上下文。

常见用途包括:

  1. 认证信息
  2. Trace ID / Request ID
  3. 租户信息
  4. 调用来源
  5. 灰度标记

要注意的是:

  1. 它不适合承载大对象
  2. 敏感信息需要配合 TLS 和脱敏日志
  3. 不同代理、中间件可能会对 header 大小有限制

10.2 gRPC 状态码比 HTTP 状态码更贴近 RPC 语义

常见状态码包括:

  1. OK
  2. Canceled
  3. Unknown
  4. InvalidArgument
  5. DeadlineExceeded
  6. NotFound
  7. AlreadyExists
  8. PermissionDenied
  9. Unauthenticated
  10. ResourceExhausted
  11. FailedPrecondition
  12. Aborted
  13. OutOfRange
  14. Unimplemented
  15. Internal
  16. Unavailable
  17. DataLoss

这里面最容易混淆的是下面几个。

10.3 DeadlineExceededCanceledUnavailable 的区别

DeadlineExceeded

  1. 调用在 deadline 内没完成
  2. 不一定是服务端慢,也可能是排队、网络、代理、重试、连接建立等任一阶段耗尽了预算

Canceled

  1. 通常是调用方主动取消
  2. 也可能是上游链路取消后向下传播

Unavailable

  1. 服务暂时不可用
  2. 常见于连接失败、RST、GOAWAY、实例摘除、DNS/resolver 问题、代理拒绝等
  3. 往往适合有限重试,但不能无脑重试

要学会区分:

DeadlineExceeded 更像“时间预算耗尽”,Unavailable 更像“服务通道当前不可用”,Canceled 更像“这次调用被主动终止”。


11. gRPC 中最重要的治理概念:Deadline

如果只让我选一个最该认真掌握的 gRPC 机制,我会选 deadline

因为一旦你开始做微服务链路,所有问题都会收敛到一句话:

这次调用还值不值得继续等?

11.1 为什么必须有 deadline

没有 deadline 的 RPC 非常危险。

因为只要链路某处卡住:

  1. goroutine / 线程会堆积
  2. 连接会被长期占用
  3. 上游调用者也会被拖死
  4. 故障会向整个调用链扩散

这就是典型的“雪崩”起点之一。

11.2 timeout 和 deadline 的区别

二者概念相关,但不完全一样。

timeout 通常表示“从现在开始最多等多久”。

deadline 更像“最晚到哪个绝对时刻”。

在多跳调用链里,deadline 的表达往往更好,因为它天然适合继续向下传播剩余预算。

举个例子:

  1. 用户请求总预算 800ms
  2. Gateway 自己已经花掉 120ms
  3. 再调用用户服务时,不应该重新给 800ms,而应该只给剩余预算

这就是 deadline 传播的价值。

11.3 deadline 传播的工程意义

调用链如果不传播 deadline,经常会出现一种很糟糕的现象:

  1. 上游已经超时返回了
  2. 下游还在继续干活
  3. 更下游也在继续占资源
  4. 最终系统做了大量“没人还关心结果”的无效工作

所以正确做法通常是:

  1. 入口请求设总 budget
  2. 每一跳向下传递剩余 deadline
  3. 下游及时监听取消信号并尽早停止无效工作

11.4 服务端必须配合检查 context

很多人以为设置了客户端超时就结束了,其实不够。

如果服务端 handler 不检查 ctx.Done(),或者业务代码完全不响应取消,那即使调用方超时退出,服务端也可能继续跑到底。

这类问题在线上非常常见。


12. 流控与背压:为什么流式调用并不总是越快越好

只要你开始使用 streaming,就必须理解 flow control。

12.1 没有背压会发生什么

假设服务端不断推送消息,而客户端处理速度跟不上。

如果没有背压机制,就会出现:

  1. 发送缓存不断膨胀
  2. 内存越来越大
  3. 延迟越来越高
  4. 整个连接上其他 stream 也可能受影响

HTTP/2 的流控就是为了解决这个问题。

12.2 流控不是错误,而是保护机制

很多人看到吞吐下降,第一反应是“是不是网络坏了”。

实际上,有时只是因为:

  1. 接收端消费太慢
  2. 窗口没有及时更新
  3. 某个大流占住了连接级窗口

所以当你做大消息流、批量同步、日志订阅时,除了业务逻辑本身,还要关注:

  1. 单条消息大小
  2. 发送节奏
  3. 消费端处理能力
  4. 是否把不同流量混跑在同一条连接上

12.3 大消息不一定是好设计

很多“性能优化”最后都变成了“大包传输”,但在 gRPC 里这未必是好事。

因为大消息会带来:

  1. 编解码成本升高
  2. 更明显的流控阻塞
  3. 内存峰值上升
  4. 出错重传代价更大

在很多场景里,把超大响应拆成 server streaming,体验反而更稳定。


13. 连接、Keepalive 与健康探测

13.1 gRPC 是长连接友好的

gRPC 通常会复用底层 HTTP/2 长连接,而不是每次请求都重新建连。

这带来性能收益,但也引入了长连接系统的典型问题:

  1. 连接表面活着,实际上对端已经不可用
  2. NAT / LB / 防火墙会清理长时间空闲连接
  3. 单连接承载过多请求时,局部抖动影响面更大

13.2 Keepalive 的作用

gRPC 的 keepalive 机制,主要目的是:

  1. 检测连接是否仍然可用
  2. 避免中间网络设备把连接悄悄清掉

但 keepalive 也不是越激进越好。

如果 ping 太频繁,可能:

  1. 增加不必要的网络与 CPU 开销
  2. 触发服务端的反滥用限制
  3. 被代理或 LB 识别为异常连接行为

所以它应该根据网络环境和基础设施特性调优,而不是照抄示例值。

13.3 应用健康检查和连接健康不是一回事

一个 TCP/HTTP2 连接可用,不代表应用真的健康。

例如:

  1. 进程还活着,但线程池打满了
  2. 数据库已经不可用
  3. 下游依赖挂了
  4. 服务准备下线但还没摘流量

所以生产环境里通常还要配合:

  1. gRPC Health Checking Protocol
  2. readiness / liveness
  3. 实例摘除和优雅下线机制

14. 服务发现与负载均衡

14.1 gRPC 客户端负载均衡和传统反向代理思路不完全一样

很多 HTTP 系统习惯把负载均衡都交给 Nginx、Envoy、SLB。

而 gRPC 里常见两种模式:

  1. Proxy LB:客户端连一个代理,由代理转发到后端实例
  2. Client-side LB:客户端自己拿到实例列表并在本地选路

Client-side LB 的优点是少一跳,感知更及时;缺点是客户端逻辑更复杂,对服务发现和刷新机制要求更高。

14.2 Resolver + Balancer

从概念上看,gRPC 客户端常常需要两个角色:

  1. resolver:把一个服务名解析成后端地址列表
  2. balancer:在这些地址之间做选路

例如:

  1. DNS resolver
  2. Consul / Etcd / Kubernetes resolver
  3. pick_first
  4. round_robin

14.3 pick_firstround_robin

这是最常被混淆的两个策略。

pick_first

  1. 选一个可用地址建立连接
  2. 后续请求基本都复用这条连接
  3. 更简单,但流量分布可能不均

round_robin

  1. 在多个 ready 子连接间轮询
  2. 分布更均匀
  3. 对连接状态管理要求更高

但要注意,真正的“均匀”还会受到请求耗时差异、连接复用、长流式调用等因素影响。不要把它理解成严格数学平均。

14.4 长流式请求会改变负载均衡形态

如果请求是短平快的 unary,那么轮询比较直观。

但如果你有大量长时间存在的 streaming 调用,那么:

  1. 连接一旦建立,会长时间绑在某些实例上
  2. 后续新请求的均匀性未必能修正已有热点
  3. 单实例可能因为历史连接过多而持续偏热

所以流式场景下的负载均衡,往往要结合连接数、消息速率、实例能力做更谨慎的设计。


15. gRPC 与 REST 应该怎么选

这不是一个“谁淘汰谁”的关系,而是接口类型和系统边界不同。

15.1 更适合 gRPC 的场景

  1. 内部微服务调用
  2. 多语言服务之间的强契约通信
  3. 高频调用、低延迟敏感场景
  4. 需要 streaming 的场景
  5. 希望统一拦截器、超时、认证、可观测性能力的场景

15.2 更适合 REST/JSON 的场景

  1. 对外开放 API
  2. 浏览器直接访问
  3. 第三方集成方很多、技术栈复杂
  4. 需要更强的人类可读性和调试便利性
  5. 网关、缓存、CDN、生态工具高度依赖传统 HTTP 语义

15.3 一个很常见的现实架构

很多团队最后会形成这样的分层:

  1. 外部接口:REST/JSON 或 GraphQL
  2. 内部服务:gRPC
  3. 边界层:Gateway / BFF 负责协议转换

这通常是一个比较务实的组合。


16. 生产环境里最容易踩的坑

16.1 没有统一 deadline 策略

表现为:

  1. 有的接口 300ms
  2. 有的接口永不超时
  3. 有的中间层重试 3 次,但每次都是完整超时
  4. 整体链路预算完全失控

这类系统表面能跑,故障时会非常脆弱。

16.2 无脑重试

重试不是免费午餐。

如果下游已经过载,而你在 UnavailableDeadlineExceeded 时全量放大重试,只会让问题更严重。

要先区分:

  1. 哪些方法幂等
  2. 哪些错误值得重试
  3. 重试预算是多少
  4. 是否要加退避和抖动

16.3 把 streaming 当成无限消息管道

流式调用看起来很优雅,但如果你不处理:

  1. 背压
  2. 心跳
  3. 断线重连
  4. 消息顺序
  5. 重放语义

那它很快就会变成一个难以维护的“私有消息协议”。

16.4 忽视中间代理对 gRPC 的支持程度

常见问题包括:

  1. 不支持 HTTP/2 end-to-end
  2. 不支持 trailer
  3. 超时配置只按 HTTP 请求理解
  4. 对长连接和流式请求处理不佳

很多“gRPC 自己有问题”的结论,最后查下来其实是链路中的代理配置不正确。

16.5 消息模型设计过于粗暴

例如:

  1. 一个请求里塞几十上百个可选字段
  2. 复用同一个 message 到完全不同语义的接口
  3. 把错误信息塞进业务字段而不是返回合适状态码

这会让 Proto 很快变成一锅粥。


17. 排障时应该怎么想

gRPC 出问题时,我建议按下面这个层次去排,而不是一上来就盯业务代码。

17.1 先判断是哪一层的问题

大致可以分成:

  1. 名字解析层:resolver、DNS、服务发现
  2. 连接层:TCP、TLS、HTTP/2 建连
  3. 协议层:header、trailer、metadata、stream 生命周期
  4. 调用治理层:deadline、retry、cancel、LB
  5. 业务处理层:handler、本地锁、线程池、数据库、下游依赖

17.2 看到 DeadlineExceeded 时先问自己

  1. budget 设得是否合理
  2. 超时发生在建连、排队、发送、服务端处理、还是响应回传阶段
  3. 是否有重试把总耗时放大了
  4. 服务端是否其实已经完成了工作,只是响应没回来

17.3 看到 Unavailable 时先问自己

  1. 实例是否真的可达
  2. 负载均衡器是否还认为它健康
  3. 是否发生了连接断开、GOAWAY、RST_STREAM
  4. resolver 返回的地址是否过期
  5. 中间代理是否限制了 HTTP/2 / gRPC

17.4 看到 Canceled 时先问自己

  1. 是不是上游用户请求已经断开
  2. 是不是某层 BFF / Gateway 超时后主动取消
  3. 是不是服务端自己因为内部逻辑取消了子任务

17.5 常用调试手段

生产排障时,一般可以组合使用:

  1. 访问日志和应用日志
  2. trace / span
  3. gRPC interceptor 打点
  4. 连接状态与子连接状态观测
  5. 反射与命令行调试工具

比如 grpcurl 是非常实用的调试工具,尤其在开启 reflection 时,可以快速验证:

  1. 服务是否可达
  2. 方法是否存在
  3. 请求响应结构是否符合预期
  4. metadata / TLS / 认证是否配置正确

18. 优雅停机为什么对 gRPC 更重要

在传统短请求 HTTP 系统里,优雅停机已经很重要;在 gRPC 里,它通常更重要。

因为 gRPC 更依赖长连接,也更可能存在长生命周期 stream。

如果一个实例被粗暴杀掉,可能导致:

  1. 正在执行的 unary 请求直接失败
  2. 流式连接全部中断
  3. 客户端收到 Unavailable
  4. 上游出现重试风暴

更理想的下线流程通常是:

  1. 先从服务发现或 LB 中摘流量
  2. 停止接收新请求
  3. 给已有请求一个排空窗口
  4. 对长 stream 设定合理终止策略
  5. 最后再真正退出进程

这和数据库连接池、任务队列、消费者组等资源清理要配合设计。


19. 一个靠谱的 gRPC 工程实践清单

如果你准备在生产里系统使用 gRPC,我建议至少做到下面这些事:

  1. 所有 RPC 都有明确 deadline,禁止无限等待
  2. deadline 在调用链中向下传播
  3. 区分幂等和非幂等接口,再决定是否重试
  4. 统一 interceptor 做日志、trace、认证和指标
  5. 统一错误码映射,避免到处乱返回 Internal
  6. Proto 评审独立进行,严格管理字段编号与兼容性
  7. 为 streaming 接口设计心跳、背压和断线重连策略
  8. 明确 keepalive、max message size、连接池和并发限制
  9. 接入健康检查、优雅下线和服务发现摘流机制
  10. 预备好 grpcurl、trace、连接状态日志等排障工具链

如果这些都没有,gRPC 的高级特性越多,系统越容易变成“看起来很现代,实际很难救火”的状态。


20. 最后再回到那个最根本的问题

gRPC 值得学吗?

我认为非常值得。

但学习重点不应该只停留在:

  1. proto 怎么写
  2. 代码怎么生成
  3. Client 怎么调 Server

真正应该掌握的是它背后的那些工程语义:

  1. 契约如何演进
  2. 超时如何传播
  3. 流控如何保护系统
  4. 长连接如何治理
  5. 错误如何分类
  6. 服务发现和负载均衡如何影响调用行为

当你把这些点串起来之后,gRPC 就不再只是“性能更好的 HTTP API”,而会变成你理解整个微服务调用链的一把很好用的钥匙。

一句话总结:

gRPC 的价值,不只是让调用更快,而是让远程调用这件事更像一门受约束、可治理、可演进的工程体系。

如果你准备真正把它用到生产里,接下来最值得深入的三个主题通常是:

  1. Proto 兼容性和 API 演进规范
  2. Deadline / Retry / Circuit Breaker 的协同策略
  3. 基于 Envoy / Service Mesh / Kubernetes 的 gRPC 生产治理