自省 自行 自醒

PG 18 让 NUMA 可观测、可感知

Word count: 2.6kReading time: 11 min
2026/03/05
loading

前言

在 18 的新版本中,包含了诸多惹眼特性,比如异步 I/O、BTREE Skip Scan、UUIDv7 等等,其实在 18 的新版中,还引入了一项很重要的特性 — NUMA 感知,一直以来,PG 就缺乏 NUMA 的可观测性,希望能看到 buffer 属于哪个 node、shared memory 分布、memory context 的 NUMA 信息等,PG 18 终于补足了这块拼图,让 NUMA 从玄学、从黑洞变为”可观测的工程问题”。

NUMA

Andres Freund 在 2024 年的 pgconf 上有一篇名为 NUMA vs PostgreSQL 的主题,介绍了 NUMA 的重要性以及未来的优化方向等。

作者开门见山就提到 Moore’s law is dead:CPU 不再靠单核变快,而是靠更多 core、更多 chiplet/tile,虽然吞吐提升了,但是延迟问题更糟了,尤其是跨 chiplet、跨 socket 的访问延迟变大:

  • 一个 socket 里可能有多个 chiplet / tile
  • 每个 tile 有自己的 core、L3、内存控制器
  • 不同 tile / socket 之间访问成本不同

image-20260305163258962

作者也量化了大致的性能影响,简而言之 —— NUMA 最怕的不是”偶尔访问远端内存”,而是高频共享热点、锁竞争、反复跨节点访问同一批共享元数据,而这些又是 PG 很容易出现的。

  • 本地内存基础延迟:约 80–140ns
  • 跨 socket 再增加:约 80–100ns
  • 跨 tile 再增加:约 30ns
  • 最糟糕的是:争用锁
  • 对哈希表等延迟敏感的结构来说,也比较糟糕
  • 吞吐也会下降
  • 如果数据已经在 L1/L2/L3 cache,差异则不太明显。

在 Linux 上,默认的访问策略是 Local node,真正分配发生在首次访问 (first touch),而不是 mmap() / malloc() 时,所以像 pg_prewarm() 这类操作会造成不平衡,也就是说,谁先 touch,页面就更可能落在谁所在的 NUMA 节点。

前面也提到,在 18 版本以前,**PG 最大的问题之一是数据库层面原生看不见 NUMA 的分布,既然看不见,那么就无法定位 NUMA 所带来的问题。**不过我们还是可以从操作系统层面进行观察,通过 /proc/$pid/numa_maps 看进程地址空间的 NUMA 分布,所以在 18 以前的版本,如果你怀疑某个 postgres 进程、shared memory、backend 有 NUMA 倾斜,可以先从这个文件入手。举个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-bash-4.2$ psql
psql (18.3)
Type "help" for help.

postgres=# select count(*) from pgbench_accounts ;
count
---------
1000000
(1 row)

postgres=# select pg_backend_pid();
pg_backend_pid
----------------
52155
(1 row)

[root@sdw19 ~]# cat /proc/52155/numa_maps
...
01631000 default heap anon=190 dirty=190 N1=190 kernelpagesize_kB=4
...
7f1f19ba5000 default file=/dev/zero\040(deleted) dirty=2888 mapmax=10 active=369 N0=415 N1=2473 kernelpagesize_kB=4
...
7f1f19961000 default file=/dev/shm/PostgreSQL.1556637348 dirty=7 active=2 N1=7 kernelpagesize_kB=4
  1. 第一行表示这一段堆内存的 190 页 全部落在 node1。

  2. 第二行代表共享内存也主要在 N1 (N0=415 N1=2473);同时 default 表示这表示该进程使用的是 Linux 默认 NUMA 策略

  3. 第三行说明该后端进程映射到的某个 PG 共享内存段,在它实际触碰到的页里,也全在 N1

因此,以上种种说明:

  • 进程调度偏向某个 node,52155 进程在执行查询时主要跑在 node1 的 CPU 上,那它分配/触碰到的匿名页就会偏向 N1。
  • 共享内存初始化/预热阶段的 first-touch 偏向某个 node:尤其是 shared_buffers 这类大块区域,启动/预热时在哪个 node 的 CPU 上触碰多,就偏到哪里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@sdw19 ~]# taskset -pc 52155
