Running bare metal PostgreSQL on ZFS

Ellie Huxtable

在裸機上以 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 update
apt 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=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 設定備份,並建立一個可在發生問題時 failover 的 hot standby(熱待命)!

然後將 dataset 複製過去,讓這個新資料庫正式上線 🚀

原文由 Ellie Huxtable 發布

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