自架服務 PostgreSQL io_uring UUIDv7 資料庫效能

PostgreSQL 18 把 I/O 路徑整個重寫:io_uring、UUIDv7、skip scan 對自架 VPS 資料庫的實際意義

過去二十年 Postgres 每個 backend 一次只能等一次磁碟 I/O。18.0 之後 io_method 切到 io_uring 或 worker,冷 cache 讀取可以做到 2 到 3 倍加速。UUIDv7、skip scan、pg_createsubscriber 一起把自架資料庫的效能地基跟升級路徑重新畫過。

PostgreSQL 18 從 2025 年 9 月釋出到現在快滿一年,點版本一路推進。表面上這是個常規大版本,但翻進 release notes 就會發現:Postgres 這次真的動了 I/O 路徑。過去二十年每個 backend 一次只能發一個磁碟讀取請求、發完就阻塞等回應,這個設計在 NVMe 時代早已成為明顯瓶頸。18 之後有了 asynchronous I/O 子系統,冷 cache 讀取實測可以做到 2 到 3 倍加速。對把資料庫塞在自架 VPS 上的團隊,這個版本值得認真評估。

除了 async I/O,UUIDv7、skip scan、pg_createsubscriber 這三件事各自解決了自架場景長年吃虧的問題。合起來看,18 不是「多加幾個功能」的版本,是把過去幾個結構性痛點一次處理完的版本。

一個 I/O 請求都要等,是 Postgres 過去二十年沒動的地方

Postgres 的 buffer manager 過去採取的是同步 I/O 模型:backend 要讀一個 8KB 的 page,就對 kernel 發一次 pread(),然後阻塞在那裡等資料回來。Linux kernel 幾年前就已經支援 io_uring、AIO 這類異步機制,MySQL InnoDB 也早就把並行 I/O 塞進 buffer pool,Postgres 卻遲遲沒動——原因不是不知道,而是要改動的地方觸及整個 storage manager 的鎖與生命週期,工程規模太大。

18 開始,這件事終於做完。新的 io_method 參數控制 backend 用哪一種底層路徑發 I/O,可選值是 sync(維持舊行為)、worker(用 background worker 幫忙發 I/O)、io_uring(直接呼叫 Linux io_uring)。預設是 worker,工作原理是 backend 把要讀的 page 丟到一個 shared queue,一批 io_worker process 從 queue 拉出來實際去 I/O,讀完再把結果回填給發起 backend。這個模式的好處是不挑核心版本、跨平臺行為一致;io_uring 則能省掉 context switch,延遲更低,但需要 Linux 5.1 以上核心且 Postgres build 時要有對應支援。

自架 VPS 幾乎都跑得動 io_uring。Debian 13、Ubuntu 24.04、AlmaLinux 9 系列核心都在 6.x,io_uring 支援完整。真正要注意的是 Postgres 官方 apt/dnf 套件在 build 時是否啟用了 --with-liburing——PGDG 的官方 repo 從 18 開始就把這個 flag 打開了,直接 apt install postgresql-18 進來的版本就能用。

io_method 該選哪個,效果差別在哪

worker 是保守選擇。它把並行度從單一 backend 拉到一個 process pool,io_workers 預設 3 個。單核 VPS 或者 vCPU 少於 4 的機器,這個值大概就夠。但對 8 vCPU 以上、跑 OLAP 或 report 查詢的機器,把 io_workers 拉到 vCPU 數的四分之一到二分之一(例如 8 核開 3、16 核開 6)通常都有肉眼可見的差別。

io_uring 是最激進的選項。因為省掉了 worker queue 的中間步驟,每一次 I/O 的 latency 更接近裸 kernel 呼叫的水準。在跑 sequential scan 或 bitmap heap scan 的查詢上,公開 benchmark 顯示 cold cache 情境的吞吐量比 sync 提升 2 到 3 倍。但 io_uring 也吃 file descriptor,在 shared_buffers 大、連線數多的機器上要調高 max_files_per_process 才不會撞到限制。

真正決定 async I/O 有沒有效果的關鍵,其實是 effective_io_concurrency。這個參數告訴 planner「同時能發幾個 I/O 請求」,過去它在 sync 模式下影響有限,18 之後直接掛鉤到 async 執行路徑。NVMe SSD 的 VPS 這個值該設在 100 到 300 之間,200 是不錯的起點。跑在 EBS 或雲端 block storage 上的 VPS,跨過 150 之後邊際效益就下滑得很快。

一個實務建議:不要一次動三個參數。先開 io_method = worker、加 effective_io_concurrency = 200,用 pg_stat_io 看 read/hit 累積計數的變化,同時對 pg_stat_activity 抽樣 wait_event_type='IO' 的分佈跑一週。真的看到 DataFileRead 這類 wait 佔比明顯下降但還有優化空間,再切 io_uring

UUIDv7 不只是「加了個新函式」

過去 Postgres 開發者要用 UUID 當 primary key,通常寫 gen_random_uuid() 生成 UUIDv4。UUIDv4 是純隨機值,好處是碰撞機率低、不洩漏時序資訊,缺點是插入時 B-tree 的 hot page 會不斷跳動——每次 insert 都要載入新的 index page 進 buffer,寫入放大嚴重、cache 命中率低落。這個問題在寫入密集的應用(訊息、log、audit trail)上會被放大。

