Running bare metal PostgreSQL on ZFS

Ellie Huxtable

在裸機上用 ZFS 執行 PostgreSQL

原文由 Ellie Huxtable 于 發布,訂閱此部落格

我正在為 Atuin 架設新的 postgres 伺服器!這次我們會加上熱備援(hot replica),讓整體變得更加可靠。Atuin 已經好幾年沒有發生過停機或資料庫問題了,但我不想繼續賭運氣。

你可能也會對我為 Atuin API 映像檔所做的 hetzner k3s 設定有興趣

一開始我會先做相當精簡的設定,之後再慢慢調整。我還不確定哪些選項最適合我的工作負載,所以先保持簡單就好。

順帶一提,Atuin 的查詢並不複雜。我們主要就是儲存相當大量的資料,然後(大致上)循序讀取。幾乎沒有什麼 join,也幾乎沒有複雜的查詢。

我們經常會遇到突發狀況,需要盡可能快速地寫入或讀取數萬、甚至數十萬筆資料——不過這同樣相當循序,完全稱不上「複雜」。

理想上,我們會盡可能地壓縮資料。雖然 Atuin 主要儲存的是加密資料(不太能壓縮),但仍有不少未加密的資料,像是 JSON 的填充資料、時間戳記等等。把它們壓起來!這能有效減少磁碟使用量,也能降低 IO——代價是多耗一點 CPU。

由於我們的查詢很單純,這點 CPU 成本完全可以接受。

設定環境

我是在 Hetzner 上透過競標買來的幾台機器上執行這些服務。它們配備 Ryzen CPU,以及 4 顆 1TB 的 NVMe SSD。

其中兩顆 SSD 會做成簡單的 RAID 鏡像,用來存放作業系統和日誌。另外兩顆則用來放我的 ZFS 檔案系統與 postgres。

這樣做會因為鏡像而犧牲掉 50% 的儲存空間,不過既然是裸機硬體,總有可能遇到磁碟故障。我希望確保即使有磁碟壞掉,資料庫也能持續運作,直到我切換到備援節點、並由機房人員更換磁碟為止。

# Install zfs
apt install zfsutils-linux

# check version
zfs version

用 lsblk 檢查你的磁碟配置

NAME        MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
nvme1n1     259:0    0 953.9G  0 disk
├─nvme1n1p1 259:1    0    32G  0 part
│ └─md0       9:0    0    32G  0 raid1 [SWAP]
├─nvme1n1p2 259:2    0     1G  0 part
│ └─md1       9:1    0  1022M  0 raid1 /boot
├─nvme1n1p3 259:3    0   128G  0 part
│ └─md2       9:2    0 127.9G  0 raid1 /var
├─nvme1n1p4 259:4    0     1K  0 part
└─nvme1n1p5 259:5    0 792.9G  0 part
  └─md3       9:3    0 792.7G  0 raid1 /
nvme0n1     259:6    0 953.9G  0 disk
nvme3n1     259:7    0 953.9G  0 disk
nvme2n1     259:8    0 953.9G  0 disk
├─nvme2n1p1 259:9    0    32G  0 part
│ └─md0       9:0    0    32G  0 raid1 [SWAP]
├─nvme2n1p2 259:10   0     1G  0 part
│ └─md1       9:1    0  1022M  0 raid1 /boot
├─nvme2n1p3 259:11   0   128G  0 part
│ └─md2       9:2    0 127.9G  0 raid1 /var
├─nvme2n1p4 259:12   0     1K  0 part
└─nvme2n1p5 259:13   0 792.9G  0 part
  └─md3       9:3    0 792.7G  0 raid1 /

/dev/nvme1n1 和 /dev/nvme2n1 已經被作業系統佔用,但 /dev/nvme0n1 和 /dev/nvme3n1 還空著,正好拿來做 ZFS 鏡像。

建立鏡像

zpool create postgres mirror -o ashift=12 /dev/nvme0n1 /dev/nvme3n1

執行後很快就完成了!

再用 lsblk 檢查一次,就能看到已經有使用痕跡了

nvme0n1     259:6    0 953.9G  0 disk
├─nvme0n1p1 259:14   0 953.9G  0 part
└─nvme0n1p9 259:15   0     8M  0 part
nvme3n1     259:7    0 953.9G  0 disk
├─nvme3n1p1 259:18   0 953.9G  0 part
└─nvme3n1p9 259:19   0     8M  0 part

用 zpool status 確認一下

  pool: postgres
 state: ONLINE
config:

	NAME         STATE     READ WRITE CKSUM
	postgres     ONLINE       0     0     0
	  mirror-0   ONLINE       0     0     0
	    nvme0n1  ONLINE       0     0     0
	    nvme3n1  ONLINE       0     0     0

errors: No known data errors

這個儲存池預設會掛載在 /postgres。不錯!

Postgres

在建立 dataset 之前,我們得先把 postgres 裝好。Postgres 需要先完成初始化,才能搬到 ZFS 上。有點麻煩,但也只能這樣。

