Running bare metal PostgreSQL on ZFS

Ellie Huxtable

ZFS上でベアメタルのPostgreSQLを動かす

原文は Ellie Huxtable により に公開されました。 このブログを購読する

Atuin用の新しいPostgresサーバーを構築しています!今回はAtuin向けにホットレプリカを導入して、信頼性を大幅に高めることにしました。Atuinはここ2年ほど障害もデータベースの問題も起きていませんが、運を試すようなことはしたくありません。

AtuinのAPIイメージ用に構築したhetzner k3sのセットアップにも興味があるかもしれません

まずはかなりミニマルな構成から始めて、後でチューニングしていく予定です。どのオプションがワークロードに最適かまだわからないので、シンプルにしておきます。

なお、Atuinのクエリは複雑ではありません。基本的に大量のデータを保存し、(ほぼ)シーケンシャルに読み取るだけです。JOINも複雑なクエリもほとんどありません。

数万〜数十万行をできるだけ速く書き込み・読み込む必要があるバーストが頻繁に発生しますが、これもかなりシーケンシャルで、決して「複雑」なものではありません。

できればデータは可能な限り圧縮したいと考えています。Atuinは主に暗号化されたデータを保存しており(これはあまり圧縮できません)、一方でJSONのパディングやタイムスタンプなど、暗号化されていないデータもそれなりにあります。それらを圧縮します!これによりディスク使用量をうまく削減できるだけでなく、多少のCPUコストと引き換えにI/Oも削減できます。

クエリがシンプルなので、CPUコストは問題ありません。

セットアップ

今回はオークションで購入したHetznerのマシン2台で実行しています。Ryzen CPUを搭載し、1TBのNVMe SSDが4本刺さっています。

うち2本はシンプルなRAIDミラーとして動かし、OSとログを保存します。残りの2本に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はOS用に使用済みですが、/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

上記記事のひとつでは、fdatasync使用時に一部のNVMeハードウェアが書き込み成功を報告するものの、実際には正しく書き込まれていないという問題が書かれていました。多くのドライブは、データが揮発性のライトキャッシュに保存された時点で書き込み成功を報告しますが、実際には永続化されていません。これを以下のコマンドで無効にします:

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でバックアップを設定し、万が一の際にフェイルオーバーできるホットスタンバイを構築します!

その後、データセットをコピーして、この新しいデータベースを本番環境にします 🚀

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント