15 Dezember 2025

PostgreSQL 18 Asynchrone Festplatten-E/A – Detaillierter Einblick in die Implementierung

KI-generiertes Bild, das eine asynchron laufende Datenbank symbolisiertPostgreSQL 17 führte Streaming-E/A ein – das Gruppieren mehrerer Seitenlesevorgänge in einen einzigen Systemaufruf und die Verwendung intelligenterer posix_fadvise()-Hinweise. Allein das führte in einigen Workloads zu bis zu ~30 % schnelleren sequenziellen Scans, aber es war immer noch strikt synchron: Jeder Backend-Prozess führte einen Lesevorgang aus und wartete dann darauf, dass der Kernel Daten zurückgab, bevor er fortfuhr. Vor PG17 las PostgreSQL typischerweise eine 8-kB-Seite gleichzeitig.

PostgreSQL 18 geht den nächsten logischen Schritt: ein vollständiges asynchrones E/A-Subsystem (AIO), das mehrere Lesevorgänge gleichzeitig ausführen kann, während die Backends weiterhin nützliche Arbeit leisten. Lesevorgänge werden überlappend statt nur serialisiert. Das AIO-Subsystem ist gezielt auf Operationen ausgerichtet, die ihre zukünftigen Blocknummern im Voraus kennen und mehrere Lesevorgänge im Voraus ausführen können:
  • Sequenzielle Heap-Scans, wie einfache SELECT- und COPY-Operationen, die viele Daten streamen
  • VACUUM auf großen Tabellen und Indizes
  • ANALYZE-Stichproben
  • Bitmap-Heap-Scans

Autovacuum profitiert ebenfalls von dieser Änderung, da seine Worker dieselben VACUUM/ANALYZE-Codepfade verwenden. Andere Operationen bleiben vorerst synchron:

  • B‑Baum-Indexscans / Index‑Only-Scans
  • Wiederherstellung & Replikation
  • Alle Schreiboperationen INSERT, UPDATE, DELETE, WAL-Schreibvorgänge
  • Kleine OLTP-Lookups, die eine einzelne Heap-Seite berühren

Es wird erwartet, dass zukünftige Arbeiten die Abdeckung erweitern, insbesondere Index‑Only-Scans und einige Optimierungen des Schreibpfads.

Signifikante Verbesserungen für Cloud-Volumes

Community-Benchmarks zeigen, dass PostgreSQL 18 AIO die Kaltcache-Datenlesevorgänge in Cloud-Setups mit netzwerkgebundenem Speicher, bei denen die Latenz hoch ist, deutlich verbessert. Die AWS-Dokumentation besagt, dass die durchschnittliche Latenz von Block Express-Volumes „unter 500 Mikrosekunden für 16 KiB E/A-Größe“ liegt, während die Latenz von General Purpose-Volumes 800 Mikrosekunden überschreiten kann. Einige Artikel legen nahe, dass unter hoher Last jeder physische Block, der von der Festplatte gelesen wird, etwa 1 ms kosten kann, während die Seitenverarbeitung in PostgreSQL viel günstiger ist. Indem wir viele Seiten in einem Lesevorgang kombinieren, kosten alle diese Seiten zusammen jetzt etwa 1 ms. Und indem wir mehrere Leseanforderungen gleichzeitig parallel ausführen, zahlen wir diese 1 ms Latenz effektiv nur einmal pro Batch.

Asynchrone E/A-Methoden

Das neue Subsystem kann in einem von drei Modi ausgeführt werden, die über den Parameter io_method mit den möglichen Werten „worker“ (Standard), „io_uring“ und „sync“ konfiguriert werden. Wir werden erläutern, wie die einzelnen Modi funktionieren, und dann zeigen, wie die asynchrone E/A in unserer Umgebung überwacht werden kann.

io_method = sync

