DeepSeek总结的Postgres 性能衰退

张开发
2026/4/18 17:51:36 15 分钟阅读

分享文章

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_NONEarm64、x86、powerpc、riscv、s390、loongarch。所有架构都是。内核现在只提供PREEMPT_FULL和PREEMPT_LAZY。为什么这会影响 PostgreSQL 的性能PostgreSQL 在多个地方使用自旋锁尤其是在其缓冲区管理器中。自旋锁是一种锁机制线程在等待锁可用时不会进入睡眠状态而是会在一个紧密循环中不断检查。想想《怪物史莱克》中坐在后座被绑住的驴子“我们到了吗我们到了吗我们到了吗”这听起来很浪费但对于非常短期的锁PostgreSQL 的缓冲区管理器使用的那种这实际上比让线程休眠再唤醒的开销要快。自旋锁背后的关键假设是持有锁的线程将很快释放它。没有人会在一个 20 纳秒的关键部分中间抢占该线程。在PREEMPT_NONE下这个假设成立。锁持有者一直运行直到完成。其他自旋等待的线程不会等太久。在PREEMPT_LAZY下内核可以决定抢占一个持有自旋锁的线程。该线程被暂停。等待该锁的所有其他线程继续自旋燃烧 CPU 周期直到调度器决定恢复原始线程。下面是几个线程下的情况PREEMPT_LAZY下的自旋锁竞争线程 1 在持有锁时被抢占所有其他线程无用地自旋直到它被恢复理论上这是一个真正的问题。在实践中实际发生的事情更有趣。基准测试实际显示了什么AWS 的 Salvatore Dipietro 在一个 96 个 vCPU 的 Graviton4 实例上运行了pgbench1024 个客户端96 个线程使用超过 100GB 的共享缓冲区池。与 Linux 6.x 相比他得到了 0.51 倍的吞吐量并向内核邮件列表报告了这一点。基准测试脚本明确设置了huge_pagesoff。这一个细节关系重大。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-2Mi或hugepages-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_EXTENSION和EXPERT1标志编译的内核。对于使用标准发行版的绝大多数 DBA 和 DevOps 工程师来说这使得“正确”的修复实际上无法实现。正如俗话所说这就像被告知在有人拿走梯子时紧紧抓住你的刷子。你实际上应该怎么做在采取任何行动之前使用你自己的工作负载和你自己的配置进行基准测试。在 96 核 ARM 机器上故意禁用大页的合成pgbench结果不能代表你的生产系统。如果你控制你的主机并且已经启用了大页或 THP升级到 Linux 7.0 并进行测量。你可能根本看不到任何性能衰退。如果你的主机上既没有启用大页也没有启用 THP在考虑内核升级之前先启用其中一个。对于显式大页在主机上设置vm.nr_hugepages并在postgresql.conf中设置huge_pages try。对于 THP在主机上设置transparent_hugepagealways。这两种方法都解决了底层的缺页错误问题。无论 Linux 7.0 如何这都是很好的建议。如果你在容器中运行 PostgreSQL并且无法在主机级别控制 THP 或大页请注意这对于大缓冲区池来说是一个普遍的性能问题。值得向管理你基础设施的人提出这个问题并且值得在你的部署架构选择中考虑进去。如果你使用的是搭载 Linux 7.0 的 Ubuntu 26.04 LTS不要恐慌。测试你实际的工作负载。如果你看到性能衰退首先检查你的大页配置。来源:Salvatore Dipietro 在 Linux 内核邮件列表上的讨论Hacker News 讨论TLB (Translation Lookaside Buffer)是 CPU 的一个缓存用于存储最近的虚拟内存到物理内存地址的转换。当 CPU 需要访问内存时它首先检查 TLB。如果转换不在那里“TLB 未命中”它必须遍历页表来找到物理地址这明显更慢。使用 4KB 页面和一个非常大的缓冲区池TLB 很快就会填满未命中变得频繁。更大的页面减少了 TLB 中需要的条目数量从而显著降低了未命中率。[↩︎]大页 (Huge Pages)也称为 HugeTLB 页面是一种 Linux 机制用于预先分配大的内存页面通常是 2MB 或 1GB而不是默认的 4KB。它们必须在使用前显式预留内核在启动时或通过vm.nr_hugepages预留一个池应用程序显式请求它们。PostgreSQL 通过postgresql.conf中的huge_pages try|on|off默认是try支持这一点。由于池是预先分配并锁定在内存中的大页不能被换出并且在初始分配后不会发生缺页错误。[↩︎]透明大页 (THP)是一种不同的机制。它不需要显式预分配内核会在后台自动将一组 4KB 页面提升为更大的页面对应用程序透明。不需要更改postgresql.conf。权衡之处在于可预测性较低内核后台对页面的提升和降级有时可能会导致延迟峰值。THP 通过/sys/kernel/mm/transparent_hugepage/enabled进行系统范围的控制这是一个对主机全局的 sysfs 路径不属于任何 Linux 命名空间。[↩︎]Incus 暴露了limits.hugepages.[size]以通过 hugetlb cgroup 限制容器的大页使用量但这需要主机上提供 hugetlb cgroup并且主机大页池必须首先预先分配。请参阅 Incus 实例选项文档。[↩︎]这来自一个法国笑话一个疯子正在重新粉刷他的天花板就在他到达梯子顶端时他的同伙说“紧紧抓住你的刷子我要把梯子拿走了。” [↩︎]PostgreSQL Linux 性能管理

更多文章