news 2026/4/17 2:03:22

DeepSeek总结的Postgres 性能衰退

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DeepSeek总结的Postgres 性能衰退

来源:https://mydbanotebook.org/posts/postgres-performance-regression-are-we-there-yet/

Postgres 性能衰退:我们到了吗?

2026年4月15日 · 2402 词 · 预计阅读 12 分钟

每年,PostgreSQL 都在变得更快。研究人员对从 8 版到 16 版的优化器进行基准测试,发现每个主要版本平均有 15% 的性能提升。这是十年来持续、可衡量的进步。该项目自 1996 年以来一直在这样做。

因此,当有头条新闻声称 Linux 7.0 刚刚将 PostgreSQL 的吞吐量减半时,DBA、系统管理员和 DevOps 开始恐慌(尤其是那些使用计划尽快搭载 Linux 7.0 内核的 Ubuntu 26.04 LTS 的人)。在按下恐慌按钮之前,值得先深吸一口气。

实际情况更加微妙。让我带你了解一下实际发生了什么,这对你的部署意味着什么,以及你应该真正担心什么。

首先,一点内核理论

请耐心听我说。我保证这是必要的。

你的操作系统内核负责决定哪个程序在何时运行在 CPU 上。这被称为调度。当一个程序在执行过程中被中断以让另一个程序运行时,这被称为抢占

如果你在服务器上运行 PostgreSQL 已有一段时间,你会知道标准的建议:PostgreSQL 应该拥有这台机器。它应该是这台主机上唯一重要的东西。我们通常会禁用 OOM killer,这样内核就不会在内存压力下决定牺牲 PostgreSQL 的后台进程。我们调整vm.swappiness以将 PostgreSQL 的数据保留在内存中。整个理念是:阻止操作系统对数据库进行“二次猜测”。

抢占模式符合相同的理念。把它想象成一位正在进行精细手术的外科医生。你不希望有人在手术中途拍拍他们的肩膀说:“你能快点看看别的东西吗?”当 PostgreSQL 深入到一个关键部分时,我们同样不希望内核打断它。

不同的抢占模式代表了不同的权衡。

  • PREEMPT_NONE(旧的服务器默认值): 内核几乎从不中断正在运行的程序。程序一直运行,直到它自愿放弃 CPU,或者直到它进行系统调用。更少的上下文切换,更多的时间做实际工作。

    • PREEMPT_NONE:无中断,线程运行直到自愿让出
  • PREEMPT_FULL: 内核几乎可以在任何点中断任何正在运行的程序。这对响应能力和实时应用程序很有好处。但对服务器工作负载的吞吐量不利。

    • PREEMPT_FULL:中断可能发生在任何地方、任何时间点
  • PREEMPT_LAZY(Linux 7.0 新增): 一个折衷方案。内核会抢占线程,但是是“懒惰”地,等待一个自然的机会,而不是立即强制中断。旨在降低PREEMPT_FULL的开销,同时保持内核调度模型的简洁。

    • PREEMPT_LAZY:仅在自然调度边界中断

在过去的 20 多年里,服务器内核都搭载了PREEMPT_NONE。PostgreSQL 就是考虑到这个现实而构建的。

Linux 7.0 改变了这一点。Peter Zijlstra 的提交7dadeaa6e851在现代架构上移除了PREEMPT_NONE:arm64、x86、powerpc、riscv、s390、loongarch。所有架构都是。内核现在只提供PREEMPT_FULLPREEMPT_LAZY

为什么这会影响 PostgreSQL 的性能?

PostgreSQL 在多个地方使用自旋锁,尤其是在其缓冲区管理器中。

自旋锁是一种锁机制,线程在等待锁可用时,不会进入睡眠状态,而是会在一个紧密循环中不断检查。想想《怪物史莱克》中坐在后座被绑住的驴子:“我们到了吗?我们到了吗?我们到了吗?”这听起来很浪费,但对于非常短期的锁(PostgreSQL 的缓冲区管理器使用的那种),这实际上比让线程休眠再唤醒的开销要快。

自旋锁背后的关键假设是:持有锁的线程将很快释放它。没有人会在一个 20 纳秒的关键部分中间抢占该线程。

PREEMPT_NONE下,这个假设成立。锁持有者一直运行直到完成。其他自旋等待的线程不会等太久。

