原文地址:https://pganalyze.com/blog/5mins-postgres-19-reduced-timing-overhead-explain-analyze
等待 Postgres 19:使用 RDTSC 降低 EXPLAIN ANALYZE 的计时开销
卢卡斯·菲特尔 | 卢卡斯·菲特尔 | 2026年4月11日
在今天的"5分钟 Postgres"第122集中,我们将讨论即将发布的 Postgres 19 版本,以及 Postgres instrumentation 处理中的一项更改如何通过使用 RDTSC 指令降低 EXPLAIN ANALYZE 中计时测量的开销,以及为什么这将使得在更多工作负载中可以启用 auto_explain.log_timing。
我们将深入探讨最近提交的一项更改,这项更改由我(卢卡斯)与安德烈斯·弗洛恩德和大卫·盖尔共同完成。详情请见下方的完整记录和示例。
分享本集: 点击此处 在 LinkedIn 上分享本集。欢迎订阅我们的通讯并订阅我们的 YouTube 频道。
目录
- 慢速计时测量的问题
- RDTSC 与 RDTSCP
- 新的
timing_clock_sourcePostgres 设置 - Postgres 19 开发分支的现场演示
- 我们在本集"5分钟 Postgres"中讨论的内容
- 完整记录
完整记录
欢迎回到"5分钟 Postgres"!今天我们要讨论的是即将发布的 Postgres 19 版本中的一项更改,它将降低 EXPLAIN ANALYZE 的计时开销。
这是我与安德烈斯·弗洛恩德和大卫·盖尔共同贡献的一项更改,实际上我们已经为此工作了几年。但在这个版本中,我们基本上坐下来,真正解决了使这项工作可行的所有微小细节。这项更改最近已提交到 Postgres 19 开发分支,需要明确的是,如果发现任何问题,它仍可能在最终发布中被移除,但现在,我认为它有很大机会保留下来。
Postgres 19 将于 9 月或 10 月发布,功能冻结刚刚完成,测试版将于今年 5 月左右发布。现在,让我向你展示更多关于这项更改的内容。
慢速计时测量的问题
早在 2020 年,安德烈斯·弗洛恩德就启动了一个邮件列表讨论,他在其中指出,当你在查询上运行 EXPLAIN ANALYZE 时,它看起来比实际运行速度慢得多。所以在这个例子中,安德烈斯创建了一个包含 5000 万行的表:
sql
CREATE TABLE lotsarows(key int not null);
INSERT INTO lotsarows SELECT generate_series(1, 50000000);
VACUUM FREEZE lotsarows;
非常简单的表,然后他在该表上运行了一个 COUNT(*):
sql
SELECT count(*) FROM lotsarows;
如果我在没有 EXPLAIN 的情况下运行 COUNT(*),运行时间约为 1900 毫秒。如果我运行带有 TIMING OFF(在当时那个版本中也带有 BUFFERS OFF)的 EXPLAIN ANALYZE,运行时间约为 2300 毫秒。现在,如果我打开 TIMING ON,运行时间比实际时间增加了一倍多。我的查询不再是 1900 毫秒,而是需要 4200 毫秒:
text
-- 最佳三次之一:
SELECT count(*) FROM lotsarows;
Time: 1923.394 ms (00:01.923)
-- 最佳三次之一:
EXPLAIN (ANALYZE, TIMING OFF) SELECT count(*) FROM lotsarows;
Time: 2319.830 ms (00:02.320)
-- 最佳三次之一:
EXPLAIN (ANALYZE, TIMING ON) SELECT count(*) FROM lotsarows;
Time: 4202.649 ms (00:04.203)
首先,这是一个问题,因为它歪曲了我的实际性能。如果我使用 EXPLAIN ANALYZE 进行测试,却没有意识到计时有开销,我基本上会认为我的查询比实际要慢。另一个问题是,如果你运行 auto_explain,通常我们建议人们关闭 log_timing。例如,在 pganalyze 的安装说明中,我们喜欢推荐人们使用 auto_explain,但今天我们总是告诉人们关闭计时,因为我们认为在大多数生产系统上,在不更了解你的工作负载的情况下使用它是不安全的。
如果我们更详细地研究这个问题,安德烈斯基本上做了一个性能分析,他查看了开销来自哪里:
text
- 95.49% 0.00% postgres postgres [.] agg_retrieve_direct (inlined)
- agg_retrieve_direct (inlined)
- 79.27% fetch_input_tuple
- ExecProcNode (inlined)
- 75.72% ExecProcNodeInstr
+ 25.22% SeqNext
- 21.74% InstrStopNode
+ 17.80% __GI___clock_gettime (inlined)
- 21.44% InstrStartNode
+ 19.23% __GI___clock_gettime (inlined)
+ 4.06% ExecScan
+ 13.09% advance_aggregates (inlined)
1.06% MemoryContextReset
RDTSC 与 RDTSCP
首先,在该性能分析中,我们看到了 InstrStartNode 和 InstrStopNode 调用。这些基本上是当 instrumentation 开启时,Postgres 添加的调用,也就是当我运行 EXPLAIN ANALYZE 时,我们可以看到大部分时间都花在了 clock_gettime 函数上。在现代 Linux 系统上,这实际上不是一个系统调用。相反,它直接调用 RDTSCP。RDTSCP 基本上是 CPU 上的一条特殊指令,它获取所谓的时间戳计数器。
可以把时间戳计数器看作一个持续增长的值,它基本上在计数周期,但它以一种不受功率水平变化或其他可能导致其偏差的问题影响的方式计数。所以它实际上非常可靠。现在的问题是,RDTSCP 所做的是等待所有先前的指令完成------这里说的指令是指 CPU 指令。所以基本上发生的情况是,计时本身不仅仅是获取时间,它还阻止了其他活动的发生。
它阻止 CPU 有效地并行运行指令。然而,有一条不同的指令,叫做不带 P 的 RDTSC。这条指令基本上没有这种对其他并发指令的阻塞。所以当使用这条指令时,它会显著降低计时的性能开销。
在安德烈斯当时运行的特定示例中,查询不是花费 4200 毫秒,而实际上只花了 2600 毫秒:
text
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ QUERY PLAN │
├───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Aggregate (cost=846239.20..846239.21 rows=1 width=8) (actual time=2610.235..2610.235 rows=1 loops=1) │
│ -> Seq Scan on lotsarows (cost=0.00..721239.16 rows=50000016 width=0) (actual time=0.006..1512.886 rows=50000000 loops=1) │
│ Planning Time: 0.028 ms │
│ Execution Time: 2610.256 ms │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
(4 rows)
Time: 2610.589 ms (00:02.611)
这在当时主要是一个原型。所以,许多复杂性,以及为什么花了这么长时间才实现的部分原因是,我们需要确保这能在 Postgres 所使用的各种不同系统上工作。
新的 timing_clock_source Postgres 设置
根据邮件列表上的讨论,我们最终添加的事情之一是控制是否使用这个新计时方法的新设置。因此,通过新的 timing_clock_source 设置,你基本上可以控制在足够现代、拥有正确指令的 x86-64 CPU 上是否自动使用 TSC 时钟源。你可以强制使用使用系统时钟的旧方法,或者你可以显式设置 TSC 时钟源。
现在在 Postgres 中,我们基本上分成了两种不同的用例。对于像 EXPLAIN ANALYZE 这样的场景,我们不一定非常关心非常短、极其精确的测量,它更关乎累积时间,我们使用 RDTSC 指令;而在其他场景中,我们关心更高的精度,并且运行时仍然很短,我们使用开销更高的 RDTSCP 指令。现在有很多支持代码使这在不同的环境中工作,如果你对其工作原理感兴趣,可以查看 instr_time.c 文件。
Postgres 19 开发分支的现场演示
我想向你展示一个真实的例子,说明这项改进在 19 分支中现在是什么样子。这里我有一个 SSH 客户端,因为我现在的机器实际上是一台 MacBook。这个初始版本只会专注于在 x86-64 架构上实现快速计时。ARM 有一条类似的指令,但 ARM 机器还存在一些未解决的问题。所以现在我通过 SSH 连接到另一台机器。这台机器就在我旁边,就是这个小小的 Framework Desktop,但它是一台 x86 机器。
现在我可以做的是,我已经构建好了我的 Postgres 分支。我首先要运行 pg_test_timing 工具,它基本上测量计时的开销。这里我们得到三种不同的测量结果:
text
System clock source: clock_gettime (CLOCK_MONOTONIC)
Average loop time including overhead: 18.80 ns
Histogram of timing durations:
<= ns % of total running % count
0 0.0000 0.0000 0
1 0.0000 0.0000 0
3 0.0000 0.0000 0
7 0.0000 0.0000 0
15 12.7533 12.7533 20353931
31 87.2357 99.9890 139225930
...
Clock source: RDTSCP
Average loop time including overhead: 16.94 ns
Histogram of timing durations:
<= ns % of total running % count
0 0.0000 0.0000 0
1 0.0000 0.0000 0
3 0.0000 0.0000 0
7 0.0000 0.0000 0
15 31.1807 31.1807 55204578
31 68.8159 99.9966 121836600
...
Fast clock source: RDTSC
Average loop time including overhead: 11.69 ns
Histogram of timing durations:
<= ns % of total running % count
0 0.0000 0.0000 0
1 0.0000 0.0000 0
3 0.0000 0.0000 0
7 0.0000 0.0000 0
15 83.5188 83.5188 214321443
31 16.4789 99.9977 42287217
...
TSC frequency in use: 2993629 kHz
TSC frequency from calibration: 2994357 kHz
TSC clock source will be used by default, unless timing_clock_source is set to 'system'.
我们有内置的时钟源 clock_gettime。获取一次时间测量需要 18 纳秒。然后我们检查 RDTSCP,它会阻塞乱序指令。这需要 16.9 纳秒。然后如果我们使用 RDTSC 运行,需要 11.6 纳秒。显然,RDTSC 在这里开销更小,在这个测试计时程序中我得到了 50% 的收益。我还看到了使用了哪个频率,然后我也看到了这个新的时钟源是否会被默认使用。如果我不想使用它,我必须显式地将 timing_clock_source 设置为 system。
顺便说一下,这样做唯一合理的原因是,如果由于某种原因你的 TSC 以某种方式被模拟,导致计时测量不稳定。那么 timing_clock_source = system 可能会为你提供那些稳定的测量结果。
现在我可以运行一个 psql 客户端,向你展示真实的例子。我也有安德烈斯作为例子创建的那个表。首先,我打开 \timing。这是在 psql 端,它会给我运行时间。现在我在做一个 SELECT COUNT(*):
text
postgres=# SELECT count(*) FROM lotsarows;
count
----------
50000000
(1 row)
Time: 268.466 ms
这是一台更现代的机器,所以同样是 5000 万行,但运行得稍快一些。所以我这里的运行时间大约是 260 - 270 毫秒。
如果我使用 EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) 运行,让我们从这里开始。我没有做很多额外的工作。我只是在计算返回了多少行:
text
postgres=# EXPLAIN (ANALYZE, TIMING OFF, BUFFERS OFF) SELECT count(*) FROM lotsarows;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=482655.97..482655.98 rows=1 width=8) (actual rows=1.00 loops=1)
-> Gather (cost=482655.75..482655.96 rows=2 width=8) (actual rows=3.00 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=481655.75..481655.76 rows=1 width=8) (actual rows=1.00 loops=3)
-> Parallel Seq Scan on lotsarows (cost=0.00..429572.40 rows=20833340 width=0) (actual rows=16666666.67 loops=3)
Planning Time: 0.174 ms
Execution Time: 297.043 ms
(8 rows)
Time: 297.535 ms
这相当简单。
然后,如果我现在打开 TIMING ON,这是使用 TSC 时钟源,我得到大约 350 毫秒的测量结果:
text
postgres=# EXPLAIN (ANALYZE, TIMING ON, BUFFERS OFF) SELECT count(*) FROM lotsarows;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=482655.97..482655.98 rows=1 width=8) (actual time=349.687..351.719 rows=1.00 loops=1)
-> Gather (cost=482655.75..482655.96 rows=2 width=8) (actual time=349.606..351.709 rows=3.00 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=481655.75..481655.76 rows=1 width=8) (actual time=347.932..347.933 rows=1.00 loops=3)
-> Parallel Seq Scan on lotsarows (cost=0.00..429572.40 rows=20833340 width=0) (actual time=0.149..201.918 rows=16666666.67 loops=3)
Planning Time: 0.186 ms
Execution Time: 351.773 ms
(8 rows)
Time: 352.171 ms
我仍然看到,大约 20% - 25% 的开销。所以它不是没有开销的,但比使用系统时钟源要好得多。
如果我执行 SET timing_clock_source = system,然后再次计时,你会看到巨大的差异:
text
SET timing_clock_source = 'system';
EXPLAIN (ANALYZE, TIMING ON, BUFFERS OFF) SELECT count(*) FROM lotsarows;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=482655.97..482655.98 rows=1 width=8) (actual time=799.624..801.496 rows=1.00 loops=1)
-> Gather (cost=482655.75..482655.96 rows=2 width=8) (actual time=799.535..801.488 rows=3.00 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=481655.75..481655.76 rows=1 width=8) (actual time=797.885..797.887 rows=1.00 loops=3)
-> Parallel Seq Scan on lotsarows (cost=0.00..429572.40 rows=20833340 width=0) (actual time=0.073..417.005 rows=16666666.67 loops=3)
Planning Time: 0.115 ms
Execution Time: 801.529 ms
(8 rows)
Time: 801.979 ms
为了清晰起见,如果我仅运行一个常规的 SELECT count(*),实际查询需要 260 毫秒:
text
postgres=# SELECT count(*) FROM lotsarows;
count
----------
50000000
(1 row)
Time: 263.824 ms
使用旧的计时时钟源,我得到的运行时间是 800 毫秒。而使用新的 TSC 时钟源,我得到 355 毫秒:
text
SET timing_clock_source = 'tsc';
EXPLAIN (ANALYZE, TIMING ON, BUFFERS OFF) SELECT count(*) FROM lotsarows;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=482655.97..482655.98 rows=1 width=8) (actual time=353.401..355.238 rows=1.00 loops=1)
-> Gather (cost=482655.75..482655.96 rows=2 width=8) (actual time=353.292..355.229 rows=3.00 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=481655.75..481655.76 rows=1 width=8) (actual time=351.081..351.082 rows=1.00 loops=3)
-> Parallel Seq Scan on lotsarows (cost=0.00..429572.40 rows=20833340 width=0) (actual time=0.131..200.584 rows=16666666.67 loops=3)
Planning Time: 0.150 ms
Execution Time: 355.291 ms
(8 rows)
Time: 355.690 ms
这是一个巨大的差异,我认为这也让我觉得,在许多系统上,我会很放心地使用开启了 log_timing 的 auto_explain,因为大多数查询都没有这么极端。需要明确的是,许多现实世界的查询在这些 instrumentation 开始和停止函数上的重复要少得多。
以前你平均会看到 5-10% 的开销,现在你可能平均会看到 2-3% 的开销,对于许多系统来说,这是在 auto_explain 中获得完整 instrumentation 数据的一个很好的权衡。
还有许多其他新特性即将到来,在接下来的节目中会听到更多相关信息。
我希望你从"5分钟 Postgres"第 122 集中学到了一些新东西。欢迎订阅我们的 YouTube 频道,订阅我们的通讯,或在 LinkedIn 上关注我们,以获取新剧集的更新!
我们在本集"5分钟 Postgres"中讨论的内容
- Postgres 19 提交 - instrumentation: 在 x86-64 上使用时间戳计数器以降低开销
- Postgres pgsql-hackers 邮件列表讨论:使用 rdtsc 降低 EXPLAIN ANALYZE 的计时开销?
- 新的
timing_clock_sourcePostgres 设置 instr_time.c中的计时 instrumentation- pganalyze 推荐的
auto_explain设置