HTTP/2 与 RPC 精讲教程 / 11 - REST vs gRPC 选型
第 11 章:REST vs gRPC 选型指南
没有银弹——理解差异,选对方案
11.1 核心差异对比
11.1.1 技术维度对比
| 维度 | REST/JSON | gRPC/Protobuf |
|---|---|---|
| 协议 | HTTP/1.1 或 HTTP/2 | HTTP/2 |
| 数据格式 | JSON (文本) | Protobuf (二进制) |
| 接口定义 | OpenAPI/Swagger | .proto 文件 (IDL) |
| 浏览器支持 | 原生支持 | 需要 gRPC-Web 代理 |
| 工具生态 | curl、Postman | grpcurl、grpcui |
| 学习曲线 | 低 | 中(需学习 Protobuf) |
| 代码生成 | 可选(如 openapi-generator) | 必须(protoc) |
| 向后兼容 | 手动管理 | 内建机制(字段编号) |
| 流式传输 | WebSocket / SSE | 四种原生模式 |
| 错误处理 | HTTP 状态码 | gRPC 状态码 + Details |
11.1.2 性能对比数据
测试环境:4 核 CPU,16GB 内存,内网环境
测试内容:10,000 次请求/响应
┌─────────────────────┬──────────┬──────────┬──────────┐
│ 指标 │ REST │ gRPC │ 差异 │
├─────────────────────┼──────────┼──────────┼──────────┤
│ 单次请求延迟 P50 │ 1.2ms │ 0.3ms │ 4x │
│ 单次请求延迟 P99 │ 3.5ms │ 0.8ms │ 4.4x │
│ 最大吞吐量 (QPS) │ 15,000 │ 85,000 │ 5.7x │
│ 序列化大小 (1KB) │ 1,024B │ 320B │ 3.2x │
│ 序列化速度 │ 基准 │ 5-10x │ - │
│ 内存占用 (每连接) │ ~8KB │ ~12KB │ 1.5x │
│ CPU 占用 (高并发) │ 基准 │ 0.3x │ - │
└─────────────────────┴──────────┴──────────┴──────────┘
注意:性能数据因场景、负载、环境不同会有显著差异
11.2 选型决策矩阵
11.2.1 场景匹配表
| 场景 | REST | gRPC | 理由 |
|---|---|---|---|
| 公开 API(第三方开发者) | ✅ | ❌ | 生态兼容性、可调试性 |
| 浏览器直接调用 | ✅ | ❌ | 原生支持 |
| 移动端后端 API | ⚠️ | ✅ | 节省带宽,性能好 |
| 微服务内部通信 | ⚠️ | ✅ | 高性能,类型安全 |
| 实时流式通信 | ❌ | ✅ | 原生支持 |
| 简单 CRUD 应用 | ✅ | ⚠️ | 开发效率高 |
| 低延迟交易系统 | ❌ | ✅ | 序列化快,延迟低 |
| 文件/媒体传输 | ✅ | ⚠️ | HTTP 原生支持 |
| IoT 设备通信 | ❌ | ✅ | 二进制协议节省带宽 |
| 快速原型开发 | ✅ | ❌ | 无需 IDL 学习成本 |
11.2.2 决策流程图
开始选型
│
├─ Q1: 需要浏览器直接调用?
│ ├─ 是 → REST(或 gRPC-Web)
│ └─ 否 ↓
│
├─ Q2: 需要流式通信?
│ ├─ 是 → gRPC
│ └─ 否 ↓
│
├─ Q3: 性能是关键约束?
│ ├─ 是 → gRPC
│ └─ 否 ↓
│
├─ Q4: 团队熟悉 Protobuf 吗?
│ ├─ 否 → REST(降低学习成本)
│ └─ 是 ↓
│
├─ Q5: 需要强类型契约?
│ ├─ 是 → gRPC
│ └─ 否 → REST
│
└─ 也可以两者并存(混用架构)
11.3 混合架构模式
11.3.1 典型混用架构
┌──────────────┐
│ 浏览器/APP │
└──────┬───────┘
│ REST / GraphQL
┌──────▼───────┐
│ API Gateway │
│ (REST 入口) │
└──────┬───────┘
│ gRPC(内部通信)
┌──────────────┼──────────────┐
│ │ │
┌──────▼──┐ ┌──────▼──┐ ┌──────▼──┐
│ 用户服务 │ │ 订单服务 │ │ 支付服务 │
│ (gRPC) │ │ (gRPC) │ │ (gRPC) │
└─────────┘ └─────────┘ └─────────┘
原则:
- 外部暴露 REST(兼容性好)
- 内部使用 gRPC(性能好)
- API Gateway 负责协议转换
11.3.2 gRPC-Gateway 实现
// 使用 gRPC-Gateway 同时提供 REST 和 gRPC 接口
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
pb "example/pb"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// 实现 gRPC 服务
type userService struct {
pb.UnimplementedUserServiceServer
}
func (s *userService) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
return &pb.GetUserResponse{
User: &pb.User{
Id: req.Id,
Name: "Alice",
Email: "alice@example.com",
},
}, nil
}
func main() {
// 启动 gRPC 服务器
lis, _ := net.Listen("tcp", ":50051")
grpcServer := grpc.NewServer()
pb.RegisterUserServiceServer(grpcServer, &userService{})
go grpcServer.Serve(lis)
// 启动 REST 代理
ctx := context.Background()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, "localhost:50051", opts)
if err != nil {
log.Fatalf("注册 REST 代理失败: %v", err)
}
// 现在同一个接口同时支持:
// gRPC: localhost:50051
// REST: GET localhost:8080/v1/users/1
fmt.Println("REST 代理启动于 :8080")
http.ListenAndServe(":8080", mux)
}
// proto 定义 REST 映射
syntax = "proto3";
package example;
import "google/api/annotations.proto";
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse) {
option (google.api.http) = {
get: "/v1/users/{id}"
};
}
rpc CreateUser(CreateUserRequest) returns (User) {
option (google.api.http) = {
post: "/v1/users"
body: "*"
};
}
rpc UpdateUser(UpdateUserRequest) returns (User) {
option (google.api.http) = {
put: "/v1/users/{id}"
body: "*"
};
}
rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty) {
option (google.api.http) = {
delete: "/v1/users/{id}"
};
}
}
11.4 迁移策略
11.4.1 从 REST 到 gRPC 的渐进迁移
阶段 1:并行运行
┌─────────────────────────────────────────┐
│ API Gateway │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ REST (旧客户端)│ │gRPC (新客户端)│ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ └────────┬────────┘ │
│ ▼ │
│ 同一个后端服务 │
└─────────────────────────────────────────┘
阶段 2:逐步迁移
- 新功能只用 gRPC 开发
- 旧 REST 接口维护不新增功能
- 客户端逐步迁移到 gRPC
阶段 3:清理
- 监控 REST 接口流量,接近零时下线
- 移除 REST 代理层
11.4.2 协议转换代理
// Envoy 代理配置示例(protocol-transcoding)
// envoy.yaml 片段
`
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: AUTO
http_filters:
- name: envoy.filters.http.grpc_json_transcoder
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
proto_descriptor: "/etc/envoy/service.pb"
services: ["example.UserService"]
- name: envoy.filters.http.router
`
11.5 业务场景对比
11.5.1 电商 API 设计
公开 API(REST):
GET /v1/products # 商品列表
GET /v1/products/{id} # 商品详情
POST /v1/orders # 创建订单
GET /v1/orders/{id} # 订单详情
内部服务通信(gRPC):
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (Order);
rpc GetOrder(GetOrderRequest) returns (Order);
rpc StreamOrderUpdates(stream OrderEvent) returns (stream OrderState);
}
11.5.2 移动端后端
移动 APP → API Gateway (REST/gRPC-Web)
考虑因素:
- 弱网环境:gRPC 二进制协议更高效
- 流量成本:Protobuf 更小,节省用户流量
- 电量消耗:更少的数据传输 = 更省电
- 开发效率:代码生成减少手动工作
推荐:移动端使用 gRPC-Web 或 gRPC over REST
11.6 注意事项
⚠️ REST 的陷阱:
- 过度设计 URI 导致接口混乱
- 缺少版本管理导致兼容性问题
- JSON 序列化性能在高并发下成为瓶颈
⚠️ gRPC 的陷阱:
- 浏览器支持有限(需要 gRPC-Web 代理)
- 调试不如 REST 直观(二进制协议)
- 学习曲线较陡(Protobuf + gRPC 工具链)
💡 实用建议:
- 面向公众的 API 优先选择 REST
- 内部微服务优先选择 gRPC
- 两者可以并存,通过 API Gateway 统一管理
- 不要为了"新潮"而强推 gRPC