pid 52155's current affinity list: 0-63
[root@sdw19 ~]# numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54 56 58 60 62
node 0 size: 128383 MB
node 0 free: 47660 MB
node 1 cpus: 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 33 35 37 39 41 43 45 47 49 51 53 55 57 59 61 63
node 1 size: 128986 MB
node 1 free: 54482 MB
node distances:
node 0 1
0: 10 21
1: 21 10
[root@sdw19 ~]# ps -o pid,psr,comm -p 52155
PID PSR COMMAND
52155 35 postgres

通过 ps 我们可以看到,CPU 35 是奇数,根据 numactl -H 映射,其属于 NUMA node 1,所以我们可以看到大段 N1 明显更多(甚至 N1=190 一边倒的情况)。其次,Andres Freund 还提到,因为 first touch 机制,pg_prewarm()、CREATE INDEX、COPY 这类操作会导致大量内存落在一个节点。

到此,我们也能看到 NUMA 的大致危害了 —— 不仅仅是远端内存访问更慢,对于 PG 这种大量共享内存与共享元数据结构的数据库中,会放大为:共享内存分配倾斜、访问局部性变差、TLB/缓存效率降低,以及热点 buffer lock 的扩展性崩溃。而 Linux 的 first-touch 分配机制让 shared_buffers 等共享区域很容易在启动或预热阶段偏向某个 NUMA node,从而带来跨节点访问与延迟抖动。

好在 PG 18 终于可以观测到 NUMA 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
-bash-4.2$ psql
psql (18.3)
Type "help" for help.

postgres=# select count(*) from pgbench_accounts ;
count
---------
1000000
(1 row)

postgres=# select pg_backend_pid();
pg_backend_pid
----------------
52155
(1 row)

postgres=# select name, numa_node, size
from pg_shmem_allocations_numa
where name in ('Buffer Blocks','Buffer Descriptors','Shared Buffer Lookup Table',
'Buffer IO Condition Variables','Buffer Strategy Status')
order by name, numa_node;
name | numa_node | size
-------------------------------+-----------+-----------
Buffer Blocks | 0 | 7553024
Buffer Blocks | 1 | 126672896
Buffer Blocks | | 0
Buffer Descriptors | 0 | 1052672
Buffer Descriptors | 1 | 0
Buffer Descriptors | | 0
Buffer IO Condition Variables | 0 | 266240
Buffer IO Condition Variables | 1 | 0
Buffer IO Condition Variables | | 0
Buffer Strategy Status | 0 | 4096
Buffer Strategy Status | 1 | 0
Buffer Strategy Status | | 0
Shared Buffer Lookup Table | 0 | 8192
Shared Buffer Lookup Table | 1 | 0
Shared Buffer Lookup Table | | 0
(15 rows)

postgres=# with x as (
select numa_node, size::numeric as sz
from pg_shmem_allocations_numa
where name='Buffer Blocks'
),
s as (
select sum(sz) as total from x
)
select x.numa_node,
x.sz,
round(100 * x.sz / s.total, 2) as pct
from x cross join s
order by x.numa_node;
numa_node | sz | pct
-----------+-----------+-------
0 | 7553024 | 5.63
1 | 126672896 | 94.37
| 0 | 0.00
(3 rows)

通过 pg_shmem_allocations_numa 视图,我们可以很清楚看到各个节点上 Block 所占用的内存大小,大部分都集中在了 Node1 上面。

那如何解决呢?简单来说,就是让操作系统和数据库尽量不要按 NUMA 拓扑去分散调度/分配,而是把机器当成一个更平坦的内存系统来用。

  1. BIOS/固件层面的关闭 NUMA,指在 BIOS 里关闭某些 NUMA 暴露方式,让系统看到的内存拓扑更统一
  2. OS/数据库层面的关闭 NUMA,比如不让进程在多个 NUMA 节点间乱跑,用 numactl 把进程绑在固定节;或者用内存交错分配 (–interleave=all);以及关闭 Linux 的自动 NUMA 平衡 (numa_balancing)