Dieser Modus schaltet AIO effektiv aus. Lesevorgänge werden über dieselbe AIO-API, aber synchron ausgeführt, wobei reguläre preadv- oder pwritev-Methoden auf dem Backend-Prozess verwendet werden, der die E/A ausgegeben hat. Diese Methode verwendet keinen zusätzlichen gemeinsam genutzten Speicher und ist hauptsächlich für Regressionstests gedacht oder wenn wir vermuten, dass sich AIO falsch verhält. Sie wird auch intern als Fallback auf die synchrone E/A für Operationen verwendet, die keine asynchrone E/A verwenden können. PostgreSQL-Kernfunktionen geben einen Fehler aus, wenn eine Erweiterung versuchen würde, die asynchrone E/A über die AIO-API zu erzwingen, wenn die globale io_method auf „sync“ gesetzt ist. Verfügbare Benchmarks zeigen, dass dieser PostgreSQL 18-Modus ähnlich wie die Streaming-E/A von PostgreSQL 17 funktioniert.

io_method = io_uring (Linux only)
Auf modernen Linux-Systemen (Kernel-Version 5.1 oder höher) kann PostgreSQL direkt mit der io_uring-Schnittstelle des Kernels kommunizieren. Die Verwendung erfordert, dass PostgreSQL mit liburing-Unterstützung gebaut wird – wir können dies in PostgreSQL mit der Funktion select from pg_config() überprüfen:
SELECT pg_config FROM pg_config() where pg_config::text ilike ’%liburing%’;
Asynchrone E/A-Operationen von PostgreSQL (sowohl io_uring als auch Worker) verwenden gemeinsam genutzte Speicherstrukturen, um die Anforderungen auszugeben und Informationen über deren Abschluss oder Fehler zu empfangen. Auf diese Weise kann der PostgreSQL AIO-Code Batching und Parallelität verwalten, ohne direkt von einer bestimmten AIO-Methode abhängig zu sein. Der PostgreSQL-Code verwaltet eine separate io_uring-Instanz für jedes Backend, einschließlich Hilfsprozesse. Die Ringe werden jedoch im Postmaster erstellt, sodass sie gemeinsam genutzten Speicher verwenden können und es keine Konflikte oder Blockierungen zwischen den Backends gibt.
Das Verarbeitungsszenario ist sehr einfach:
  1. Backends schreiben Anforderungen über die API in einen Submission-Ring im gemeinsam genutzten Speicher
  2. Der Kernel führt E/A asynchron aus und schreibt Ergebnisse in einen Completion-Ring
  3. Der Inhalt des Completion-Rings wird vom Backend mit weniger Kontextwechseln verarbeitet

Die Ausführung erfolgt weiterhin im selben Prozess wie bei der Methode „sync„, aber es werden Kernel-Worker-Threads für die parallele Verarbeitung verwendet. Dies ist typischerweise bei sehr schnellen NVMe-SSDs von Vorteil.

Die io_uring-Linux-Funktion hatte jedoch auch eine schwierige Sicherheitshistorie. Sie umgeht traditionelle Syscall-Audit-Pfade und war daher an einem großen Teil der Linux-Kernel-Exploits beteiligt. Google berichtete, dass 60 % der Linux-Kernel-Schwachstellen im Jahr 2022 io_uring betrafen und einige Sicherheitstools diese Art von Angriffen nicht aufdecken konnten. Daher deaktivieren einige Containerumgebungen io_uring vollständig.

io_method = worker

Dies ist die plattformübergreifende, „sichere“ Implementierung und der Standard in PostgreSQL 18. Der Mechanismus ist dem bestehenden parallelen Abfrageverarbeitung sehr ähnlich. Der Hauptunterschied besteht darin, dass Hintergrund-E/A-Worker langlebige, unabhängige Prozesse sind, die beim Serverstart erstellt werden, nicht kurzlebige Prozesse, die pro Abfrage erzeugt werden.

