Hotdry.

Article

Linux 7.0 调度器抢占回归:PostgreSQL 事务吞吐下降 50% 的根因与应对

分析 Linux 7.0 调度器抢占模式变更对 PostgreSQL 事务吞吐的影响,定位 PREEMPT_VOLUNTARY 与内核版本兼容性问题的根因,并给出可落地的参数配置与监控要点。

2026-04-29systems

2026 年 4 月初,AWS 工程师 Salvatore Dipietro 在 Linux 内核邮件列表发布了一项紧急 patch,原因是在 96 核 Graviton4 机器上运行 PostgreSQL 时,Linux 7.0 的事务吞吐量仅为 Linux 6.x 的一半。这一性能回归并非硬件或数据库配置变更引起,而是内核调度器抢占模式变化导致的典型案例。本文将深入剖析其技术根因,并给出生产环境可落地的应对方案。

问题现象与性能量化

在标准的 pgbench 基准测试中(scale factor 8470,约 8.47 亿行数据,1024 客户端,96 线程),两代内核的表现差异触目惊心:Linux 6.x 达到 98565 TPS(每秒事务数),而 Linux 7.0 仅为 50751 TPS,吞吐量几乎腰斩。使用 perf 工具进行热点分析后发现,55% 的 CPU 时间消耗在单个函数 s_lock 中,这是 PostgreSQL 缓冲区管理器的自旋锁实现。

这一现象在高并发写入场景下尤为突出。当大量后端进程同时竞争共享缓冲区池时,锁 contention 成为性能瓶颈的源头。但关键问题在于:为何 Linux 6.x 与 7.0 之间会出现如此巨大的差异?

PREEMPT_NONE 的移除与调度器变更

在 Linux 7.0 之前,内核提供三种抢占模式:PREEMPT_NONE(几乎不主动抢占,适用于服务器吞吐量场景)、PREEMPT_FULL(可随时抢占,适用于桌面低延迟场景)以及 Linux 6.12 引入的 PREEMPT_LAZY(在两者之间寻求平衡)。Linux 7.0 的关键变更是:在现代 CPU 架构上移除了 PREEMPT_NONE 选项,默认配置转向 PREEMPT_LAZY

这一变更的初衷是好的 ——PREEMPT_LAZY 设计上试图在不显著增加上下文切换开销的前提下提供更好的响应性。然而,对于 PostgreSQL 这类依赖短临界区自旋锁的数据库软件,抢占行为的微妙变化会导致灾难性后果。

根因分析:自旋锁与页故障的交互

PostgreSQL 的共享缓冲区池(shared_buffers)是性能的核心。默认配置下,每个数据页大小为 8KB,而底层 Linux 内存页为 4KB。在 Dipietro 的测试中,120GB 的 shared_buffers 意味着约 3100 万个潜在的 4KB 内存页。当后端进程首次访问这些虚拟内存区域时,会触发 minor page fault(轻微页故障),内核需要分配物理页面并建立映射。

问题出在 StrategyGetBuffer 函数获取自旋锁后的行为。当持有锁的后端进程在临界区内触发页故障时,它必须进入内核态处理映射。此时,调度器的行为决定了性能走向:

  • PREEMPT_NONE 模式:持有锁的进程极少被调度走,页故障处理完成后迅速释放锁,其他等待进程只需等待故障处理的时间。
  • PREEMPT_LAZY 模式:调度器可能在进程处理页故障的任意时刻将其抢走,导致锁持有时间从 “故障处理时长” 扩展为 “故障处理时长加上调度延迟”。每个等待该锁的后端进程都在自旋等待,这段时间被成倍放大。

在 96 核机器上,假设有数十个后端同时竞争该锁,调度延迟乘以等待进程数量的 CPU 浪费是惊人的。这解释了为何 55% 的 CPU 周期消耗在自旋锁等待上。

可落地参数与监控清单

针对这一回归,生产环境可从以下三个层面进行应对:

内核层面

检查当前内核的抢占模式配置。可以通过 /boot/config-$(uname -r)cat /proc/cmdline 查看启动参数。若发行版提供 PREEMPT_NONE 的回退选项,可考虑在 boot 参数中强制指定。部分云厂商已提供针对该问题的内核补丁。

PostgreSQL 配置层面

启用大页(Huge Pages)是最直接有效的解决方案。将 memory page 从 4KB 提升至 2MB 或 1GB,可将潜在页故障数量降低数个数量级:120GB 缓冲区池使用 2MB 大页仅需约 61000 个页,使用 1GB 大页仅需约 120 个页。

配置步骤如下:首先确认内核已启用大页支持(cat /proc/meminfo | grep Huge),然后在 PostgreSQL 配置文件中设置 huge_pages = on(而非默认的 try,确保失败时能够及时发现)。注意大页需要预分配,配置过大可能导致内存浪费,建议根据 shared_buffers 实际使用量精确计算。

监控与基准测试

建立基线性能指标,在内核升级前后进行对比测试。关键监控指标包括:

  • pgbench 测试的 TPS 变化幅度,若下降超过 20% 需引起重视;
  • 使用 perf top -p $(pgrep postgres) 观察 s_lock 函数的 CPU 占比;
  • 通过 /proc/interrupts 监控上下文切换频率异常增长。

若 TPS 下降且 s_lock 占比超过 30%,基本可以定位为该抢占回归问题。

技术建议与注意事项

社区曾讨论让 PostgreSQL 采用 Restartable Sequences(rseq)机制来检测并恢复被抢占的临界区,但这一方案需要修改数据库代码,且本质上是用内核特性来修复内核造成的问题,接受度有限。更务实的做法是:确认业务数据库所在内核版本,提前在测试环境验证性能表现,并准备大页配置作为防御性措施。

对于无法控制内核升级节奏的云环境用户,建议在实例规格选择时关注内核版本信息,或与云厂商确认是否已应用相关修复补丁。性能问题的排查往往需要在操作系统、数据库和应用程序之间进行跨层分析,这一次的案例再次验证了这一原则。

资料来源:The Coder Cafe (https://read.thecoder.cafe/p/linux-broke-postgresql)、Phoronix (https://www.phoronix.com/news/Linux-7.0-AWS-PostgreSQL-Drop)

systems