← 返回信息流
AI 资讯Hacker News·3 天前

PostgreSQL 的 preempt_none 参数被废弃,多数用户无影响

原标题:Preempt_none Is Dead; Your Postgres Probably Doesn't Care

速览

PostgreSQL 在最新版本中移除了 preempt_none 参数,该参数用于控制查询抢占行为,但实际使用较少。移除后,系统将默认采用更高效的抢占策略。绝大多数用户无需调整配置,此次变更对数据库性能影响极小。

AI 深度解读

背景

Linux 7.0 内核引入了一项重要变更:通过 Peter Zijlstra 的抢占清理系列补丁,在 arm64、x86、powerpc、riscv、s390 和 loongarch 架构上彻底移除了 PREEMPT_NONE 抢占模式。此前,内核提供三种抢占模式:

  • PREEMPT_NONE:内核几乎从不中断正在运行的用户空间线程,适合批处理友好的服务器工作负载,上下文切换开销最小。
  • PREEMPT_FULL:内核可在几乎所有安全点中断用户空间,低延迟但上下文切换频繁,历史上是桌面默认。
  • PREEMPT_LAZY:一种较新的折中方案,调度器可以中断,但会尽可能等待“自然”边界。

Linux 7.0 后,用户只能选择 PREEMPT_FULLPREEMPT_LAZY,多数发行版默认调整为 PREEMPT_LAZY。对于绝大多数工作负载,这一变化并无大碍,但 AWS 的 Salvatore Dipietro 发布了一项基准测试,显示在特定配置下 PostgreSQL 吞吐量降至 Linux 6.x 的 0.51 倍,引发社区广泛讨论。

核心内容

基准测试设置与结果

Salvatore Dipietro 在 LKML 上发布的测试配置如下:

  • 实例:m8g.24xlarge(96 vCPU Graviton4,运行 Amazon Linux 2023)
  • 内核:next-20260331(linux-next 快照),分别测试了包含和回退提交 7dadeaa6e851(即移除 PREEMPT_NONE 的变更)的版本
  • 存储:12× 1TB io2 卷(每卷 32000 IOPS),RAID0,XFS
  • PostgreSQL 17,pgbench simple-update 模式,1024 客户端,96 线程,预编译协议,比例因子 8470,fillfactor=90,每次运行 1200 秒

三次运行平均结果:Linux 7.0 下吞吐量仅为 Linux 6.x(即回退补丁后)的 0.51 倍。perf 分析显示,超过一半的 CPU 时间集中在单个用户空间自旋锁调用链上:

strategy_get_buffer -> SpinLockAcquire(buffer_strategy_lock)

性能回归的直接原因:自旋锁内的次缺页错误

StrategyGetBuffer 是 PostgreSQL 后端在需要缓冲区但尚未持有时的入口函数。它通过一个自旋锁 StrategyControl->buffer_strategy_lock 序列化两类操作:

  1. 从空闲链表弹出缓冲区
  2. 运行时钟扫描(clock sweep)推进 nextVictimBuffer 并返回候选缓冲区

在 96 vCPU、1024 客户端的极端并发下,任何序列化点都会变热。但该自旋锁有一个关键属性:受保护区域内可能触发次缺页错误(minor page fault)。

具体机制:当 huge_pages=off 时,PostgreSQL 的共享内存以普通 4KB 页映射。120 GB shared_buffers 对应约 3100 万个页表项(PTE)。每个页在首次访问时都会触发一次次缺页错误——虚拟内存子系统需要建立物理页并安装 PTE。次缺页错误耗时微秒级,但在自旋锁上下文中这几乎是“永恒”。1200 秒的基准测试中,由于 pgbench 均匀随机访问 127 GB pgbench_accounts 表,新页面会持续进入工作集,导致缺页不断发生。

当一个后端持有 buffer_strategy_lock 时,若它需要从空闲链表弹出缓冲区或推进时钟扫描,可能访问尚未触发的共享内存页面,从而触发次缺页错误。此时锁持有者卡在内核缺页处理程序中,而其他数十甚至数百个后端在用户空间忙等旋转,浪费 CPU。

