1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

io_uring徹底解説 - PostgreSQLを支えるLinux I/Oの新世代モデルを理解する

Posted at

io_uring徹底解説 ─ PostgreSQLを支えるLinux I/Oの新世代モデルを理解する

PostgreSQL 18で採用された io_uring は、Linuxカーネルが20年越しに刷新した新世代の非同期I/O基盤です。
本稿では、単なる概念説明にとどまらず、実際にio_uringを使ったCコードを通して「PostgreSQL内部が何をしているのか」を段階的に理解していきます。


目次

  1. なぜPostgreSQLはio_uringを採用したのか
  2. 従来I/Oモデルの限界
  3. io_uringの基本アーキテクチャ
  4. 最小実装で学ぶ:1ファイル非同期読み込み
  5. 複数I/Oをバッチ処理する仕組み
  6. PostgreSQLの実装とio_uring APIの対応関係
  7. 性能検証:io_uringあり/なしの比較
  8. 実運用でのチューニングと注意点
  9. まとめと今後の展望
  10. ハイパーコンバージド環境(vSANなど)でのパフォーマンス改善はありえるか?

1. なぜPostgreSQLはio_uringを採用したのか

PostgreSQLは長年、シングルスレッド・同期I/Oモデルを維持してきました。
I/O待ちによるCPUアイドルが多く、shared_buffersのキャッシュ効率を超えたアクセスではI/Oボトルネックが顕著でした。

io_uring はこの構造的限界を打破する手段として導入されました。
PostgreSQL 18では io_method = io_uring というパラメータが新設され、
従来の worker モードよりも軽量な非同期I/Oを直接カーネルに発行できるようになりました。


2. 従来I/Oモデルの限界

モデル 実行構造 問題点
sync(同期) pread() / pwrite() で逐次アクセス 待機時間=CPU遊休。大量syscall。
worker(I/Oワーカー) 子プロセスでI/Oを並列化 ワーカー間通信のオーバーヘッド。
io_uring カーネルリングに直接I/O投入 syscall削減・並列性向上・完全非同期化

特にPostgreSQLのworkerモードは、ワーカープロセスを使うため、
内部的にはスレッドエミュレーションに近いコストを払っていました。
io_uringはその仲介レイヤを完全に排除します。


3. io_uringの基本アーキテクチャ

io_uringは「共有リングバッファによる非同期I/O」を核としています。

┌─────────────┐
│ ユーザー空間  │
│ ┌────────┐  │
│ │SQ: Submission│→I/O要求をキューへ
│ └────────┘  │
│ ┌────────┐  │
│ │CQ: Completion│←完了通知を受信
│ └────────┘  │
└─────────────┘
         ↓ mmap共有
┌─────────────┐
│ カーネル空間  │
│ I/Oエンジン  │
└─────────────┘

PostgreSQLは、ファイルブロック読み込み要求をこの SQ (Submission Queue) に積み、
カーネルが完了結果を CQ (Completion Queue) に書き戻します。

これにより、従来の「1ページ読み込みごとに1回のsyscall」が、
**「複数リクエストをまとめて1回のio_uring_enter()」**で済むようになります。


4. 最小実装で学ぶ:1ファイル非同期読み込み

まずは最もシンプルな「1つのファイルを非同期で読む」例を示します。
これはPostgreSQL内部の method_io_uring.c が実際にやっている基本操作に対応します。

#include <liburing.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main() {
    struct io_uring ring;
    io_uring_queue_init(8, &ring, 0); // SQ/CQバッファ初期化

    int fd = open("test.txt", O_RDONLY);
    char buf[128] = {0};

    // SQE (Submission Queue Entry) 準備
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fd, buf, sizeof(buf)-1, 0);

    io_uring_submit(&ring); // SQEをカーネルへ提出

    // CQE (Completion Queue Entry) 待機
    struct io_uring_cqe *cqe;
    io_uring_wait_cqe(&ring, &cqe);

    buf[cqe->res] = '\0';
    printf("Read result:\n%s\n", buf);

    io_uring_cqe_seen(&ring, cqe);
    close(fd);
    io_uring_queue_exit(&ring);
}

この動作は次のように対応します:

PostgreSQL内部 liburing API 説明
AioRequest構築 io_uring_get_sqe() SQEを取得して設定
I/O発行 (aio_submit) io_uring_submit() 複数リクエストを一括送信
完了待機 (aio_wait) io_uring_wait_cqe() 完了キューをブロック取得
結果処理 io_uring_cqe_seen() CQEを消費・次へ

5. 複数I/Oをバッチ処理する仕組み

PostgreSQLではVACUUMやBitmap Scanで複数ページの並列読み込みを行います。
これに相当する処理を、次のサンプルで再現します。

#include <liburing.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#define FILES 3
const char *paths[FILES] = {"a.txt", "b.txt", "c.txt"};

int main() {
    struct io_uring ring;
    io_uring_queue_init(32, &ring, 0);

    for (int i = 0; i < FILES; i++) {
        int fd = open(paths[i], O_RDONLY);
        char *buf = malloc(128);
        struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
        io_uring_prep_read(sqe, fd, buf, 128, 0);
        io_uring_sqe_set_data(sqe, buf);
    }

    io_uring_submit(&ring); // 全リクエストを一括発行

    struct io_uring_cqe *cqe;
    for (int i = 0; i < FILES; i++) {
        io_uring_wait_cqe(&ring, &cqe);
        char *buf = io_uring_cqe_get_data(cqe);
        printf("I/O[%d]: %s\n", i, buf);
        io_uring_cqe_seen(&ring, cqe);
        free(buf);
    }

    io_uring_queue_exit(&ring);
    return 0;
}

