- 后端
- API网关
【免费下载链接】crystal
🔮 Graphile's Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!
本文以 PostGraphile v5 官方调试文档 为骨架,结合当前仓库中
grafserv、@dataplan/pg、graphileCLI 的源码实现,系统梳理应用出问题时的排查路径。你将掌握:如何确认 GraphQL 请求真实内容、如何解除错误掩码并安全地暴露错误详情、如何查看 PostGraphile 生成的 SQL 与 EXPLAIN 结果、如何定位 Schema 中"多出来/缺了"的内容、以及如何针对 RLS 与过度获取做性能排查,最后还能用 Chrome DevTools 直接调试 PostGraphile 进程本身。
当应用行为与预期不符时,第一步不是急着改代码,而是判断你遇到的到底属于哪一类问题——是 GraphQL 请求层面的问题、Schema 内容的问题、性能问题,还是 PostGraphile 内部实现的问题。不同类型的问题有不同的排查工具和手段,用错了方向往往会浪费大量时间。下面按文档的官方分类逐层展开。
一、GraphQL 请求出了问题
1.1 先用 Chrome Network 面板确认"你请求的就是你以为的"
很多"bug"其实源于客户端代码并没有发送你以为的请求。在动手排查服务端之前,先用浏览器开发者工具确认网络层的真实情况:
- 在 Chrome 中打开你的网站;
- 右键选择"检查"(Inspect);
- 在开发者工具中选择Network标签页;
- 在过滤框中输入
/graphql(或你实际配置的 API 路径); - 确保过滤框右侧选中的是All;
- 触发你的 GraphQL 请求(刷新页面或点击页面上相关元素);
- 检查到达的请求是否符合预期——变量是否意外为
null、请求头中的访问令牌是否正确携带等。
这一步能立刻排除掉大量"客户端写错"的情况,把焦点收敛到服务端。
1.2 在 Ruru(或 GraphiQL)中复现同一个查询
有时候用另一种方式做同一件事更容易发现问题。把出问题的查询原样拿到 Ruru(PostGraphile v5 自带的 GraphiQL 变体)里执行一次,看是否复现同样的问题。注意:Ruru 底部输入变量的位置有一个Headers标签页,可以在那里设置请求头(例如Authorization),从而模拟客户端携带的认证信息。
1.3 解除错误掩码:用preset.grafserv.maskError输出错误细节
PostGraphile 默认会对 GraphQL 错误进行掩码(mask):详细的错误信息只在服务端被记录,返回给客户端的是被裁剪过的安全内容。若你关闭了错误掩码,错误会被直接透传给客户端、不再在服务端记录——这种情况下,可以用preset.grafserv.maskError在服务端输出错误详情,并在返回客户端前对错误进行自定义加工。
文档给出的完整示例配置如下(graphile.config.mjs):
import { GraphQLError } from "postgraphile/graphql"; import { isSafeError } from "postgraphile/grafast"; import { createHash } from "node:crypto"; const sha1 = (text) => createHash("sha1").update(text).digest("base64url"); export default { //... grafserv: { maskError(error) { console.error("maskError was called with the following error:"); console.error(error); console.error("which had an originalError of:"); console.error(error.originalError); // 生产环境不建议直接返回原始 error,因为结果会发送给客户端, // 可能泄露你不想公开的实现细节。 // // return error; // 下面是一个更谨慎的实现: if (error.originalError instanceof GraphQLError) { return error; } else if ( error.originalError != null && isSafeError(error.originalError) ) { return new GraphQLError( error.originalError.message, error.nodes, error.source, error.positions, error.path, error.originalError, error.originalError.extensions ?? null, ); } else { // 用哈希值便于对相似错误分组 const hash = sha1(String(error)); console.error(`Masked GraphQL error (hash: '${hash}')`, error); return new GraphQLError( `An error occurred (logged with hash: '${hash}')`, error.nodes, error.source, error.positions, error.path, error.originalError, // 刻意清空 extensions {}, ); } }, }, };这段配置的实际行为在源码中可以得到印证:在 grafserv 的 options.ts 中,maskError是 grafserv 的一个动态选项,HTTP/WebSocket 响应中的payload.errors都会经过payload.errors.map(maskError)处理;在 grafserv 的 utils.ts 中,GraphQL over WebSocket 的onError与onNext回调同样会把错误逐条送入maskError。也就是说,无论请求走 HTTP 还是 WebSocket(订阅/实时查询),你自定义的maskError都会生效。
示例中有三个分支,值得理解其设计意图:
error.originalError instanceof GraphQLError:原始错误本身就是 GraphQL 错误(例如校验失败),这类错误是"设计内的错误",直接原样返回;isSafeError(error.originalError):grafast 提供了SafeError类型(见 grafast 的 error.ts),用于标记"可以安全展示给客户端"的错误(如 HTTP 状态码相关的错误)。这类错误返回其原始 message;- 其余情况:这是真正"不该给客户端看"的内部错误。示例用 SHA-1 哈希给错误分组,把哈希值同时写进服务端日志和返回给客户端的消息中,这样客户端上报问题时,你能通过哈希快速在日志里定位到同一批错误,同时又不会泄露堆栈、SQL 等内部细节。
⚠️安全警告:
maskError的默认实现会出于安全考虑裁剪掉大量细节。一旦你替换它,就必须谨慎对待输出给潜在攻击者的内容——例如完整的堆栈、SQL、内部路径等都可能成为攻击者探测系统内部结构的线索。调试完成后务必恢复为默认掩码行为。
💡善用
originalError属性:GraphQLError实例上带有error.originalError属性,可用于取回底层错误。与 GraphQL 错误本身相比,它通常包含更多可操作的信息(例如 PostgreSQL 驱动抛出的原始错误、带 SQLSTATE 的数据库错误等)。上面的配置示例正是围绕它展开的。
1.4 查看 PostGraphile 生成的 SQL
如果错误来自数据库层,你需要看到 PostGraphile 实际生成并执行了哪些 SQL 语句。文档提供了两种途径。
方式一:Ruru 的 Explain 功能
首先在配置中开启 explain:
export default { // ... grafast: { explain: true, }, };开启后访问 Ruru(默认地址为http://localhost:5678/graphiql),在左侧打开Explain标签页(图标是一个放大镜 🔍)。你会看到已执行的查询以及与之对应的 Grafast操作计划(operation plan);通过下拉框可以逐个查看各个 SQL 查询及其 EXPLAIN 结果。
⚠️生产环境务必关闭 Explain:Explain 会泄露你 Schema 的内部结构信息,这些信息对攻击者是有价值的,因此生产环境必须保持
explain为false。
ℹ️已知限制:目前 SQL
EXPLAIN只能通过DEBUG环境变量启用(即下文方式二),Ruru Explain 中暂时还无法直接触发 SQL 层 EXPLAIN。这是官方文档明确标注的已知问题(仓库中相关 TODO 见 debugging.md)。
方式二:DEBUG环境变量
在启动 PostGraphile 之前设置对应的 DEBUG 环境变量(该机制基于广为人知的debug包命名空间约定,命名空间形如@dataplan/pg:PgExecutor:explain):
# Bash (Linux, macOS 等) export DEBUG="@dataplan/pg:PgExecutor:explain" postgraphile # Windows 命令提示符 set DEBUG=@dataplan/pg:PgExecutor:explain & postgraphile # Windows PowerShell $env:DEBUG='@dataplan/pg:PgExecutor:explain'; postgraphile💡 上面的示例默认你当前目录下已有
graphile.config.*配置文件。如果没有,请改用 CLI 参数传入连接信息与 preset。
源码层面的印证:在 @dataplan/pg 的 executor.ts 中可以看到,PgExecutor内部通过debugFactory("@dataplan/pg:PgExecutor")创建了基础命名空间,再通过debug.extend("explain")派生出@dataplan/pg:PgExecutor:explain子命名空间。当 explain 调试开启时(executor.ts 第 228-248 行),每次 SQL 执行后还会追加一次EXPLAIN查询,其参数为COSTS, VERBOSE, BUFFERS, SETTINGS;如果被解释的语句是SELECT且当前不是 mutation 执行,还会自动加上ANALYZE(注意 mutation 不会加ANALYZE,这是为了避免真实的写副作用)。输出内容相当完整,包含:
- SQL 查询文本(经过格式化,见 formatSQLForDebugging.ts);
- 绑定参数(placeholders);
- 查询结果(超过 10 行时只展示首尾各 3 行并截断中间内容,避免刷屏);
- 数据库 NOTICE 信息;
- 执行耗时(
# DURATION); - EXPLAIN 结果。
如果开启了 explain 但某条语句没有输出 EXPLAIN,日志里会给出提示,例如(Explain disabled due to error),或(Use 'DEBUG="@dataplan/pg:PgExecutor:explain"' to enable explain)——这正好对应文档中"当前 SQL EXPLAIN 只能通过 DEBUG 启用"的限制。
二、Schema 里出现了不该有的东西
这类问题的根源通常有四类,按文档给出的顺序逐一排查。
2.1 过滤数据库 Schema
确认你的配置中只列出了想要暴露的数据库 schema。默认情况下只暴露public。如果配置里写了多个 schema,或某个扩展自带 schema 被一并引入,就会"多出"内容。
2.2 隐藏 PostgreSQL 扩展带来的资源
默认情况下,PgRemoveExtensionResourcesPlugin会从你的 Schema 中移除来自 PostgreSQL 扩展(extension)的资源与 codec。用下面的命令确认它是否处于启用状态:
npx graphile config print plugins该命令会输出你的解析后配置(resolved preset)中实际生效的插件列表——它的实现位于 graphile CLI 的 config/print 命令。如果该插件被意外禁用,扩展资源就会出现在 Schema 中。
2.3 用权限隐藏(PgRBACPlugin)
PgRBACPlugin会把 Schema 内容限制为"被 introspection 的用户(visitor role)有权限访问"的部分。如果你禁用了这个插件,这种"自动省略"就不会发生。同样用npx graphile config print plugins检查解析后的配置里到底有哪些插件。
常见的权限配置误区:
- 不要用超级用户或数据库属主连接——这样的用户拥有一切权限,
PgRBACPlugin自然什么都隐藏不了。应使用权限最小化的连接用户; - 连接串里应使用authenticator角色(关于角色创建的详细说明见 required-knowledge.md#creating-roles 一节);
- 如果怀疑授权没配对,可以在
psql里用\dp+查看表的完整权限列表。
2.4 用 smart tags 隐藏(@omit与@behavior)
@omit只对 V4 preset 生效;只使用 Amber preset 的用户应改用@behavior。当你添加一个 behavior(包括 V4 preset 把@omit转换成的那些 behavior)后,可以用下一节的npx graphile behavior debug验证它是否按预期生效。
2.5 用 behaviors 隐藏:npx graphile behavior debug详解
这是排查 behavior 类问题最核心的命令。运行:
npx graphile behavior debug它会要求你选择一个scope(实体类型),文档列出的可选 scope 如下:
pgResource—— 可以SELECT数据的地方:表、函数、视图、物化视图等;pgResourceUnique—— 表/物化视图上的唯一约束;pgCodec—— 表示标量、范围、枚举、域和复合类型(没有存储!);pgCodecAttrbute—— 复合类型上的属性(列);pgCodecRelation—— codec 与 resource 之间的关系;pgRefDefinition—— 某个@ref的定义;pgCodecRef—— 一个被应用的@ref。
接着带上 scope 再运行一次(例如npx graphile behavior debug pgResource),会列出该类型下的所有实体;再带上实体标识(例如npx graphile behavior debug pgResource users),就会输出该实体最终的 behavior 字符串及其推导过程——即哪些插件添加/移除了哪些 behavior。
关于输出的解读,文档给出两点提示:
- 形如
__ApplyBehaviors_*__的条目是新增 behavior 系统的临时机制,它代表把defaultBehaviors与可用 behaviors 做"相乘"的结果; - 形如
PgBasicsPlugin.schema.entityBehavior.*.override的条目通常来自你的 smart tags(@omit、@behavior等)所产生的 override。
命令的底层实现可以在 graphile CLI 的 behavior/debug 命令 中看到:它会加载并解析你的配置,构建 inflection 与 build 上下文,枚举所有 behavior 实体类型,再根据你传入的entityType与entityIdentifier逐级给出实体列表与最终的 behavior 推导信息。命令设计为逐级交互:不给类型 → 列出类型;给了类型不给实体 → 列出该类型下的实体;两者都给 → 输出该实体的 behavior 详情。
三、Schema 里缺了本该有的东西
这是上一节的镜像问题。文档给出的检查清单如下:
returns table(...)的函数:这种返回类型无法用注释和 smart tags 扩充(也无法注释/调整其属性),返回类型会被暴露为一个自动生成名字的 GraphQL 类型。尽量避免这种写法,应优先使用显式命名类型(returns setof my_type)。如果确实遇到需要给函数派生类型添加字段的场景,见 customization-overview.md#adding-a-field-to-a-function-derived-type;- 计算列(computed columns):必须遵循 computed-columns 中的命名与签名规则(记得函数要是
STABLE!),并且必须与它们所作用的表位于同一个 PostgreSQL schema 中; - 视图(views):视图没有外键约束,因此无法推断关系;需要按 views.md 和 relations.md 的描述给视图添加
@foreignKeysmart tags。如果引用的是视图,还要确保视图上有@primaryKey; - 数据库 schema 列表:检查配置中列出的 schema 是否正确;
- 来自扩展的资源:如果资源来自 PostgreSQL 扩展,要么禁用
PgRemoveExtensionResourcesPlugin,要么覆盖相关资源上的 behaviors; PgRBACPlugin(默认启用):检查你是否给相关 visitor role 授予了权限,并且数据库连接角色(authenticator 角色)已被授予该 visitor role(即使noinherit也成立);- smart tags 与 behaviors:用上文
npx graphile behavior debug检查; - "高级"或非核心功能:检查对应插件是否启用(例如高级过滤需要
postgraphile-plugin-connection-filter),用npx graphile config print plugins查看当前插件列表; - accessor(根级 "finder" 字段):确认你使用的是约束而非索引(见下文"缺少约束");
- 关系(relations):检查是否缺少约束或索引(见下文)。
3.1 缺少约束(Constraints)
默认情况下,PostGraphile只根据数据库约束来添加关系(relations)和 accessor(根级 "finder" 字段)。
- 对 accessor 而言,唯一索引是不够的——索引只是优化手段,而约束是你对数据"将持续成立"的声明。例如要为
userByUsername: User这种字段创建基础,应添加唯一约束(而非索引):
alter table users add constraint uniq_users_username unique (username);- 对关系而言,仅靠列名的命名约定是不够的(我们可能推断出错误的东西),必须显式添加约束来表达关系:
alter table posts add constraint fk_posts_author foreign key (author_id) references users (id); create index on posts (author_id);3.2 缺少索引(Indexes)
默认情况下,PostGraphile不会添加"反向"关系,除非存在匹配的索引——原因是没有索引时 PostgreSQL 可能需要对表做全表扫描来查找匹配记录,代价极其高昂。
解决办法是创建一个与关系匹配的索引(包括保证列的顺序一致),例如:
alter table posts add constraint fk_posts_author foreign key (organization_id, author_id) references users (organization_id, id); create index on posts (organization_id, author_id);四、性能问题排查
如果数据库 Schema 设计良好,PostGraphile 的性能表现通常很出色;但如果对数据库设计不够熟悉,性能问题可能来自很多方面。先用上文"查看生成的 SQL"一节拿到实际执行的 SQL,再对照以下清单逐项排查:
- RLS 性能:如果同样的查询直接执行比经过 PostGraphile 快得多,那么极有可能是 RLS 策略性能不佳。这是 PostGraphile 用户遇到的最常见的性能问题,但也是最容易修复的。具体写法见 required-knowledge.md#writing-performant-rls-policies;
- 只取所需(fetch only what you render,见下文);
- 索引;
- 函数:见 functions.md#understanding-function-performance;
- 视图;
- 物化视图;
- 插件;
- 复杂过滤器。
4.1 RLS 性能
详见 required-knowledge.md#writing-performant-rls-policies。
4.2 只取所需:GraphQL 不该过度获取
GraphQL 的设计理念是"要什么取什么"——不多也不少。
过度获取(Overfetching):你取回的每一份数据都应该几乎立即渲染给用户。如果某份数据要等用户交互(点击"下一页"、"展开"、"详情"按钮)才展示,就应该放到后续请求中再取。当前视图需要的数据要在一次往返内全部取回,但只取"你需要的",而不是"你猜想几秒后可能需要的"。
过度获取最常见的两个原因:
- 缺少分页;
- fragment 的错误使用。
分页:如果你让 GraphQL 取回全部邮件,却只渲染前 25 封,那就是没有告诉 GraphQL 你到底需要多少。每一个列表查询都应该包含分页限制。
渲染第二页时,不要再次执行同一个查询(它可能会重新取回你不需要的附属数据),而应该用一个复用了相同 fragment 的新查询:
# 用这个查询渲染主页面 query MainPage { notifications { count } me { avatarUrl name } feedItems(first: 5) { ...FeedItems } } # 当用户点击第二页(或无限滚动下拉)时, # 只取下一批 feed items,而不要重新取回附属数据 query MainPageMoreFeedItems($cursor: Cursor!) { feedItems(first: 5, after: $cursor) { ...FeedItems } } # feed items 的数据需求被两个查询共享 fragment FeedItems on FeedItemsConnection { nodes { title description author { name avatar } } pageInfo { hasNextPage endCursor } }Fragment 的使用:fragment 的目的不是"消除重复代码",而是把数据需求与消费这些数据的应用元素(函数、React 组件等)对应起来。被渲染组件的 fragment 应该被合并进一个能发给服务器的单一查询中,确保组件所需的全部数据(且仅这些数据)被取回。
五、更多DEBUG环境变量
系统内部大量使用 DEBUG 命名空间,以下是文档整理出的几个常用项:
graphile-build:warn—— schema 构建期间发生的"可恢复"错误的详情,通常包含如何修复问题的提示;graphile-build:SchemaBuilder—— 用于理解 hook 的执行顺序,以及 hook 调用如何嵌套——对刚接触 graphile-build 插件开发的人很有价值;@dataplan/pg:PgExecutor—— 正在执行的 SQL 查询、其输入和结果的详情;@dataplan/pg:PgExecutor:explain—— 同上一项,但额外包含 EXPLAIN 结果。
多个命名空间用逗号连接后设置到DEBUG环境变量,再在同一终端启动 PostGraphile(或你的 Node.js 服务):
# Bash (Linux, macOS 等) export DEBUG="graphile-build:warn,@dataplan/pg:*" postgraphile # Windows 命令提示符 set DEBUG=graphile-build:warn,@dataplan/pg:* & postgraphile # Windows PowerShell $env:DEBUG = "graphile-build:warn,@dataplan/pg:*"; postgraphile注意@dataplan/pg:*是通配写法,会同时开启@dataplan/pg:PgExecutor与@dataplan/pg:PgExecutor:explain等所有子命名空间。文档同时提示,PgExecutor:explain的输出内容在 executor.ts 中被完整格式化,包括 SQL 文本、占位符、结果、NOTICE、耗时与 EXPLAIN,调试 SQL 时非常直观。
六、直接调试 PostGraphile 进程
如果你是插件作者、怀疑发现了 PostGraphile 的 bug,或者只是想看看内部到底怎么运转,可以用 Chrome 的 Node 调试工具直接调试进程——加断点、异常断下、单步执行都可以:
- 在 Chrome 中访问
chrome://inspect(出于安全原因这里不能提供可点击的超链接); - 选择Open dedicated DevTools for Node,会打开一个新的 DevTools 窗口——不要关掉它;
- 用 Node.js 以
--inspect模式直接启动你的服务或 PostGraphile,例如:
# 全局安装的 PostGraphile: node --inspect `which postgraphile` -c postgres://... # 或本地安装的 PostGraphile: node --inspect node_modules/.bin/postgraphile # 或者,如果你有自己的 Node.js 应用(server.js): node --inspect server.js连接成功后,你就可以在源码上打断点、观察变量、跟踪 Grafast的执行流程了。仓库中 Grafserv、Grafast、@dataplan/pg的源码都在grafast/目录下(例如 grafast/grafserv/src、grafast/grafast/src),可以直接作为调试时的参考实现。
小结:调试决策路线图
最后把文档的核心排查思路浓缩成一张决策清单,方便遇到问题时按图索骥:
- GraphQL 请求不对→ Chrome Network 面板确认请求 → Ruru 复现 →
maskError输出错误细节; - 错误来自数据库→ Ruru Explain(需
grafast.explain: true)或DEBUG="@dataplan/pg:PgExecutor:explain"查看 SQL 与 EXPLAIN; - Schema 多了东西→ 检查
pg_schemas配置 →npx graphile config print plugins确认PgRemoveExtensionResourcesPlugin/PgRBACPlugin是否启用 → 检查连接角色权限(\dp+)→npx graphile behavior debug验证 smart tags/behaviors; - Schema 缺了东西→ 对照缺失清单 → 补唯一约束/外键约束 → 补匹配索引;
- 性能差→ 查看生成 SQL → 排查 RLS 策略 → 检查分页与 fragment 使用 → 检查索引/函数/视图/物化视图/插件/复杂过滤器;
- 怀疑 PostGraphile 自身问题→
node --inspect+chrome://inspect断点调试。
相关参考文档:除本文档外,还可进一步阅读 required-knowledge.md(角色创建与 RLS 性能)、computed-columns、views.md 与 relations.md。
- 后端
- API网关
【免费下载链接】crystal
🔮 Graphile's Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!
相关推荐
PostGraphile 调试完全指南:从 GraphQL 请求到 SQL 执行的全链路排障
PostGraphile 调试完全指南:从 GraphQL 请求到 SQL 执行的全链路排障 导读 当应用出问题时,PostGraphile 提供了一整套由浅入
后端API网关PostGraphile v4 调试完全指南:从网络请求到 SQL 与 Node 源码的逐层排查
PostGraphile v4 调试完全指南:从网络请求到 SQL 与 Node 源码的逐层排查 导读 本文围绕 PostGraphile v4 的官方调试文档
后端API网关PostGraphile 数据库函数完全指南:从性能陷阱到 SQL 内联与 GraphQL 暴露
PostGraphile 数据库函数完全指南:从性能陷阱到 SQL 内联与 GraphQL 暴露 导读 :在 PostGraphile(Crystal Mono
后端API网关
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考