一台 96 vCPU 的 Graviton4,同一套 PostgreSQL pgbench,换到 Linux 7.0 后,吞吐从 Linux 6.x 的约 98,565 TPS 掉到 50,751 TPS。
更扎眼的是 perf:约 55%-56% CPU 时间耗在 s_lock。机器不是算不过来,而是在围着一把锁空转。
这事不能被讲成“Linux 7.0 毁了 PostgreSQL”。目前能说的边界很清楚:特定硬件、特定 pgbench 负载、特定内存配置下,出现了严重性能回归。
但它也不是一个普通调参事故。它更像一次撞车:通用内核的调度取舍,撞上数据库热路径对确定性的苛刻假设。
发生了什么:TPS 腰斩,CPU 烧在锁上
这次测试条件很极端,也很有代表性。极端在并发和内存规模;有代表性在于它踩中了很多数据库系统都会害怕的路径:锁、页故障、调度延迟。
| 项目 | 信息 |
|---|---|
| 硬件 | 96 vCPU Graviton4 |
| 数据库负载 | PostgreSQL pgbench,simple update |
| 压测参数 | scale factor 8,470;1,024 clients;96 threads |
| 内存配置 | shared_buffers 120GB |
| Linux 6.x | 约 98,565 TPS |
| Linux 7.0 | 约 50,751 TPS |
| perf 线索 | s_lock 占约 55%-56% CPU |
| 直接缓解 | 启用 huge pages;PostgreSQL 建议 huge_pages=on 而不是 try |
这里的 s_lock 指向 PostgreSQL 的自旋锁路径。自旋锁的逻辑很朴素:持锁者很快会放手,其他 backend 原地转一小会儿,比睡眠再唤醒更划算。
这个前提只有一个弱点:持锁者必须真的很快。
高并发数据库最怕的不是某个慢操作。慢操作可以排队、隔离、优化。更麻烦的是一个原本应该极短的临界区,被系统层行为偶发拉长。
在 1,024 clients 这种并发下,“偶发”不会安静地发生。一个 backend 慢一点,后面一群 backend 一起烧 CPU。
为什么重要:调度延迟被 spinlock 放大了
PostgreSQL 的 shared buffer pool 缓存的是数据库数据页。这里要分清:PostgreSQL 常见 data page 是 8KB;Linux 管理内存用的普通 memory page 通常是 4KB。两者不是一回事。
问题链条大致是这样:
| 环节 | 影响 |
|---|---|
Linux 7.0 移除现代 CPU 架构上的 PREEMPT_NONE | 原来更少被打断的内核执行路径,换成新的抢占取舍 |
PREEMPT_LAZY 成为相关路径上的选择 | 它不是设计失败,但可能改变持锁进程恢复执行的时机 |
PostgreSQL StrategyGetBuffer 使用全局 spinlock | 热路径上等待者很多,延迟会被放大 |
| 持锁期间触发 minor page fault | 临界区被拉长,其他 backend 只能 spinning |
| 并发 backend 很多 | 一个持锁者的等待,变成一堆 CPU 空转 |
PREEMPT_LAZY 不该被简单骂成“坏设计”。它本来是在吞吐和响应之间做折中,对很多 server workload 可能仍然可接受。
麻烦在 PostgreSQL 这个组合太敏感。
StrategyGetBuffer 里有全局 spinlock。持锁期间如果碰到 minor page fault,持锁时间就不再是理想中的极短路径。Linux 7.0 的抢占行为再把恢复执行稍微往后推一点,等待成本会被所有 spinning backend 成倍放大。
120GB 的 shared_buffers 如果落在 4KB Linux memory page 上,大约是 3100 万页。页数越多,首次触碰未映射区域时遇到 minor page fault 的机会越多。
换成 huge pages,页数会大幅下降:
| 页大小 | 120GB 大约需要多少页 |
|---|---|
| 4KB 普通页 | 约 3100 万页 |
| 2MB huge pages | 约 61,440 页 |
| 1GB huge pages | 约 120 页 |
页数少了,潜在 page fault 和 TLB 压力都降下来。持锁时踩 fault 的概率也会低很多。
但 huge pages 不是魔法开关。它要预留内存,预留多了会浪费,预留少了会失败,还会增加容量规划成本。
所以 PostgreSQL 生产环境里,huge_pages=try 反而有风险:它不可用时会静默退回普通页。更稳的做法是用 huge_pages=on,失败就让系统明确失败。宁可启动时报错,也别让数据库悄悄跑在另一套性能假设上。
谁受影响:DBA 和 SRE 该先查三件事
最该关心这事的不是普通 PostgreSQL 用户,而是两类人。
一类是跑大内存、高并发 PostgreSQL 的 DBA。尤其是 shared_buffers 很大、连接数或并发 backend 很高、负载里频繁走 buffer replacement 路径的系统。
另一类是做内核升级、云镜像升级、发行版基线升级的 SRE。你不一定改了数据库配置,但内核默认值变了,性能假设就可能跟着变。
动作可以很具体:
- 升 Linux 7.0 前,不要只跑轻量 smoke test,要跑接近生产并发的 pgbench 或业务压测。
- perf 里如果看到
s_lock、spin、buffer strategy 相关路径异常升高,不要只盯 SQL 和索引。 - 检查 PostgreSQL huge pages 是否真的启用。
try不等于“已经用了”。 - 对大内存实例,评估 huge pages 预留量。别为了止血,把内存碎片和浪费问题丢给下一班人。
采购和迁移也要慢半拍。正在选 Graviton4 大规格实例、准备升级内核基线、或者把 PostgreSQL 压到更高并发的团队,最好先把内核版本、huge pages、pgbench 回归测试列进验收项。
这不是让大家停在旧内核上。旧内核也有自己的安全和维护成本。现实选择是:升级可以,但别把“默认能跑”当成“默认适合”。
接下来最该观察三件事:
| 观察点 | 为什么要看 |
|---|---|
| Linux 内核是否调整相关抢占路径或给出说明 | 判断这是短期回归,还是长期默认策略的一部分 |
PostgreSQL 是否减少 StrategyGetBuffer 全局 spinlock 的放大效应 | 根因不只在内核,数据库热路径也暴露了脆弱点 |
| 发行版和云厂商的默认配置 | 大多数事故不是发生在源码里,而是发生在默认镜像和默认参数里 |
我更在意第三点。
基础设施里,默认值从来不是中立的。它是某种成本分配:内核追求通用,数据库追求吞吐,运维追求少配置。大家都想省心,账单最后落到压测曲线上。
“天下熙熙,皆为利来。”放到软件栈里,就是各层都为自己的默认值辩护。可高并发数据库不吃这套。热路径要的是确定性,不是看起来更温和的平均公平。
这次少见地把问题照得很清楚:模型很简单,后果很贵。一把锁、一次 minor page fault、一点调度等待,在 1,024 个 client 面前会被放大成吞吐腰斩。
所以别把它当成 Linux 7.0 的单点八卦。它提醒的是更老的问题:通用系统一旦进入极端并发场景,温和的默认值也可能变成刀口。
