去年三月,我们组把订单服务从REST切到了GraphQL,当时我还在技术分享会上吹了一波。一年多以后,我又默默把核心链路改回了REST。这个决定不是一时冲动,而是踩过无数坑之后,知道自己用错了地方。
GraphQL最吸引我的,就是那个“按需请求”。比如订单详情页需要同时展示用户、商品和物流状态,原来要拼三次HTTP,用GraphQL一次就够。刚开始一个月我每天都很嗨,代码也写得很爽。但是等系统流量慢慢上来,问题就接踵而至——最典型的就是N+1查询。我们的user resolver会先取订单列表,再逐条查用户。一页20个订单,本来一个JOIN就能搞定,结果跑了21条SQL。我隐约记得当时有接口平均延迟从180ms涨到630ms,数据库连接池也总是告警。
后来我花了两个周末,把所有resolver都加了DataLoader,又把不少查询改成了批量查询,情况好转一些。但问题并没有消失。因为GraphQL把查询的“形状”交给了客户端,后端就要处理无限可能的组合。有一次我们的大屏客户端给了个很深的嵌套,我刚好忘了设置最大深度限制,一个请求就把数据库连接池打满,ECS CPU直接飙到100%,运维凌晨打电话叫我起来看日志,尴尬得不行。
这类问题在REST时代就很少出现。REST接口路径是固定的,后端能精确知道每个query长什么样,然后去做SQL优化、加redis缓存、走nginx的proxy_cache。同样一个订单列表页,我用REST写出来的JSON大概2.1KB,GraphQL返回带了__typename和一堆空字段,体积变成4.3KB。更关键的是,REST的GET接口可以被CDN缓存,边缘命中率能做到60%左右,GraphQL的POST请求根本没法这么玩,想自定义缓存key还要引入PersistedQueries,心智负担一下就上去了。
版本管理也是想当然的。以前REST升级,加个前缀/v2,老接口慢慢下线,心里踏实。到了GraphQL,大家喜欢说schema演化不破坏客户端。但现实是,一旦你改了类型或者字段名,前端没同步,测试环境全是Mock数据根本发现不了,发到线上马上就挂。就因为我们rename过一个status字段,出了两次事故。后来我逼着团队上了schema lint和breaking change检测,才缓了一口气。
所以如果问我GraphQL跟REST怎么选,我的观点很简单:GraphQL非常适合做BFF(Backend For Frontend)或微服务聚合层,尤其是App、Web、小程序多端共存时,它能省掉大量请求;但对于传统的、以资源为核心的内部CRUD接口,REST在缓存、可预测性和监控上依然是最稳的选择。拿我们做的后端举例,内部十几个服务没到用GraphQL的必要,但最顶上的聚合API用GraphQL确实很方便。
如果你已经决定要上GraphQL,我给你的建议是先把这三件事做好:第一,在网关层限制query深度和数量,别指望框架默认安全,我用的是graphql-depth-limit,深度直接限制6层。第二,DataLoader一定要放在业务层,不是传输层,否则它没法跨多个resolver合并数据库请求。第三,为了监控性能,每个根字段都加上trace标记,配合Apollo Tracing可以统计出耗时最高的子字段,不然出了性能问题你压根不知道是哪一环的锅。
GraphQL不是银弹,我当初就是把它当成了银弹,结果在REST完全不是问题的地方栽跟头。工具本身没有高下,关键看你是不是在合适的边界里用它。以上只是个人经验,不一定对。如果你们团队项目很庞大,或者打算做产品化API,那可以把它放到对外暴露层;如果是给内部管理系统做接口,我劝你冷静一下。