PREEMPT_LAZY下,内核可以决定抢占一个持有自旋锁的线程。该线程被暂停。等待该锁的所有其他线程继续自旋,燃烧 CPU 周期,直到调度器决定恢复原始线程。下面是几个线程下的情况:

PREEMPT_LAZY下的自旋锁竞争:线程 1 在持有锁时被抢占,所有其他线程无用地自旋,直到它被恢复

理论上,这是一个真正的问题。在实践中,实际发生的事情更有趣。

基准测试实际显示了什么?

AWS 的 Salvatore Dipietro 在一个 96 个 vCPU 的 Graviton4 实例上运行了pgbench,1024 个客户端,96 个线程,使用超过 100GB 的共享缓冲区池。与 Linux 6.x 相比,他得到了 0.51 倍的吞吐量,并向内核邮件列表报告了这一点。

基准测试脚本明确设置了huge_pages=off

这一个细节关系重大。Andres Freund 深入研究了邮件列表线程,发现了真正的罪魁祸首:不是自旋锁机制本身,而是在持有自旋锁时发生的TLB 未命中轻微缺页错误

以下是实际发生的情况。没有大页,PostgreSQL 的共享内存使用标准的 4KB 页面映射。在超过 100GB 的缓冲区池上,对每个页面的第一次访问都会导致一次轻微缺页错误。当这个轻微缺页错误发生在持有自旋锁时,该线程会停滞。等待该锁的所有其他线程继续自旋。PREEMPT_LAZY然后偶尔会调度出停滞的锁持有者,使情况变得更糟,但根本问题已经是缺页错误,而不是抢占模式。

Andres 证实了这一点:当他启用大页时,他无法重现性能衰退。当他禁用大页时,竞争出现了。Salvatore 也证实了这一点。他重新运行了基准测试,这次在系统上启用了透明大页 (THP),他发现 THP 修复了之前的行为:在 Linux 6.x 和 Linux 7.0 上,吞吐量都恢复到了大约 185k tps。大页和 THP 通过不同的机制工作,但都消除了导致竞争的 4KB 缺页错误问题。

还有第二个值得注意的细节。基准测试中处于竞争状态的自旋锁是StrategyGetBuffer(),它仅在缓冲区池预热期间触发,即 PostgreSQL 首次将页面加载到共享内存时。一旦缓冲区池达到稳定状态并且空闲列表清空,该路径就不会再被命中。该基准测试测量的是一个瞬态的预热阶段,并将其呈现为稳态性能。PostgreSQL 中至少还有一个自旋锁可能在新抢占模型下发生竞争,但其并发获取的上限要低得多。

Andres 直言不讳地说:一个 100GB 的冷缓冲区池,没有大页,运行着比 CPU 核心数多 10 倍的活动连接,并且仅在预热期间,这不是一个现实的生产场景。

那么,你应该害怕吗?

对于在裸机或专用虚拟机上配置良好的部署:可能不会

如果你已经在主机上启用了大页或 THP,并且你的工作负载不是一个具有巨大缓冲区池的极端冷启动场景,那么 Linux 7.0 的更改不太可能在稳态下影响你。在做出任何决定之前,请使用你实际的工作负载和实际配置进行基准测试。

在两种情况下,情况更加微妙。

第一:如果你在没有大页或 THP 的情况下,在高度并行的机器上运行大型共享缓冲区。在这种情况下,PREEMPT_LAZY确实会加剧预热期间的自旋锁竞争。在这种配置下,PREEMPT_NONE下也存在竞争,PREEMPT_LAZY只是让它变得更糟。解决方法是启用大页或 THP,而不是固定你的内核版本。

第二:如果你在容器中运行 PostgreSQL。这是值得花时间关注的担忧,因为它没有得到足够的重视。

真正的问题:大页、THP 和容器

大页和 THP 都能缓解性能衰退。在裸机上,启用其中任何一种都很简单。在容器化环境中,这从困难到不可能,这是一个早在 Linux 7.0 之前就存在的普遍 PostgreSQL 性能问题。

THP 由/sys/kernel/mm/transparent_hugepage/enabled控制,这是一个主机级别的 sysfs 路径。Sysfs 在 Linux 中没有命名空间隔离,这意味着容器无法修改它。主机配置了什么,该主机上的每个容器就继承什么,无法从内部覆盖。

