解析、验证、执行:一场精密的三幕剧
把一次GraphQL请求的生命周期拆开,粗略分为三阶段:**解析**、**验证**、**执行**。但多数资料会忽略一个细节,那就是解析本身是一次完整的语法分析,而执行的复杂性取决于解析出的AST节点数量——对,就是我们熟悉的算法复杂度O(…) 那个O。 当一个查询字符串抵达服务端,graphql-js 首先把它扔给一个由graphql/language 模块导出的 parser,它基于一个手写的递归下降解析器(可不是用 yacc 生成的,手写的!),逐字符扫描,生成一棵文档AST。这一步的时间复杂度尚可接受,O(n),n是字符串长度。但紧接着的**验证阶段**,才是第一个可能阴你的地方。验证阶段会遍历AST,运用一系列规则检查(比如字段是否存在、参数类型是否匹配、片段是否重名等)。这些规则中,有些需要对整个schema进行图遍历,如果schema很大,并且查询用了复杂的片段展开,验证可能会耗费数百毫秒——对于某些要求p99延迟<50ms的系统,这就是不可饶恕的。
不过,最精彩的还是**执行阶段**。执行器拿到的是一棵扁平化的操作AST(已经去除了片段展开和变量替换)。它采用一个经典的递归访问模式:对每个字段,调用对应的 resolver 函数,然后等所有并发 resolver 完成,再组装响应。注意,这里的并发是指:所有同一层级的字段 resolver 会被并行执行(通过 Promise.all),但父级必须等待子级完成。这完全就是一个并行树遍历算法,只不过树的形状由客户端查询决定——你完全可以让一个恶意客户端构造一个宽度100、深度10的查询,瞬间产生100^10个并发任务,这算什么?指数级资源消耗。

性能不是玄学:一组压测数字的启示
去年我们重构一个电商详情页,原先的REST接口需要串行调用5个服务:商品信息、库存、促销、评价、推荐,总耗时平均2.3s(p95 3.1s)。我们用GraphQL改了一版,通过一个拼合查询并行拉取数据,耗时降到了0.8s(p95 1.2s),网络传输体积减少了约40%。单看这个数据,老板高兴得当场拍板全面推广。 但好景不长,一次大促压测直接打脸。 我们模拟了真实用户流量,包含大量带有多层嵌套的查询(比如查商品→关联促销→促销规则→适用商品→商品详情……),结果服务器的CPU使用率呈指数级增长,在第7层深度时,单次查询的执行时间已经超过20秒。更令人头疼的是,这种查询在CPU Profile中根本看不出局部热点,因为消耗分散在无数个resolver调用中。后来我们通过 `graphql-depth-limit` 限制了最大深度为4,并且引入了一个基于成本的复杂度计算——给每个字段赋予一个复杂度值,总复杂度超过阈值就拒绝查询。这种做法在GitHub的API v4里被广泛应用,他们甚至公开了计算算法。
三个落地陷阱与正确姿势

回归工程本质