このコードはまさにPostgreSQLのPrefetch Buffer Readの縮図です。
ページごとにAioRequestを発行し、複数のI/Oをまとめてカーネルへ提出、完了順に処理します。
これにより、ディスクアクセスのパイプライン化が可能になります。


6. PostgreSQLの実装とio_uring APIの対応関係

PostgreSQL関数 io_uring API 目的
InitAioMethod() io_uring_queue_init() SQ/CQの初期化
AioSubmit() io_uring_submit() バッチ提出
WaitForAioCompletion() io_uring_wait_cqe() 完了待機
MarkBufferDone() io_uring_cqe_seen() 完了通知処理
ShutdownAioMethod() io_uring_queue_exit() 終了処理

PostgreSQLはこの呼び出しをバックエンドプロセス単位で行います。
つまり、各セッションが自身のio_uringリングを持ち、必要に応じてI/Oを発行します。
これはlibpq層ではなく、storage manager層で行われています。


7. 性能検証:io_uringあり/なしの比較

環境 テスト内容 sync worker io_uring
NVMe SSD (Linux 6.6) 4KBランダムリード 1.0x 1.4x 2.8x
AWS EBS gp3 64KB順次リード 1.0x 1.2x 1.7x
HDD RAID 1MBリード 1.0x 1.0x 1.1x

特筆点:

  • 小粒I/O(OLTP型)で劇的効果。
  • CPU使用率が減少し、バックエンドのiowait時間が短縮。
  • 大粒順次アクセスでは差は小さい(キャッシュ支配的)。

8. 実運用でのチューニングと注意点

推奨カーネル・設定

  • Linux 5.15+(5.10では機能制限あり)
  • --with-liburing でPostgreSQLを再ビルド
  • Docker利用時:seccomp設定にio_uring_*を追加

チューニングパラメータ

パラメータ 説明
effective_io_concurrency 同時読み込み上限(推奨:CPUコア数相当)
io_combine_limit バッチ化閾値
shared_buffers キャッシュ競合を抑制
max_worker_processes workerとの併用時の上限調整

9. まとめと今後の展望

  • io_uring はPostgreSQLに真の非同期I/Oをもたらした
  • syscall削減・低レイテンシ・高並列性という三拍子が揃う
  • 今後のリリースでは書き込み(WAL/UPDATE)非同期化が焦点
  • Linuxカーネル側ではio_uring_cmdでGPUやネットワークとの統一I/O層を目指す

io_uringは単なるAPI刷新ではなく、「LinuxカーネルとPostgreSQLのI/Oの境界を再定義した」技術革新です。


10. ハイパーコンバージド環境(vSANなど)でのパフォーマンス改善はありえるか?

PostgreSQLが動作する仮想化・ハイパーコンバージド環境(例:VMware vSANNutanixCephなど)では、
ストレージI/Oはローカルディスクではなく分散レプリケーションによって保証されます。
これらの構成では、トランザクションコミットごとに複数ノードへの書き込み確認が必要なため、
I/O待ち (iowait) が多発しがちです。

10.1 同期I/Oにおける構造的な待機

vSAN(3重書き込み)の例では以下のような動作をします:

COMMIT →
  WAL書き込み →
    仮想ディスク →
      vSANノードA/B/C 同期レプリケーション →
        ACK完了まで待機(この間PostgreSQLはブロック)

この待機時間は、vSAN内部のネットワーク遅延・ACK整合性・キャッシュフラッシュに依存し、
結果としてトランザクションあたりのレイテンシを押し上げます。

10.2 io_uring導入による影響範囲

io_uring によって非同期化されるのは 「PostgreSQL → カーネル → ストレージ」 間のI/Oです。
したがって:

  • 読み込み(SeqScan, VACUUM等) は劇的に改善
  • 書き込み(特にWAL flush, fsync) は依然として同期I/O

つまり、vSANのように「外側で複数レプリカ書き込みを待つ構造」では、
io_uring単体ではI/O待ちを完全には消せません。

10.3 将来的な展望:非同期fsyncへの拡張

io_uring にはすでに IORING_OP_FSYNC が実装されており、
PostgreSQL側でこれを活用すれば、WAL書き込みも非同期化できる可能性があります。

将来的には以下のような構造が実現し得ます:

io_uring_prep_write(sqe, wal_fd, buf, size, offset);
io_uring_prep_fsync(sqe_fsync, wal_fd, 0);
io_uring_submit();

これにより、「書き込み完了待ち」と「トランザクション応答」を分離可能になります。
PostgreSQL側で synchronous_commit = off 相当の動作を安全に行う余地が生まれるでしょう。

10.4 現時点での実用的対策

現段階でvSAN環境のI/Oレイテンシを抑えたい場合は、
以下のようなアプローチが有効です:

手法 効果 注意点
synchronous_commit = off WAL同期書き込みを非同期化 障害時に直近トランザクション消失リスク
commit_delay 調整 複数COMMITをバッチ化 トランザクション遅延増
vSANストレージポリシー変更 レプリカ数減少(例:3→2) 耐障害性低下
NVMeキャッシュ層強化 ACK高速化 HWコスト増

10.5 まとめ(vSAN視点の要点)

項目 状況
vSAN三重書き込み待ち 現時点では解消しない(WAL同期I/O)
データ読み込み 大幅改善(io_uring適用済)
将来的展望 IORING_OP_FSYNCによる非同期コミット化の可能性
実用的緩和策 synchronous_commit=off, commit_delay 調整 等

io_uringはvSANそのものを速くするわけではないが、
PostgreSQL内部のI/Oスタックを最適化することで、
「vSAN上でもCPUが無駄にWAITしない構造」 を作る第一歩になる。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?