Recently I’ve switched my PostgreSQL playground from Linux to FreeBSD. Not because I don’t like Linux anymore, just because I like trying out other operating systems and see what works the same and what is different. When you install FreeBSD and stick to the defaults, you’ll get a ZFS root filesystem and usually I don’t create separate partitions/mounts in my playgrounds for PostgreSQL so it went there as well. While playing a bit I got the impression that the whole system was faster than my previous Linux playground so I though it would be good to do some tests on Linux to compare the performance of ext4 (which is the default on Debian) and ZFS. So here we go.

I’ve used a t3.medium instance and two 20GB gp3 disks for the tests, one of them using an ext4 filesystem and the other one for ZFS. The version of PostgreSQL is 17, the operating system is Debian 12.

As ZFS is not available by default in the Debian repositories (I am not going into the licensing stuff), there are some extra steps to follow which are described in the Debian Wiki. Because ZFS comes as a kernel module, the kernel sources need to be available before it can be installed. For the AWS AMI for Debian this is:

postgres@ip-10-0-1-159:/home/postgres/ [pg17] apt search kernel | grep headers | grep cloud-amd64

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

linux-headers-6.1.0-21-cloud-amd64/stable-security 6.1.90-1 amd64
linux-headers-6.1.0-22-cloud-amd64/stable 6.1.94-1 amd64
linux-headers-6.1.0-23-cloud-amd64/stable-security 6.1.99-1 amd64
linux-headers-6.1.0-25-cloud-amd64/stable 6.1.106-3 amd64
linux-headers-6.1.0-26-cloud-amd64/stable-security 6.1.112-1 amd64
linux-headers-6.10.11+bpo-cloud-amd64/stable-backports 6.10.11-1~bpo12+1 amd64
linux-headers-6.10.6+bpo-cloud-amd64/stable-backports 6.10.6-1~bpo12+1 amd64
linux-headers-6.9.10+bpo-cloud-amd64/stable-backports 6.9.10-1~bpo12+1 amd64
linux-headers-6.9.7+bpo-cloud-amd64/stable-backports 6.9.7-1~bpo12+1 amd64
linux-headers-cloud-amd64/stable-security 6.1.112-1 amd64

postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo apt install -y linux-headers-cloud-amd64

Once that is ready, the backports repository needs to be added and then ZFS can be installed:

postgres@ip-10-0-1-159:/home/postgres/ [pg17] cat /etc/apt/sources.list
# See /etc/apt/sources.list.d/debian.sources
deb http://deb.debian.org/debian bookworm-backports main contrib non-free
postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo apt update
postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo apt install -t stable-backports zfsutils-linux -y

There will be a warning about the license incompatibilities, but as mentioned above I am not going into that topic:

The installation will take some time as the kernel modules will get build. Once it is done, the modules are available and can be loaded (happens automatically after a reboot):

...
Done.

zfs.ko:
Running module version sanity check.
 - Original module
   - No original module exists within this kernel
 - Installation
   - Installing to /lib/modules/6.1.0-26-cloud-amd64/updates/dkms/

spl.ko:
Running module version sanity check.
 - Original module
   - No original module exists within this kernel
 - Installation
   - Installing to /lib/modules/6.1.0-26-cloud-amd64/updates/dkms/
depmod...
Building initial module for 6.10.11+bpo-cloud-amd64
...
postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo modprobe zfs
postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo lsmod | grep zfs
zfs                  5771264  0
spl                   135168  1 zfs

As I’ve mentioned earlier, there are two disks I am going to use here:

postgres@ip-10-0-1-159:/home/postgres/ [pg17] lsblk | grep 20
nvme1n1      259:0    0   20G  0 disk 
nvme2n1      259:1    0   20G  0 disk 

The first one will be used for the ext4 file system:

postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo mkfs.ext4 /dev/nvme1n1
mke2fs 1.47.0 (5-Feb-2023)
Creating filesystem with 5242880 4k blocks and 1310720 inodes
Filesystem UUID: 8fb9e2b2-9b47-4963-8a2b-a6728a9bb16a
Superblock backups stored on blocks: 
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 
        4096000

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done   

postgres@ip-10-0-1-159:/home/postgres/ [pg17] mkdir -p /u02/pgdata/ext4
postgres@ip-10-0-1-159:/home/postgres/ [pg17] cat /etc/fstab | grep pgdata
/dev/nvme1n1 /u02/pgdata/ext4 ext4 noatime 0 0
postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo mount -a
postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo chown postgres:postgres /u02/pgdata/ext4
postgres@ip-10-0-1-159:/home/postgres/ [pg17] df -h | grep pgdata
/dev/nvme1n1      20G   24K   19G   1% /u02/pgdata/ext4

