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 進行翻譯

留言