Skip to content

gRPC — 把远程调用做成"本地函数"

Protobuf 极致压缩的序列化方案与 HTTP/2 多路复用的传输协议天衣无缝地组合,让远程调用几乎像本地函数一样快、结构化、且支持双向流。它直接定义了云原生时代的服务通信标准。

一、背景:微服务时代,通信成了瓶颈

2010 年代中期,互联网公司纷纷从单体架构转向微服务。系统拆成几十上百个服务后,一个核心问题浮出水面:服务之间怎么高效通信?

当时主流方案有两种:

  1. RESTful API(HTTP/JSON)—— 人类可读,跨语言友好,但数据量大(JSON 文本冗余多),序列化/反序列化慢,且 HTTP/1.1 有先天缺陷
  2. 消息队列 / 自定义 TCP 协议 —— 性能好,但需要维护私有协议,语言绑定困难,调试成本极高

HTTP/1.1 的核心问题是队头阻塞:一个 TCP 连接同一时间只能处理一个请求/响应。虽然可以通过连接池(keep-alive + 多连接)缓解,但每个连接都要做三次握手、TLS 握手,延迟高、资源浪费严重。一个服务聚合三个下游数据就要开三个连接,端口和内存开销激增。

Google 内部早就遇到了这个问题。他们每天有数万亿次服务调用,每个搜索请求背后涉及数百个微服务的级联调用。用 JSON + HTTP/1.1 显然不行。他们需要一个高性能、跨语言、支持流式通信的 RPC(Remote Procedure Call)框架。

2015 年,Google 开源了 gRPC,将内部经验产品化。

二、核心痛点:远程调用能不能像本地函数一样简单高效?

gRPC 要解决三个关键问题:

① 性能问题

JSON 序列化将数字变成字符串再解析,CPU 开销大、体积大。一次用户请求背后可能是几十次服务级联调用,每次调用都要序列化/反序列化,累加起来的性能损失不可忽视。

② 模式单一的问题

HTTP 请求-响应模式太死板。有些场景需要持续推送(比如日志收集、实时监控),有些需要双向流通信(比如 AI 对话、协同编辑),还有些需要单向通知(Fire and Forget)。传统 REST API 对这类场景很不友好。

③ 强类型与跨语言问题

团队使用不同的语言(Go、Java、Python、C++……)。REST API 通过文档约定接口格式,但文档容易过时,调用方常常"猜"参数是什么类型。服务变更后,下游可能默默出错直到运行时才暴露。

不解决会怎样?

微服务的通信延迟会成为系统瓶颈,拖慢整个架构;流式场景只能靠 WebSocket 或自定义协议拼凑;跨团队的接口协作会变成无休止的文档拉锯战。

三、核心实现方案:Protobuf + HTTP/2 双核驱动

gRPC 的技术栈可以拆成三层:

3.1 接口定义层:Protocol Buffers

客户端和服务端要约定"你传什么、我返回什么"。gRPC 用 Protobuf 来做这件事。

protobuf
service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

这看起来像定义了一个函数签名,但实际上做了三件事:

  • 定义接口契约(service + rpc)
  • 定义数据结构(message)
  • 自动生成代码(根据 .proto 文件生成各语言的客户端/服务端代码)

类比:这就像签合同。两边照着同一份合同干活,谁也不会"听岔了"。而且如果接口变更,代码编译时就会报错,不用等到线上崩了才发现。

Protobuf 的序列化有多省?

JSON 表示 {"query": "hello", "page_number": 1} 大约 37 字节。Protobuf 用同样的数据,大约 8 字节:

  • 每个字段用 field number + wire type 打头(Tag = (field_number << 3) | wire_type,只有 1~2 字节)
  • 整数用 Varint 编码:小数字占 1 字节,大数字自动膨胀,而不是固定 4/8 字节
  • 字符串直接存储原始字节,不转义引号和换行符

Protobuf 的核心思想是按 Schema 编码——双方都提前知道数据结构,所以传输时不需要携带字段名,只传字段编号和值。JSON 则是自描述编码——每条消息都带着字段名字符串,冗余度极大。

3.2 传输层:HTTP/2

Protobuf 搞定"串行化"的问题,但"怎么发过去"交给了 HTTP/2。

HTTP/2 的关键特性,每一项都是为 gRPC 量身定做的:

① 二进制分帧(Binary Framing Layer)

