DB Internals AtlasPostgreSQL 18.6 Home About

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_methodWho 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_uringThe 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.
syncEligible 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). Raise io_workers (up to 32) or try io_uring.
  • Fast local NVMe and data mostly in the OS cache: little difference between methods; worker adds a small hand-off cost.
  • io_uring needs 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 with worker.
  • 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 = sync on 18 to isolate the effect of AIO.

For developers: inside the source

Where it lives

Key data structures

  • PgAioHandle: one in-flight I/O; state follows PgAioHandleState (PGAIO_HS_IDLE, HANDED_OUT, DEFINED, STAGED, SUBMITTED, COMPLETED_IO, COMPLETED_SHARED, COMPLETED_LOCAL). Each backend owns io_max_concurrency handles; pg_get_aios() (aio_funcs.c) exposes them as pg_aios.
  • IoMethodOps: the per-method callbacks (submit, needs_synchronous_execution, wait_one, shmem sizing).
  • Worker method: PgAioWorkerSubmissionQueue (a ring of handle indexes, 64 entries) under AioWorkerSubmissionQueueLock, and PgAioWorkerControl with an idle_worker_mask and one latch per worker slot (max MAX_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 every io_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_aios in a loop while a big SELECT count(*) runs shows the queue depth; pg_stat_io attributes 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