Typischer Ablauf:
  1. Beim Serverstart erstellt der Postmaster einen Pool von E/A-Worker-Prozessen. Die Anzahl wird durch den Parameter io_workers mit einem Standardwert von 3 gesteuert. Benchmarks legen jedoch nahe, dass diese Zahl auf vielen Kernmaschinen höher sein sollte, typischerweise zwischen ¼ und ½ der verfügbaren CPU-Threads. Der beste Wert hängt von der Workload und der Speicherlatenz ab.
  2. Backends übermitteln Leseanforderungen in eine gemeinsam genutzte Speicher-Submission-Queue. Diese Submission-Queue ist im Allgemeinen ein Ringpuffer, in den mehrere Backends gleichzeitig schreiben können. Sie enthält nur Metadaten über die Anforderung – Handle-Indizes, nicht den vollständigen Anforderungsdatensatz. Es gibt nur eine Submission-Queue für den gesamten Cluster, nicht pro Datenbank oder pro Backend. Die tatsächlichen Details der Anforderung werden in einer separaten Speicherstruktur gespeichert.
  3. Es wird geprüft, ob die Anforderung synchron ausgeführt werden muss oder asynchron verarbeitet werden kann. Die synchrone Ausführung kann auch gewählt werden, wenn die Submission-Queue voll ist. Dies vermeidet Probleme mit der gemeinsam genutzten Speichernutzung unter extremer Last. Im Falle einer synchronen Ausführung verwendet der Code den Pfad für die oben beschriebene „sync“-Methode.
  4. Die Anforderungsübermittlung im gemeinsam genutzten Speicher weckt einen E/A-Worker auf, der die Anforderung abruft und traditionelle blockierende read() / pread()-Aufrufe ausführt. Wenn die Queue noch nicht leer ist, kann der aufgeweckte Worker 2 zusätzliche Worker aufwecken, um sie parallel zu verarbeiten. Im Code wird erwähnt, dass dies in Zukunft auf konfigurierbare N Worker erweitert werden kann. Dieses Limit hilft, das sogenannte „Thundering Herd Problem“ zu vermeiden, bei dem ein einzelner Submitter zu viele Worker aufwecken würde, was zu Chaos und Sperren für andere Backends führen würde.
  5. Eine Einschränkung für die asynchrone E/A ist die Tatsache, dass Worker nicht einfach von Backends geöffnete Dateideskriptoren wiederverwenden können, sondern Dateien in ihrem eigenen Kontext neu öffnen müssen. Wenn dies für einige Arten von Operationen nicht möglich ist, wird für diese spezifische Anforderung der synchrone E/A-Pfad verwendet.
  6. Wenn Worker eine Anforderung ohne Fehler abschließen, schreiben sie Datenblöcke in gemeinsam genutzte Puffer, legen das Ergebnis in eine Completion-Queue und signalisieren das Backend.
  7. Aus der Perspektive des Backends wird die E/A „asynchron“, da das „Warten“ in Worker-Prozessen stattfindet, nicht im Abfrageprozess selbst.
Vorteile dieses Ansatzes:
  • Funktioniert auf allen unterstützten Betriebssystemen
  • Einfache Fehlerbehandlung: Wenn ein Worker abstürzt, werden Anforderungen als fehlgeschlagen markiert, der Worker beendet und ein neuer Worker vom Postmaster erzeugt
  • Vermeidet die Sicherheitsbedenken bezüglich der Linux io_uring-Schnittstelle
  • Der Nachteil sind zusätzliche Kontextwechsel und mögliche Konflikte in der gemeinsam genutzten Speicher-Queue, aber für viele Workloads macht die Möglichkeit, Lesevorgänge einfach zu überlappen, das leicht wett.
  • Diese Methode verbessert die Leistung auch dann, wenn alle Blöcke nur aus dem lokalen Linux-Speichercache kopiert werden, da dies jetzt parallel erfolgt

Tuning der neuen E/A-Parameter

PostgreSQL 18 fügt mehrere Parameter im Zusammenhang mit Festplatten-E/A hinzu oder aktualisiert sie. Wir haben bereits io_method und io_workers behandelt; sehen wir uns die anderen an. Weitere neue Parameter sind io_combine_limit und io_max_combine_limit. Sie steuern, wie viele Datenseiten PostgreSQL in einer einzigen AIO-Anforderung gruppiert. Größere Anforderungen führen typischerweise zu einem besseren Durchsatz, können aber auch die Latenz und die Speichernutzung erhöhen. Werte ohne Einheiten werden in 8-kB-Datenblöcken interpretiert. Mit Einheiten (kB, MB) stellen sie direkt die Größe dar – sollten jedoch Vielfache von 8 kB sein.

Der Parameter io_max_combine_limit ist eine feste Obergrenze beim Serverstart, io_combine_limit ist der vom Benutzer einstellbare Wert, der zur Laufzeit geändert werden kann, aber das Maximum nicht überschreiten darf. Die Standardwerte für beide sind 128 kB (16 Datenseiten). Die Dokumentation empfiehlt jedoch, unter Unix bis zu 1 MB (128 Datenseiten) und unter Windows 128 kB (16 Datenseiten – aufgrund von Einschränkungen in internen Windows-Puffern) einzustellen. Wir können mit höheren Werten experimentieren, aber basierend auf HW- und OS-Limits erreichen die AIO-Vorteile nach einer bestimmten Chunk-Größe ein Plateau; ein zu hohes Einstellen hilft nicht und kann sogar die Latenz erhöhen.