如果我们不能从 BIOS 和 Linux 启动层彻底弱化 NUMA,那么至少可以通过关闭自动 NUMA 平衡、关闭 zone reclaim、并用 numactl --interleave=all 启动 PostgreSQL,来获得类似关闭 NUMA的效果:

  • vm.zone_reclaim_mode = 0,表示当 NUMA 系统某个 NODE 内存不足的时候,会从相邻的 NODE 分配内存,避免内核为了优先使用本地 NODE 内存,做过度回收,从而导致数据库性能抖动;并且 PG 的机制,是比较依赖于缓存的。
  • kernel.numa_balancing = 0,表示关闭 Linux 自动 NUMA 平衡,避免内核在后台扫描页、迁移页,减少额外开销和不确定性。
  • numactl interleave=all,顾名思义,用 numactl 启动 PG,让内存页在所有 NUMA 节点上交错分配。目的是避免 shared memory 被 first-touch 集中打到单个 NUMA 节点
  • 当然也可以使用 cpuset 的方式进行绑核,这是 Linux 提供的一种把进程/线程固定到指定 CPU 核心集合 (以及可选的内存 NUMA 节点) 里运行的隔离机制,通过把 PG 进程限定在指定 CPU 核心和 NUMA 内存节点内运行,减少跨 NUMA 远端访存与锁争用

让我们再次启动一下数据库,同时观察一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
-bash-4.2$ numactl --interleave=all pg_ctl start
waiting for server to start....2026-03-05 18:25:07.037 CST [42338] LOG: redirecting log output to logging collector process
2026-03-05 18:25:07.037 CST [42338] HINT: Future log output will appear in directory "log".
done
server started

postgres=# select count(*) from pgbench_accounts ;
count
---------
1000000
(1 row)

postgres=# select pg_backend_pid();
pg_backend_pid
----------------
43601
(1 row)

postgres=# select name, numa_node, size
from pg_shmem_allocations_numa
where name in ('Buffer Blocks','Buffer Descriptors','Shared Buffer Lookup Table',
'Buffer IO Condition Variables','Buffer Strategy Status')
order by name, numa_node;
name | numa_node | size
-------------------------------+-----------+----------
Buffer Blocks | 0 | 67112960
Buffer Blocks | 1 | 67112960
Buffer Blocks | | 0
Buffer Descriptors | 0 | 524288
Buffer Descriptors | 1 | 528384
Buffer Descriptors | | 0
Buffer IO Condition Variables | 0 | 135168
Buffer IO Condition Variables | 1 | 131072
Buffer IO Condition Variables | | 0
Buffer Strategy Status | 0 | 0
Buffer Strategy Status | 1 | 4096
Buffer Strategy Status | | 0
Shared Buffer Lookup Table | 0 | 4096
Shared Buffer Lookup Table | 1 | 4096
Shared Buffer Lookup Table | | 0
(15 rows)

postgres=# with x as (
select numa_node, size::numeric as sz
from pg_shmem_allocations_numa
where name='Buffer Blocks'
),
s as (
select sum(sz) as total from x
)
select x.numa_node,
x.sz,
round(100 * x.sz / s.total, 2) as pct
from x cross join s
order by x.numa_node;
numa_node | sz | pct
-----------+----------+-------
0 | 67112960 | 50.00
1 | 67112960 | 50.00
| 0 | 0.00
(3 rows)

这一次各位便可以观察到 —— 同样的操作,内存均匀多了!根据 Andres Freund 实验结果:

  • 默认情况下平均延迟 382.700 ms,标准差 68.596 ms
  • interleave=all:平均延迟 352.581 ms,标准差 7.276 ms。

说明不只是平均延迟变好,抖动大幅下降,NUMA 会导致性能很不稳定!

小结

如果我们在 PG 上观察到下面这些现象,就应该立刻把 NUMA 当成优先排查方向之一:

  1. shared_buffers (尤其是 Buffer Blocks) 在 NUMA 节点之间分配明显偏斜:例如某个 NODE 占了绝大多数内存页,另一个 NODE 几乎没有.
  2. OLTP 高并发下 TPS 不随核数增长,甚至扩核反降 — 多 NUMA 节点加入后,热点页与共享元数据 (buffer lock、procarray 等) 的竞争成本被放大,导致扩展性崩溃
  3. 延迟抖动明显:平均延迟还行,但 P95/P99 或标准差很难看 — 部分请求被远端访存、锁竞争或内核回收拖慢
  4. sys% CPU 占比异常偏高,常见原因包括:自旋锁/争用导致更多内核调度开销、NUMA balancing 迁页扫描、内存回收/压缩 (reclaim/compaction) 等
  5. 明明可用内存很多,却出现明显的 swap 使用/增长

参考

NUMA vs PostgreSQL

DB吐槽大会,第81期 - PG 未针对 NUMA 优化

CATALOG
  1. 1. 前言
  2. 2. NUMA
  3. 3. 小结
  4. 4. 参考