在裸機上以 ZFS 執行 PostgreSQL
我正在為 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 mirror 運作,用來存放作業系統與日誌。另外兩顆則會用來放置我的 ZFS 檔案系統與 postgres。
這樣做會因為鏡像而犧牲 50% 的儲存空間,不過由於這是 bare metal(裸機)硬體,仍有可能發生磁碟故障。我希望確保資料庫能持續運作,直到我 failover(容錯移轉)至 replica,且技術人員完成磁碟更換為止。
# 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 mirror。
建立 mirror
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 updateapt install postgresql-16 postgresql-contrib-16
systemctl enable postgresql
systemctl start postgresql接著,停止 postgres。將其資料搬到暫存位置,建立 datasets,再把資料搬回來。或許也可以用 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 block size 相同的 8kb 可以獲得更高的 tps,但 Atuin 的存取大多是循序性的,且會讀寫大量資料。我可以更改 postgres 的 block size(這是編譯選項),但會留待日後再嘗試。
一開始,我會先嘗試預設的 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 page cache)。剩下的部分則由 postgres 的 shared_buffers 使用。
echo 51539607552 >> /sys/module/zfs/parameters/zfs_arc_max若要讓設定在重新開機後保持生效,請在 /etc/modprobe.d/zfs.conf 中設定:
options zfs zfs_arc_max=51539607552Postgres 設定
以下並非 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 設定備份,並建立一個可在發生問題時 failover 的 hot standby(熱待命)!
然後將 dataset 複製過去,讓這個新資料庫正式上線 🚀
隨機一篇部落格