PostgreSQL 18 führt auch die Einstellung io_max_concurrency ein, die die maximale Anzahl von E/As steuert, die ein Prozess gleichzeitig ausführen kann. Die Standardeinstellung -1 bedeutet, dass der Wert automatisch basierend auf anderen Einstellungen ausgewählt wird, aber er darf 64 nicht überschreiten.

Ein weiterer verwandter Parameter ist effective_io_concurrency – die Anzahl der gleichzeitigen E/A-Operationen, die gleichzeitig auf dem Speicher ausgeführt werden können. Der Wertebereich liegt zwischen 1 und 1000, der Wert 0 deaktiviert asynchrone E/A-Anforderungen. Der Standardwert ist jetzt 16, einige Community-Artikel empfehlen, auf modernen SSDs bis zu 200 zu gehen. Die beste Einstellung hängt von der spezifischen Hardware und dem Betriebssystem ab, einige Artikel warnen jedoch auch davor, dass ein zu hoher Wert die E/A-Latenz für alle Abfragen erheblich erhöhen kann.

So überwachen Sie asynchrone E/A

pg_stat_activity
Für io_method = worker sind Hintergrund-E/A-Worker in pg_stat_activity als backend_type = ‚io worker‘ sichtbar. Sie zeigen die Werte wait_event_type / wait_event Activity / IoWorkerMain an, wenn sie im Leerlauf sind, oder typischerweise IO / DataFileRead, wenn sie mit der Arbeit beschäftigt sind.
SELECT pid, backend_start, wait_event_type, wait_event, backend_type
FROM pg_stat_activity
WHERE backend_type = 'io worker';


  pid | backend_start.                | wait_event_type | wait_event   | backend_type
------+-------------------------------+-----------------+--------------+--------------
   34 | 2025-12-09 11:44:23.852461+00 | Activity        | IoWorkerMain | io worker
   35 | 2025-12-09 11:44:23.852832+00 | Activity        | IoWorkerMain | io worker
   36 | 2025-12-09 11:44:23.853119+00 | IO              | DataFileRead | io worker
   37 | 2025-12-09 11:44:23.8534+00   | IO              | DataFileRead | io worker
Wir können pg_stat_io mit pg_stat_activity kombinieren, um zu sehen, welche Backends AIO-Anforderungen ausgeben, welche Abfragen sie ausführen und wie ihr aktueller AIO-Status ist:
SELECT a.pid, a.usename, a.application_name, a.backend_type, a.state, a.query,
ai.operation, ai.state AS aio_state, ai.length AS aio_bytes, ai.target_desc
FROM pg_aios ai
JOIN pg_stat_activity a ON a.pid = ai.pid
ORDER BY a.backend_type, a.pid, ai.io_id;


 -[ RECORD 1 ]----+------------------------------------------------------------------------
 pid              | 58
 usename          | postgres
 application_name | psql
 backend_type     | client backend
 state            | active
 query.           | explain analyze SELECT ........
 operation        | readv
 aio_state        | SUBMITTED
 aio_bytes        | 704512
 target_desc      | blocks 539820..539905 in file "pg_tblspc/16647/PG_18_202506291/5/16716"
 -[ RECORD 2 ]----+------------------------------------------------------------------------
 pid              | 159
 usename          | postgres
 application_name | psql
 backend_type     | parallel worker
 state            | active
 query            | explain analyze SELECT ........
 operation        | readv
 aio_state        | SUBMITTED
 aio_bytes        | 704512
 target_desc      | blocks 536326..536411 in file "pg_tblspc/16647/PG_18_202506291/5/16716"

