PostgreSQL 18 在 2025 年 9 月把 asynchronous I/O 子系統推出來時,社群的第一反應是「終於」。同步 I/O 這條扛了二十年的舊路徑一次翻掉,冷 cache 讀取 2 到 3 倍加速的實測到處都在轉。過了將近一年,operator 圈子的抱怨集中到同一件事上:io_workers 是靜態值,OLAP 尖峰時 3 個 worker 不夠用、閒時段 8 個 worker 白佔記憶體,只要負載形狀會變,就得手動調兩次。PostgreSQL 19 Beta 3 在 2026 年 8 月 13 日釋出,io_method 這條路徑走完了:worker pool 改成自動伸縮,跟著 queue depth 升降,operator 只要設上下界就好。
同一版本裡另一條被拉平的曲線是 autovacuum。過去所有版本 autovacuum 一次只能單執行緒清一個 index,帶多 index 的大表要清好幾小時。19 之後 autovacuum 能拿 parallel worker 去平行處理 index vacuuming 跟 cleanup。兩個改動疊起來,VPS 上的 worker 預算分配邏輯必須重新想過:max_parallel_workers、autovacuum_max_parallel_workers、io_max_workers 三個池的關係到底該怎麼配。
靜態 io_workers 為什麼撐不住一整年
18 版本的 async I/O 有三個 io_method 選項:sync 維持舊行為、worker 用一組 background process 代發 I/O、io_uring 直接走 Linux 5.1+ 的 io_uring 介面。預設是 worker,io_workers 這顆旋鈕預設值 3,代表整個 cluster 共用 3 個 process 幫所有 backend 收 I/O 請求。
問題出在「共用」跟「靜態」同時成立。一支 report query 跑起來能把 3 個 worker 全部佔滿,這段時間 OLTP session 的 buffer miss 就得排隊,尾延遲被直接拉高。反過來把 io_workers 開到 8 應付尖峰,非尖峰時段這 8 個 worker 全部閒置也不會退回去,記憶體跟 CPU cache 都被壓一份基本開銷。SaaS 或多租戶場景更痛,租戶行為完全不可預測。
社群給出的暫時建議是「按 vCPU 數開 25% 到 50%」,這個粗略公式在均值負載下堪用,但完全處理不了負載變異。19 之前的 workaround 是用 ALTER SYSTEM SET io_workers 加 pg_reload_conf() 手動重跑,或者接一個 cron 依時段換值,兩種做法都很土。
19 的自動伸縮怎麼跑
PostgreSQL 19 把 io_workers 這個靜態值換掉,新的 API 是四個參數:io_min_workers(下限,預設 1)、io_max_workers(上限,預設 8)、io_worker_idle_timeout(閒置多久後退場,預設 60s)、io_worker_launch_interval(新增 worker 之間的最短間隔,預設 200ms)。行為模式跟一般的 thread pool library 一樣:queue depth 上升時逐步 spawn 到上限,queue 排空後閒置 worker 逾時退回。
實務上 operator 只需要設兩個上下界,剩下交給系統。Crunchy Data 明確給的建議是「18 上手動調過 io_workers 的環境,19 上先把它拔掉重測預設值」。這句話的意思是原本用時段條件開幾個 worker 的邏輯全部可以刪,讓 io_min_workers=1、io_max_workers=8 這組預設先跑一週,再視 pg_stat_io 的 worker 使用率決定是否往上加上限。
io_uring 派系不受這組參數影響。io_uring 走的是每個 backend 各自向 kernel 提交 I/O 的路徑,本來就沒有共用 worker pool 的概念。這個差異也解釋了為什麼一部分 benchmark 顯示 io_uring 在 mixed workload 下比 worker 更穩——不用搶 shared queue。但 io_uring 在嚴格容器環境常被 seccomp 擋掉,PGDG apt 套件在 build 時要看 --with-liburing flag,Debian 13 之後預設有開,Alpine 上打包的 Postgres image 就常常沒有。VPS 環境如果 host kernel 支援 io_uring 且不是嚴格 container,直接 io_method=io_uring 通常最直接。
Parallel autovacuum 拿的是 max_parallel_workers 的額度
autovacuum 的老問題是單執行緒。一張 500GB 的表帶 8 個 index,autovacuum 得依序掃 heap、依序清 index、依序做 cleanup,中間 dead tuple 累積、bloat 上升、query plan 走偏,剩下只能人工開 VACUUM (PARALLEL 4) 手動處理,或者裝 pg_repack 這類 extension 補。
19 版本把 parallel index vacuum 塞進 autovacuum 本身。兩個新設定:cluster 層級的 autovacuum_max_parallel_workers(預設 0,等於關閉)、per-table 的 autovacuum_parallel_workers(預設 -1,跟隨全域)。要啟用得手動設 autovacuum_max_parallel_workers = 2 或更高。
關鍵細節是 worker 來源。parallel autovacuum 的 worker 不是獨立的 process pool,而是從既有的 max_parallel_workers 借。這個池同時供 parallel query 用。也就是說,一支 report query 用掉 4 個 worker,同時 autovacuum 想開 2 個 worker 清 index,如果 max_parallel_workers 只有 4,autovacuum 會拿不到 worker、退回單執行緒。這是 19 版本在 VPS 上最容易踩雷的地方——不加大 max_parallel_workers,就等於在 autovacuum 跟 query 之間打架。
平行化的邊界也要看清楚。autovacuum 只在 index vacuuming 跟 index cleanup 兩個階段並行,heap scanning 跟 heap vacuuming 仍然單執行緒。每個 worker 一次處理一個 index,所以帶 2 個 index 的表就算開 8 個 worker 也只會用 2 個。此外每個 worker 各自佔一份 maintenance_work_mem,開 4 個 worker 加主 process 等於 5 倍記憶體。8GB VPS 上 maintenance_work_mem=512MB 加 4 個 parallel autovacuum worker,peak 就是 2.5GB,得算清楚。
VPS 上的三池預算怎麼分
以 8 vCPU、16GB RAM 的 VPS 跑 PostgreSQL 19、負載是中量 OLTP 加偶爾 report 的典型配置為例,比較合理的起點是:
1 | max_parallel_workers = 6 |
邏輯是 report query 最多吃 3 個 worker、parallel autovacuum 保留 2 個、剩下 1 個做緩衝;I/O worker 池獨立,上限吃到 vCPU 一半。這組值不是最優解,是不會馬上出事的起點,之後看 pg_stat_io 的 writes 分佈跟 pg_stat_progress_vacuum 的 index phase 時間再收斂。
小型 VPS(2 vCPU、4GB RAM)走另一條路:max_parallel_workers = 2、autovacuum_max_parallel_workers = 0 保持關閉、io_min_workers = 1、io_max_workers = 2、io_method = io_uring(如果 kernel 允許)。這個規模開 parallel autovacuum 收益很小,io_uring 又能省掉 worker overhead,資源全部丟給 OLTP session 反而順。
一個容易被忽略的邊界:max_parallel_workers 是從 max_worker_processes 這個更大的池切出來的,後者還要供 logical replication、background worker extension(例如 pg_cron、TimescaleDB)用。同時跑幾個 replication slot 加 parallel autovacuum 加 pg_cron 的環境,max_worker_processes 至少要拉到 max_parallel_workers + 8 才安全。
除了 worker 還有兩個預設反轉
EXPLAIN (ANALYZE, IO) 是 19 新加的選項,把 async I/O 活動 per plan node 展開:發出去幾個讀、實際完成幾個、傳輸多少 bytes。18 上要對照這些資訊得手動去撈 pg_stat_io,現在合成一個報表。慢 query 診斷流程可以省掉一半 join。
另外兩個容易漏掉的預設反轉值得放進升級 checklist:JIT 在 19 預設關掉(12 以來一直是預設開),原因是 large analytical query 上 cost model 判斷失準常常 JIT 反而更慢;toast 壓縮預設從 pglz 換成 lz4,寫入吞吐提升明顯但只影響新資料,舊 toast value 仍用 pglz。從 18 升 19 之後 query plan 該重新對一次,尤其是跑分析 workload 的環境。
REPACK 原生化那條線也值得順手提。19 把 REPACK ... CONCURRENTLY 做進 core,不需要 exclusive lock 就能重寫 bloated 表,替代掉多年來裝 pg_repack extension 的做法。前提是表要有 primary key 或 index-based replica identity,執行期需要約 2 倍的暫存空間。跟 parallel autovacuum 一起用能把「表已經腫到 autovacuum 追不上」這種舊帳處理掉。
NCSE Network 的臺灣 VPS 拿來測 PostgreSQL 19 剛好
PostgreSQL 19 的 GA 排在 2026 年秋季,Beta 3 已經進入 feature freeze。要在正式版落地前把 worker 預算重畫、parallel autovacuum、JIT 預設關閉這幾件事驗過一遍,直接開一臺乾淨 VPS 跑 PGDG beta apt repo 是最快的路徑。NCSE Network 提供的 VPS 主機跑在臺灣是方電訊機房、Intel Gold CPU 加 NVMe SSD,Debian 13、Ubuntu 26.04 等映像檔開機即用,kernel 6.x 全支援 io_uring。想了解相關方案細節,可以到 ncse.tw 查看。