1. はじめに
PostgreSQL18(2025年9月25日リリース)の目玉機能の一つが、非同期I/Oの導入です。
これまでのPostgreSQLは、ディスクI/Oを同期的に処理していました。1回のI/Oリクエストを発行したら、その完了を待ってから次の処理に進む、という流れです。非同期I/Oはこの制約を取り払い、複数のI/Oリクエストを並行して発行できるようにします。
「並行してI/Oが走る」と聞くと難しそうに聞こえますが、本記事では実際にt3.micro(Amazon Linux 2023)とRHEL 10の2環境でpgbenchを動かしながら、17と18の数字の違いを観察していきます。
なお、結論を先に述べると、今回の検証環境(t3.micro + EBS gp3)ではPostgreSQL18の非同期I/Oの効果を再現することはできませんでした。原因の特定には至っていませんが、EBSのレイテンシ、CPU不足、workerプロセスの管理コストなど複数の要因が絡み合っている可能性があります。非同期I/Oの効果を実感するにはNVMe SSD直結環境や高スペックなインスタンスが必要と考えられます。それでも pg_stat_io を通じてI/Oの挙動の違いを数字で観察することはできますので、その点に着目しながら読み進めてください。
本記事は「pgbenchで見るPostgreSQL18の変化」シリーズの第1弾です。
PostgreSQL17との比較を中心に、非同期I/Oが実際の読み取り処理にどう影響するかを手を動かして確認していきます。
1.1 この記事で確認すること
- PostgreSQL18の非同期I/Oの仕組みを理解する
-
io_methodパラメータの設定と動作の違いを確認する - pgbench標準テーブルを使ったSeq Scanワークロードで17と18の性能差を実測する
-
pg_stat_ioビューでI/O状況を観察する
1.2 検証環境
環境A(Amazon Linux 2023)
| 項目 | 内容 |
|---|---|
| OS | Amazon Linux 2023 |
| PostgreSQL | 17.8 / 18.3 |
| インスタンスタイプ | t3.micro(2 vCPU、1GiB メモリ) EBS 30GB gp3 |
| io_method | sync(PG17) / worker(PG18デフォルト) |
| ベンチマークツール | pgbench |
環境B(RHEL 10)
| 項目 | 内容 |
|---|---|
| OS | Red Hat Enterprise Linux 10.1 |
| PostgreSQL | 18.4(PGDG RPM) |
| インスタンスタイプ | t3.micro(2 vCPU、1GiB メモリ) EBS 30GB gp3 |
| io_method | io_uring |
| ベンチマークツール | pgbench |
2. 非同期I/Oとは
2.1 これまでの同期I/Oの動き
PostgreSQL17までのI/Oは、基本的に**同期(Synchronous)**でした。バッファキャッシュにないブロックを読み込む際、以下の流れで処理が進みます。
[プロセス] → read()システムコール発行 → [OSカーネル] → ディスクからブロックを読み込む
↑ここで待機 ↓
[プロセス] ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← ← 完了通知を受け取る
Seq Scanで大量のブロックを読み込む場合、「読んで待つ・読んで待つ」を繰り返すことになり、CPUが手待ち状態になる時間が発生します。
2.2 PostgreSQL18の非同期I/Oの動き
PostgreSQL18では、複数のブロックをまとめてI/Oリクエストとして発行できるようになりました。
[プロセス] → 複数ブロックのI/Oリクエストをまとめて発行
↓
I/O完了を待つ間、別の処理(次のリクエスト準備など)を実行
↓
完了したブロックから順次処理
効果が出やすいのは以下のケースです。
- Seq Scan:大きなテーブルを先頭から末尾まで読むワークロード
- Bitmap Heap Scan:インデックスで絞り込んだ後、ヒープを読みに行く処理
- VACUUM:dead tupleを探しながらテーブル全体を走査する処理
2.3 io_method パラメータ
PostgreSQL18では io_method パラメータで動作モードを切り替えられます。
| 値 | 説明 |
|---|---|
worker(デフォルト) |
バックグラウンドワーカーを使った非同期I/O。io_workers で並行数を設定 |
sync |
従来の同期I/O。PG17以前と同じ動作 |
io_uring |
Linux の io_uring API を使った非同期I/O。カーネルのビルドオプション依存のため、環境によっては選択不可 |
今回の検証環境(Amazon Linux 2023)では io_uring は使用できませんでした。io_method は worker を使います。
io_uring を設定しようとしたところ、以下のエラーが発生しました。
LOG: invalid value for parameter "io_method": "io_uring"
HINT: Available values: sync, worker.
RHEL 10.1(カーネル 6.12)にPGDG RPMのPostgreSQL18をインストールしたところ、インストール後にio_uring を設定する事で利用可能でした。Rocky Linux 10 / AlmaLinux 10でも同様に使用できると考えられますが、本記事ではRHEL 10のみで確認しています。
3. postgresql.confの設定
3.1 検証用の設定項目
今回の検証に合わせて、以下のパラメータを設定します。
| パラメータ | PG17デフォルト | PG18デフォルト | 検証値 | 説明 |
|---|---|---|---|---|
io_method |
(なし) | worker |
worker |
非同期I/OのI/Oバックエンド。PG18で新設 |
io_workers |
(なし) | 3 |
3 |
worker モード時のバックグラウンドワーカー数 |
effective_io_concurrency |
1 |
16 |
16 |
通常クエリ(Seq Scanなど)の並行I/O数。PG18でデフォルトが引き上げられた |
maintenance_io_concurrency |
10 |
16 |
16 |
VACUUM等のメンテナンス処理の並行I/O数。同じくPG18でデフォルトが引き上げられた |
shared_buffers |
128MB |
128MB |
128MB |
t3.microに合わせてデフォルトのまま。バッファキャッシュに収まらないデータを読み出すことで非同期I/Oの効果が測りやすくなる |
autovacuum |
on |
on |
off |
計測中に自動VACUUMが走って結果がぶれないよう無効化する |
logging_collector |
off |
off |
on |
ログをファイルに蓄積する(PGDGのRPMパッケージではデフォルトで on の場合あり) |
track_io_timing |
off |
off |
on |
pg_stat_io の read_time / write_time を有効化する |
autovacuum = off はあくまで検証目的の設定です。本番環境では必ず有効にしてください。
postgresql.conf を変更したら、PostgreSQLを再起動してください。
$ sudo systemctl restart postgresql
3.2 設定確認
$ psql -U postgres -d pgbench_test -c "SELECT name, setting FROM pg_settings WHERE name IN ('io_method', 'io_workers', 'effective_io_concurrency', 'maintenance_io_concurrency', 'track_io_timing') ORDER BY name;"
name | setting
-----------------------------+---------
effective_io_concurrency | 16
io_method | worker
io_workers | 3
maintenance_io_concurrency | 16
track_io_timing | on
(5 rows)
4. pgbenchで性能を比較する
4.1 pgbench標準テーブルの確認
pgbenchの標準テーブル(pgbench_accounts など)を使います。shared_buffers に収まらないデータ量であることが重要です。
スケールファクタ100で初期化すると pgbench_accounts が1,000万行になります。
$ pgbench -i -s 100 -U postgres pgbench_test
pgbench_accounts が1,281MB(1,000万行)あり、t3.micro(shared_buffers 128MB)に対して十分な大きさです。Seq Scanでバッファキャッシュに収まらないブロックを読み出すことができます。
テーブルサイズを確認します。
$ psql -U postgres -d pgbench_test -c "SELECT relname, pg_size_pretty(pg_relation_size(oid)) AS table_size FROM pg_class WHERE relname LIKE 'pgbench_%' ORDER BY pg_relation_size(oid) DESC;"
relname | table_size
----------------------+------------
pgbench_accounts | 1281 MB
pgbench_accounts_pkey| 214 MB
pgbench_tellers | 48 kB
pgbench_tellers_pkey | 40 kB
pgbench_branches_pkey| 16 kB
pgbench_branches | 8192 bytes
pgbench_history | 0 bytes
(7 rows)
4.2 カスタムスクリプトの作成
pgbench_accounts に対してSeq Scanを繰り返すスクリプトを用意します。
$ mkdir -p ./pgsql
$ cat > ./pgsql/test18_seq_scan.sql << 'EOF'
SELECT count(*) FROM pgbench_accounts WHERE filler IS NOT NULL;
EOF
filler 列はインデックスを持たないため、必ずSeq Scanになります。
4.3 pgbenchの実行
バッファキャッシュをクリアしてから実行することで、ディスクからの読み取りが発生する条件をそろえます。
# バッファキャッシュをクリア(検証環境のみ)
$ sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
# PostgreSQL17での実行
$ pgbench -c 2 -j 2 -T 60 \
-f ./pgsql/test18_seq_scan.sql \
--no-vacuum \
-U postgres pgbench_test
pgbench結果 PostgreSQL17
``` pgbench (17.8) transaction type: ./pgsql/test18_seq_scan.sql scaling factor: 1 query mode: simple number of clients: 2 number of threads: 2 maximum number of tries: 1 duration: 60 s number of transactions actually processed: 14 number of failed transactions: 0 (0.000%) latency average = 8870.114 ms initial connection time = 16.238 ms tps = 0.225476 (without initial connection time) ```$ sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
# PostgreSQL18での実行(worker)
$ pgbench -c 2 -j 2 -T 60 \
-f ./pgsql/test18_seq_scan.sql \
--no-vacuum \
-U postgres pgbench_test
pgbench結果 PostgreSQL18
``` pgbench (18.3) transaction type: ./pgsql/test18_seq_scan.sql scaling factor: 1 query mode: simple number of clients: 2 number of threads: 2 maximum number of tries: 1 duration: 60 s number of transactions actually processed: 12 number of failed transactions: 0 (0.000%) latency average = 10227.448 ms initial connection time = 15.959 ms tps = 0.195552 (without initial connection time) ```4.4 実測結果
TPS
| バージョン | io_method | io_workers | effective_io_concurrency | 1回目 | 2回目 | 3回目 |
|---|---|---|---|---|---|---|
| PostgreSQL17 | sync | - | 1 | 0.225 | 0.212 | 0.225 |
| PostgreSQL18 | worker | 3 | 16 | 0.196 | 0.201 | 0.196 |
| PostgreSQL18 | worker | 1 | 16 | 0.198 | 0.202 | 0.200 |
| PostgreSQL18 | worker | 1 | 1 | 0.214 | 0.230 | 0.215 |
平均レイテンシ (ms)
| バージョン | io_method | io_workers | effective_io_concurrency | 1回目 | 2回目 | 3回目 |
|---|---|---|---|---|---|---|
| PostgreSQL17 | sync | - | 1 | 8870.114 | 9451.279 | 8881.796 |
| PostgreSQL18 | worker | 3 | 16 | 10227.448 | 9933.554 | 10208.560 |
| PostgreSQL18 | worker | 1 | 16 | 10105.138 | 9922.754 | 10019.021 |
| PostgreSQL18 | worker | 1 | 1 | 9354.123 | 8689.363 | 9285.401 |
io_workers = 3 / effective_io_concurrency = 16(PostgreSQL18デフォルト)ではPostgreSQL17より性能が低下したが、io_workers = 1 / effective_io_concurrency = 1 に絞り込むとPostgreSQL17と同等かわずかに上回る結果となりました。t3.micro + EBS環境では並行I/O数を増やすことでオーバーヘッドが生じており、PostgreSQL18の非同期I/Oの恩恵を受けるにはより高スペックなインスタンスが適していると思われます。
5. pg_stat_io でI/Oの変化を観察する
PostgreSQL16から導入された pg_stat_io ビューでは、I/Oの詳細をバックエンドの種別・コンテキスト単位で確認できます。track_io_timing = on を設定していると read_time が計測できます。
5.1 Seq Scan実行の確認
# 統計情報リセット
$ psql -U postgres -d pgbench_test -c "SELECT pg_stat_reset_shared('io');"
# SeqScan実行
$ psql -U postgres -d pgbench_test -c "SELECT count(*) FROM pgbench_accounts WHERE filler IS NOT NULL;"
# 統計情報の確認
$ psql -U postgres -d pgbench_test -c "SELECT backend_type, object, context, reads, round(read_time::numeric, 2) AS read_time_ms, evictions FROM pg_stat_io WHERE reads > 0 ORDER BY reads DESC;"
統計情報 pg_stat_io PostgreSQL17
```shell backend_type | object | context | reads | read_time_ms | evictions ------------------------------------------------------------------------ background worker | relation | bulkread | 108895 | 14460.39 | 0 client backend | relation | bulkread | 55040 | 7233.36 | 0 client backend | relation | normal | 42 | 13.86 | 0 background worker | relation | normal | 7 | 2.19 | 0 (4 rows) ```統計情報 pg_stat_io PostgreSQL18
```shell backend_type | object | context | reads | read_time_ms | evictions ----------------------------------------------------------------------- background worker | relation | bulkread | 8565 | 16176.25 | 96 client backend | relation | bulkread | 4284 | 8071.74 | 48 client backend | relation | normal | 25 | 17.40 | 25 (3 rows) ```pg_stat_io の結果(context = 'bulkread' がSeq Scanに対応する読み取り):
background worker
| context | 項目 | PostgreSQL17 | PostgreSQL18 |
|---|---|---|---|
| bulkread | reads | 108,895 | 8,565 |
| bulkread | read_time_ms | 14,460.39 | 16,176.25 |
| bulkread | evictions | 0 | 96 |
| normal | reads | 7 | - |
| normal | read_time_ms | 2.19 | - |
| normal | evictions | 0 | - |
client backend
| context | 項目 | PostgreSQL17 | PostgreSQL18 |
|---|---|---|---|
| bulkread | reads | 55,040 | 4,284 |
| bulkread | read_time_ms | 7,233.36 | 8,071.74 |
| bulkread | evictions | 0 | 48 |
| normal | reads | 42 | 25 |
| normal | read_time_ms | 13.86 | 17.40 |
| normal | evictions | 0 | 25 |
PostgreSQL18のread回数はPostgreSQL17の約1/13に減っています。一方でread_time_msはPostgreSQL18の方がやや高く、1回のI/Oリクエストあたりのレイテンシが大きいことが分かります。
6. VACUUMへの影響を確認する
非同期I/OはVACUUMにも効果があります。maintenance_io_concurrency パラメータがVACUUMの並行I/O数を制御します。
dead tupleを意図的に作ってからVACUUMを計測します。毎回同じ条件になるよう、VACUUM後に再度UPDATEしてdead tupleを作ってから次の計測を行います。
# dead tupleを作る
psql -U postgres -d pgbench_test -c "UPDATE pgbench_accounts SET filler = repeat('x', 84) WHERE aid <= 500000;"
# VACUUM前の状態確認
psql -U postgres -d pgbench_test -c "SELECT n_dead_tup, n_live_tup FROM pg_stat_user_tables WHERE relname = 'pgbench_accounts';"
# VACUUM時間計測
time psql -U postgres -d pgbench_test -c "VACUUM ANALYZE pgbench_accounts;"
VACUUM所要時間 (real)
| バージョン | io_method | io_workers | maintenance_io_concurrency | 1回目 (秒) | 2回目 (秒) | 3回目 (秒) |
|---|---|---|---|---|---|---|
| PG17 | sync | - | 10 | 7.846 | 7.739 | 7.782 |
| PG18 | worker | 3 | 16 | 15.924 | 16.100 | 15.451 |
| PG18 | worker | 1 | 1 | 16.031 | 16.265 | 17.338 |
VACUUMもSeq Scanと同様に、io_workers や maintenance_io_concurrency を下げてもPostgreSQL18の遅さは改善しませんでした。特にPostgreSQL18はPostgreSQL17の約2倍の時間を要しており、Seq Scan時と同様にI/O並列化のメリットよりもオーバーヘッドの影響が大きかった可能性があります。
7. RHEL 10で io_uring を試す
Amazon Linux 2023ではPGDGパッケージが io_uring 未対応だったが、RHEL 10.1(カーネル6.12)にPGDG RPMのPostgreSQL18.4をインストールしたところ、postgresql.confのio_methodにio_uring を設定する事で有効化できました。
postgres=# SHOW io_method;
io_method
-----------
io_uring
(1 row)
7.1 pgbenchの実行
同じカスタムスクリプトと条件で3回計測しました。
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
pgbench -c 2 -j 2 -T 60 -f ./pgsql/test18_seq_scan.sql --no-vacuum -U postgres pgbench_test
pgbench結果 RHEL10 / PostgreSQL18
``` pgbench (18.4) transaction type: ./pgsql/test18_seq_scan.sql scaling factor: 1 query mode: simple number of clients: 2 number of threads: 2 maximum number of tries: 1 duration: 60 s number of transactions actually processed: 14 number of failed transactions: 0 (0.000%) latency average = 9487.901 ms initial connection time = 17.325 ms tps = 0.210795 (without initial connection time) ```pgbench (18.4)
transaction type: ./pgsql/test18_seq_scan.sql
scaling factor: 1
query mode: simple
number of clients: 2
number of threads: 2
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 14
number of failed transactions: 0 (0.000%)
latency average = 9590.597 ms
initial connection time = 17.188 ms
tps = 0.208538 (without initial connection time)
pgbench (18.4)
transaction type: ./pgsql/test18_seq_scan.sql
scaling factor: 1
query mode: simple
number of clients: 2
number of threads: 2
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 14
number of failed transactions: 0 (0.000%)
latency average = 9166.191 ms
initial connection time = 17.308 ms
tps = 0.218193 (without initial connection time)
7.2 環境の比較(PostgreSQL17・18(AL2023)・18(RHEL10))
TPS
| バージョン / 環境 | OS | io_method | io_workers | effective_io_concurrency | 1回目 | 2回目 | 3回目 |
|---|---|---|---|---|---|---|---|
| PostgreSQL17 | Amazon Linux 2023 | sync | - | 1 | 0.225 | 0.212 | 0.225 |
| PostgreSQL18 | Amazon Linux 2023 | worker | 3 | 16 | 0.196 | 0.201 | 0.196 |
| PostgreSQL18 | Amazon Linux 2023 | worker | 1 | 16 | 0.198 | 0.202 | 0.200 |
| PostgreSQL18 | Amazon Linux 2023 | worker | 1 | 1 | 0.214 | 0.230 | 0.215 |
| PostgreSQL18 | RHEL 10 | io_uring | - | 16 | 0.211 | 0.209 | 0.218 |
平均レイテンシ (ms)
| バージョン / 環境 | OS | io_method | io_workers | effective_io_concurrency | 1回目 | 2回目 | 3回目 |
|---|---|---|---|---|---|---|---|
| PostgreSQL17 | Amazon Linux 2023 | sync | - | 1 | 8870.114 | 9451.279 | 8881.796 |
| PostgreSQL18 | Amazon Linux 2023 | worker | 3 | 16 | 10227.448 | 9933.554 | 10208.560 |
| PostgreSQL18 | Amazon Linux 2023 | worker | 1 | 16 | 10105.138 | 9922.754 | 10019.021 |
| PostgreSQL18 | Amazon Linux 2023 | worker | 1 | 1 | 9354.123 | 8689.363 | 9285.401 |
| PostgreSQL18 | RHEL 10 | io_uring | 3 | 16 | 9487.901 | 9590.597 | 9166.191 |
io_uring(RHEL 10)はPostgreSQL18デフォルト(worker / effective_io_concurrency = 16)と比べてTPSが改善し、PostgreSQL17に近い水準となりました。ただしt3.micro + EBSという条件は同じであり、EBSレイテンシのボトルネックは変わらないため、劇的な差にはなりませんでした。io_uring の検証に関しては、それなりに高スペックな環境が必要と思われます。
8. もっと深く学びたい方へ
公式ドキュメント
| ページ | URL |
|---|---|
| pg_stat_io | https://www.postgresql.jp/document/18/html/monitoring-stats.html#MONITORING-PG-STAT-IO-VIEW |
| io_method パラメータ | https://www.postgresql.jp/document/18/html/runtime-config-resource.html#GUC-IO-METHOD |
| effective_io_concurrency | https://www.postgresql.jp/document/18/html/runtime-config-resource.html#GUC-EFFECTIVE-IO-CONCURRENCY |
| PostgreSQL18 リリースノート | https://www.postgresql.jp/document/18/html/release-18.html |
書籍
| 書籍名称 | 内容 |
|---|---|
| [改訂3版]内部構造から学ぶPostgreSQL―設計・運用計画の鉄則 | I/O周りのアーキテクチャを理解するのに役立ちます |
| PostgreSQL実践入門 ──アーキテクチャ、運用監視、性能改善 | 最新版の実践知識として参照しています |
9. まとめ
本記事では、PostgreSQL18の非同期I/Oについて以下を確認しました。
- PostgreSQL18のデフォルト
io_methodはworker。バックグラウンドワーカーが複数のI/Oリクエストを並行処理する -
io_uringはカーネルのビルドオプション依存のため、利用前にSHOW io_methodで選択肢を確認する -
effective_io_concurrencyとmaintenance_io_concurrencyのデフォルトがPostgreSQL17の1/10からPostgreSQL18で両方16に引き上げられた -
track_io_timing = onにしておくとpg_stat_ioのread_timeでI/O待ち時間の変化を数字で追える
t3.micro + EBS(gp3)環境での検証では、PostgreSQL18(io_method = worker)はPostgreSQL17と比べてTPSがやや低下する結果となりました。pg_stat_io のPostgreSQL18のread回数に関しては、非同期I/Oにより1回のI/O要求でより多くのブロックを扱い、PostgreSQL18では非同期I/OによりI/O要求の集約方法が変化するため、readsの単純比較だけで実際のディスクアクセス量を評価することはできません。
pgbenchで大容量のデータでの試験は可能ですが、優位な差が発生する状況に持っていくのは、今回は出来なかったのが非常に残念です。ですが、pg_stat_ioを確認する事で、差が確認できることはこの記事から学べるポイントとなっています。情報を集めて、優位な差が現れる条件を探していきたいと思います(t3.microのCPU不足、EBS gp3のレイテンシ etc...)。