把 PostgreSQL JIT 生成的 LLVM 代码调试清楚:三个 GUC 的实战用法

2026-08-18 37 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:7 分钟

PostgreSQL 的 JIT 不只是一个“打开或关闭”的性能开关。需要分析生成代码时,可以通过 jit_dump_bitcode 检查 LLVM bitcode,通过 jit_debugging_support 让 JIT 编译函数能够被 GDB 识别,再用 jit_profiling_support 把这些函数接入 perf 等性能分析工具。三个 GUC 分别解决“生成了什么”“如何调试”和“时间花在哪里”这几类问题。

三个 GUC 分别解决什么

jit_dump_bitcode 用于导出 PostgreSQL 为 JIT 编译生成的 LLVM bitcode。bitcode 是 LLVM 的中间表示,适合确认表达式、函数或查询片段最终交给 LLVM 的内容。它更偏向离线检查和编译链路分析。

jit_debugging_support 用于支持调试 JIT 编译函数。启用后,调试器可以获得与这些函数相关的调试信息,从而让 GDB 更容易定位 JIT 代码,而不是只看到一段没有符号的动态机器码。

jit_profiling_support 则面向性能剖析工具。它让 JIT 生成的函数能够被 perf 等工具识别和统计,便于区分 SQL 执行时间究竟消耗在解释执行、JIT 生成代码,还是其他 PostgreSQL 内部函数上。

这些设置通常用于开发、测试和性能调查环境。生产环境中长期开启调试信息或 bitcode 导出,可能增加文件、权限和性能方面的管理成本,因此应当按会话或临时实例启用。

一个可复制的检查流程

下面的例子假设 PostgreSQL 已经安装并启用了 JIT,且当前用户有执行测试查询和修改会话参数的权限。先创建一个临时测试表,再打开查询计划中的 JIT 信息:

CREATE TEMP TABLE jit_demo AS
SELECT i AS id, md5(i::text) AS payload
FROM generate_series(1, 100000) AS s(i);

ANALYZE jit_demo;

SET jit = on;
SET jit_above_cost = 0;
SET jit_inline_above_cost = 0;
SET jit_optimize_above_cost = 0;

EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
SELECT sum(length(payload))
FROM jit_demo
WHERE id % 7 = 0;

jit_above_cost = 0 只是为了在实验中尽量触发 JIT,不代表适合所有生产查询。实际应用应根据执行计划、查询延迟和 CPU 使用情况设置阈值。EXPLAIN 输出中的 JIT 部分可以帮助确认本次执行是否真的使用了 JIT。

接着,可以在同一个会话中打开 bitcode 导出:

SET jit_dump_bitcode = on;

EXPLAIN (ANALYZE, SETTINGS)
SELECT sum(length(payload))
FROM jit_demo
WHERE id % 7 = 0;

不同 PostgreSQL 版本、打包方式和运行环境对 bitcode 文件的保存位置与命名细节可能存在差异。执行查询后,应在 PostgreSQL 进程的工作目录、相关临时目录或实例配置指定的位置检查新生成的文件。可以先确认当前实例的基本信息:

SELECT version();
SHOW data_directory;
SHOW config_file;
SHOW jit_dump_bitcode;

如果参数不存在,通常意味着当前 PostgreSQL 版本或构建没有提供该 GUC;不要把它直接写入全局配置文件,先确认目标版本的文档和安装包能力。

用 GDB 观察 JIT 函数

调试 JIT 代码前,需要在目标会话中启用调试支持:

SET jit = on;
SET jit_above_cost = 0;
SET jit_debugging_support = on;

SELECT sum(length(payload))
FROM jit_demo
WHERE id % 7 = 0;

然后从另一个终端找到对应的 PostgreSQL 后端进程,并附加 GDB:

ps -ef | grep '[p]ostgres'

gdb -p <backend_pid>
(gdb) info functions
(gdb) continue

<backend_pid> 替换为实际执行查询的后端 PID。为了稳定复现,可以让查询会话执行一个持续时间更长的测试查询,或者在开发环境中设置断点后再运行查询。GDB 是否能显示完整函数名、源位置和可用的调试信息,取决于 PostgreSQL、LLVM 以及相关组件的构建方式。这个开关并不会把 JIT 代码变成普通的静态 PostgreSQL 源码函数,调试体验仍然受编译器和版本影响。

用 perf 分析 JIT 代码

如果问题是“JIT 到底花了多少 CPU”,可以在测试实例上启用 profiling 支持:

SET jit = on;
SET jit_above_cost = 0;
SET jit_profiling_support = on;

SELECT sum(length(payload))
FROM jit_demo
WHERE id % 7 = 0;

然后用 Linux perf 采集对应后端进程:

sudo perf record -g -p <backend_pid> -- sleep 15
sudo perf report

也可以先启动查询,再在查询运行期间采样。报告中若能识别 JIT 生成的函数,就可以把它们与 PostgreSQL 的其他符号放在一起比较。若报告仍然只有地址或信息不完整,需要检查内核权限、perf 配置、PostgreSQL/LLVM 构建选项以及目标版本对 profiling 支持的实现情况。

该怎样选择和收敛设置

可以按下面的顺序排查:

  1. 想知道 LLVM 实际生成了什么,使用 jit_dump_bitcode,并保存查询、版本和执行计划作为实验记录。
  2. 想在运行时观察 JIT 函数,使用 jit_debugging_support,配合 GDB 和一个可重复的测试查询。
  3. 想判断 JIT 代码的 CPU 消耗,使用 jit_profiling_support,配合 perf recordperf report
  4. 调查结束后用 RESET 恢复会话设置,避免把实验参数带入后续请求。
RESET jit_dump_bitcode;
RESET jit_debugging_support;
RESET jit_profiling_support;
RESET jit_above_cost;
RESET jit_inline_above_cost;
RESET jit_optimize_above_cost;

这三个 GUC 的价值不在于让所有查询都更快,而在于把 JIT 这一层变成可观察、可验证的执行路径。先用 bitcode 确认生成内容,再用 GDB 或 perf 验证运行时行为,通常比只比较“开启 JIT”和“关闭 JIT”的总耗时更容易找到真正的瓶颈。测试时还应记录 PostgreSQL 版本、LLVM 版本、硬件、数据规模和查询计划,因为这些因素都会影响结果。


相关推荐