概要
本記事では、KVM 仮想マシンにおけるディスク I/O パフォーマンスをテーマに、io_uring を有効化した場合と無効化した場合で、どれほど実際の数値に差が出るのかを検証しています。
テストでは、ゲスト OS 側のアプリケーションは従来どおり libaio を利用しつつ、ホスト側の QEMU に対してのみ io_uring を有効化するという構成で比較を実施しました。
io_uring とは
Linux カーネルが提供する 次世代の高速 I/O インターフェースです。
従来の AIO(async I/O)では実現できなかった効率的なノンブロッキング I/O を、ユーザー空間とカーネル空間の間に「リングバッファ(queue)」を置く仕組みで実現します。
主に以下の改善により、I/O のレイテンシとスループットが全体的に改善されます。
- システムコールの回数を減らす
- コピー処理などの無駄を減らす
- I/O の完了通知が高速になる
QEMU における io_uring の有効化と無効化
今回、KVM 上の VM に対して io_uring を有効にします。まず、KVM 側で io_uring を利用可能か確認します。
grep IO_URING /boot/config-$(uname -r)以下の出力であれば問題ありません。
CONFIG_IO_URING=y次に、XML を編集して io_uring を有効化します。KVM 上の既存の VM: io-uring-test をテストに使用しました。
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2'/>
<source file='/var/lib/libvirt/images/io-uring-test.qcow2'/>
<target dev='vda' bus='virtio'/>
<address type='pci' domain='0x0000' bus='0x03' slot='0x00' function='0x0'/>
</disk>devices.disk type.driver を変更することで、io_uring の有効化と無効化を切り替えることができます。
無効化時:
<driver name='qemu' type='qcow2'/>有効化時:
これにより、QEMU プロセス(ホスト側)の I/O パスに io_uring が使用されます。
<driver name='qemu' type='qcow2' io='io_uring'/>今回、virsh edit コマンドを使用して書き換えました。
sudo virsh edit io-uring-test変更後、以下のように VM を再起動します。
virsh reboot io-uring-testテストコマンド
テストコマンドは fio コマンドを使用しました。これはゲスト OS 上で実行します。
sudo fio --name=uringtest \
--filename=/tmp/test.img \
--size=4G \
--rw=randread \
--bs=4k \
--iodepth=32 \
--ioengine=libaio \
--direct=1 \
--numjobs=1 \
--time_based=1 \
--runtime=60 \
--group_reporting| オプション | 意味 / 役割 | ポイント / 解説 |
|---|---|---|
--name=uringtest | ジョブの名前 | ログや出力の見分けに使用される識別名 |
--filename=/tmp/test.img | テスト対象のファイル | ここに指定したファイルへ I/O を実行。存在しない場合は自動作成 |
--size=4G | テスト対象ファイルのサイズ | 4GB の test.img を対象にランダム READ を行う。容量は I/O パターンに影響 |
--rw=randread | 読み書きのパターン | ランダムリード。ランダム I/O の性能を見るために一般的 |
--bs=4k | ブロックサイズ | 4KB 単位で READ を発行。実運用に近い設定 |
--iodepth=32 | 発行するキュー深度 | 一度に 32 個の I/O を飛ばす。ディスクや virtio の並列性能が見える |
--ioengine=libaio | 使う I/O エンジン | ここは aio ベース(ゲスト側)。io_uring の比較ではここを固定 |
--direct=1 | OS バッファを bypass する | ページキャッシュを使わず、純粋なディスク性能を測定 |
--numjobs=1 | 並列ジョブ数 | 1 ジョブのみ。CPU コアやスレッドを増やすと IOPS も変わる |
--time_based=1 | 時間で制御するモード | 何バイト読むかではなく、一定時間読み続ける方式 |
--runtime=60 | テスト実行時間 | 60 秒間連続で I/O を生成し続ける |
--group_reporting | グループ単位で結果をまとめる | numjobs を増やした時でも読みやすい一括出力にする |
テスト結果
テスト結果は以下のようになりました。
io_uring 無効時
uringtest: (g=0): rw=randread, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=32
fio-3.28
Starting 1 process
uringtest: Laying out IO file (1 file / 4096MiB)
Jobs: 1 (f=1): [r(1)][100.0%][r=23.8MiB/s][r=6086 IOPS][eta 00m:00s]
uringtest: (groupid=0, jobs=1): err= 0: pid=1646: Sat Nov 22 14:03:41 2025
read: IOPS=8213, BW=32.1MiB/s (33.6MB/s)(1925MiB/60008msec)
slat (usec): min=3, max=7963, avg=16.49, stdev=26.93
clat (nsec): min=1581, max=60802k, avg=3877259.54, stdev=3523016.01
lat (usec): min=17, max=60811, avg=3894.00, stdev=3525.64
clat percentiles (usec):
| 1.00th=[ 28], 5.00th=[ 46], 10.00th=[ 66], 20.00th=[ 135],
| 30.00th=[ 603], 40.00th=[ 1795], 50.00th=[ 3490], 60.00th=[ 4948],
| 70.00th=[ 6194], 80.00th=[ 7242], 90.00th=[ 8455], 95.00th=[ 9503],
| 99.00th=[12125], 99.50th=[13566], 99.90th=[18220], 99.95th=[21365],
| 99.99th=[43254]
bw ( KiB/s): min=20456, max=174104, per=100.00%, avg=32954.39, stdev=25474.00, samples=119
iops : min= 5114, max=43526, avg=8238.56, stdev=6368.49, samples=119
lat (usec) : 2=0.01%, 4=0.01%, 10=0.01%, 20=0.11%, 50=5.99%
lat (usec) : 100=10.32%, 250=8.15%, 500=3.89%, 750=3.60%, 1000=2.72%
lat (msec) : 2=6.32%, 4=12.10%, 10=43.18%, 20=3.55%, 50=0.06%
lat (msec) : 100=0.01%
cpu : usr=5.85%, sys=18.29%, ctx=210097, majf=0, minf=44
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=100.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.1%, 64=0.0%, >=64=0.0%
issued rwts: total=492859,0,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=32
Run status group 0 (all jobs):
READ: bw=32.1MiB/s (33.6MB/s), 32.1MiB/s-32.1MiB/s (33.6MB/s-33.6MB/s), io=1925MiB (2019MB), run=60008-60008msec
Disk stats (read/write):
dm-0: ios=492420/169, merge=0/0, ticks=1881168/1288, in_queue=1882456, util=99.96%, aggrios=493367/164, aggrmerge=0/5, aggrticks=1891895/1155, aggrin_queue=1893179, aggrutil=99.88%
vda: ios=493367/164, merge=0/5, ticks=1891895/1155, in_queue=1893179, util=99.88%io_uring 有効時
uringtest: (g=0): rw=randread, bs=(R) 4096B-4096B, (W) 4096B-4096B, (T) 4096B-4096B, ioengine=libaio, iodepth=32
fio-3.28
Starting 1 process
uringtest: Laying out IO file (1 file / 4096MiB)
Jobs: 1 (f=1): [r(1)][100.0%][r=28.3MiB/s][r=7236 IOPS][eta 00m:00s]
uringtest: (groupid=0, jobs=1): err= 0: pid=1642: Sat Nov 22 14:14:26 2025
read: IOPS=9425, BW=36.8MiB/s (38.6MB/s)(2209MiB/60008msec)
slat (usec): min=3, max=12701, avg=23.28, stdev=44.30
clat (nsec): min=1376, max=31411k, avg=3367318.26, stdev=3263867.26
lat (usec): min=17, max=31425, avg=3390.92, stdev=3266.57
clat percentiles (usec):
| 1.00th=[ 24], 5.00th=[ 44], 10.00th=[ 70], 20.00th=[ 123],
| 30.00th=[ 221], 40.00th=[ 603], 50.00th=[ 2933], 60.00th=[ 4359],
| 70.00th=[ 5538], 80.00th=[ 6587], 90.00th=[ 7767], 95.00th=[ 8717],
| 99.00th=[11207], 99.50th=[12518], 99.90th=[16188], 99.95th=[18220],
| 99.99th=[23462]
bw ( KiB/s): min=25624, max=94802, per=100.00%, avg=37792.75, stdev=15803.08, samples=119
iops : min= 6406, max=23700, avg=9448.03, stdev=3950.80, samples=119
lat (usec) : 2=0.01%, 4=0.01%, 10=0.03%, 20=0.40%, 50=5.70%
lat (usec) : 100=10.16%, 250=15.64%, 500=7.12%, 750=1.80%, 1000=0.79%
lat (msec) : 2=3.14%, 4=12.51%, 10=40.59%, 20=2.07%, 50=0.03%
cpu : usr=9.51%, sys=28.70%, ctx=201485, majf=1, minf=45
IO depths : 1=0.1%, 2=0.1%, 4=0.1%, 8=0.1%, 16=0.1%, 32=100.0%, >=64=0.0%
submit : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.0%, 64=0.0%, >=64=0.0%
complete : 0=0.0%, 4=100.0%, 8=0.0%, 16=0.0%, 32=0.1%, 64=0.0%, >=64=0.0%
issued rwts: total=565626,0,0,0 short=0,0,0,0 dropped=0,0,0,0
latency : target=0, window=0, percentile=100.00%, depth=32
Run status group 0 (all jobs):
READ: bw=36.8MiB/s (38.6MB/s), 36.8MiB/s-36.8MiB/s (38.6MB/s-38.6MB/s), io=2209MiB (2317MB), run=60008-60008msec
Disk stats (read/write):
dm-0: ios=565174/95, merge=0/0, ticks=1848428/72, in_queue=1848500, util=99.98%, aggrios=566004/90, aggrmerge=0/7, aggrticks=1862109/115, aggrin_queue=1862296, aggrutil=99.90%
vda: ios=566004/90, merge=0/7, ticks=1862109/115, in_queue=1862296, util=99.90%テスト結果の比較
KVM ホスト側の io_uring を有効化しただけで、ゲスト OS の IOPS とスループットが約 15% 程度向上しました。レイテンシも一貫して低下しており、特に p50〜p95 の改善が非常に明確です。これは「単なるピーク性能」ではなく、VM の日常的な I/O 応答そのものが速くなることを意味します。
| 指標 | io_uring 無効 | io_uring 有効 | 差分 |
|---|---|---|---|
| IOPS | 8,213 | 9,425 | +14.7% |
| BW | 32.1 MiB/s | 36.8 MiB/s | +14.6% |
| 平均レイテンシ | 3.89 ms | 3.39 ms | -0.5 ms |
| p50 | 3,490 µs | 2,933 µs | -557 µs |
| p70 | 6,194 µs | 5,538 µs | -656 µs |
| p90 | 8,455 µs | 7,767 µs | -688 µs |
| p95 | 9,503 µs | 8,717 µs | -786 µs |
| sys CPU | 18.29% | 28.70% | +10.4% |
| disk util | 99.88% | 99.90% | ± |
今回の fio の比較において、特に注目すべきなのは p50〜p95 のレイテンシが大幅に改善している点です。これらは平均値とは異なり、システムの “実際の応答性” を最もよく反映する指標です。
p50(中央値):
普段の I/O のレイテンシ。ここが下がるほど、OS の基本動作が軽くなります。
p95:
遅い側 5% を除いた領域。“時々重くなる” という現象やジッターを示すため、ワークロード安定性に強く関わります。
特に以下のような、細かい I/O が大量に発生する環境では p95 の低下が大きな体感改善につながります。
- Kubernetes ノード
- Filebeat / Loki / journald などのログ処理
- CI/CD エージェント
- 軽量 DB(SQLite / Redis などの混在環境)
今回の比較結果では、以下の変化が確認でき、I/O パスの最適化によって応答遅延が滑らかに抑えられています。
- io_uring 無効: p50〜p95 のレイテンシが全体的に高め
- io_uring 有効: 同区間が明確に低下し、レイテンシの「山」が削れる
IOPS やスループットの向上に加え、「I/O のばらつきが減る=体感が軽くなる」という効果が得られている点が非常に重要です。
また、sys CPU の使用率が 18.29% → 28.70% と増加していますが、これは性能悪化ではありません。io_uring は I/O をより積極的にカーネル側で処理するため、sys CPU が増えるのは設計上自然な挙動であり、その結果として I/O の待ち時間が削減され、レイテンシ(特に p50〜p95)が大きく改善しています。
総合評価
今回の検証により、io_uring を有効化すると qcow2 を使用している場合でも確実に性能が向上することが明確になりました。
特に注目すべき点は、ゲスト側のアプリケーションが io_uring に対応していなくても効果が表れるという事実です。fio では ioengine=libaio を指定していましたが、ホストの QEMU が io_uring を用いてストレージにアクセスすることで I/O パス上の無駄が削減され、そのまま性能改善に繋がっています。
つまり、アプリケーション側の対応状況に依存せず、ホスト側で io_uring を有効化するだけでメリットが得られるということになります。
特に以下のような用途では、有効化による効果が顕著に現れる傾向が確認できました。
- 小さな I/O が高頻度で発生する VM(Kubernetes ノード、ログ集約、CI/CD、軽量 DB など)
- qcow2 バックエンド(vda)を利用している環境
- Ceph RBD や ZFS をバックエンドに持ち、ホスト側の I/O オーバーヘッドが大きくなりがちな構成
fio の結果では、IOPS とスループットが概ね 15% 程度向上し、レイテンシについても p50〜p95 を中心に全域で低下しました。特に tail latency(遅延が最も大きい I/O)が改善されており、I/O の応答性の安定化にも寄与しています。
総合すると、現在の KVM 基盤において io_uring の有効化は標準設定にしても問題ないレベルの信頼性と性能向上を示すと言えます。





