gRPC — 把远程调用做成"本地函数"
Protobuf 极致压缩的序列化方案与 HTTP/2 多路复用的传输协议天衣无缝地组合,让远程调用几乎像本地函数一样快、结构化、且支持双向流。它直接定义了云原生时代的服务通信标准。
一、背景:微服务时代,通信成了瓶颈
2010 年代中期,互联网公司纷纷从单体架构转向微服务。系统拆成几十上百个服务后,一个核心问题浮出水面:服务之间怎么高效通信?
当时主流方案有两种:
- RESTful API(HTTP/JSON)—— 人类可读,跨语言友好,但数据量大(JSON 文本冗余多),序列化/反序列化慢,且 HTTP/1.1 有先天缺陷
- 消息队列 / 自定义 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 来做这件事。
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 个连接是常见配置)的主要原因包括:
- 性能优化:避免 TCP 队头阻塞(一个数据包的丢失,会阻塞住整个连接里所有后续的数据),提高吞吐量
- 资源隔离:大请求不影响小请求
- 负载均衡:更好地利用多核 CPU
- 容错性:单连接故障不影响整体可用性
- 突破限制:突破单个连接的并发流限制
gRPC 对比 Dubbo
在"单一长连接上支持多路并发请求"这一点上,gRPC 和 Dubbo 是相似的,都能大幅减少连接数。只是 gRPC 站在了标准协议 HTTP/2 的肩膀上,而 Dubbo 自己实现了一套高效的二进制协议。
四、总结
gRPC 的厉害之处在于:它不是"发明新的通信协议",而是把 Protobuf(极致压缩的序列化方案)和 HTTP/2(多路复用、流式原生的传输协议)天衣无缝地组合起来,做到了既快又结构化还能双向流。
可以说,gRPC 是 Google 把自己内部大规模微服务的通信实践打包后送给整个行业的礼物。今天几乎每一个云原生项目(Kubernetes、Etcd、Istio……)的底层通信都在用 gRPC。它对微服务架构的普及,贡献不亚于 Docker 和 Kubernetes。