頁表隨連線數線性膨脹:Postgres 共享記憶體的隱藏成本
Fernando Simões(frn.sh)· 2026-10-01
Linux 的頁表成本不是「約 1/512」就算完:每個位址空間都要有自己的一份 PTE,共享同一塊記憶體的行程越多,頁表就越逼近資料本身的大小。這個常被忽略的乘數,正是資料庫伺服器在連線數上升時被 OOM killer 帶走的原因之一。
原本的問題
x86_64 上每個 4 KiB 資料頁對應一個 8 byte 的 PTE,所以單一行程映射的頁表約為資料的 1/512。但如果 N 個位址空間映射同一頁,頁表成本就是 N/512;512 個行程映射相同的頁,頁表就和資料頁一樣大。
Khalid Aziz 在 mshare 的 RFC 信件中舉了實例:一台 512GB 記憶體的資料庫伺服器,300GB 的 SGA 被 1500 個以上的 client 共享,系統因 OOM 當機;最壞情況下光是 PTE 就要 878GB 以上。他提出的方向是新增 mshare() 系統呼叫,讓行程共享 PTE。
實測數字
PostgreSQL 的 shared_buffers 正好是這種形狀:一大塊共享記憶體,每個 backend 行程各自映射。ClickHouse 的 Kaushik Iska 在 2026-07-14 的文章中,用 r7i.4xlarge(128GB)、Postgres 16、shared_buffers = 32GB,讓每條連線完整掃過 15.6GB 的快取表,再讀 /proc/meminfo 的 PageTables:
| 連線數 | 4KB 頁 | 2MB huge pages |
|---|---|---|
| 25 | 789 MB | 18 MB |
| 100 | 3.06 GB | 58 MB |
| 200 | 6.12 GB | 111 MB |
4KB 頁每條連線增加 31.1MB,與算式「15.6GB ÷ 4KB × 8 byte ≈ 31.2MB」吻合。同一台機器上,select-only 的 pgbench 在 100 個 client 下從 373,083 TPS 升到 418,087 TPS,差 12%。
Percona 的 Jobin Augustine 在 2021 年的測試只用 80 條連線、192GB 記憶體的機器,改用 HugePages 後頁表總量只有 61MB,先前則超過 25GB。
影響範圍
- 跑 Postgres 的人:用
grep PageTables /proc/meminfo看頁表佔用;用grep HugetlbPages /proc/<postmaster pid>/status確認 huge pages 真的生效,值為 0 代表已退回 4KB 頁。 - 設定 huge_pages 的人:預設值
try會無聲退回 4KB 頁,ClickHouse 的做法是設成on,拿不到就拒絕啟動。
# 舊:預設 try,失敗時靜默退回 4KB
huge_pages = try
# 新:保留頁池後強制使用
huge_pages = 'on'
huge_page_size = 0 # 系統預設,2MBhuge pages 的代價
保留 huge pages 需要尚未碎片化的連續記憶體。ClickHouse 的文章直說,在負載下的機器上「同樣的請求常會失敗」,所以他們在開機早期就保留,並在配置前先 drop_caches 與 compact_memory。另外 Postgres 的共享記憶體段比 shared_buffers 大,該文在 32GB 的設定下量到 32.75GB 的段,因此要用 postgres -C shared_memory_size 量出實際大小再回推。
NUMA 機器是另一個變體:多份頁表可省遠端走訪,但要付記憶體與同步成本。原文引用 Mitosis 與 Hydra 兩篇論文說明這個取捨,本文未逐一查證其數字。
補充:原文作者推導 1/512 與 N/512 的算式屬其自述,本文以 ClickHouse 的實測表交叉驗證。