作者指出通过计数 batch loader 的调用次数(而非响应时间)能更早发现 N+1 问题,因为本地小数据集和内存缓存会掩盖真实网络下的性能退化。
通过计算解析器调用次数而非查询次数,可以更早地发现 GraphQL N+1 问题。使用临时 mock 服务器来计数 batch loader 的调用次数,比测量响应时间更有效,因为延迟测试会将问题隐藏在小本地数据集、内存缓存以及开发机飞速的执行速度之下。经典的错误做法是:添加一个深层嵌套字段,运行一条返回三篇文章的查询,看到响应很快,就交付了。该查询可能调用了三次 author loader 而非一次,但每次调用代价极低,直到在线上环境以五十篇文章、冷缓存和远程存储运行时才会暴露问题。
所以这里的规则不在于渲染图或优化解析器,而在于观察一个应该保持平坦的数值。当你的 schema 声明一篇文章有一个 author,而一个 author 有多篇文章时,任何返回十篇文章并请求每个 author 的查询应该恰好产生一个大小为十的 author 批次,而不是十个大小为一的批次。如果你的 mock GraphQL 服务器能够记录每个批次的大小,并在模式违反这一预期时失败,你就拥有了一个回归门禁,它不依赖模型、网络或你对"够快"的直觉。
从一个将 author 视为远程资源的解析器 map 开始。get_author_batch 函数接收一个 key 列表,每次调用时记录其长度,然后返回这些 key 对应的伪造记录。服务器其余部分只是一个包含 Post 类型、Author 类型以及 author 上嵌套的 posts 字段的 GraphQL schema,使查询能够通过同一解析器循环回来。嵌套的目的不是创建一个真实的 API,而是创建一个可以多种方式遍历的形状。
以下是一个带有计数器的极简 Python fixture,不是框架:
batch_sizes = []
def get_author_batch(keys):
batch_sizes.append(len(keys))
return [{"id": key, "name": f"author-{key}"} for key in keys]
断言同样极简:执行查询后,找到某个 loader 的最大批次大小,并与响应中的唯一 key 数量进行比较。如果查询请求十个不同的 author,最大批次应该是十,调用总次数应该是一。如果你看到十次大小为一的调用,你就在任何数据真正变慢之前发现了 N+1 问题。
这就是自由模型可以发挥作用的地方——它不能帮你写解析器,但能做别的事。声明:本文是 MonkeyCode 产品推广的一部分。你可以给模型提供 schema,要求它生成一组查询文档,以不同方式遍历相同的嵌套关系:重复字段的别名、重新包含 author 的片段、改变列表长度的分页参数,以及深入两到三层的循环。模型不判断结果是否正确,只生成 GraphQL 字符串,由你现有的 mock runner 执行。MonkeyCode 的免费模型访问权限对于这类语料生成已经足够,因为任务范围窄,输出易于审查,而且免费服务器选项提供了一个可丢弃的 HTTP 主机来运行计数 mock,无需仅为测试一个字段而引入额外的基础设施。
人类编写测试时,往往会在显而易见的查询处停下。让模型变化遍历方式,会产生那些刁钻的情况:片段通过两个不同的别名展开同一个 author,loader 对每个别名各调用一次,即使底层 key 相同。模型还会生成这样的场景:嵌套查询请求 posts { author { posts { author } } },你的批处理策略必须在第一个批次尚未完成时就重新进入同一个 loader。正是在这些情况下,一个正确但低效的解析器会躲在小小的 fixture 背后。
runner 可以是对生成的文档进行简单循环,免费服务器只需要一个暴露每次请求批次日志的端点。你可以在 CI 中运行它:为整个测试文件启动一次服务器,执行语料库,然后检查记录的调用模式是否符合某条规则,比如"在同一请求中,同一组 key 不会被任何 loader 调用超过一次"。该规则并不完美,也不旨在证明性能。它是一种启发式方法,能捕捉最常见的结构性 bug。
批处理层在查询需要多轮传递时可能合法地多次调用 loader,此时严格的调用计数断言会产生误报。如果你的 dataloader 在整个请求范围内对 key 进行去重,批次大小可能小于唯一 key 的数量,因为某些 author 已经被兄弟字段加载过。模型不了解这些缓存细节,所以你必须使断言与 loader 的实际契约保持一致,而不是将生成的查询当作一刀切的基准。对于 GraphQL 层已完全自动批处理和去重的团队,这种技术可能收益甚微;它最有用武之地是你维护着手写解析器、可能意外引入嵌套循环的场景。
不要用这个来调优延迟或证明端点够快。如果你的后端有意地对每个 key 进行一次往返(因为该资源无法批处理),它不会告诉你任何关于查询成本的有用信息。其价值仅限于发现查询形状强制重复本应分组的工作的场景,而这恰恰是常规测试在生产环境之前往往漏掉的那类 bug。从十份生成的查询文档开始,在 mock loader 内放一个计数器,加上一条能用一句话解释的规则;如果批次数目令你惊讶,解析器正在试图告诉你什么。