베어메탈에서 ZFS로 PostgreSQL 운영하기
postgres 서버를 새로 구축하고 있습니다. Atuin용이죠! 이번에는 핫 레플리카를 구성해서 안정성을 훨씬 더 높이려고 합니다. Atuin은 지난 몇 년간 장애나 데이터베이스 문제가 전혀 없었지만, 운을 시험하고 싶진 않습니다.
hetzner k3s 설정도 함께 참고해 보세요. Atuin API 이미지용으로 구성한 것입니다.
처음에는 꽤 최소한의 설정으로 시작하고 나중에 튜닝할 예정입니다. 어떤 옵션이 제 워크로드에 가장 잘 맞을지 아직 확신이 서지 않아서 최대한 단순하게 유지하려고 합니다.
참고로 Atuin의 쿼리는 복잡하지 않습니다. 대부분 꽤 많은 양의 데이터를 저장하고 (대체로) 순차적으로 읽는 형태입니다. 조인도 거의 없고 복잡한 쿼리도 거의 없습니다.
종종 수만에서 수십만 건의 행을 최대한 빠르게 쓰거나 읽어야 하는 버스트 구간이 있는데, 이 역시 대체로 순차적이고 전혀 "복잡"하지 않습니다.
가능하면 데이터를 최대한 압축하고 싶습니다. Atuin은 대부분 암호화된 데이터를 저장해서 압축이 잘 되지는 않지만, JSON 패딩이나 타임스탬프 등의 형태로 암호화되지 않은 데이터도 꽤 있습니다. 이 부분을 압축하는 거죠! CPU를 조금 쓰는 대신 디스크 사용량과 IO를 크게 줄일 수 있습니다.
쿼리가 단순한 만큼 CPU 비용은 감당할 만합니다.
설정하기
헤츠너 옥션에서 구매한 머신 두 대에서 이 작업을 진행하고 있습니다. 라이젠 CPU에 1TB NVMe SSD가 4개씩 장착된 사양입니다.
SSD 두 개는 단순 RAID 미러로 구성해 OS와 로그를 저장할 예정입니다. 나머지 두 개에는 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 partzpool 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를 설정해야 합니다. ZFS로 옮기기 전에 postgres가 초기화를 수행해야 합니다. 번거롭지만 어쩔 수 없습니다.
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 updateapt 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_walZFS 설정
다음을 포함해 여러 자료를 많이 참고했습니다.
- 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=51539607552Postgres 설정
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로 백업을 구성하고, 문제가 생겼을 때 페일오버할 수 있는 핫 스탠바이를 설정할 예정입니다!
그 다음 데이터셋을 옮기고, 이 새로운 데이터베이스를 프로덕션으로 전환할 계획입니다 🚀
글을 무작위로 읽기