目录

gRPC 深度拆解:现代高性能 RPC 框架的事实标准

gRPC 深度拆解:现代高性能 RPC 框架的事实标准

核心判断

gRPC 的核心贡献不是"远程调用"——DCOM/CORBA/RMI/Thrift 都做过 RPC。它赢在三件事的组合

  1. HTTP/2 作为传输层:多路复用、头部压缩、二进制分帧——这些都"白嫖"了 HTTP/2 的现成基础设施
  2. Protocol Buffers 作为 IDL:编译期类型检查 + 强 schema 演进规则 + 多语言代码生成
  3. 四种 streaming 模型:unary、server streaming、client streaming、bidirectional——一次定义满足实时通信、双向消息、长任务进度上报

这套组合让 gRPC 既适合微服务 A→B 单次调用,又适合视频流、IoT 上报、协同编辑等长连接场景。这是它能从 Google 内部替代 Stubby 后主导 2015 年后微服务生态的原因。

项目坐标

维度数据
仓库grpc/grpc(多语言 SDK 在子目录)
Stars约 45k
主语言C/C++(核心库),其他语言独立 SDK
LicenseApache 2.0
当前主版本1.6x(核心库),各语言 SDK 版本独立
协议HTTP/2 + Protocol Buffers v3(protobuf)

为什么是 HTTP/2

HTTP/1.1 的问题(微服务场景下放大):

  • 连接数:每请求一个连接(或 keep-alive 但串行),QPS 高时端口耗尽
  • 队头阻塞:单连接上的请求必须按顺序响应,前一个慢就阻塞后一个
  • 头部冗余:每个请求都带完整 headers,几十 KB 的 Cookie/Auth 重复传

HTTP/2 解决:

  • 多路复用:单连接承载多个并发流(stream)
  • 二进制分帧:帧(frame)作为最小传输单位,二进制而非文本
  • 头部压缩:HPACK 算法,重复 header 增量压缩
  • 服务器推送:服务端可以主动推送资源(gRPC 不常用)

gRPC 直接用 HTTP/2 做传输,每一个 gRPC 方法调用对应一个 HTTP/2 stream。这就是为什么 gRPC 能同时承载 1000+ 并发请求到同一个后端服务。

Protocol Buffers 作为 IDL

syntax = "proto3";

package helloworld.v1;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
  rpc Chat (stream ChatMessage) returns (stream ChatReply);  // bidirectional streaming
}

message HelloRequest {
  string name = 1;
  int32 age = 2;
}

message HelloReply {
  string message = 1;
}
# 安装 protoc 编译器
brew install protobuf

# 生成 Go 代码
protoc --go_out=. --go-grpc_out=. helloworld.proto

生成出来的代码包括:

  • GreeterClient 接口(客户端 stub)
  • GreeterServer 接口(服务端要实现的接口)
  • 所有 message 的 marshal/unmarshal 函数
  • HTTP/2 帧的序列化逻辑

Protobuf 的设计亮点:

  • 字段编号(field number)不重不漏:客户端/服务端可以用不同字段集合,运行时按编号匹配
  • wire format 二进制紧凑:相比 JSON 平均小 3-10 倍
  • schema 演进规则:删除字段保留编号、新字段用新编号——保证向后兼容

四种通信模式

service Chat {
  rpc Unary (Msg) returns (Reply);                     // 一元调用
  rpc ServerStream (Msg) returns (stream Reply);      // 服务端流
  rpc ClientStream (stream Msg) returns (Reply);      // 客户端流
  rpc Bidirectional (stream Msg) returns (stream Reply);  // 双向流
}

1. Unary(一元调用)

经典 RPC:客户端发一个请求,服务端返回一个响应。GetUser(id) → User 是最常见模式。

2. Server streaming

客户端发一个请求,服务端返回流式响应(多个消息)。

用例:

  • 股票行情推送:Subscribe(symbol) → stream<Tick>
  • 日志聚合:Tail(filter) → stream<LogEntry>
  • 大数据集分页:ListAll() → stream<Item>
func (s *server) Subscribe(req *Req, stream Chat_SubscribeServer) error {
    for {
        select {
        case <-stream.Context().Done():
            return nil
        case msg := <-s.updates:
            if err := stream.Send(msg); err != nil { return err }
        }
    }
}

3. Client streaming

客户端发流式请求,服务端返回一个汇总响应。

用例:

  • IoT 设备批量上传遥测数据
  • 文件分片上传
  • 客户端日志上报

4. Bidirectional streaming

客户端和服务端各自发独立的消息流。

用例:

  • 实时聊天(双向文字消息)
  • 协同编辑(多人操作同步)
  • 游戏状态同步

每种 streaming 模式对应一个 HTTP/2 stream。多个流跑在单 TCP 连接上。

生产工程实践

1. 错误码体系

gRPC 定义了一套状态码:

Code含义
OK成功
CANCELLED客户端主动取消
UNKNOWN未知错误
INVALID_ARGUMENT客户端参数错误
DEADLINE_EXCEEDED超时
NOT_FOUND资源不存在
ALREADY_EXISTS资源已存在
PERMISSION_DENIED权限不足
RESOURCE_EXHAUSTED配额耗尽
FAILED_PRECONDITION系统状态不满足
UNAVAILABLE服务不可用
INTERNAL服务器内部错误

与 HTTP 状态码不同——gRPC 状态码语义更细,覆盖了"暂时不可用"(UNAVAILABLE)和"系统状态不对"(FAILED_PRECONDITION)的区别。在负载均衡和重试策略里要用上。

2. 拦截器(Interceptor)

gRPC 提供 unary 和 streaming 两种拦截器机制:

// unary 拦截器
func LoggingInterceptor(ctx context.Context, req interface{},
    info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    log.Printf("method=%s start", info.FullMethod)
    resp, err := handler(ctx, req)
    log.Printf("method=%s err=%v", info.FullMethod, err)
    return resp, err
}

s := grpc.NewServer(grpc.UnaryInterceptor(LoggingInterceptor))

拦截器用于:

  • 认证(OAuth/JWT 校验)
  • 日志 / 监控(OpenTelemetry)
  • 重试 / 熔断
  • 限流

3. 超时与截止时间(Deadline)

每个 gRPC 调用都有 deadline(绝对截止时间)。服务端可以检查 ctx.Err()

ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()

resp, err := client.SayHello(ctx, &pb.HelloRequest{Name: "world"})
if err != nil {
    if status.Code(err) == codes.DeadlineExceeded {
        // 处理超时
    }
}

截止时间应在客户端设置,而不是服务端硬编码——这样客户端可以基于 SLA 动态调整。

4. 负载均衡

gRPC 客户端内置几种 LB 策略:

  • pick_first:试第一个连接直到成功(默认)
  • round_robin:轮询多个连接
  • grpclb:服务端从 control plane 拉 LB 决策
  • xDS:与 Envoy / Istio 集成的 LB

生产环境推荐:

  • 服务端在 K8s 上:用 xDS 或服务网格(Istio/Linkerd)做 LB
  • 直连 IP 列表:用 round_robin

5. mTLS(双向 TLS)

服务间通信加密 + 身份认证:

creds, _ := credentials.NewServerTLSFromFile("server-cert.pem", "server-key.pem")
s := grpc.NewServer(grpc.Creds(creds))

或者用 SPIFFE/SPIRE 自动管理证书。

与 REST / GraphQL / Thrift 的取舍

维度gRPCRESTGraphQLThrift
性能★★★★★★★★★★★★★★
浏览器友好❌(需 grpc-web)
流式✅ 四种模式subscription(单连接)有限
Schema 演进protobuf 严格OpenAPI(弱)SDL(强)Thrift IDL(强)
多语言✅ 11+ 语言任何 HTTP 客户端任何 HTTP 客户端
工具生态grpcurl、grpcui、envoycurl、Postmangraphql-playgroundThrift 工具链
学习曲线
服务间调用★★★★★★★★★★★

决策建议

  • 内部微服务 + 多语言 + 高 QPS → gRPC
  • 前端直接调用 + 公开 API → REST 或 GraphQL
  • 已有 Thrift 资产 → 继续 Thrift
  • 流式为主(视频、IoT、协同)→ gRPC 或 WebSocket
  • 调试友好优先 → REST

gRPC-Web:浏览器支持

浏览器无法直接用 HTTP/2 的 gRPC(HTTP/1.1 限制 + 没有 HTTP trailers)。gRPC-Web 是官方子项目,让浏览器通过 envoy 代理访问 gRPC 后端:

Browser (gRPC-Web client)
    ↓ HTTP/1.1 + gRPC-Web framing
Envoy proxy (gRPC-Web → gRPC translation)
    ↓ HTTP/2 + native gRPC
gRPC server

如果前端要直接调后端,gRPC-Web 是可行方案;但实际项目里更多是 REST/HTTP API 网关 + 后端服务间用 gRPC

何时用 / 何时不用

适合

  • 内部微服务 A→B 调用(高 QPS、低延迟要求)
  • 多语言系统(C++/Go/Python/Java 互相调用)
  • 流式数据(实时消息、IoT、视频帧)
  • 移动 App 与后端长连接

不适合

  • 公开 API 给第三方开发者(REST/GraphQL 友好)
  • 浏览器为主的前端场景(gRPC-Web 或 REST)
  • 极小项目(REST 单文件足够)
  • 单语言系统(不需要 IDL 多语言生成)

实战起步建议

  1. 先定义 proto:服务接口 + 消息体
  2. 用 buf 管理 proto 仓库buf.build 是 proto 的 lint + breaking change 检测工具
  3. 生成多语言 stub:每个服务一种语言
  4. 服务端先实现 unary(最简单),再考虑 streaming
  5. 加拦截器:认证 / 日志 / 监控
  6. 用 grpcurl 调试grpcurl -plaintext localhost:50051 list
  7. 生产环境:Envoy / 服务网格做 LB、可观测性、mTLS

参考资源