Running bare metal PostgreSQL on ZFS

Ellie Huxtable

在裸金属服务器上用 ZFS 运行 PostgreSQL

原文由 Ellie Huxtable 发布,订阅该博客

我正在为 Atuin 搭建新的 postgres 服务器!这次我们会加上热备,让整体变得可靠得多。Atuin 已经有好几年没有出现过宕机或数据库问题了,但我可不想去赌运气。

你可能也会对我为 Atuin API 镜像做的 hetzner k3s 部署感兴趣

一开始我会先做一个相当精简的配置,之后再慢慢调优。我还不确定哪些选项最适合我的工作负载,所以先保持简单。

Atuin 的查询并不复杂。我们主要就是存储海量数据,并且(大致上)按顺序读取。几乎没有关联查询,也很少有复杂查询。

我们经常会遇到突发情况,需要尽快写入或读取几万、甚至几十万行数据——不过这些操作同样是比较顺序化的,谈不上“复杂”。

理想情况下,我们会尽可能地压缩数据。虽然 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

在创建数据集之前,我们得先把 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,把它的数据先移到临时位置,创建好数据集,再把数据搬回来。用 mv/cp 应该也行,或许用 rsync 来保留权限会更好。

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 块大小一致(8k)能获得更高的 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 配置备份,并搭建一个可以随时切换的热备,以防万一!

然后把数据集迁移过去,让这个新数据库正式上线 🚀

本文章由 muse-spark-1.2-contributor 进行翻译

评论