gRPC 基础:从零认识这个「高性能」家伙

先问一个问题:你平时调远程服务用什么?REST?JSON?

我以前也这么干。直到有一次,我负责的一个金融风控系统需要高频推送交易特征数据。REST 接口的延迟老是下不来,JSON 解析还特别占 CPU。嗯,那个项目让我彻底转向了 gRPC。

gRPC 是什么?简单说——它是一个高性能、跨语言的远程过程调用(RPC)框架。由 Google 开源,基于 HTTP/2 协议,默认用 Protocol Buffers(简称 Protobuf)做序列化。

说白了,你写一个本地的函数调用,gRPC 帮你把这个调用发送到另一台机器上执行,然后把结果返回给你。整个过程对开发者是透明的。

核心概念一览

理解 gRPC,必须先搞懂四个核心概念。我给你理一理:

概念 作用 类比(你更容易理解的)
Service 定义一组远程方法的逻辑分组 像 Java 里的接口
Method Service 里的单个 RPC 调用 接口里的方法签名
Message 请求和响应的数据结构 DTO 或结构体
Stub 客户端用于调用远程方法的代理对象 代理类,你只管调用

Service 与 Method

在 gRPC 中,你先用 .proto 文件定义 Service。举个例子:

service TokenService {
  rpc CreateToken (CreateTokenRequest) returns (TokenResponse);
  rpc ValidateToken (ValidateTokenRequest) returns (ValidationResult);
}

这个 TokenService 就是一个 Service,它包含两个 Method:CreateToken 和 ValidateToken。每个 Method 都有明确的输入和输出消息类型。

我的经验:Service 的粒度不要太大。我个人习惯每个独立的业务边界对应一个 Service。比如认证放在 AuthService,令牌管理放在 TokenService。太臃肿的 Service 后期很难维护。

Message

Message 定义数据结构:

message CreateTokenRequest {
  string user_id = 1;
  int64  expiry_seconds = 2;
}

message TokenResponse {
  string  = 1;
  int64  created_at = 2;
}

每个字段都有一个唯一的编号(1、2、3...),这个编号用于二进制编码,不能重复。你在后续升级时,不要随意修改已有字段的编号,否则会破坏兼容性。

我曾经踩过的坑:有一次我在 Message 里删掉了一个不再使用的字段,但没有用 reserved 关键字标记。结果新老版本客户端互相通信时,解析直接崩溃。嗯,后来我养成了一个习惯:只要字段不再用,先用 reserved 占住编号,再删除。

Stub

Stub 是客户端侧的代理。你编译 .proto 文件后,gRPC 会生成 Stub 类。你的业务代码只需要调用 Stub 上的方法,Stub 负责序列化、发送请求、接收响应、反序列化。

比如在 Java 中:

TokenServiceGrpc.TokenServiceBlockingStub stub = 
    TokenServiceGrpc.newBlockingStub(channel);

TokenResponse response = stub.createToken(
    CreateTokenRequest.newBuilder()
        .setUserId("u-123")
        .setExpirySeconds(3600)
        .build()
);

你只管 stub.createToken(...),背后所有的网络通信细节——都封装好了。

完整调用流程长啥样?

我画了一张图,把整个调用链路展示出来。你看看,从客户端代码到服务端代码,中间发生了什么:

gRPC 完整调用流程 客户端代码 stub.createToken() 客户端 Stub 序列化 / 反序列化 HTTP/2 网络传输 二进制帧 / 流复用 服务端 Stub 反序列化 / 序列化 服务端代码 业务逻辑处理 ← 响应返回路径(反向) 💡 关键点:Protobuf 序列化发生在 Stub 层,HTTP/2 负责传输,业务代码无感知。 你只需要关注 .proto 定义和业务逻辑,剩下的交给 gRPC 框架。 客户端进程 服务端进程 网络边界 * HTTP/2 支持多路复用,多个 RPC 请求可以共享同一条 TCP 连接 * Protobuf 序列化体积比 JSON 小 3-10 倍,解析性能高 10-100 倍

从图上你能看到:客户端代码调用 Stub → Stub 序列化成 Protobuf → HTTP/2 传输 → 服务端 Stub 反序列化 → 业务代码处理 → 再原路返回。一层一层,分工明确。

和其他 RPC 框架的对比

你肯定会问:gRPC 跟 REST、Thrift 比,到底好在哪?我直接给你一张对比表,你一看就明白:

维度 gRPC REST Thrift
传输协议 HTTP/2 HTTP/1.1 TCP / HTTP
序列化格式 Protobuf(二进制) JSON / XML(文本) Thrift Binary / Compact
性能 高(低延迟、高吞吐) 中(解析开销大) 高(二进制编码)
代码生成 全自动(从 .proto) 手动或依赖 Swagger 全自动(从 .thrift)
流式通信 原生支持(4 种模式) 仅 Server-Sent Events 部分支持
跨语言 丰富(10+ 语言) 任何语言皆可 丰富(与 gRPC 相当)
学习曲线 中(需学 .proto) 低(人人都会) 中(需学 .thrift)
浏览器支持 需 gRPC-Web 转译 原生 差

我的选择建议:

  • 微服务内部通信 → 首选 gRPC。性能好,代码自动生成,省心。
  • 对外公开 API、浏览器直接调用 → 选 REST。兼容性好,谁都能调。
  • 已有 Thrift 技术栈 → 不需要硬迁。但新项目我建议看看 gRPC,生态更活跃。

gRPC 的优势,到底在哪儿?

我挑最影响你日常开发的三个点说:

  1. HTTP/2 多路复用:多个 RPC 调用共享同一个 TCP 连接,没有了 REST 下的连接开销。你想想看,100 个请求共用一个连接 vs 100 个连接,差距有多大。
  2. Protobuf 序列化性能碾压:二进制格式,体积小、解析快。我当初在风控系统里把 JSON 替换成 Protobuf 后,单个请求的 CPU 开销下降了 70%。
  3. 四种通信模式:一元(Unary)、服务端流、客户端流、双向流。同一套 .proto 定义,想怎么玩就怎么玩。

gRPC 的局限,你也该知道

这里有个坑:gRPC 默认的负载均衡是客户端侧实现的。你需要额外部署一个服务发现组件(比如 etcd、Consul),并且 Stub 启动时要配置好 resolved 地址。我第一次搭建时以为它跟 REST 一样靠 Nginx 反代就行了——结果发现完全不是一回事。

另外,浏览器没法直接发 gRPC 请求。你需要通过 gRPC-Web 代理转一层,或者改用 gRPC-Web 客户端库。这一点在做 Web 前端时要注意。

好,这就是 gRPC 的基础面貌。从核心概念到调用流程,再到选型对比,你应该有个整体手感了。后面我们会一步步深入,从 .proto 文件的编写开始,真正把 gRPC 跑起来。