Linux 7.0 在特定情境下讓 PostgreSQL 效能腰斬:解析搶佔式排程引發的效能退化 (★
108 分 🔥)
文章指出,AWS 工程師 Salvatore Dipietro 在 96 個 vCPU(虛擬 CPU)的 Graviton4 機器上,以 pgbench(PostgreSQL 標準效能測試工具)測試 PostgreSQL,發現 Linux 7.0 相較 Linux 6.x 吞吐量幾乎腰斬:每秒交易數從約 98,565 筆降到 50,751 筆。透過 perf(Linux 效能剖析工具)追蹤後,CPU 有約 55% 時間耗在 PostgreSQL 的 s_lock,也就是 StrategyGetBuffer 路徑中的自旋鎖(spinlock)。這個函式負責在共享緩衝池中尋找可用緩衝區;在高並行情境下,大量後端行程會同時競爭同一把鎖。
文章將問題追溯到 Linux 7.0 移除現代 CPU 架構上的 PREEMPT_NONE,改以 PREEMPT_LAZY 或 PREEMPT_FULL 為主。PREEMPT_NONE 過去較常用於伺服器,核心較少在工作進行中介入;PREEMPT_LAZY 則試圖兼顧回應性與吞吐量。PostgreSQL 的共享緩衝池若使用預設 4 KB Linux 記憶體頁,在 120 GB shared_buffers 下會對應到約 3,100 萬個記憶體頁;當某個後端行程持有自旋鎖時觸發 minor page fault(次要分頁錯誤,代表首次存取時需要建立實體記憶體映射),其他行程就會在鎖外空轉等待。PREEMPT_LAZY 可能讓持鎖行程在處理 page fault 期間被排程器暫停,使鎖持有時間從「處理 page fault 的時間」延長為「page fault 加上等待重新排程的時間」,並由所有等待中的後端行程共同放大 CPU 浪費。
文章提出的主要緩解方式是啟用 huge pages(巨頁),例如 2 MB 或 1 GB 記憶體頁。以 120 GB shared_buffers 來看,4 KB 頁可能產生約 3,100 萬個首次存取映射點,2 MB 巨頁可降到約 61,440 個,1 GB 巨頁則約 120 個;同時也能降低 TLB(Translation Lookaside Buffer,位址轉譯快取)的壓力。文章建議 PostgreSQL 的 huge_pages 設為 on,而不是預設 try,避免系統沒有配置好巨頁時悄悄退回 4 KB 頁。不過巨頁需要預先保留記憶體,可能造成其他行程可用記憶體減少,也可能因配置粒度過大而浪費部分空間。文中也提到 Intel 核心工程師 Peter Zijlstra 提出可重新啟動序列(rseq, Restartable Sequences)相關方向,但 PostgreSQL 社群對「為了恢復升級核心前原本就有的效能而修改資料庫」反應並不熱烈,並牽涉 Linux 長期強調的不破壞 userspace(使用者空間程式相容性)原則。
Hacker News 討論普遍認為標題有誇大之嫌,因為這不是 PostgreSQL 在 Linux 7.0 上全面故障,而是在大型 shared_buffers、4 KB 記憶體頁、巨頁未啟用、高並行壓力等條件下暴露的效能退化。有留言者直指這比較像是「120 GB PostgreSQL 卻關閉 huge pages」導致鎖競爭從糟糕變成災難,並質疑 AWS 為何用這種配置測試,也有人希望 RDS 或 Aurora 生產環境不是如此設定。另一些人補充,巨頁過去名聲不穩多半與 transparent huge pages(THP,透明巨頁)混淆;對大型 PostgreSQL 或 VM 這類單一大型記憶體區塊,明確配置 huge pages 已經是常見效能調校,甚至有人分享曾在 pgbench 看到 10% 到 20% 左右提升。
討論中也有重要技術修正:有留言指出,PREEMPT_NONE、PREEMPT_LAZY、PREEMPT_FULL 指的是核心可被搶佔的程度,不是使用者空間行程是否能被搶佔;Linux 的使用者空間程式本來就會被排程器切換。另有留言修正文章對 rseq 方案的描述,實際提案較接近 time-slice extension(時間片延長):當行程持有 s_lock 時設定請求位元,若核心準備在此時搶佔該行程,就延長一次時間片讓它先釋放鎖,而不是單純偵測被搶佔後重試臨界區。整體而言,社群共識較接近「這是提醒大型 PostgreSQL 部署應正確配置 huge pages 的警訊」,同時也反映核心排程變更即使在少數配置下造成重大效能回歸,仍會引發對相容性與預設值的爭論。
👥
52 則討論、評論 💬
https://news.ycombinator.com/item?id=47949585