选错 API 范式的代价
团队里最常见的技术债,不是框架旧了,不是依赖过期了。是三年前选的那个 API 通信方式,今天拖慢了整个产品迭代。
一个 B2B SaaS 平台对外暴露 RESTful 接口,客户接起来舒服,文档清晰。但内部微服务之间也用 REST,每次调用都要序列化 JSON、走 HTTP/1.1、扛住 N+1 查询。性能瓶颈就这样一层一层堆起来。
另一个团队做移动端 App,后端给了标准 REST 接口。前端为了渲染一个页面,要调六个接口、拼装数据、处理冗余字段。用户觉得卡,产品经理觉得慢,后端觉得已经给了接口。
这些问题的根源都一样:在错误的场景里用了错误的 API 范式。
Richardson 成熟度模型:理解 REST 的四个层次
Leonard Richardson 在 2008 年提出了一个分级模型,把 Web API 的设计水平分成四个层次。这个模型到今天依然是评估 REST API 质量的最佳标尺。
Level 0 — The Swamp of POX。 所有请求都发往同一个端点,用同一个 HTTP 方法。本质上就是 XML-RPC 或 JSON-RPC 套了个 HTTP 的壳。SOAP 就是这个层次的典型代表。
Level 1 — Resources。 开始引入资源的概念。每个实体有自己的 URI,比如 /users/123、/orders/456。但所有操作仍然用 POST 方法完成。
Level 2 — HTTP Verbs。 正确使用 HTTP 语义。GET 读取、POST 创建、PUT 更新、DELETE 删除。状态码也有了正确的含义,201 表示创建成功,404 表示资源不存在。
Level 3 — Hypermedia Controls。 也就是 HATEOAS。响应里包含链接,客户端可以通过这些链接发现下一步能做什么。API 变成了一台状态机,客户端不需要硬编码任何 URI。
大多数团队的 REST API 停留在 Level 1 到 Level 2 之间。真正做到 Level 3 的项目极少,因为 HATEOAS 的实现成本高,客户端 SDK 的支持也有限。
Level 2 的现实困境
Level 2 的 REST API 是行业里最常见的形态。它足够好用,能覆盖大多数 CRUD 场景。但业务复杂度上升之后,它的局限性开始暴露。
Over-fetching 和 Under-fetching。 一个 /users/123 接口返回了用户的完整信息,包括地址、偏好、历史订单摘要。但前端只需要用户名和头像。多出来的字段浪费带宽,也增加了序列化成本。反过来,前端要展示用户主页,需要用户信息、最近订单、收藏夹。三个接口,三次网络请求。
版本管理的痛苦。 字段要废弃、结构要调整、语义要变更。REST API 的版本管理方案五花八门:URL 路径里加 /v2/,Header 里加 Accept: application/vnd.api+json;version=2,查询参数里加 ?version=2。没有统一标准,每个团队自己选。
文档和实现的脱节。 OpenAPI 规范写得再漂亮,实现代码一旦和文档不同步,调用方就会踩坑。手动维护文档的成本随接口数量线性增长。
这些困境不是 REST 的设计缺陷。它们是用一种通用协议解决所有问题时必然产生的摩擦。
gRPC:为机器间通信而生
gRPC 由 Google 在 2015 年开源,脱胎于内部使用了十多年的 Stubby 框架。它的设计目标很明确:解决微服务之间的高性能通信问题。
gRPC 基于 Protocol Buffers 定义接口。一个 .proto 文件就是接口的单一事实来源。从它可以生成十几种语言的客户端和服务端代码。接口定义和实现代码之间不存在脱节的可能。
传输层使用 HTTP/2。多路复用、头部压缩、双向流式传输,这些特性让 gRPC 在延迟和吞吐量上远超传统 REST。一个 TCP 连接上可以同时跑几十个请求,不用担心队头阻塞。
序列化用的是 Protobuf 的二进制格式。比 JSON 小 3 到 10 倍,解析速度快 20 到 100 倍。对于高频调用的内部服务,这个差距直接影响机器成本。
gRPC 的四种通信模式
gRPC 不只是"更快的 REST"。它提供了四种通信模式,覆盖了分布式系统中几乎所有的交互模式。
Unary RPC。 一发一收,和传统 REST 调用最像。客户端发一个请求,服务端回一个响应。适合简单的查询和命令。
Server Streaming RPC。 客户端发一个请求,服务端以流的形式持续返回数据。适合大结果集的分批拉取、实时通知、日志订阅。
Client Streaming RPC。 客户端以流的形式持续发送数据,服务端汇总后返回一个响应。适合批量上传、数据导入、指标聚合。
Bidirectional Streaming RPC。 双向流。客户端和服务端可以同时收发数据。适合实时协作、聊天、游戏状态同步。
这四种模式组合起来,能表达几乎任何分布式通信场景。REST 里需要用 WebSocket 或 SSE 补丁才能实现的实时能力,gRPC 原生就支持。
gRPC 不适合的场景
gRPC 强大,但不是万能的。它在几个方面存在明确的短板。
浏览器兼容性差。 浏览器无法直接发起 gRPC 调用。HTTP/2 的帧格式和 Protobuf 的二进制编码都不在浏览器的原生能力范围内。需要 gRPC-Web 做一层代理转换,增加了部署复杂度。
可读性差。 Protobuf 的二进制格式对人类不友好。调试的时候抓包看到的是一堆乱码,不像 JSON 那样可以直接阅读。排查问题需要额外的工具链支持。
学习曲线陡。 Protobuf 的 schema 设计有自己的范式。oneof、map、repeated、enum 的使用需要经验。版本兼容性(forward、backward、full)的规则也需要团队达成共识。
生态绑定。 gRPC 的生态围绕 Protobuf 展开。如果你的系统里有大量非 Protobuf 的组件,集成成本不低。
GraphQL:为前端需求而生
GraphQL 由 Facebook 在 2015 年开源,解决的是一个很具体的问题:前端需要的数据和后端提供的接口之间存在结构性错位。
在 REST 模式下,后端设计接口,前端适配接口。接口的粒度、字段的组合、数据的嵌套方式,都由后端决定。前端的需求变了,就要找后端加接口或改接口。
GraphQL 把这个关系反过来了。后端定义数据的类型和关系,前端自己决定要什么。一个查询里可以跨多个资源类型、指定字段、嵌套关联。一次请求,精确拿到需要的数据。
|
|
这个查询在 REST 模式下至少需要三次调用:获取用户信息、获取最近订单、获取每个订单的明细。GraphQL 一次搞定,而且不会多拿一个多余字段。
GraphQL 的代价
GraphQL 的灵活性不是免费的。它引入了一组新的工程挑战。
查询复杂度控制。 前端可以写出嵌套十几层的查询,把数据库打穿。后端必须实现查询深度限制、复杂度分析、超时控制。否则一个恶意查询就能拖垮整个服务。
N+1 问题。 GraphQL 的 resolver 机制天然容易产生 N+1 查询。查询 100 个用户及其订单,如果没有 DataLoader 做批处理,就是 1 次用户查询加 100 次订单查询。
缓存困难。 REST 的 GET 请求可以被 HTTP 缓存、CDN 缓存、浏览器缓存。GraphQL 的所有操作都走 POST,标准的 HTTP 缓存机制全部失效。需要引入 Persisted Queries 或 Apollo Client 这样的客户端缓存方案。
Schema 治理。 Schema 是 GraphQL 的核心资产。随着业务增长,Schema 会变得庞大且难以变更。一个字段一旦发布,就很难删除或改名。需要专门的 Schema Registry 和变更审批流程。
三种范式的核心差异对比
把 REST、gRPC、GraphQL 放在一起,从几个关键维度做对比:
| 维度 | REST | gRPC | GraphQL |
|---|---|---|---|
| 传输协议 | HTTP/1.1 或 HTTP/2 | HTTP/2 | HTTP/1.1 或 HTTP/2 |
| 数据格式 | JSON(为主) | Protobuf(二进制) | JSON |
| 接口定义 | OpenAPI / Swagger | .proto 文件 | Schema + Type System |
| 代码生成 | 可选,依赖工具 | 内置,多语言 | 可选,依赖工具 |
| 流式支持 | SSE / WebSocket | 原生四种模式 | Subscription(WebSocket) |
| 浏览器兼容 | 原生支持 | 需要 gRPC-Web 代理 | 原生支持 |
| 缓存能力 | HTTP 标准缓存 | 不支持标准缓存 | 需要客户端缓存方案 |
| 学习曲线 | 低 | 中高 | 中 |
| 调试友好度 | 高(JSON 可读) | 低(二进制) | 中(有 Playground) |
| 性能 | 中 | 高 | 中 |
这张表不是用来判断谁好谁差的。它的作用是让团队在做选型时,把每个维度的 trade-off 摆到台面上来。
选型决策矩阵:三大场景
下面给出三个典型业务场景的选型建议。这不是标准答案,是经过多个项目验证后的经验总结。
场景一:B2B 对外 API
B2B 场景的核心诉求是:易用性、稳定性、文档质量。
客户方的开发者是你无法控制的变量。他们的技术栈各异,使用的语言从 Java 到 Python 到 Go 都有。他们希望拿到 API 文档就能快速集成,不希望学习新的协议或工具链。
推荐方案:REST Level 2。
理由很直接。REST 是所有开发者都熟悉的范式。JSON 格式通用、可读、好调试。OpenAPI 规范可以自动生成文档和 SDK。HTTP 缓存和 CDN 能减轻服务端压力。版本管理虽然有痛点,但客户能理解 /v1/ 和 /v2/ 的区别。
在这个场景里引入 gRPC 或 GraphQL,会增加客户的集成成本。除非你的客户明确要求,否则不要增加不必要的复杂度。
如果 API 的使用方超过 20 个,建议额外投入做 API Gateway。统一鉴权、限流、日志、版本路由。Kong、APISIX、Envoy 都是成熟的选择。
场景二:内部微服务通信
内部服务的核心诉求是:性能、类型安全、演进能力。
调用方和被调用方都在你的控制范围内。技术栈可以统一,部署环境可以定制。你需要的是尽可能低的延迟、尽可能高的吞吐、以及接口变更时的平滑过渡。
推荐方案:gRPC。
Protobuf 的强类型定义天然适合微服务之间的契约。一个 .proto 文件的变更,编译期就能发现所有受影响的调用方。不需要等到运行时才报字段缺失或类型不匹配的错误。
HTTP/2 的多路复用和头部压缩在高频调用场景下节省的连接数和带宽非常可观。Protobuf 的二进制序列化让 CPU 和内存开销降到很低。
流式 RPC 为事件驱动架构提供了原生支持。服务间的数据同步、事件广播、实时状态推送,都不需要额外引入消息队列来做。
对于服务网格(Service Mesh)场景,gRPC 和 Envoy 的配合几乎是原生级别的。流量管理、熔断、重试、链路追踪,都能在基础设施层面完成,业务代码无感知。
场景三:移动端 / 前端密集型应用
移动端和前端密集型应用的核心诉求是:减少请求次数、精确控制数据、快速迭代。
移动网络环境不稳定,延迟高、带宽有限。每一个多余的请求都直接影响用户体验。前端页面的数据需求变化快,不可能每次都等后端出新接口。
推荐方案:GraphQL。
GraphQL 的"一次请求、精确数据"模型完美匹配移动端的需求。一个页面需要的所有数据,不管来自多少个后端资源,都可以用一个查询拿到。请求次数从 N 降到 1。
字段级别的选择权在前端手里。新增一个展示字段不需要后端配合,删掉一个不用的字段也不影响其他调用方。前端和后端的迭代节奏解耦了。
Schema 的类型系统给前端提供了自动补全和类型检查的能力。开发体验显著提升,联调时间大幅缩短。
如果移动端有离线需求,GraphQL 配合 Apollo Client 或 Relay 的本地状态管理,可以实现比较优雅的离线优先架构。
混合架构:真实世界的选择
现实中很少有团队只用一种 API 范式。更常见的是混合架构,在不同的边界使用不同的技术。
一个典型的组合是:
- 对外 API 网关 → REST。 面向第三方开发者和 B2B 客户。
- API 网关 → 内部服务 → gRPC。 网关把 REST 请求转换成 gRPC 调用,路由到对应的微服务。
- 前端 → API 网关 → GraphQL。 前端通过 GraphQL 查询聚合数据,网关把查询拆分成多个 gRPC 调用。
|
|
这个架构的好处是每个边界都用最合适的协议。对外接口简单易用,内部通信高性能,前端灵活取数。
代价是架构复杂度高。网关层需要同时支持 REST 和 GraphQL 的路由、鉴权、限流。gRPC 和 GraphQL 之间的转换需要专门的 BFF(Backend For Frontend)层来处理。
选型时的隐藏成本
很多团队在选型时只看技术特性,忽略了隐藏的工程成本。
团队技能储备。 团队里没有人用过 Protobuf,gRPC 的落地周期会比预期长两到三倍。GraphQL 的 Schema 设计和 Resolver 编排也需要专门的学习投入。
工具链成熟度。 REST 的工具链最成熟:Postman、Insomnia、curl、各种语言的 HTTP 客户端。gRPC 有 BloomRPC(已停维)、Evans、grpcurl。GraphQL 有 GraphiQL、Apollo Studio、Altair。生态的丰富度直接影响开发效率。
运维可观测性。 REST 的请求和响应可以直接用日志记录,监控指标的采集也比较直接。gRPC 的二进制格式需要专门的拦截器来记录请求内容。GraphQL 的查询复杂度分析需要额外的中间件。
迁移成本。 从 REST 迁移到 gRPC 或 GraphQL 不是改个协议那么简单。接口的语义模型、错误处理模式、认证授权机制都需要重新设计。渐进式迁移比推倒重来更稳妥。
演进路径建议
如果你的系统目前是纯 REST,想演进到更合适的架构,建议分步走。
第一步:先把 REST 做到 Level 2。 统一 HTTP 方法语义、状态码使用、错误响应格式、分页规范。这一步的 ROI 最高,投入最小。
第二步:内部高频服务试点 gRPC。 选两三个调用量最大、延迟最敏感的服务间通信链路,用 gRPC 替换 REST。在 API 网关层做协议转换,对外接口不变。
第三步:前端密集型场景引入 GraphQL。 在 BFF 层加一个 GraphQL 端点,把已有的 gRPC 服务作为数据源。前端逐步从 REST 迁移到 GraphQL。
第四步:建立统一的 API 治理体系。 包括接口注册中心、Schema Registry、变更审批流程、兼容性检查、自动化测试。
每一步都可以独立交付价值,不需要一次性完成整个迁移。
决策流程图
当你面对"这个新服务应该用什么 API 范式"的问题时,可以按以下逻辑判断:
|
|
这个流程图不是绝对的。但它能帮你在技术评审会上把讨论从"我喜欢哪个"变成"场景需要什么"。
几个容易踩的坑
坑一:过早引入 GraphQL。 如果你的后端只有五六个资源类型,前端也只有两三个页面,GraphQL 带来的收益远不如它的工程成本。等复杂度真正上来了再引入。
坑二:gRPC 的 deadline 不设默认值。 gRPC 的 RPC 调用如果不设 deadline,默认是无限等待。一个下游服务挂了,上游的线程池会被耗尽,雪崩效应就此开始。每个 RPC 都必须设置合理的超时。
坑三:REST API 的错误码设计混乱。 有的团队用 HTTP 状态码表达业务错误,有的团队永远返回 200 然后在 body 里放 error_code。两种做法都有道理,但不能混用。在团队层面统一一种,写进规范,强制执行。
坑四:GraphQL 的 Schema 不做 Federation。 当 GraphQL Schema 膨胀到几千个类型的时候,单体 Schema 的维护会变成噩梦。Apollo Federation 或类似方案能让多个团队各自维护自己的 Subgraph,组合成一个统一的 Supergraph。
坑五:忽略 API 的幂等性设计。 网络不可靠,重试是常态。如果 PUT 和 DELETE 不是幂等的,重试就会产生重复操作。POST 操作需要引入幂等键(Idempotency Key)。这个问题三种范式都会遇到,但经常被忽视。
面向未来的考量
API 范式还在演进。几个值得关注的趋势:
gRPC 的浏览器支持在改善。 Connect Protocol(由 Buf 团队推出)试图在 gRPC 和 REST 之间搭桥,让同一个服务端同时支持两种协议。如果这个方向成熟,gRPC 的浏览器短板会被补齐。
GraphQL 的 Federation 生态在成熟。 Apollo Federation 2.0 和 GraphQL Mesh 让多团队、多数据源的 Schema 组合变得可行。大型组织的 GraphQL 落地门槛在降低。
REST 的标准化在推进。 JSON:API 规范(RFC 9421 在推进中)试图统一 REST API 的响应格式、分页、过滤、排序。如果标准落地,REST 的碎片化问题会缓解。
tRPC 在 TypeScript 生态中崛起。 如果你的前后端都用 TypeScript,tRPC 可以在不写 Schema 的情况下实现端到端类型安全。它不是通用方案,但在特定技术栈下极其高效。
选择 API 范式不是一次性决策。它是一个随业务增长持续评估和调整的过程。关键是建立评估框架,让每次决策都有据可依,而不是凭直觉或跟风。