第三章 API协议与选择:RESTful、GraphQL、gRPC 与版本控制
做企业级接入,选协议就像选交通工具。去机场坐地铁,搬家叫货车,临时买瓶水骑共享单车——没有的好坏,只有合不合适的场景。我见过太多团队一上来就拍脑袋定协议,结果后期接口改不动、客户端骂娘、服务端不敢动版本号。今天咱们就把这四种方案掰开揉碎了聊,顺便把版本控制这块硬骨头啃干净。
3.1 RESTful API — 老牌王者,但别滥用
REST 深入人心,资源导向、动词用 HTTP 方法、状态码清晰。但你想想看,真的每个接口都符合 REST 规范吗?我在项目中遇到过好几回:团队说用 REST,实际写出来的接口全是 POST /api/v1/doSomething,压根没遵循资源化设计。
核心原则
- 资源 URL:
/users、/orders/{id},不用动词 - HTTP 方法:GET(查)、POST(增)、PUT(全量改)、PATCH(部分改)、DELETE(删)
- 状态码:200 正常、201 创建、400 参数错、404 不存在、409 冲突、500 服务端炸
以我个人习惯,REST 最适用的是 CRUD 密集型场景——用户管理、订单查询、商品列表。但如果你要在一个请求里拿多个嵌套资源(比如用户 + 订单 + 商品详情),REST 就尴尬了:要么让客户端发多个请求,要么服务端搞个聚合接口,设计上开始拧巴。
我曾经踩过的坑: 用 REST 做实时推送接口。客户端轮询 /notifications 每 2 秒一次,数据库连接池直接被打满。后来换了 WebSocket + gRPC 双向流,问题才解决。REST 不适合高频实时场景,别硬套。
3.2 GraphQL — 灵活查询,谨防性能黑洞
GraphQL 解决的是“数据过取”和“数据不足”问题。客户端想要什么字段,自己定义查询结构,服务端只返回精确的数据。听起来很完美?嗯,这里要注意:客户端的一次查询,可能在服务端引发 N 次数据库查询。
我当年接一个电商后台的 GraphQL 网关,前端写了个查询:
query {
orders(status: "shipped") {
id
user { name email }
items { product { name price } quantity }
}
}
看着简单吧?实际解析下来,order 先查一次,每个 order 的 user 再查一次,每个 order 的 items 又查一次,每个 item 的 product 再查一次。如果订单有 100 条,数据库查询次数就是 1 + 100 + 100 + 100 = 301 次!
避坑指南: 一定要用 DataLoader 做批量缓存 + 批量加载(Batching + Caching)。另外,对查询深度和复杂度设置阈值,防止恶意查询把服务端拖垮。我曾经给一个公开 API 的 GraphQL 端点加了最大深度 7 层、每个请求最多 100 个节点的限制,之后再没出现过 OOM。
GraphQL 适合:前端团队强势、多端(Web/iOS/Android)场景、数据模型频繁变更的产品。不适合:纯粹的内部 RPC、简单 CRUD(杀鸡用牛刀)。
3.3 gRPC — 高性能二进制协议,微服务标配
gRPC 基于 HTTP/2 + Protobuf,序列化快、支持双向流、强类型定义。说白了就是让服务之间通信像本地函数调用一样顺畅。我个人非常喜欢它的一点:自动生成桩代码,只要写好 .proto 文件,客户端和服务端代码一键生成,类型永远没错。
看一个简单的 .proto 定义:
syntax = "proto3";
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
}
message GetUserRequest {
int32 user_id = 1;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
}
这里 ListUsers 用了服务端流式返回,客户端可以一边接收一边处理,不用等全部数据。如果换成 REST 搞这个,就得用分页或者 WebSocket,麻烦不少。
gRPC 的适用场景
| 场景 | 推荐度 | 原因 |
|---|---|---|
| 微服务内部通信 | ★★★★★ | 低延迟、强类型、流式支持 |
| 移动端/前端 | ★★☆☆☆ | 浏览器原生不支持 HTTP/2 太完美,需要 gRPC-Web 代理 |
| 外部开放 API | ★★★☆☆ | 需要额外提供 REST 转码,生态不如 REST 成熟 |
| IoT / 低带宽 | ★★★★☆ | Protobuf 比 JSON 小 3~10 倍 |
注意: gRPC 的负载均衡需要客户端感知服务发现(DNS + 后端名称解析),不能像 HTTP 那样直接挂 Nginx 轮询。我曾在 K8s 里用 gRPC 时没配 Headless Service 和 gRPC 专用负载均衡策略,结果连接全跑到一个 pod 上,其他 pod 空载。调了一下午才找到原因。
3.4 接口版本控制 — 逃不掉的难题
不管选哪种协议,版本控制都是刚需。老版本不能立刻下掉,新版本又要快速上线。常见的方案有三种,我把它们的优劣用表格列出来:
| 方案 | 实现方式 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| URL 路径版本 | /api/v1/users → /api/v2/users | 简单直观,调试直接看 URL | 容易产生大量冗余代码,路由膨胀 | 团队小、版本迭代慢 |
| 请求头版本 | Accept: application/vnd.myapp.v1+json | URL 整洁,一个端点可返回多版本 | 调试隐藏,客户端容易忘传头 | API 网关统一处理 |
| Header / 参数版本 | ?version=1 | 与 URL 路径类似,更灵活 | 缓存容易混淆,语义不明确 | 快速原型 |
我个人习惯用 URL 路径版本 + 配合 API 网关做路由分发。为什么?因为你看日志或者抓包时,一眼就知道哪个版本的接口出了问题。如果藏在请求头里,排查时得多一步。
我常用的版本策略:
- 主版本号(v1、v2)放在路径,次版本号(v1.1)不体现在 URL,通过请求头字段
X-API-Minor-Version控制小变更 - 每个版本维护一个独立的 handler 包,方便后期独立下线
- 新接口功能必须新增版本号,不允许在旧版本里加参数导致非兼容变化
- 使用 semantic versioning 风格:API 版本
MAJOR.MINOR,破坏性变更就升 MAJOR
// 我的 Go 项目结构示例
apis/
├── v1/
│ ├── handler.go
│ ├── order.go
│ └── user.go
├── v2/
│ ├── handler.go
│ ├── order.go
│ └── user.go
└── router.go // 根据版本号路由到对应 handler
3.5 协议选择决策框架
说到这,你可能会问:到底该用哪个?我画了一张决策流程图,帮你快速落定:
你看这张图,核心问题就两个:谁在调用 + 性能要求有多高。如果调用方是浏览器,你又想给前端足够灵活的数据查询能力,那就选 GraphQL;如果只想简单暴露 CRUD,REST 足矣。内部服务之间,需要高性能 RPC 的用 gRPC,追求简单调试的用 REST。记住:不要为了炫技而选 gRPC,也不要因为怕麻烦死守 REST。
3.6 我的实战建议
- 新项目起步: 如果不是非常明确的高性能 RPC 场景,先用 REST。混熟了再局部引入 gRPC 或 GraphQL。我见过一个团队上来就全上 gRPC,结果前端对接时发现浏览器不支持,又加了一层 gRPC-Web 代理,徒增复杂度。
- 版本策略: 在 API 网关层统一做版本映射,不要在各个微服务里各搞一套。网关可以按版本号路由到不同上游,也能做灰度切换。
- 文档自动化: 不管用哪个协议,都要求自动生成文档。REST 用 OpenAPI(Swagger),GraphQL 用 Introspection 自动生成 Playground,gRPC 用 protoc + grpcurl 生成手册。
- 兼容性测试: 每次发布新版本,跑一遍老版本的集成测试。我曾经因为改了某个 gRPC 字段的编号(从 4 改成 5),导致旧客户端解析出错,全量回滚才救回来。Protobuf 的字段编号一旦发布绝不能改。
协议选择没有银弹。懂原理、看场景、留余地——这就是我做了多年 API 集成后最想说的。