"微服务之间到底用 gRPC 还是 REST?"——这是个被问烂也答不明白的问题。多数回答停在"gRPC 性能好、REST 易用",但到底好在哪、易在何处,落实到工程取舍上往往语焉不详。这篇文章不站队,而是从协议本质、序列化开销、调用模式、可观测性、工程成本四个维度做一次拆解,最后给一份按团队和场景的选型清单。

一、本质差异:文本 vs 二进制

理解两者分野,先看它们站在哪个协议栈上。

维度RESTgRPC
底层协议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 μs6.2 μs
Protocol Buffers178 字节0.9 μs1.1 μs
MessagePack236 字节2.1 μs2.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 二进制,需要 grpcurlpostman 的 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 文本协议做了深度优化,缓存语义(ETagCache-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 —— QPS
  • grpc_msg_sent_total / grpc_msg_received_total —— 流量

gRPC 的状态码用一组规范的 status.Code(OK、NotFound、DeadlineExceeded、ResourceExhausted 等 17 个),比 HTTP 状态码语义更贴近 RPC。OpenTelemetry 对 gRPC 有原生 instrumentation,链路追踪几乎是零成本接入。

八、选型建议:按场景而不是按信仰

真正要决策时,参考下面这张表。

场景推荐理由
面向外部/第三方开放 APIREST生态、缓存、文档标准成熟
移动端 / Web 前端REST(或 Connect)体积小、断网友好、原生支持
内部微服务高频同步调用gRPC低延迟、强类型、流式
大数据量 / 实时流(行情、日志、IM)gRPC双向流 + 背压
多语言团队(Java + Go + Python)gRPCproto 契约统一、代码生成
小团队 / 快速验证业务REST工具链轻、上手快
对外的内部网关聚合层(BFF)gRPC 内部 + REST 对外内外兼修,最常见组合

实际工程中,最常见的架构是混合:外部 API 暴露 REST,网关以下用 gRPC 串联内部服务。这样既保留开放性,又拿到内网性能收益。Connect 协议(Buf 公司推出)进一步把 REST 风格的 HTTP/JSON 调用和 gRPC 的 proto 契约统一起来,是值得关注的折中方案。

九、总结

gRPC 和 REST 不是替代关系,是分层关系。前者解决机器到机器的强类型高效通信,后者服务人与系统的开放协作。选型的核心不是哪个更好,而是"这个调用是在哪一层发生、谁在消费、QPS 和延迟要求如何"。

下次再被问到这个问题,别只说"gRPC 性能好"。把它放回具体的链路、具体的团队、具体的维护成本里,答案自然会浮现。