显式大页也有同样的限制。主机内核在容器启动前通过vm.nr_hugepages预留一个池。如果主机配置了,容器可以从该池中消费,但它不能创建或调整池的大小。

Incus,一个完全开源的系统容器管理器,确实允许limits.hugepages通过 hugetlb cgroup 限制给定容器可以消费多少个大页,但主机池必须首先存在。在 Incus 中运行 PostgreSQL 的一位读者正好报告了这个问题:你必须在主机级别预先分配池的大小。太小,PostgreSQL 无法使用它。太大,你就浪费了其他容器无法触及的物理内存。这个池是静态的:更改它意味着停止容器,调整主机配置,然后重新启动。

在 Docker 中,你需要在容器启动之前在主机上设置vm.nr_hugepages,这需要对主机的 root 访问权限。在 Docker Desktop 上,情况更糟,因为你根本无法控制底层的 Linux 虚拟机。

在 Kubernetes 中,节点必须在 kubelet 能够将大页宣传为可调度资源之前预先分配大页。你在 Pod 规范中声明hugepages-2Mihugepages-1Gi,但节点必须首先准备好池。在托管节点池上,EKS、GKE、AKS,你通常根本无法控制节点级别的内核配置。从 Pod 内部更改 THP 设置需要将主机的/sys挂载为卷,或者部署一个特权的 DaemonSet,这两种方法大多数集群管理员都不会批准。

这一点的重要性超出了 Linux 7.0 的故事。使用大缓冲区池和 4KB 页面运行 PostgreSQL 一直是一个性能问题。Linux 7.0 事件只是以一种戏剧性的方式使其变得可见。如果你的 PostgreSQL 在容器中运行,并且你无法在主机级别控制 THP 或大页,那么你已经在损失性能了。行业已经严重倾向于容器化部署,但尚未完全解决这个问题。

Linux 社区的解决方案:rseq

几十年来,PREEMPT_NONE充当了 PostgreSQL 的“请勿打扰”标志,确保一个线程可以完成其工作而不被打断。Linux 7.0 移除了这个标志。虽然新的“懒惰”模式试图保持礼貌,但如果线程在错误的时刻被暂停,它会引入一定程度的不确定性,可能将一个 20 纳秒的锁变成一个毫秒级的瓶颈。

可重启序列 (rseq)是一种 Linux 内核机制,允许用户空间代码向内核发出信号,表明它正处于关键部分,因此内核会延迟抢占,直到锁被释放:

带 rseq 前后对比:没有 rseq,内核盲目抢占;有了 rseq,它能识别关键部分标志并等待

问题不在于前进的方向。问题在于,移除PREEMPT_NONE和引入rseq发生在同一个版本中,两者之间没有过渡期。淘汰某样东西的正常方式是让新方法 alongside 旧方法一起发布,弃用旧方法,给人们时间迁移,然后移除它。这一步被跳过了。

内核团队提出的官方解决方案是采用可重启序列 (rseq) 来缓解这些性能衰退。然而,有一个问题:必要的切片扩展在 Linux 7.0 中默认未启用。它需要一个使用CONFIG_RSEQ_SLICE_EXTENSIONEXPERT=1标志编译的内核。对于使用标准发行版的绝大多数 DBA 和 DevOps 工程师来说,这使得“正确”的修复实际上无法实现。正如俗话所说,这就像被告知在有人拿走梯子时紧紧抓住你的刷子。

你实际上应该怎么做?

在采取任何行动之前:使用你自己的工作负载和你自己的配置进行基准测试。在 96 核 ARM 机器上故意禁用大页的合成pgbench结果,不能代表你的生产系统。

  • 如果你控制你的主机,并且已经启用了大页或 THP,升级到 Linux 7.0 并进行测量。你可能根本看不到任何性能衰退。
  • 如果你的主机上既没有启用大页也没有启用 THP,在考虑内核升级之前,先启用其中一个。对于显式大页,在主机上设置vm.nr_hugepages,并在postgresql.conf中设置huge_pages = try。对于 THP,在主机上设置transparent_hugepage=always。这两种方法都解决了底层的缺页错误问题。无论 Linux 7.0 如何,这都是很好的建议。
  • 如果你在容器中运行 PostgreSQL,并且无法在主机级别控制 THP 或大页,请注意,这对于大缓冲区池来说是一个普遍的性能问题。值得向管理你基础设施的人提出这个问题,并且值得在你的部署架构选择中考虑进去。
  • 如果你使用的是搭载 Linux 7.0 的 Ubuntu 26.04 LTS,不要恐慌。测试你实际的工作负载。如果你看到性能衰退,首先检查你的大页配置。