HTTP/1.1 把请求头和正文当成文本逐行发送。HTTP/2 把所有数据切分成二进制帧(Frame),类型包括 HEADERS 帧(放头信息)、DATA 帧(放数据体)、PRIORITY 帧等。

类比:HTTP/1.1 是一辆只能拉一个包裹的卡车,每趟都要从头开到终点。HTTP/2 是一辆多节车厢的火车,每节车厢可以装不同的包裹,编组站可以灵活重组。

② 多路复用(Multiplexing)

多个 gRPC 调用可以共享一个 TCP 连接。每个调用对应一个流(Stream),帧上标有 Stream ID。接收方按 ID 重组即可。

这意味着:服务 A 需要同时调服务 B、C、D——以前需要三个连接,现在只要一个。线上改造案例中,连接数可以减少 80%~90%。

③ 流优先级和流量控制

关键请求可以优先传输。比如一个接口同时依赖多个下游,某个下游的响应更重要,可以标记为高优先级流。

④ HPACK 头部压缩

HTTP/2 用 HPACK 算法将请求头压缩。gRPC 的元数据(类似 HTTP 头)经过 HPACK 后体积极小,首次传输建立一个字典后,后续请求只传索引号。

3.3 核心机制:gRPC 的四种通信模式

这是 gRPC 相比 REST 最不一样的地方:

模式客户端服务端场景
Unary RPC(一元)发一个请求回一个响应普通 API 调用
Server Streaming(服务端流)发一个请求连续返回多个消息实时推送、大文件下载
Client Streaming(客户端流)连续发送多个消息统一回复一次批量上传、日志上报
Bidirectional Streaming(双向流)双方自由收发双方自由收发AI 对话、协同编辑

这些流式模式在 HTTP/2 里是怎么实现的?

当 gRPC 开始一个双向流调用时,客户端在 HTTP/2 连接上打开一个 Stream,发 HEADERS 帧(携带请求元数据),然后客户端和服务端通过这个 Stream 互相发送 DATA 帧。每个 DATA 帧前面有一个 5 字节的 gRPC 消息头(Message-Length + Flags),用来标识消息边界。流结束后,服务端发送 HEADERS 帧(Trailers 标志位)通知结束。

底层本质上就是对着同一个 HTTP/2 Stream 持续读写帧。

3.4 连接管理:Channel 机制

gRPC 在客户端维护了一个叫 Channel 的抽象。一个 Channel 代表到一个服务端地址的连接池,内部可以包含多条 HTTP/2 连接。Channel 负责:

  • 服务发现集成(DNS 解析、Consul、K8s Service)
  • 负载均衡策略(round-robin、pick-first、加权)
  • 连接健康检查(HTTP/2 PING 帧保活)
  • 重试和超时(自动重试幂等请求)
  • 优雅关闭(发 GOAWAY 帧后等待正在处理的请求完成)

HTTP/2 的多路复用确实允许多个 RPC 调用共享单个连接。但实际工程实现中,gRPC 使用连接池(通常 2-4 个连接是常见配置)的主要原因包括:

  1. 性能优化:避免 TCP 队头阻塞(一个数据包的丢失,会阻塞住整个连接里所有后续的数据),提高吞吐量
  2. 资源隔离:大请求不影响小请求
  3. 负载均衡:更好地利用多核 CPU
  4. 容错性:单连接故障不影响整体可用性
  5. 突破限制:突破单个连接的并发流限制

gRPC 对比 Dubbo

在"单一长连接上支持多路并发请求"这一点上,gRPC 和 Dubbo 是相似的,都能大幅减少连接数。只是 gRPC 站在了标准协议 HTTP/2 的肩膀上,而 Dubbo 自己实现了一套高效的二进制协议。

四、总结

gRPC 的厉害之处在于:它不是"发明新的通信协议",而是把 Protobuf(极致压缩的序列化方案)和 HTTP/2(多路复用、流式原生的传输协议)天衣无缝地组合起来,做到了既快又结构化还能双向流。

可以说,gRPC 是 Google 把自己内部大规模微服务的通信实践打包后送给整个行业的礼物。今天几乎每一个云原生项目(Kubernetes、Etcd、Istio……)的底层通信都在用 gRPC。它对微服务架构的普及,贡献不亚于 Docker 和 Kubernetes。

Move fast and break things