WALG_DIRECT_IO は、WAL-G v3.0.9 のドキュメントで「An experimental feature」(実験的な機能)と書かれている設定です1。本番で有効にする前に、手元の環境で動作を確かめてください。
1. はじめに
ClickHouse のブログに、ClickHouse Managed Postgres(以下、Managed Postgres)のバックアップで Direct I/O(OS のページキャッシュを通さずにファイルを読む方式)を使う理由を説明した記事が公開されました2。ページキャッシュを通す通常の読み込み(以下、通常の読み込み)で DB 全体を読むと、Linux は読んだ内容をページキャッシュに残し、その分 PostgreSQL がページキャッシュに載せていたデータを追い出す、という説明です。ブログの検証機では、次のことが起きたと書かれています。
a buffered backup evicted all 40 GiB of a table
通常の読み込みのバックアップで、ページキャッシュにあった 40 GiB のテーブルがすべて追い出された、という一文です。
Managed Postgres ではこの設定が最初から有効で、利用者は切り替えられません2。ただ、この設定はバックアップツール WAL-G の WALG_DIRECT_IO で、ブログの検証も WAL-G v3.0.9 で行われています2。自分で PostgreSQL と WAL-G を用意すれば、有効・無効を切り替えて比べられます。そこで、EC2 に PostgreSQL と WAL-G を用意し、バックアップのあとでページキャッシュに何が残るかを比べてみました。
1.1. 結論(先出し)
- 通常の読み込みでは、42.5 GiB の DB のバックアップを始めてから 22〜23 秒で、ページキャッシュが空きメモリ(約 9 GiB)を使い切った。バックアップ後の空きは 0.5 GiB 前後だった
- Direct I/O では、バックアップの前後でテーブルとインデックスのキャッシュ量がバイト単位で一致し、空きメモリは約 8.9 GiB 残った(3 回とも)
- Direct I/O を WAL-G のデフォルトの 128 KiB 単位で読むと、通常の読み込みより 3 回の平均で約 25 秒(約 2 割)長くかかった。Managed Postgres と同じ決め方(1 MiB 単位・並列 4)では 130.9〜133.3 秒で、通常の読み込み(125.2〜134.0 秒)と同じ範囲だった
1.2. 検証ゴール
| # | 確かめること | 確認できれば OK の条件 |
|---|---|---|
| 1 | バックアップのあとでページキャッシュに何が残るか | 3 回とも、通常の読み込みではバックアップが読んだデータの分だけキャッシュが増え、Direct I/O ではテーブル・インデックスごとのキャッシュ量と空きメモリがほとんど変わらないことが確かめられている |
| 2 | Direct I/O で所要時間がどう変わり、設定でどこまで短くできるか | WAL-G のデフォルトの読み込み単位と、読み込み単位・並列数を変えた設定で、バックアップの所要時間を比べられている |
2. 検証環境
| 項目 | 内容 |
|---|---|
| インスタンス | Amazon EC2 i8ge.xlarge(東京リージョン、4 vCPU、メモリ 32 GiB) |
| ストレージ | インスタンスストアの NVMe 1 本(2,500 GB)、ext4(noatime) |
| OS | Ubuntu 24.04 LTS(arm64)、カーネル 7.0.0-1014-aws |
| PostgreSQL | 18.6(PGDG)、shared_buffers = 8GB
|
| WAL-G | v3.0.9(wal-g-pg-24.04-aarch64)、圧縮は lz4 |
| バックアップの保存先 | Amazon S3(同じリージョン、VPC のゲートウェイエンドポイント経由) |
| データ | DB 全体で 42.5 GiB(pgbench のスケール 2700 と、別に作った 3.1 GiB のテーブル) |
検証の条件は、ClickHouse のブログの検証をもとに決めました(3.2 章)。
3. 構成と測り方
3.1. WAL-G の 3 つの設定
WAL-G のバックアップ(wal-g backup-push)では、読み込み方を次の設定で決めます1。
| 設定 | 意味 | デフォルト |
|---|---|---|
WALG_DIRECT_IO |
true で、バックアップの読み込みを Direct I/O にする |
無効 |
WALG_DIRECT_IO_BLOCK_COUNT |
Direct I/O で 1 回に読むブロック数(1 ブロック 4 KiB) | 32(128 KiB) |
WALG_UPLOAD_DISK_CONCURRENCY |
ディスクを読む並列数 | 1 |
Direct I/O はページキャッシュを通さないので、OS の先読みも使えません。その分は、1 回に読む量と並列数を増やして補います。
3.2. ClickHouse のブログの検証をもとに決めた条件
条件は、ClickHouse のブログの検証2を、メモリに対する比率をそろえて小さくしたものです。読み込み単位と並列数は、ブログに載っている Managed Postgres の設定の生成コードに、今回のインスタンスを当てはめて決めました。ブログの値は、ブログの表記(GB と GiB)のまま載せています。
| 項目 | ブログの検証 | この記事 |
|---|---|---|
| インスタンス | i8ge.12xlarge(48 vCPU、384 GiB) | i8ge.xlarge(4 vCPU、32 GiB) |
| NVMe | 4 本の RAID0 | 1 本(RAID なし) |
shared_buffers |
96 GB(メモリの 1/4) | 8 GB(メモリの 1/4) |
| DB の大きさ | 467 GB(メモリの約 1.2 倍) | 42.5 GiB(メモリの約 1.3 倍) |
| 未参照テーブル(事前にページキャッシュに読み込み、その後は読まないテーブル) | 40 GiB | 3.1 GiB |
| 主キー検索(バックアップ中に実行し続ける、主キーで 1 行を取得する SELECT) | 16 クライアント | 4 クライアント |
バックアップを動かす cgroup の CPU の重み(CPUWeight) |
25(デフォルトは 100) | 25 |
| 1 回に読むブロック数(Direct I/O) | NVMe の本数 × 256(1 本あたり 1 MiB) | 256(1 MiB) |
| ディスクを読む並列数(Direct I/O) | vCPU 数(NVMe を多く積んだインスタンスか、vCPU が 2 以下の場合。それ以外は半分) | 4 |
| ディスクを読む並列数(通常の読み込み) | vCPU 数の半分 | 2 |
「未参照テーブル」は、この記事での呼び名です。「NVMe を多く積んだ」かどうかの判定条件は、ブログのコードには載っていません。ブログの i8ge.12xlarge では Direct I/O の並列数が vCPU 数と同じ 48 になっているので、同じ系列の i8ge.xlarge も同じ扱いにしました。
3.3. 比べた構成
| 構成 | WALG_DIRECT_IO |
1 回に読む量 | 並列数 |
|---|---|---|---|
| 通常の読み込み(並列 2) | 無効 | OS に任せる | 2 |
| Direct I/O(128 KiB・並列 2) | true |
128 KiB(WAL-G のデフォルト) | 2 |
| Direct I/O(1 MiB・並列 4) | true |
1 MiB(256 ブロック。Managed Postgres と同じ決め方) | 4 |
3 つとも、圧縮(lz4)と S3 へのアップロードの並列数(4)は同じにしました。Direct I/O(128 KiB・並列 2)は、読み込み単位だけの差を見るために、並列数を通常の読み込みと同じ 2 にしています。
3.4. 測ったもの
| 何を | どう測ったか |
|---|---|
| テーブル・インデックスごとのキャッシュ量 |
fincore3 で、各テーブル・インデックスのファイルがページキャッシュに載っている量を、バックアップの前後で集計 |
| メモリ全体 |
free -m の空きと buff/cache(前後)、vmstat 1 の cache 列(1 秒ごと) |
| バックアップの所要時間 |
/usr/bin/time -v で測った WAL-G の実行時間 |
fincore では、pgbench_accounts(33.8 GiB)、その主キーのインデックス(5.6 GiB)、未参照テーブル(3.1 GiB)を見ました。
4. 1 回分の流れ
構成ごとに次の流れを 1 回とし、3 構成を、実行順を入れ替えながら 3 回ずつ実行しました。
- PostgreSQL を止め、OS のページキャッシュを空にして(
echo 3 > /proc/sys/vm/drop_caches)起動する -
pg_prewarm4 のreadモードで、未参照テーブルの全体と、pgbench_accountsとその主キーのインデックスの先頭 1/4 をページキャッシュに読み込む - 主キー検索(
pgbench_accountsの先頭 1/4 の範囲が対象)を 4 クライアントで開始し、60 秒待つ - キャッシュ量を測り、30 秒後にバックアップを始める
- バックアップが終わって 30 秒後に主キー検索を止め、キャッシュ量を測る
未参照テーブルは、読み込んだあと一切読みません。主キー検索は、バックアップ中も DB が使われている状態を作るために実行しています。
WAL-G は、構成ごとの環境変数を読んで実行します。Direct I/O(1 MiB・並列 4)の環境変数は次のとおりです。
WALG_S3_PREFIX=s3://<バケット名>/backups
AWS_REGION=ap-northeast-1
WALG_COMPRESSION_METHOD=lz4
WALG_UPLOAD_CONCURRENCY=4
WALG_DIRECT_IO=true
WALG_DIRECT_IO_BLOCK_COUNT=256
WALG_UPLOAD_DISK_CONCURRENCY=4
バックアップは、CPU の重み(CPUWeight)を 25 に下げた cgroup の中で動かしました(3.2 章)。
systemd-run --scope -q -p CPUWeight=25 \
sudo -u postgres env $(xargs < direct_io_1mib.env) wal-g backup-push /dat/pg18
5. 結果
5.1. バックアップのあとでページキャッシュに残ったもの
バックアップ前後のキャッシュ量です(各構成 3 回)。
| 項目 | 通常の読み込み(並列 2) | Direct I/O(128 KiB・並列 2) | Direct I/O(1 MiB・並列 4) |
|---|---|---|---|
pgbench_accounts |
8.44 → 12.53〜12.61 GiB | 8.44 → 8.44 GiB | 8.44 → 8.44 GiB |
| 主キーのインデックス | 1.41 → 5.65 GiB(全体) | 1.41 → 1.41 GiB | 1.41 → 1.41 GiB |
| 未参照テーブル | 3.11 → 3.11 GiB(全体) | 3.11 → 3.11 GiB | 3.11 → 3.11 GiB |
| buff/cache の増加 | 8,744〜8,836 MiB | 121〜151 MiB | 116〜147 MiB |
| バックアップ後の空き | 472〜503 MiB | 9,102〜9,165 MiB | 9,119〜9,160 MiB |
通常の読み込みでは、バックアップが読んだ pgbench_accounts と主キーのインデックスがそのままページキャッシュに残り、測った 3 つのキャッシュ量は合わせて 8.3〜8.4 GiB 増えました。free の buff/cache も同じくらい(8.5〜8.6 GiB)増え、空きメモリはバックアップ前の約 9 GiB から 0.5 GiB 前後まで減っています。Direct I/O では、3 つのキャッシュ量はバックアップの前後でバイト単位で一致しました。Direct I/O でも buff/cache が 0.1 GiB 前後増えたのは、測った 3 つ以外のファイル(計測中に書き出した pgbench や vmstat のログなど)の分と考えられます。
ページキャッシュの量の推移を図 1 に示します。
図 1: バックアップ中のページキャッシュの量(各構成の 1 回目)
通常の読み込みでは、バックアップを始めてから 22〜23 秒(3 回とも)で、ページキャッシュがメモリ 30.7 GiB(free の total)のうち約 30.1 GiB まで増えて止まりました。これは所要時間 125〜134 秒のうち最初の 2 割足らずの時点で、残りの時間はキャッシュの量が増えないまま読み込みが続いています。Direct I/O では、バックアップの間もキャッシュの量はほとんど変わりませんでした。
ディスクから読んだ量(time -v の File system inputs)は、Direct I/O が 42.6 GiB で DB 全体とほぼ同じでした。通常の読み込みは 32.75 GiB(3 回とも同じ値)で、一部をページキャッシュから読んでいます。
5.2. バックアップの所要時間
| 構成 | 1 回目 | 2 回目 | 3 回目 |
|---|---|---|---|
| 通常の読み込み(並列 2) | 134.0 秒 | 125.2 秒 | 126.2 秒 |
| Direct I/O(128 KiB・並列 2) | 153.3 秒 | 152.7 秒 | 153.8 秒 |
| Direct I/O(1 MiB・並列 4) | 133.3 秒 | 131.9 秒 | 130.9 秒 |
Direct I/O(128 KiB・並列 2)は、3 回の平均で通常の読み込みより約 25 秒(約 2 割)長くかかりました。Direct I/O(1 MiB・並列 4)は 130.9〜133.3 秒で、通常の読み込みの 125.2〜134.0 秒と同じ範囲でした。
5.3. 読み込み単位と並列数を変えたとき
主キー検索を実行せず、バックアップだけを実行したときの所要時間です(各 1 回)。
| 読み方 | 並列 1 | 並列 2 | 並列 4 | 並列 8 |
|---|---|---|---|---|
| 通常の読み込み | 77.5 秒 | 50.9 秒 | 46.0 秒 | 46.2 秒 |
| Direct I/O・128 KiB(デフォルト) | 113.4 秒 | 60.1 秒 | 47.4 秒 | 46.8 秒 |
| Direct I/O・1 MiB | 67.4 秒 | 50.1 秒 | 46.1 秒 | 45.8 秒 |
| Direct I/O・4 MiB | 60.5 秒 | 49.4 秒 | 46.1 秒 | 45.8 秒 |
並列 1 では、Direct I/O・128 KiB が通常の読み込みの約 1.5 倍かかりました。並列 1・2 で読み込み単位を 1 MiB 以上にすると、通常の読み込みより短くなっています。並列 4 以上では、どの読み方も 45.8〜47.4 秒でした(秒数は WAL-G のログの開始と終了の差)。
どの値も、5.2 章(主キー検索を実行しながらの計測)より短くなっています。主キー検索を実行していないためと考えられます。5.2 章の計測では、バックアップ中の vmstat の idle の平均は 0.0〜0.2% で、4 vCPU がほぼ使い切られていました。
6. 考察
6.1. Direct I/O でバックアップの何が変わるか
通常の読み込みのバックアップは、読んだデータで空きメモリを使い切り、バックアップが終わっても空きは 0.5 GiB 前後のままでした。Direct I/O では、空きメモリが約 8.9 GiB 残りました(5.1 章)。WAL-G のドキュメントには、WALG_DIRECT_IO は「without flushing the disk cache」(ページキャッシュの内容を追い出さずに)読む設定と書かれています1。今回の結果は、この説明と一致します。
通常の読み込みでは、バックアップのたびに空きメモリがバックアップの読んだデータで占められます。そのため、バックアップのあとにほかの処理がメモリを必要とすると、ページキャッシュを回収してから使うことになると考えられます。
6.2. 有効にするときの設定の目安
Direct I/O を true にするだけで、ほかをデフォルトのままにすると、読み込みは 128 KiB 単位・並列 1 です1。この組み合わせは、5.3 章で通常の読み込み(並列 1)の約 1.5 倍かかりました。並列数を 2 にそろえても、128 KiB のままでは、5.2 章のとおり約 2 割長くかかっています。
5.3 章では、並列数か読み込み単位のどちらかを上げると、通常の読み込みとの差がなくなりました。有効にするときは、WALG_UPLOAD_DISK_CONCURRENCY か WALG_DIRECT_IO_BLOCK_COUNT をデフォルトから上げ、手元のディスクで所要時間を確かめるのがよいと考えられます。
並列 4 以上でどの読み方も 45.8〜47.4 秒になったのは、ディスクの読み込みより先に、圧縮か S3 へのアップロードがボトルネックになっていた可能性があります(切り分けていません)。
6.3. 未参照テーブルは追い出されなかった
ClickHouse のブログの検証では、通常の読み込みのバックアップで、ページキャッシュに読み込んであった 40 GiB のテーブルが、開始から 20 秒以内に追い出されています2。今回は、通常の読み込みの 3 回とも、未参照テーブル(3.1 GiB)はバックアップ後も全体がページキャッシュに残りました。カーネル 7.0 で有効になっていた MGLRU(メモリが足りないときに回収するページを選ぶ仕組みの一つ)5 を無効にした状態でも 3 回実行しましたが、同じく全体が残っています。
何が追い出されるかは、メモリの余裕、データの大きさ、カーネルのページ回収の方式で変わると考えられます。今回は、ブログの検証のように未参照テーブルが追い出される現象は再現できませんでした。また、バックアップ中のクエリへの影響は扱っていません。今回は 4 vCPU がほぼ使い切られており(5.3 章)、この環境の値は一般化できないためです。
7. まとめ
WAL-G の WALG_DIRECT_IO を有効にすると、バックアップの前後で、測ったテーブルとインデックスのページキャッシュの量は変わらず、空きメモリは約 8.9 GiB 残りました。通常の読み込みでは、42.5 GiB の DB のバックアップで約 9 GiB の空きが 22〜23 秒で使い切られました。Direct I/O は WAL-G のデフォルト(128 KiB)のままだと約 2 割長くかかりますが、読み込み単位と並列数を Managed Postgres と同じ決め方にすると、通常の読み込みと同じ範囲の時間で終わりました。
参考
-
WAL-G for PostgreSQL(v3.0.9)(
WALG_DIRECT_IO・WALG_DIRECT_IO_BLOCK_COUNT・WALG_UPLOAD_DISK_CONCURRENCYの説明) ↩ ↩2 ↩3 ↩4 -
What is direct I/O, and why does ClickHouse Managed Postgres use it for backups?(ClickHouse 公式ブログ。2026 年 10 月 2 日) ↩ ↩2 ↩3 ↩4 ↩5
-
fincore(1)(Ubuntu 24.04 では
util-linux-extraパッケージに含まれる) ↩ -
pg_prewarm — リレーションデータをバッファキャッシュにプリロードする(PostgreSQL 18 文書。
readモードはブロックを読み込む) ↩ -
Multi-Gen LRU(Linux カーネルのドキュメント。
/sys/kernel/mm/lru_gen/enabledで実行中に切り替えられる) ↩