来源:

  • Salvatore Dipietro 在 Linux 内核邮件列表上的讨论
  • Hacker News 讨论

  1. TLB (Translation Lookaside Buffer)是 CPU 的一个缓存,用于存储最近的虚拟内存到物理内存地址的转换。当 CPU 需要访问内存时,它首先检查 TLB。如果转换不在那里(“TLB 未命中”),它必须遍历页表来找到物理地址,这明显更慢。使用 4KB 页面和一个非常大的缓冲区池,TLB 很快就会填满,未命中变得频繁。更大的页面减少了 TLB 中需要的条目数量,从而显著降低了未命中率。[↩︎]

  2. 大页 (Huge Pages)(也称为 HugeTLB 页面)是一种 Linux 机制,用于预先分配大的内存页面,通常是 2MB 或 1GB,而不是默认的 4KB。它们必须在使用前显式预留:内核在启动时或通过vm.nr_hugepages预留一个池,应用程序显式请求它们。PostgreSQL 通过postgresql.conf中的huge_pages = try|on|off(默认是try)支持这一点。由于池是预先分配并锁定在内存中的,大页不能被换出,并且在初始分配后不会发生缺页错误。[↩︎]

  3. 透明大页 (THP)是一种不同的机制。它不需要显式预分配,内核会在后台自动将一组 4KB 页面提升为更大的页面,对应用程序透明。不需要更改postgresql.conf。权衡之处在于可预测性较低:内核后台对页面的提升和降级有时可能会导致延迟峰值。THP 通过/sys/kernel/mm/transparent_hugepage/enabled进行系统范围的控制,这是一个对主机全局的 sysfs 路径,不属于任何 Linux 命名空间。[↩︎]

  4. Incus 暴露了limits.hugepages.[size]以通过 hugetlb cgroup 限制容器的大页使用量,但这需要主机上提供 hugetlb cgroup,并且主机大页池必须首先预先分配。请参阅 Incus 实例选项文档。[↩︎]

  5. 这来自一个法国笑话:一个疯子正在重新粉刷他的天花板,就在他到达梯子顶端时,他的同伙说“紧紧抓住你的刷子,我要把梯子拿走了。” [↩︎]

PostgreSQL Linux 性能管理

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 2:02:32

HBA卡深度解析:从基础原理到企业级应用实战

1. HBA卡基础入门:从硬件结构到工作原理 第一次接触HBA卡是在2013年某银行的存储系统升级项目。当时客户的一台关键业务服务器突然无法识别存储阵列,整个业务系统面临瘫痪风险。经过排查发现,正是HBA卡上的一个微小电容烧毁导致链路中断。这…

作者头像 李华
网站建设 2026/4/17 2:02:13

Zotero Citation插件:三步搞定Word文献引用的终极指南

Zotero Citation插件:三步搞定Word文献引用的终极指南 【免费下载链接】zotero-citation Make Zoteros citation in Word easier and clearer. 项目地址: https://gitcode.com/gh_mirrors/zo/zotero-citation 还在为学术论文中的文献引用而烦恼吗&#xff1f…

作者头像 李华
网站建设 2026/4/17 2:00:23

三堵墙逼出来的智慧——V3障碍与感知

「当AI学会发脾气」—— 一个类脑认知系统的诞生记 7个版本迭代Python脚本,教会AI像人一样焦虑、兴奋、犯错和成长 📚 全系列文章: 从零开始:给AI装一个最简单的"大脑"让AI"看见"世界——可视化的力量当AI遇到…

作者头像 李华
网站建设 2026/4/17 1:54:12

Pads Layout 过孔操作全解析:从设置到实战技巧

1. Pads Layout过孔基础:从理解到设置 过孔是PCB设计中连接不同层的关键元素,就像高楼里的电梯一样让信号在不同楼层间穿梭。在Pads Layout中,过孔操作看似简单,但实际使用时新手常会遇到各种"坑"。我第一次用Pads做四层…

作者头像 李华