The second disk is for the ZFS file system:

postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo zpool create pgpool /dev/nvme2n1
postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo zpool list
NAME     SIZE  ALLOC   FREE  CKPOINT  EXPANDSZ   FRAG    CAP  DEDUP    HEALTH  ALTROOT
pgpool  19.5G   130K  19.5G        -         -     0%     0%  1.00x    ONLINE  -
postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo zfs create pgpool/pgzfs
postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo zfs set mountpoint=/u02/pgdata/zfs pgpool/pgzfs
postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo zfs set compression=zstd pgpool/pgzfs
postgres@ip-10-0-1-159:/home/postgres/ [pg17] df -h | grep zfs
pgpool/pgzfs      19G  128K   19G   1% /u02/pgdata/zfs
postgres@ip-10-0-1-159:/home/postgres/ [pg17] sudo chown postgres:postgres /u02/pgdata/zfs

As you can see above I’ve enabled compression using zstd (lz4 is another option, as well as others).

For the ext4 filesystem I’ll initialize PostgreSQL as we do it usually, which means enabling data checksums (this will anyway be the default starting with PostgreSQL 18):

ostgres@ip-10-0-1-159:/home/postgres/ [pg17] initdb --pgdata=/u02/pgdata/ext4/pg --data-checksums
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "C.UTF-8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

Data page checksums are enabled.

creating directory /u02/pgdata/ext4/pg ... ok
creating subdirectories ... ok
selecting dynamic shared memory implementation ... posix
selecting default "max_connections" ... 100
selecting default "shared_buffers" ... 128MB
selecting default time zone ... Etc/UTC
creating configuration files ... ok
running bootstrap script ... ok
performing post-bootstrap initialization ... ok
syncing data to disk ... ok

initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

    pg_ctl -D /u02/pgdata/ext4/pg -l logfile start

For the ZFS filestem we don’t need this, as ZFS is doing this anyway:

postgres@ip-10-0-1-159:/home/postgres/ [pg17] initdb --pgdata=/u02/pgdata/zfs/pg
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "C.UTF-8".
The default database encoding has accordingly been set to "UTF8".
The default text search configuration will be set to "english".

Data page checksums are disabled.

creating directory /u02/pgdata/zfs/pg … ok
creating subdirectories … ok
selecting dynamic shared memory implementation … posix
selecting default "max_connections" … 100
selecting default "shared_buffers" … 128MB
selecting default time zone … Etc/UTC
creating configuration files … ok
running bootstrap script … ok
performing post-bootstrap initialization … ok
syncing data to disk … ok

initdb: warning: enabling "trust" authentication for local connections
initdb: hint: You can change this by editing pg_hba.conf or using the option -A, or --auth-local and --auth-host, the next time you run initdb.

Success. You can now start the database server using:

    pg_ctl -D /u02/pgdata/zfs/pg -l logfile start

The only other two bits I’ll change for the ZFS instance, is to turn off full_page_writes as torn writes do not happen on ZFS and to turn off wal_init_zero which is also not required on ZFS (and of course another port):

postgres@ip-10-0-1-159:/home/postgres/ [pg17] echo "wal_init_zero='off'" >> /u02/pgdata/zfs/pg/postgresql.auto.conf
postgres@ip-10-0-1-159:/home/postgres/ [pg17] echo "full_page_writes='off'" >> /u02/pgdata/zfs/pg/postgresql.auto.conf
postgres@ip-10-0-1-159:/home/postgres/ [pg17] echo "port=5433'" >> /u02/pgdata/zfs/pg/postgresql.auto.conf

Ready to start both clusters:

postgres@ip-10-0-1-159:/home/postgres/ [pg17] pg_ctl --pgdata=/u02/pgdata/ext4/pg/ --log=/u02/pgdata/ext4/pg/log.log start
waiting for server to start.... done
server started
postgres@ip-10-0-1-159:/home/postgres/ [pg17] pg_ctl --pgdata=/u02/pgdata/zfs/pg/ --log=/u02/pgdata/zfs/pg/log.log start
waiting for server to start.... done
server started

No we’re ready to start with the tests, and this is the topic for the next post.