pg_aios: Current AIO handles
PostgreSQL 18 führt mehrere neue Beobachtungsfunktionen ein, die uns helfen, die asynchrone E/A in Aktion zu überwachen. Die neue Systemansicht pg_aios listet aktuell verwendete asynchrone E/A-Handles auf – im Wesentlichen „E/A-Anforderungen, die vorbereitet, ausgeführt oder abgeschlossen werden“.
Die wichtigsten Spalten für jedes Handle sind:
  • pid: Backend, das die E/A ausgibt
  • io_id, io_generation: identifizieren ein Handle über die Wiederverwendung hinweg
  • state: HANDED_OUT, DEFINED, STAGED, SUBMITTED, COMPLETED_IO, COMPLETED_SHARED, COMPLETED_LOCAL
  • operation: invalid, readv (vektorisierter Lesevorgang) oder writev (vektorisierter Schreibvorgang)
  • off, length: Offset und Größe der E/A-Operation
  • target, target_desc: was wir lesen/schreiben (typischerweise Relationen)
  • result: UNKNOWN, OK, PARTIAL, WARNING, ERROR
Wir können einige einfache Statistiken aller aktuell laufenden E/As generieren, gruppiert nach Status und Ergebnis:
-- Zusammenfassung der aktuellen AIO-Handles nach Status und Ergebnis
SELECT state, result, count(*) AS cnt, pg_size_pretty(sum(length)) AS total_size
FROM pg_aios GROUP BY state, result ORDER BY state, result;

 state            | result  | cnt | total_size
------------------+---------+-----+------------
 COMPLETED_SHARED | OK      | 1   | 688 kB
 SUBMITTED        | UNKNOWN | 6   | 728 kB

-- In-flight async I/O handles
SELECT COUNT(*) AS aio_handles, SUM(length) AS aio_bytes FROM pg_aios;

 aio_handles | aio_bytes
-------------+-----------
   7         | 57344

-- Sessions currently waiting on I/O
SELECT COUNT(*) AS sessions_waiting_on_io FROM pg_stat_activity WHERE wait_event_type = 'IO';

 sessions_waiting_on_io
------------------------
  9
Oder wir können es verwenden, um Details zu aktuellen AIO-Anforderungen anzuzeigen:
SELECT pid, state, operation, pg_size_pretty(length) AS io_size, target_desc, result
FROM pg_aios ORDER BY pid, io_id;

 pid | state     | operation | io_size    | target_desc                                                             | result
-----+-----------+-----------+------------+-------------------------------------------------------------------------+---------
  51 | SUBMITTED | readv     | 688 kB     | blocks 670470..670555 in file "pg_tblspc/16647/PG_18_202506291/5/16716" | UNKNOWN
  63 | SUBMITTED | readv     | 8192 bytes | block 1347556 in file "pg_tblspc/16647/PG_18_202506291/5/16719"         | UNKNOWN
  65 | SUBMITTED | readv     | 688 kB     | blocks 671236..671321 in file "pg_tblspc/16647/PG_18_202506291/5/16716" | UNKNOWN
  66 | SUBMITTED | readv     | 8192 bytes | block 1344674 in file "pg_tblspc/16647/PG_18_202506291/5/16719"         | UNKNOWN
  67 | SUBMITTED | readv     | 8192 bytes | block 1337819 in file "pg_tblspc/16647/PG_18_202506291/5/16719"         | UNKNOWN
  68 | SUBMITTED | readv     | 688 kB     | blocks 672002..672087 in file "pg_tblspc/16647/PG_18_202506291/5/16716" | UNKNOWN
  69 | SUBMITTED | readv     | 688 kB     | blocks 673964..674049 in file "pg_tblspc/16647/PG_18_202506291/5/16716" | UNKNOWN
pg_stat_io: Cumulative I/O stats
Die Katalogansicht pg_stat_io wurde in PostgreSQL 16 eingeführt, aber PostgreSQL 18 erweitert sie um Byte-Zähler (read_bytes, write_bytes, extend_bytes) und eine bessere Abdeckung von WAL- und Bulk-I/O-Kontexten. Die Timing-Spalten werden jedoch nur gefüllt, wenn wir die Timing-Parameter aktivieren – track_io_timing – Standard ist aus.
Eine praktische, clientseitige Ansicht der Beziehungs-I/O:
SELECT backend_type, context, sum(reads) AS reads, 
pg_size_pretty(sum(read_bytes)) AS read_bytes, 
round(sum(read_time)::numeric, 2) AS read_ms, sum(writes) AS writes, 
pg_size_pretty(sum(write_bytes)) AS write_bytes,
round(sum(write_time)::numeric, 2) AS write_ms, sum(extends) AS extends,
pg_size_pretty(sum(extend_bytes)) AS extend_bytes
FROM pg_stat_io
WHERE object = 'relation' AND backend_type IN ('client backend')
GROUP BY backend_type, context
ORDER BY backend_type, context;

 backend_type   | context   | reads   | read_bytes | read_ms   | writes | write_bytes | write_ms | extends | extend_bytes