Ubuntu 22.04 套件庫裡的版本有點舊,所以直接安裝 postgres 官方來源的版本

sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'

wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -

apt update
apt install postgresql-16 postgresql-contrib-16
systemctl enable postgresql
systemctl start postgresql

接著,先把 postgres 停掉。把資料移到暫存位置,建立好 dataset,再把資料搬回來。也可以用 rsync 而不是 mv/cp 來保留權限。

systemctl stop postgresql

# move postgres data to temp
mv /var/lib/postgresql/16/main/pg_wal /tmp/pg_wal
mv /var/lib/postgresql /tmp/postgresql

# create the datasets
zfs create postgres/data -o mountpoint=/var/lib/postgresql
zfs create postgres/wal -o mountpoint=/var/lib/postgresql/16/main/pg_wal

# switcharoo the data back
cp -r /tmp/postgresql/* /var/lib/postgresql
cp -r /tmp/pg_wal/* /var/lib/postgresql/16/main/pg_wal

# sort perms
chmod -R 0700 /var/lib/postgresql
chmod -R 0700 /var/lib/postgresql/16/main/pg_wal
chown -R postgres: /var/lib/postgresql

# start postgres once more
systemctl start postgresql

檢查一下是否一切正常:

zfs list

NAME                 USED  AVAIL     REFER  MOUNTPOINT
postgres             600K   922G       24K  /postgres
postgres/db           72K   922G       24K  /postgres/db
postgres/db/base      24K   922G       24K  /postgres/db/base
postgres/db/pg_wal    24K   922G       24K  /postgres/db/pg_wal

設定 ZFS

我參考了不少資料,包括:

  • https://vadosware.io/post/everything-ive-seen-on-optimizing-postgres-on-zfs-on-linux/
  • https://bun.uptrace.dev/postgres/tuning-zfs-aws-ebs.html

上面其中一篇文章提到,有些 NVMe 硬體在使用 fdatasync 時會回報寫入成功,但實際上卻沒有正確寫入。許多硬碟會在資料還只停留在揮發性寫入快取時就回報寫入成功,但實際上根本還沒落盤。用以下指令來停用這個行為:

apt install nvme-cli
nvme set-feature -f 6 -v 0 /dev/nvme0n1
nvme set-feature -f 6 -v 0 /dev/nvme3n1

我花了不少時間思考最佳的 recordsize。雖然把 recordsize 設成跟 postgres 區塊大小一樣的 8kb 會得到更高的 tps,但 Atuin 的存取大多是循序的,而且會一次讀寫大量資料。我可以改 postgres 的區塊大小(這是編譯時的選項),不過這個就留到以後再考慮。

一開始,我會先試試預設的 128k,看看表現如何。數值較小可能更快,但數值較大通常能得到更好的壓縮比。不過這取決於很多因素,所以我會實際測量看看。

# enable compression
zfs set compression=zstd-3 postgres

# disable access time (so, so many writes...)
zfs set atime=off postgres

# enable improved extended attributes
zfs set xattr=sa postgres

# zfs set recordsize=16k postgres

接著我在 postgres 那邊把 full_page_writes = off 關掉——ZFS 本來就不會寫入不完整的頁面,所以這個選項有點多餘。

接下來,我們會把系統記憶體的 75% 分給 ARC(ZFS 的頁面快取)。剩下的則留給 postgres 的 shared_buffers 使用。

echo 51539607552 >> /sys/module/zfs/parameters/zfs_arc_max

要讓設定在重開機後依然生效,請在 /etc/modprobe.d/zfs.conf 中加入以下設定

options zfs zfs_arc_max=51539607552

Postgres 設定

以下不是 ZFS 專屬的,而是一些通用的 postgres 調校

# 25% of 64GB
shared_buffers = 16GB

work_mem = 8MB

# make vaccuums/etc faster
maintenance_work_mem = 1GB

# tell the planner how much the ZFS ARC will likely cache
effective_cache_size = 48GB

還有不少可以調校的地方,但老實說,這幾個設定幫助最大。Postgres 預設的記憶體設定真的非常小!

到這個階段,我重新開機一次,確保一切正常、而且設定都有正確保留下來。

丟點負載來試試

我想確認系統表現至少還算正常,所以跑了 pgbench,得到以下結果

scaling factor: 50
query mode: simple number of clients: 20 
number of threads: 4 
maximum number of tries: 1 
number of transactions per client: 100000 
number of transactions actually processed: 2000000/2000000 
number of failed transactions: 0 (0.000%) 
latency average = 2.863 ms 
initial connection time = 10.287 ms 
tps = 6984.804317 (without initial connection time)

還不錯!雖然不太能代表我的實際工作負載,但至少證明系統沒有整個壞掉。之後我會再進一步調校。

下一步

接下來我會用 pgbackrest 設定備份,並建置一個熱備援(hot standby),以便出問題時可以切換過去!

然後把資料集複製過去,讓這個新的資料庫正式上線 🚀

本文章由 muse-spark-1.2-contributor 進行翻譯

留言