PREEMPT_LAZY 的放大效应

PREEMPT_NONE 不会在持有自旋锁的后端等待锁释放时抢占用户空间线程,PREEMPT_LAZY 则可能。当调度器决定抢占,自旋锁的持有时间会从“微秒级缺页服务”膨胀为“缺页服务时间 + 调度器重新控制该后端的时间”。等待旋转的队列变长,浪费的 CPU 时间呈复合增长。

因此,内核抢占模式变更并非创造了 bug,而是将既有 bug——在 4KB 页映射加上 100+ GB 共享内存的极端并行场景下,于自旋锁内触发缺页——放大了约一倍。

巨页为何消除问题

使用巨页(huge pages)后,同一 120 GB shared_buffers 的映射方式变为:

  • 4KB 页:约 3100 万次缺页
  • 2MB 巨页:约 6.1 万次缺页
  • 1GB 巨页:约 120 次缺页(取决于硬件支持)

巨页有两个效果:

  1. 首次访问时的缺页数量骤降——内核通常在 mmap 时通过 MAP_HUGETLB 一次性映射整个巨页,因此冷启动缺页风暴消失。
  2. TLB 压力降低 4~6 个数量级,使缓冲区管理器的热路径在 TLB 中保持命中,而非频繁抖落。

Andres Freund 复现时发现:huge_pages=on 或透明巨页(THP)正常工作的情况下,回归不出现;huge_pages=off 时回归出现。变量不是内核版本,而是共享内存映射方式。Dipietro 的补丁建议恢复 PREEMPT_NONE 为默认,Freund 的回应本质上是:内核正在执行你的配置;你的 shared_buffers 映射方式错了。

关键要点

  • 回归真实但范围极窄:Linux 7.0 移除 PREEMPT_NONE 仅在极端并发(96 vCPU + 1024 客户端)、巨大共享内存(120 GB)、4KB 小页映射、且工作集持续引入新页面的特定配置下,导致 PostgreSQL 吞吐量腰斩。绝大多数用户不会遇到。
  • 根本原因不在内核:性能瓶颈的根源是自旋锁内触发次缺页错误,这属于应用程序配置问题(小页映射巨量共享内存)。内核抢占模式更改只是放大了既有问题。
  • 巨页是唯一解决方案:使用 huge_pages=on(或系统级巨页/透明巨页)可将缺页数量从数千万降至数万,并消除 TLB 抖动,彻底避免该回归。任何有类似规模共享内存的数据库都应启用巨页。
  • 对内核升级的合理态度:不需要因该基准测试就推迟内核升级。该测试暴露的是特定配置下的性能路径,而非内核损坏。对于日常 PostgreSQL 服务器,升级到 Linux 7.0 并保持默认 PREEMPT_LAZY 是安全的,前提是正确配置巨页。
  • Linux 7.0 变更的工程意义PREEMPT_LAZY 在绝大多数工作负载下接近 PREEMPT_NONE 的行为,同时为实时响应提供了更好的支持。移除 PREEMPT_NONE 简化了内核,但暴露了部分用户空间代码对调度器敏感的错误假设。

意义与影响

该事件对数据库运维、内核开发和性能工程均有启示:

  • 数据库配置的最佳实践:任何使用巨大共享内存的数据库(PostgreSQL、MySQL、Oracle 等)都应积极采用巨页。这不仅是性能优化,更是避免意外回归的关键。shared_buffers 超过数十 GB 时,巨页带来的 TLB 压力和缺页开销下降是数量级的。
  • 性能调试的范式:当遇到内核升级导致性能下降时,不应立即归咎于内核。应系统分析热点路径、锁竞争、内存访问模式。本例中最终定位到自旋锁内缺页,显示了系统级分析(结合 perf、内核配置、应用程序内存
查看原文 →thebuild.com