Should io_method=worker remain the default?
Jeff Davis <pgsql@j-davis.com>
From: Jeff Davis <pgsql@j-davis.com>
To: pgsql-hackers@postgresql.org
Date: 2025-09-03T06:47:48Z
Lists: pgsql-hackers
Has there already been a discussion about leaving the default as
io_method=worker? There was an Open Item for this, which was closed as
"Won't Fix", but the links don't explain why as far as I can see.
I tested a concurrent scan-heavy workload (see below) where the data
fits in memory, and "worker" seems to be 30% slower than "sync" with
default settings.
I'm not suggesting that AIO overall is slow -- on the contrary, I'm
excited about AIO. But if it regresses in some cases, we should make a
conscious choice about the default and what kind of tuning advice needs
to be offered.
I briefly tried tuning to see if a different io_workers value would
solve the problem, but no luck.
The good news is that io_uring seemed to solve the problem.
Unfortunately, that's platform-specific, so it can't be the default. I
didn't dig in very much, but it seemed to be at least as good as "sync"
mode for this workload.
Regards,
Jeff Davis
Test summary: 32 connections each perform repeated sequential scans.
Each connection scans a different 1GB partition of the same table. I
used partitioning and a predicate to make it easier to script in
pgbench.
Test details:
Machine:
AMD Ryzen 9 9950X 16-Core Processor
64GB RAM
Local storage, NVMe SSD
Ubuntu 24.04 (Linux 6.11, liburing 2.5)
Note: the storage didn't matter much, because the data fits in
memory. To get consistent results, when changing between data
directories for the 17 and 18 tests, I had to drop the filesystem cache
first to make room, then run a few scans to warm it with the data from
the right data directory.
For simplicity I disabled parallel query, but that didn't seem to have
a big effect. Everything else was set to the default.
Setup (checksums enabled):
=> create table t(sid int8, c0 int8, c1 int8, c2 int8, c3 int8, c4
int8, c5 int8, c6 int8, c7 int8) partition by range (sid);
$ (for i in `seq 0 31`; do
echo "create table t$(printf "%02d" $i) partition of t for
values from ($i) to ($((i+1)));";
done) | ./bin/psql postgres
$ (for i in `seq 0 31`; do
echo "insert into t$(printf "%02d" $i) select $i, 0, 1, 2, 3,
4, 5, 6, 7 from generate_series(0, 10000000);";
done) | ./bin/psql postgres
=> vacuum analyze; checkpoint;
Script count.sql:
SELECT COUNT(*) FROM t WHERE sid=:client_id;
pgbench:
./bin/pgbench --dbname=postgres -M prepared -n -c 32 -T 60 \
-f count.sql
Results:
PG17:
tps = 36.209048
PG18 (io_method=sync)
tps = 34.014890
PG18 (io_method=worker io_workers=3)
tps = 23.938509
PG18 (io_method=worker io_workers=16)
tps = 16.734360
PG18 (io_method=io_uring)
tps = 35.546825
Commits
-
meson: Build numeric.c with -ftree-vectorize.
- 9016fa7e3bcd 19 (unreleased) landed
- 87ea6e9b66b6 18.0 landed
- 509c779293f2 16.11 landed
- 15f9eeef624f 17.7 landed
-
meson: build checksums with extra optimization flags.
- 2de24ca6ca51 16.11 landed
- e25453a3631f 17.7 landed
- a7024398b80a 18.0 landed
- 9af672bcb245 19 (unreleased) landed