----------------+-----------+---------+------------+-----------+--------+-------------+----------+---------+--------------
 client backend | bulkread  | 13833   | 9062 MB    | 124773.28 | 0      | 0 bytes     | 0.00     |         |
 client backend | bulkwrite | 0       | 0 bytes    | 0.00      | 0      | 0 bytes     | 0.00     | 0       | 0 bytes
 client backend | init      | 0       | 0 bytes    | 0.00      | 0      | 0 bytes     | 0.00     | 0       | 0 bytes
 client backend | normal    | 2265214 | 17 GB      | 553940.57 | 0      | 0 bytes     | 0.00     | 0       | 0 bytes
 client backend | vacuum    | 0       | 0 bytes    | 0.00      | 0      | 0 bytes     | 0.00     | 0       | 0 bytes


-- Top-Tabellen nach gelesenen Heap-Blöcken und Cache-Trefferrate

SELECT relid::regclass AS table_name, heap_blks_read, heap_blks_hit,
ROUND( CASE WHEN heap_blks_read + heap_blks_hit = 0 THEN 0
ELSE heap_blks_hit::numeric / (heap_blks_read + heap_blks_hit) * 100 END, 2) AS cache_hit_pct
FROM pg_statio_user_tables
ORDER BY heap_blks_read DESC LIMIT 20;

 table_name           | heap_blks_read | heap_blks_hit | cache_hit_pct
----------------------+----------------+---------------+---------------
 table1               | 18551282       | 3676632       | 16.54
 table2               | 1513673        | 102222970     | 98.54
 table3               | 19713          | 1034435       | 98.13
 ...


-- Top-Indizes nach gelesenen Indexblöcken und Cache-Trefferrate

SELECT relid::regclass AS table_name, indexrelid::regclass AS index_name,
idx_blks_read, idx_blks_hit 
FROM pg_statio_user_indexes
ORDER BY idx_blks_read DESC LIMIT 20;

 table_name | index_name      | idx_blks_read | idx_blks_hit
------------+-----------------+---------------+--------------
 table1     | idx_table1_date | 209289        | 141
 table2     | table2_pkey     | 37221         | 1223747
 table3     | table3_pkey     | 9825          | 3143947
...
Um eine Baseline vor/nach einem Testlauf zu erstellen, können wir die Statistiken zurücksetzen (als Superuser):

SELECT pg_stat_reset_shared(‚io‘);

Führen Sie dann unsere Arbeitslast aus und fragen Sie

pg_stat_io

erneut ab, um zu sehen, wie viele Bytes gelesen/geschrieben wurden und wie viel Zeit für das Warten auf E/A aufgewendet wurde.

Fazit

Das neue asynchrone I/O-Subsystem von PostgreSQL 18 ist ein bedeutender Schritt zur Verbesserung der I/O-Leistung für große Scans und Wartungsarbeiten. Durch die Überlappung von Lesevorgängen und die Möglichkeit, mehrere Anfragen gleichzeitig zu bearbeiten, können moderne Speichersysteme besser genutzt und die Abfragezeiten für datenintensive Workloads verkürzt werden. Mit den neuen Beobachtungsfunktionen in pg_aios und pg_stat_io können DBAs und Entwickler die AIO-Aktivität überwachen und Parameter optimieren, um die Leistung für ihre spezifischen Workloads zu optimieren. Da sich PostgreSQL ständig weiterentwickelt, können wir weitere Verbesserungen am AIO-Subsystem und eine breitere Abdeckung von Operationen erwarten, die von asynchronem I/O profitieren können.

PostgreSQL ist ein eingetragenes Warenzeichen der PostgreSQL Community Association of Canada.

Kategorien: PostgreSQL®
Tags: planetpostgres planetpostgresql postgresql 18 PostgreSQL®

JM

über den Autor

Josef Machytka


Beitrag teilen: