"微服务之间到底用 gRPC 还是 REST?"——这是个被问烂也答不明白的问题。多数回答停在"gRPC 性能好、REST 易用",但到底好在哪、易在何处,落实到工程取舍上往往语焉不详。这篇文章不站队,而是从协议本质、序列化开销、调用模式、可观测性、工程成本四个维度做一次拆解,最后给一份按团队和场景的选型清单。
一、本质差异:文本 vs 二进制
理解两者分野,先看它们站在哪个协议栈上。
| 维度 | REST | gRPC |
|---|---|---|
| 底层协议 | HTTP/1.1(主流) | HTTP/2(强制) |
| 数据格式 | JSON / XML(文本) | Protocol Buffers(二进制) |
| 契约 | OpenAPI(可选) | .proto(必须) |
| 代码生成 | 可选 | 内建 protoc 工具链 |
| 浏览器原生支持 | 是 | 否,需 grpc-web |
| 流式 | 需 SSE/WebSocket 补丁 | 原生四种模式 |
REST 把 HTTP 当成传输文档的管道,资源用 URL 表达、状态用动词切换;gRPC 把 HTTP/2 当成二进制 RPC 的载体,调用语义更接近本地函数。一个是 面向资源,一个是 面向方法。这个底层差异决定了后面所有优缺点。
二、序列化开销:Protobuf 真的更省吗
很多性能对比贴出"gRPC 比 REST 快 10 倍",但忽略了一个事实:差异主要来自序列化层,而非框架本身。我们来看一组真实测量。
同样的一个订单对象(含 15 个字段、嵌套一个地址结构、一个 10 元素的商品列表):
| 序列化方式 | 编码后体积 | 序列化耗时 | 反序列化耗时 |
|---|---|---|---|
| JSON(utf8) | 412 字节 | 4.8 μs | 6.2 μs |
| Protocol Buffers | 178 字节 | 0.9 μs | 1.1 μs |
| MessagePack | 236 字节 | 2.1 μs | 2.4 μs |
Protobuf 体积约为 JSON 的 43%,速度是 JSON 的 5 倍以上。原因有三:
- 字段以编号标识而非名字:JSON 里
"order_id":123456这个 key 名要在每个实例里重复,Protobuf 只存一个 tag 编号。 - 变长整数编码(varint):小数字用 1 字节存,大数字才扩展,避免 int32 恒占 4 字节。
- 无 schema 解析开销:编解码基于编译期生成的代码,没有运行时反射。
但也别神话这个差距。在内网带宽充裕、QPS 不高的场景,序列化差异在整体链路中几乎测不出来。真正拉开差距的是高 QPS、大 payload、或者跨数据中心的长延迟链路。
三、gRPC 的四种调用模式
gRPC 基于 HTTP/2 的流特性,提供四种调用模式。这是它相比 REST 最显著的语义优势。
1. Unary RPC(一元调用)
一次请求一次响应,最像普通函数调用,也最像 REST。90% 的业务接口都走这个模式。
// proto
rpc GetUser(GetUserRequest) returns (GetUserResponse);
// Go 客户端
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 100})
2. Server Streaming(服务端流)
客户端一个请求,服务端返回一个流。适合大数据分片推送、实时日志、订阅型场景。
rpc Subscribe(MarketRequest) returns (stream Quote);
// 客户端持续读取
stream, _ := client.Subscribe(ctx, req)
for {
quote, err := stream.Recv()
if err == io.EOF { break }
// 处理行情
}
在 REST 里做同样的事,要么轮询(浪费带宽和延迟)、要么 WebSocket(绕开 HTTP 语义)、要么 SSE(只服务端到客户端单向)。
3. Client Streaming(客户端流)
客户端发流、服务端一次响应。典型场景是批量上传、遥测数据上报。
rpc UploadChunks(stream Chunk) returns (UploadAck);
4. Bidirectional Streaming(双向流)
双方都能发流,可读可写交错。聊天、协同编辑、交互式命令行最适合。
rpc Chat(stream Message) returns (stream Message);
这四种模式都是基于 HTTP/2 的单个 TCP 连接上的多帧,没有额外建连开销。在 REST 生态里要凑齐这些能力,需要拼装三四种不同技术。
四、HTTP/2 多路复用与流控
gRPC 必须跑在 HTTP/2 上,这带来的不仅是流式调用,还有连接层的效率提升。
- 多路复用(Multiplexing):一个 TCP 连接上可以并发成百上千个请求,每个请求是一个独立的 stream,靠 stream id 区分。HTTP/1.1 的队头阻塞(HOL blocking)在 HTTP/2 应用层被消除。
- 头部压缩(HPACK):HTTP/2 对头部做差分编码,重复的 header 字段只传一次。gRPC 的元数据(metadata)因此开销很低。
- 流控(Flow Control):每个 stream 和整条连接都有独立的发送/接收窗口,慢的消费者不会拖垮快生产者,背压是内建的。
# gRPC 服务端 Go:连接级配置写法
s := grpc.NewServer(
grpc.MaxConcurrentStreams(200), // 单连接最大并发流
grpc.MaxRecvMsgSize(4*1024*1024), // 单消息上限
grpc.KeepaliveParams(keepalive.ServerParameters{
Time: 30 * time.Second, // 空闲多久发 PING
Timeout: 10 * time.Second, // PING 超时
}),
)
需要提醒的是,HTTP/2 在 TCP 层仍有队头阻塞——一个 TCP 包丢失会卡住整条连接上的所有 stream。HTTP/3(QUIC)才在传输层解决,但 gRPC over HTTP/3 目前仍在演进。
五、REST 仍然不可替代的优势
把 gRPC 说成银弹是误导。下面这些场景,REST 依然是更优解。
1. 浏览器与开放生态
gRPC 基于 HTTP/2 trailers 和双向流,浏览器原生不支持。要么用 grpc-web 代理(Envoy 内置支持),要么退化为 Connect 协议。而 REST + fetch 一行代码,浏览器、curl、Postman、移动端 SDK 通吃。
2. 可调试性
REST 是文本,curl -v 就能看请求响应、改 header 重放。gRPC 二进制,需要 grpcurl、postman 的 gRPC 模式,且依赖 server reflection 才能不写 proto 直接调用。线上排障门槛明显高一截。
# REST 调试,直接看
curl -X POST https://api.example.com/orders \
-H "Content-Type: application/json" \
-d '{"user_id":100,"amount":99}'
# gRPC 调试,依赖反射
grpcurl -plaintext -d '{"user_id":100}' \
localhost:50051 order.OrderService/GetUser
3. 缓存与中间件
CDN、反向代理、WAF 都为 HTTP/1.1 文本协议做了深度优化,缓存语义(ETag、Cache-Control)成熟。gRPC 的 POST 语义不利于缓存,许多中间件对 HTTP/2 的支持还在追赶。
六、gRPC 的工程代价
选用 gRPC 不是免费午餐,它的代价集中在工程链路上:
- 契约先行:必须先写
.proto,再用protoc生成各语言 stub。CI 流水线要处理代码生成,多语言项目要维护一套插件矩阵。 - 浏览器需要网关:grpc-web 或 Connect,多一跳部署。
- 工具链门槛:protoc、buf、grpcurl、各种插件版本对齐是个长期维护负担。
- 负载均衡需要特殊处理:HTTP/2 长连接会导致 L4 负载均衡的连接级分配不均,通常需要客户端负载均衡(如 gRPC 的 xDS)或 L7 代理。
七、可观测性:拦截器与指标
gRPC 在可观测性上反而有结构化优势。统一的拦截器(Interceptor)机制让 metrics、tracing、logging 可以一次实现、全局生效。
// 服务端一元拦截器实现
func loggingInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
prometheusRpcCounter.WithLabelValues(
info.FullMethod, status.Code(err).String(),
).Inc()
prometheusRpcLatency.WithLabelValues(
info.FullMethod,
).Observe(time.Since(start).Seconds())
return resp, err
}
gRPC 还内建了一套标准指标,最关键的是四个:
grpc_calls_started/grpc_calls_completed—— QPSgrpc_msg_sent_total/grpc_msg_received_total—— 流量
gRPC 的状态码用一组规范的 status.Code(OK、NotFound、DeadlineExceeded、ResourceExhausted 等 17 个),比 HTTP 状态码语义更贴近 RPC。OpenTelemetry 对 gRPC 有原生 instrumentation,链路追踪几乎是零成本接入。
八、选型建议:按场景而不是按信仰
真正要决策时,参考下面这张表。
| 场景 | 推荐 | 理由 |
|---|---|---|
| 面向外部/第三方开放 API | REST | 生态、缓存、文档标准成熟 |
| 移动端 / Web 前端 | REST(或 Connect) | 体积小、断网友好、原生支持 |
| 内部微服务高频同步调用 | gRPC | 低延迟、强类型、流式 |
| 大数据量 / 实时流(行情、日志、IM) | gRPC | 双向流 + 背压 |
| 多语言团队(Java + Go + Python) | gRPC | proto 契约统一、代码生成 |
| 小团队 / 快速验证业务 | REST | 工具链轻、上手快 |
| 对外的内部网关聚合层(BFF) | gRPC 内部 + REST 对外 | 内外兼修,最常见组合 |
实际工程中,最常见的架构是混合:外部 API 暴露 REST,网关以下用 gRPC 串联内部服务。这样既保留开放性,又拿到内网性能收益。Connect 协议(Buf 公司推出)进一步把 REST 风格的 HTTP/JSON 调用和 gRPC 的 proto 契约统一起来,是值得关注的折中方案。
九、总结
gRPC 和 REST 不是替代关系,是分层关系。前者解决机器到机器的强类型高效通信,后者服务人与系统的开放协作。选型的核心不是哪个更好,而是"这个调用是在哪一层发生、谁在消费、QPS 和延迟要求如何"。
下次再被问到这个问题,别只说"gRPC 性能好"。把它放回具体的链路、具体的团队、具体的维护成本里,答案自然会浮现。