去年秋天,我们花了两周做技术预研,认真评估把主要接口层从 REST 换成 GraphQL。最后的决定是不换。
这篇算是那次决策的记录。我想尽量公平地写——GraphQL 确实解决了真问题,我们不换不是因为它不好,而是我们的处境不划算。
为什么会想换
动机很具体,不是赶时髦。当时有三个痛点:
- 移动端首页要展示 7 类内容,得调 7 个接口,弱网下首屏经常要 3 秒以上。
- 同一个「商品」概念,列表页要 6 个字段,详情页要 30 个,我们做了三个不同的接口来适配,字段各种重叠。
- 产品每次改版都要后端配合调字段,前端等排期,节奏很难受。
这三条正好是 GraphQL 的经典宣传场景,所以我们决定认真试试。
Spike 做了什么
我们挑了商品域做原型,两个人两周,实现了约 60% 的字段,接移动端首页做真实压测。
好的部分是真的好:首页从 7 次请求变成 1 次,弱网首屏从 3.2 秒降到 1.4 秒。
然后我们开始遇到问题。
问题一:N+1 是默认行为,不是意外
GraphQL 的解析器逐字段执行。查 20 个商品,每个要取店铺信息,不做处理就是 21 次数据库查询。这是结构性的,不是谁写错了。
解决方案是 DataLoader 那一套——把同一轮的请求攒起来批量查。这方案是成熟的,但意味着每一个关联字段都要额外写一个 loader,还要处理好缓存作用域。我们估算了一下,全量迁移大概要写 40 多个 loader,而且很容易漏写,漏写的后果是性能悄悄退化。
问题二:缓存策略几乎要重做
这是压垮天平的那一根。我们的 REST 接口有一整套缓存体系:CDN 缓存详情页、网关按 URL 限流、Redis 按接口缓存,全都建立在「GET + URL 即缓存键」这个前提上。
GraphQL 所有请求都是 POST /graphql,请求体千变万化。CDN 没法缓存,网关没法按接口限流,Redis 的缓存键要自己从查询语句里算,还得处理不同查询之间的字段重叠。
持久化查询能缓解,把语句预先注册成 id,但那又引入版本管理和发布协同的复杂度——某种意义上又回到了「前端改字段要走流程」的老问题。
问题三:可观测性断层
我们的监控面板是按接口维度组织的:哪个接口慢了、哪个接口错误率高、哪个接口 QPS 异常。切到 GraphQL 之后,所有请求在网关看来都是同一个端点。
要恢复同等观测能力,得在 GraphQL 层自己做字段级埋点,再重建整套看板和告警。能做,但工作量不小。
算了笔账
最后我们把成本列出来:40 多个 loader、缓存体系重建、监控告警重建、把接口级权限改造成字段级、团队学习成本(后端 5 人、前端 2 人,都没用过)。乐观估计 3 个月,悲观估计 5 个月。
而收益呢?主要就是首页那 1.8 秒,以及前端加字段的自由度。
我们最后做了什么
选了两条便宜得多的路:
- 给首页做了一个聚合接口。 内部并发调那 7 个服务,一次返回。200 行代码,两天上线,首屏 1.5 秒——和 GraphQL 原型基本持平。
- 给商品接口加了
fields参数。 调用方传fields=id,title,price,服务端按需返回。这是个「穷人版 GraphQL」,覆盖了过度获取问题的 80%,同时保留了 URL 缓存键和接口维度监控。
两项加起来花了大概三周。
什么情况下我会选 GraphQL
我不想把这篇写成「GraphQL 不行」。如果换个处境,我的答案会不一样:
- 如果我们有 5 个以上差异很大的客户端,字段需求各不相同,维护 N 套接口的成本会远超 GraphQL 的建设成本;
- 如果数据本身是强关联的图结构(社交关系、组织架构),REST 的资源模型会很别扭;
- 如果团队里有人真正用过并踩过坑,学习成本能砍掉一半以上。
我们三条都不满足。两个客户端、数据结构基本是树、团队零经验。
一点方法论上的收获
这次评估让我养成一个习惯:把新技术要解决的问题拆到最小粒度,然后逐个问「有没有更便宜的解法」。
「首屏慢」的最小解法是聚合接口,「字段冗余」的最小解法是 fields 参数。把大问题拆成小问题,很多时候会发现你并不需要那个大方案。