UUIDv7 把「時間戳前綴 + 隨機後綴」寫進 RFC 9562。前 48 bit 是毫秒級 unix timestamp,其餘是亂數。18 內建 uuidv7() 函式,直接寫在 default expression 就能用。因為時間單調遞增,新插入的 row 幾乎都會落到 index 的右邊界,寫入變成 append 樣式,B-tree 的 fill factor 也用得更完整。

從 UUIDv4 遷移過去不必動 schema,欄位型別仍然是 uuid、儲存仍然是 16 byte。實測顯示,寫入密集場景的 insert throughput 可以提升 20% 到 40%,index 大小同時縮小 15% 上下。缺點是暴露了大致的建立時間,對隱私敏感的欄位仍然應該用 UUIDv4。

skip scan 拯救那些多欄索引的邊界情境

過去在 Postgres 上遇過這種情境:建了 (tenant_id, created_at) 的複合索引,但某個查詢只帶 created_at 過濾、沒帶 tenant_id,planner 就決定用 seq scan。原因是 B-tree 的搜尋規則要求從最左欄開始,缺一欄就不能用。

18 的 skip scan 直接繞過這個限制。缺前綴欄位時,planner 會產生一個「skip array」把可能的前綴值當成迭代條件,用 index 一次跳一段。這對 tenant 數量不多、每 tenant 資料量大的多租戶應用特別有效——過去為了避免 seq scan,通常會被迫多建一份 (created_at) 的單欄索引,現在可以省掉。

skip scan 不是萬靈丹。前綴欄位基數(cardinality)很高時,跳過的成本反而超過直接掃描,planner 會自動決定不用。理解這個機制就好,不需要顯式開關。

pg_createsubscriber 把主升級的停機壓進一秒

Postgres 過去做 major version upgrade 有三條路:pg_dumpall(久、大 downtime)、pg_upgrade(in-place、通常 30 秒到數分鐘)、logical replication(複雜、多環節)。18 把 pg_createsubscriber 這支工具打磨到能實際用的程度,等於把 logical replication 那條路做成「三個指令跑完」的操作。

流程是這樣:在舊 primary(例如 17.x)旁邊架一臺 physical standby,跑 pg_createsubscriber 把這臺 standby 原地轉成 logical subscriber,然後對這臺已經是 subscriber 身分的節點跑 pg_upgrade 升到 18.x。舊 primary 仍在接寫入,新節點透過 logical replication 追上,切換時只要 promote 新節點、把 app 的連線字串改指過去,實際 writeable downtime 通常在一秒內。

這對自架 VPS 有實務意義:過去自架 Postgres 團隊做主版本升級,常見做法是在半夜掛維護頁做 pg_upgrade,1 分鐘內做完就萬幸。有了這條 zero-downtime 路徑,白天升級變成可行選項。前提是有兩臺以上機器可以組 standby 架構,單機 VPS 仍然只能走傳統 pg_upgrade。

升不升 18,哪些條件先看

需要跑重讀取工作負載(analytics、report、search)又跑在 NVMe 上的 VPS,18 幾乎沒有理由不升。async I/O 帶來的效能提升是免費的,切一下 io_method 就有。

用 UUID 當 primary key 且寫入密集的應用,也應該優先評估。把 default 從 gen_random_uuid() 換成 uuidv7(),改個 migration 就能享有 index locality 改善。舊資料不必回填,維持 UUIDv4 也不會有衝突,新舊混用是常態。

還在跑 15、16 這種還在支援期的版本、又沒有明顯 I/O 瓶頸的環境,倒不必急著跳。18 是新版本,一些 extension(TimescaleDB、pg_partman、PostGIS 這些)雖然多半已經支援,但實際遇到 bug 的機會仍然比 17 高。等後續幾個點版本累積得更成熟一點再升是穩健策略。

最不建議的做法是「一次跨兩個大版本」。從 14 直接跳 18 這種操作,statistics 遺失、planner 行為變化、extension 相容性三個問題會同時炸開。走 14 → 17 → 18 這種台階式路線,每一步都用 pg_createsubscriber 或 pg_upgrade 對接,遠比一次到位安全。

跑資料庫最實際的兩個變數是 CPU 單核效能與磁碟延遲。PostgreSQL 18 的 async I/O 讓後者變得容易攤平,但前者仍然直接影響 checkpoint、autovacuum、planner 的表現。NCSE Network 在臺灣是方電訊機房提供搭載 Intel Gold CPU 與 NVMe SSD 的 VPS 主機,單核效能與磁碟 IOPS 都在同價位段裡站得住腳,跑自架 Postgres 18 需要的 io_uring 支援、liburing 套件都在預設核心與套件庫裡就位。想把資料庫從雲端託管拉回自己控制的環境,這是可以直接接的下一步。

需要穩定的雲端主機?

NCSE Network 提供企業級 VPS,7 天免費試用,臺灣是方電訊機房,99% SLA 保證。

查看 VPS 方案 →