API 设计演进:REST、GraphQL、gRPC 混合架构实战
过去三年我把同一个 API 网关重构了三回。从纯 REST 到 REST + GraphQL 混搭,再到最终引入 gRPC 形成三协议混合架构。如果你正在纠结"选哪个"——成年人不做选择,全都要。但得搞清楚每种协议该放在哪一层。
背景:为什么搞混合架构
2023 年初我接手了一个中台项目,核心业务是内容管理 + 实时协作 + 数据看板。初期选型很单纯:REST 一把梭。
到第 6 个月问题开始冒头:
- 移动端首页需要聚合 7 个接口的数据,首屏加载 3s+
- 协作模块需要实时推送,REST 轮询把服务器打崩过一次
- 数据看板每次查询返回大量冗余字段,前端解析耗时占了一半
这不是 REST 的错,是我用错了场景。于是开始分层改造。
第一层:REST 做资源管理,不动摇
REST 最擅长的是资源 CRUD,这部分我完全没动。用户管理、内容条目、配置信息这些标准资源操作,REST 语义清晰、工具链成熟、调试方便,换掉没有任何好处。
# FastAPI 示例:资源型接口保持 REST
@router.get("/articles/{article_id}")
async def get_article(article_id: str):
article = await article_service.get(article_id)
return {"id": article.id, "title": article.title, "content": article.content, "status": article.status}
但 REST 在聚合查询和字段裁剪上的短板是硬伤——/articles?fields=id,title 这种伪 GraphQL 方案,维护成本越滚越大。
第二层:GraphQL 做 BFF,解决聚合和裁剪
我在前端和后台之间加了一层 BFF(Backend For Frontend),跑的 GraphQL。核心思路:BFF 层只做编排和裁剪,不做业务逻辑。
# Strawberry GraphQL schema 示例
@strawberry.type
class Article:
id: str
title: str
content: str
author_id: str
@strawberry.field
async def author(self) -> Author:
# 内部调用 REST API 或直查数据库
return await author_loader.load(self.author_id)
@strawberry.type
class Query:
@strawberry.field
async def article(self, id: str) -> Article:
return await article_service.get(id)
@strawberry.field
async def dashboard(self, team_id: str) -> Dashboard:
return await dashboard_service.aggregate(team_id)
踩过的坑:
N+1 问题。 如果不处理,GraphQL 解析器会逐条查询关联数据。用 DataLoader 做批量加载是必须的,上面代码里的 author_loader 就是干这个的。
from strawberry.dataloader import DataLoader
async def load_authors(ids: list[str]) -> list[Author]:
authors = await author_service.batch_get(ids)
# 保持顺序一致
author_map = {a.id: a for a in authors}
return [author_map[uid] for uid in ids]
author_loader = DataLoader(load_fn=load_authors)
查询深度限制。 不设限的话,恶意查询能把你的数据库打穿。我在网关层限制了最大深度 5 层:
# 限制查询深度
LIMIT_DEPTH = 5
class DepthValidator:
def __init__(self, max_depth: int = LIMIT_DEPTH):
self.max_depth = max_depth
def validate(self, query: str) -> bool:
# 通过解析 GraphQL AST 检查深度
...
到这个阶段,架构已经是 REST(资源层)+ GraphQL(BFF 层)。前端首屏从 3s 降到 800ms,接口调用次数减少 60%。
第三层:gRPC 做服务间通信,打通内网
服务间通信原本用的也是 REST。随着微服务拆细,A 调 B、B 调 C 再调 D,单个请求的延迟像滚雪球。
换 gRPC 的原因就两个:
- Protobuf 序列化比 JSON 快 3-10 倍
- 双向流(streaming)天然适合实时推送
// proto/article.proto
service ArticleService {
rpc GetArticle (ArticleRequest) returns (ArticleResponse);
rpc StreamArticleUpdates (StreamRequest) returns (stream ArticleEvent);
}
message ArticleRequest {
string id = 1;
repeated string fields = 2; // 支持字段选择
}
message ArticleResponse {
string id = 1;
string title = 2;
string content = 3;
int64 version = 4;
}
Python 这边用 grpc.aio 实现,异步调用性能比同步版好很多:
import grpc.aio
from article_pb2 import ArticleRequest
from article_pb2_grpc import ArticleServiceStub
async def get_article_via_grpc(article_id: str):
async with grpc.aio.insecure_channel("article-service:50051") as channel:
stub = ArticleServiceStub(channel)
response = await stub.GetArticle(ArticleRequest(id=article_id))
return response
踩坑记录:
注意: gRPC 的负载均衡默认是客户端轮询。如果用的是 Kubernetes,建议配 headless service + 客户端侧主动重连,否则会出现连接倾斜。
第四层:三协议混合架构落地
最终架构长这样:
客户端 ──HTTP──▶ 网关层 (REST + GraphQL) ──gRPC──▶ 微服务集群
│
├── REST 接口: 对外 API、第三方集成
├── GraphQL 接口: 移动端、前端 SPA
└── gRPC 内部: 服务间通信、实时推送
关键决策原则:
| 场景 | 协议 | 原因 |
|---|---|---|
| 外部 API / 第三方集成 | REST | 生态最好,调试工具最多 |
| 前端数据聚合 | GraphQL | 灵活裁剪,减少网络请求 |
| 服务间通信 | gRPC | 高性能、强类型、流式传输 |
| 文件上传 | REST | gRPC 处理大文件不优雅 |
| 实时推送 | gRPC stream | 双向流,协议天然支持 |
网关层的路由我用了一个简单的分发器:
from fastapi import FastAPI, Request
from strawberry.asgi import GraphQL
app = FastAPI()
# REST 路由
app.include_router(rest_router, prefix="/api/v1")
# GraphQL 路由
app.mount("/graphql", GraphQL(schema, debug=False))
# gRPC 不经过网关,走内部 DNS
前端调用时,通过一个轻量客户端判断走哪个接口:
// api-client.ts
class ApiClient {
async getArticle(id: string) {
// 列表页只关心标题——走 GraphQL
return this.graphql(`{ article(id: "${id}") { id title } }`);
}
async createArticle(data: FormData) {
// 文件上传——走 REST
return this.rest.post("/api/v1/articles", data);
}
subscribeArticleUpdates(teamId: string) {
// 实时推送——走 WebSocket(网关转发到 gRPC stream)
return this.ws.subscribe(`/ws/articles?team_id=${teamId}`);
}
}
性能对比数据
同样 1000 并发下的压测结果(单次查询含 3 次服务间调用):
| 方案 | P50 | P99 | 吞吐量 |
|---|---|---|---|
| 纯 REST(全部 HTTP) | 45ms | 210ms | 2200 req/s |
| REST + GraphQL BFF | 32ms | 150ms | 3100 req/s |
| REST + GraphQL + gRPC 内部 | 18ms | 78ms | 5800 req/s |
gRPC 是内部延迟改善最明显的一步——服务间调用从 HTTP 序列化/反序列化的开销中解放出来。
现在还存在的问题
架构不算完美。有三件事我还在持续优化:
- 协议转换损耗。 Gateway 要把 gRPC 的二进制数据转成 JSON 返回给前端,这部分有约 3-5ms 的固定开销。
- 调试复杂度。 以前 curl 一把梭,现在要同时维护 REST 文档、GraphQL playground、gRPC reflection 三套调试方案。
- 团队学习成本。 新同学上手要同时理解三种协议的语义,前期效率下降明显。
延伸思考
如果你的项目规模还很小(日均请求 <10 万),REST 足够用,别折腾。混合架构的价值在系统复杂度到拐点后才体现。这个拐点我的经验是:
- 前端有 2 个以上客户端(移动端 / Web / 小程序)
- 服务数超过 5 个
- 存在 2 种以上数据交互模式(请求-响应 / 实时推送 / 批量查询)
到不了这个量级,REST 依然是性价比最高的选择。到了,就按层渐进改造——不要一次性推翻重写,从收益最高的那一层开始动。

评论已关闭!