Background processes
I/O workers
PostgreSQL 18 adds asynchronous I/O. With the default io_method = worker, a few io worker processes perform reads on behalf of backends, so a scan can keep many reads in flight instead of waiting for each one.
In plain words
Before PostgreSQL 18, a backend that needed a page from disk asked the operating system for it and waited. A sequential scan on slow (for example network-attached cloud) storage spent most of its time waiting, one read after another.
PostgreSQL 18 can ask for many reads at once and keep working while they complete. Where the reads are done depends on io_method: by default (worker) a few helper processes called io workers (io_workers, default 3) take read requests from a shared queue and perform them. With io_uring (Linux only) the backend hands them straight to the kernel. With sync it behaves like before.
Neighbouring blocks are also combined into one bigger read (up to io_combine_limit, 128 kB). Try the controls: on slow storage, more reads in flight make a big difference; on fast local disks, much less.
For DBAs and operators
What uses AIO in 18
Reads that go through the read stream interface: sequential scans, bitmap heap scans, VACUUM (heap and several index types), ANALYZE sampling, pg_prewarm and a few others. Writes (checkpointer, bgwriter, backends evicting dirty pages) and WAL are still synchronous in 18. Index scans of B-trees do not yet prefetch heap pages.
The three methods
io_method | Who performs the read |
|---|---|
worker (default) | The backend puts the I/O into a shared-memory submission queue and wakes an io worker; the worker does preadv() and marks the I/O complete in shared memory. Works everywhere. |
io_uring | The backend submits to its own io_uring; the kernel completes it. Linux only, needs a build with liburing; no extra processes, less overhead with many IOPS. |
sync | Eligible I/O is executed synchronously, as in PostgreSQL 17 (prefetch advice with posix_fadvise is still issued). |
io_method and io_max_concurrency need a restart; io_workers only a reload: the postmaster starts or stops workers. io_max_concurrency = -1 picks a value from shared_buffers and the number of processes, at most 64 I/Os per process. How far a stream reads ahead is bounded by effective_io_concurrency (default raised from 1 to 16 in 18) or maintenance_io_concurrency for maintenance work; 0 disables issuing asynchronous I/O requests (no read-ahead).
How to see it
$ ps -ef | grep "io worker"
postgres 847 842 0 17:28 ? 00:00:00 postgres: io worker 0
postgres 848 842 0 17:28 ? 00:00:00 postgres: io worker 1
postgres 849 842 0 17:28 ? 00:00:00 postgres: io worker 2
SELECT pid, state, operation, length, target_desc FROM pg_aios;
pid | state | operation | length | target_desc
-------+------------------+-----------+--------+---------------------------------------
1857 | COMPLETED_SHARED | readv | 131072 | blocks 5183..5198 in file "base/5/16437"pg_aios (new in 18) lists the I/O handles in use right now: state goes HANDED_OUT → DEFINED → STAGED → SUBMITTED → COMPLETED_IO → COMPLETED_SHARED. In pg_stat_activity io workers have backend_type = 'io worker' and wait on IoWorkerMain when idle; a backend waiting for an AIO shows wait event AioIoCompletion (IO), or AioIoUringExecution when it waits for its own io_uring completions. pg_stat_io has rows for io worker too, but reads an io worker performs are counted for the backend that issued them, both in pg_stat_io and in EXPLAIN (ANALYZE, BUFFERS).
Tuning and support notes
- Many concurrent scans on slow storage with few workers: the shared queue becomes the bottleneck (backends waiting in
AioIoCompletion, all io workers busy). Raiseio_workers(up to 32) or tryio_uring. - Fast local NVMe and data mostly in the OS cache: little difference between methods;
workeradds a small hand-off cost. io_uringneeds a server built with liburing and a kernel that allows it (some container seccomp profiles and hardened kernels block it). If it is not available, stay withworker.- An io worker is a normal child of the postmaster: if one is killed with SIGKILL the postmaster performs a crash restart; workers are not attached to any database and run no queries.
- Comparing performance with 17? Set
io_method = syncon 18 to isolate the effect of AIO.
For developers: inside the source
Where it lives
src/backend/storage/aio, with the design documentREADME.md. Generic code inaio.c,aio_io.c,aio_callback.c,aio_target.c; methods inmethod_sync.c,method_worker.c,method_io_uring.c.read_stream.c: the read stream used by scans;bufmgr.c:StartReadBuffers()/WaitReadBuffers().- Headers:
aio.h,aio_internal.h,read_stream.h.
Key data structures
PgAioHandle: one in-flight I/O;statefollowsPgAioHandleState(PGAIO_HS_IDLE,HANDED_OUT,DEFINED,STAGED,SUBMITTED,COMPLETED_IO,COMPLETED_SHARED,COMPLETED_LOCAL). Each backend ownsio_max_concurrencyhandles;pg_get_aios()(aio_funcs.c) exposes them aspg_aios.IoMethodOps: the per-method callbacks (submit,needs_synchronous_execution,wait_one, shmem sizing).- Worker method:
PgAioWorkerSubmissionQueue(a ring of handle indexes, 64 entries) underAioWorkerSubmissionQueueLock, andPgAioWorkerControlwith anidle_worker_maskand one latch per worker slot (maxMAX_IO_WORKERS= 32).
Call flow of one read (worker method)
read_stream_next_buffer() -> read_stream_look_ahead()
-> read_stream_start_pending_read() -> StartReadBuffers()
-> AsyncReadBuffers() -> pgaio_io_acquire_nb()
-> smgrstartreadv() -> mdstartreadv() -> FileStartReadV()
-> pgaio_io_start_readv() -> pgaio_io_stage()
-> pgaio_submit_staged() -> pgaio_worker_submit()pgaio_worker_submit() puts the handles in the queue and sets the latch of an idle worker; if the queue is full, it performs the I/O synchronously in the backend. IoWorkerMain() consumes an index, wakes up to IO_WORKER_WAKEUP_FANOUT (2) more idle workers when the queue is deep, reopens the file with pgaio_io_reopen() (the worker has no relcache/smgr state of the backend), runs pgaio_io_perform_synchronously() and the shared completion callbacks, which mark the buffers valid. The backend later calls WaitReadBuffers(), which waits on the handle if it is not complete (AioIoCompletion). Idle workers sleep in WaitLatch() (IoWorkerMain). The postmaster grows or shrinks the pool in maybe_adjust_io_workers() after a reload.
Experimenting
src/test/modules/test_aio: SQL functions and TAP tests that drive handles directly, for everyio_method.- Set
debug_io_direct = data(developer option, needs a restart) to bypass the OS cache and make I/O waits visible on a small test box. SELECT * FROM pg_aiosin a loop while a bigSELECT count(*)runs shows the queue depth;pg_stat_ioattributes the reads to the issuing backend.
Parameters that shape this
io_method io_workers io_max_concurrency io_combine_limit io_max_combine_limit effective_io_concurrency maintenance_io_concurrency
Related topics
Asynchronous I/O Scan methods Shared buffers Background writer Postmaster Process model VACUUM