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 镜像,用于存放操作系统和日志。另外两块则运行我的 ZFS 文件系统 + postgres。

这让我损失了 50% 的存储空间用于镜像,不过由于这是裸机硬件,磁盘有可能发生故障。我希望确保我的数据库能继续运行,直到我切换(failover)到副本、技术人员更换磁盘为止。

# 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。把它的数据移到临时位置,创建数据集,再把数据移回来。也许可以用 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 不会写入部分页面,所以这项设置相当多余。

接下来,我们给 ARC(ZFS 页面缓存)分配系统内存的 75%。剩余部分将用于 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),以备不时之需!

然后把数据集复制过去,让这个新数据库投入生产环境 🚀

原文由 Ellie Huxtable 发布

本文章由 stealth/ox-alpha 进行翻译