ストレージスタックとI/Oパス ─ ブロック層・キュー深度・NVMe多重キュー
なぜNVMe SSDは速いのにキュー深度1では本領を発揮できないのか。アプリからデバイスまでのI/Oパスとblk-mq・多重キューを原理から押さえ、IOPSとレイテンシの勘所が掴めます。
- 1.I/OはVFS・ページキャッシュ・ブロック層・ドライバを順に下り、各層でマージや並べ替え・アドレス変換を受けてデバイスへ届く。
- 2.blk-mqはコア毎のソフトウェアキューとハードウェアキューでロック競合を排し、NVMeはコア毎にSQ/CQ対を持ち多数の要求を並行発行する。
- 3.達成IOPSはリトルの法則によりキュー深度÷平均レイテンシで決まり、深度を上げるとIOPSは飽和まで伸びるが個々のレイテンシは延びる。
アプリの1回のreadは何層を下るか
アプリケーションが read() を1回呼ぶと、その要求はカーネル内のいくつもの層を順に下り、最終的にストレージデバイスへ届きます。各層には固有の役割があり、ここを把握しないとボトルネックの所在を見誤ります。
アプリ (read/write, またはio_uring/AIO)
│ システムコール
VFS (Virtual File System) ... ファイル抽象を統一、ファイルシステムへ振り分け
│
ページキャッシュ ... ヒットすればここで完了(デバイスへ行かない)
│ ミス時
ファイルシステム (ext4/XFS等) ... ファイル内オフセット -> ブロック番号へ変換
│ bio(ブロックI/O要求)を発行
ブロック層 (blk-mq) ... requestへまとめ、マージ・スケジュール
│
デバイスドライバ (NVMe/SCSI) ... ハードウェアのキューへ投入
│
ストレージデバイス
ここで決定的に重要なのは、多くのreadはデバイスまで到達しないという点です。ページキャッシュにヒットすれば、物理I/Oを一切起こさずメモリコピーだけで返ります。デバイスまで下りるのはキャッシュミス、ダイレクトI/O(O_DIRECT)、あるいは書き戻し(writeback)のときです。
ページキャッシュは読んだブロックをメモリに保持し、再アクセスをデバイスI/Oなしで返します。書き込みも通常はいったんキャッシュ上の「ダーティページ」になり(write-back)、後でまとめてデバイスへ書き戻されます。これによりランダムな小書き込みが順次の大書き込みへ束ねられます。同じ「速い層でなるべく止める」発想はキャッシュとメモリ階層と地続きで、DRAMヒットとデバイスアクセスの速度差(ナノ秒対マイクロ秒)が階層化の動機です。
ブロック層が果たす仕事 ─ bioからrequestへ
ファイルシステムはファイル内オフセットをデバイス上のセクタ範囲へ変換し、bio(ブロックI/O記述子。転送先メモリページ・対象セクタ・方向を持つ)を発行します。ブロック層はこのbioを受け取り、requestへとまとめてデバイスドライバへ橋渡しします。途中で行われる主な処理が2つあります。
- マージ: 隣接セクタへの複数bioを1つのrequestへ結合する。発行回数とオーバーヘッドが減り、特にHDDではシーク削減に直結する。
- スケジューリング(並べ替え): requestの順序を入れ替えて効率や公平性を上げる。HDD時代はシーク最小化(エレベータ法)が主目的だったが、SSD/NVMeではシークが無いため、目的は公平性やレイテンシ制御へ移っている。
複数のbio(連続セクタ)
bio: sector 100-107
bio: sector 108-115 --マージ--> request: sector 100-123
bio: sector 116-123
NVMe SSDではデバイス内部が高度に並列で、ホスト側で順序を作り込む利得は小さくなります。Linuxでは none(並べ替えなし、最小オーバーヘッド)が高速NVMeの既定として広く使われ、レイテンシの公平性が要るときに mq-deadline、cgroupでの帯域制御に bfq を選ぶ、という使い分けになります。回転HDDかフラッシュかで最適なスケジューラが変わる点が要点です。
なぜblk-mqが必要だったか ─ 単一キューの限界
かつてのブロック層は、デバイスごとに1本の要求キューを1つのスピンロックで守っていました。シングルキューでは、I/Oを発行する全コアがこのロックを奪い合います。コア数が増え、デバイスが毎秒数十万〜百万IOPSを捌けるNVMe世代になると、このロック競合とキャッシュライン共有がそのままスループットの天井になりました。
そこで導入されたのが blk-mq(Multi-Queue Block Layer) です。キューを2段に分けて競合を排します。
blk-mqの2段キュー構造
コア0 コア1 コア2 コア3 ... 各コアにソフトウェアキュー(per-CPU)
│ │ │ │
└──────┴───┬──┴──────┘
▼ マッピング
HWキュー0 HWキュー1 ... デバイスのハードウェアキューに対応
(NUMA/コア群ごと)
▼
NVMeデバイス
- ソフトウェアキュー(per-CPU): コアごとに専用。同じコアからの発行はロックを取り合わず、ローカルに積める。
- ハードウェアキュー: デバイスが持つ実際のキュー数に対応。ソフトウェアキューはこれへ多対一または一対一で写像される。
要点は、ロックをコア単位に分割してグローバルな1点競合を消したこと、そしてデバイスが複数キューを持つならコアからデバイスまで並列パスを通せることです。これによりNVMeの並列性をソフトウェア側が初めて活かせるようになりました。
NVMeの多重キュー ─ コア毎のSQ/CQ対
旧来のAHCI/SATAはキューが事実上1本(NCQで深度32)で、コア間で共有せざるを得ませんでした。NVMe はこの前提を覆し、最大で数万本のキューを定義できる仕様として設計されています。中核はSQ(Submission Queue)とCQ(Completion Queue)の対です。
NVMeのキュー対(コアごとに割り当て)
コアN ─┬─ SQ(投入キュー) : ホストがコマンドを書き込む
└─ CQ(完了キュー) : デバイスが完了通知を書き込む
動作:
1. ホストがSQへコマンドを書く
2. ホストがSQの Tail Doorbell レジスタを更新(「N件積んだ」)
3. デバイスがSQからコマンドをフェッチし実行
4. デバイスがCQへ完了エントリを書き、MSI-X割り込みを上げる
5. ホストが処理し CQ Head Doorbell を更新
決定的な設計が、SQ/CQ対をコアごとに持てることです。コアNは自分専用のSQ/CQだけを触るため、キューを巡るロックもキャッシュライン共有も発生しません。blk-mqのハードウェアキューはこのNVMeのSQ/CQ対へ自然に対応します。さらにSQは深いキューを許し、ホストはドアベルを叩く前に複数コマンドをまとめて積んでから一度に通知できるため、コマンド発行あたりのオーバーヘッド(MMIO書き込み回数)が下がります。完了側も割り込み合体で複数完了を1割り込みに束ねられます。この「コア毎キュー+多重発行+合体」が、NVMeが百万IOPS級を捌ける構造的な理由です。
| 観点 | AHCI / SATA | NVMe |
|---|---|---|
| キュー本数 | 1本 | 最大で多数(コア毎に確保可) |
| キュー深度 | 32(NCQ) | 1キューあたり最大65535 |
| コア間競合 | 共有キューで競合 | コア毎キューで競合なし |
| 完了通知 | 共有割り込み | キュー毎MSI-X・割り込み合体 |
| 発行の単位 | コマンド毎 | ドアベルでまとめて通知 |
SQへ積むたびにドアベルレジスタへMMIO書き込みをすると、その都度PCIe越しの書き込みが走ります。1コマンドごとに叩くと発行コストが積み上がるため、複数コマンドを積んでから1回だけ叩く(バッチ発行)のが定石です。逆に深度1の同期I/Oでは毎回1往復が露出し、デバイスの帯域ではなく1往復のレイテンシが性能を決めます。この往復にはPCIeの経路も効くため、PCIeのアーキテクチャの理解が効いてきます。
キュー深度・IOPS・レイテンシ ─ リトルの法則
ここまでの「キューを深くできる」という話が、性能にどう効くのかを原理で押さえます。鍵は待ち行列理論のリトルの法則です。定常状態で、システム内に滞留する平均要求数(=実効キュー深度 QD)は、到着率(=達成IOPS)と平均応答時間(=レイテンシ L)の積に等しくなります。
リトルの法則: QD = IOPS × L
これを変形すると
IOPS = QD / L
この一本の式が、ストレージ性能の挙動をほぼ説明します。
- 深度1(QD=1): 1要求の完了を待ってから次を出す同期I/O。IOPS は
1 / Lで頭打ち。レイテンシ100マイクロ秒なら最大1万IOPSにしかならず、デバイスの帯域は全く使い切れない。 - 深度を上げる: 複数要求をデバイスへ同時に滞留させると、デバイス内部の並列性(NVMeの多チャネル・多ダイ並列)が稼働し、IOPS が深度に比例して伸びる。
- 飽和点: デバイスの並列度(さばける最大同時数)を超えて深度を増やすと、IOPS はもう伸びず頭打ちになる。このときさらに深くしても、増えた要求はキューで待つだけなのでレイテンシ L だけが線形に増える。
キュー深度を上げたときの典型的な挙動
IOPS ┤ ____________ 飽和(帯域上限)
│ /
│ /
│ / ← 線形に伸びる領域(並列度に余裕)
│ /
└/───────────────────► キュー深度
L ┤ / 飽和後はレイテンシだけ増える
│ /
│__________/
└───────────────────► キュー深度
ベンチマークの高IOPS値は、たいてい深いキュー深度(QD=32や64)で測られます。しかし深度を飽和点より上げても、IOPS は伸びずに個々の要求のレイテンシだけが悪化します。レイテンシ重視のワークロード(同期書き込み、データベースのコミット)ではむしろ浅い深度が要求され、そこではIOPSではなく1往復のレイテンシが支配的です。「最大IOPSの数値」と「実アプリで体感するレイテンシ」は別物で、リトルの法則がその両者を結びます。深度はスループットとレイテンシを天秤にかけるツマミだ、と捉えるのが正確です。
実効キュー深度を高く保つには、アプリが非同期に多数のI/Oを出し続ける必要があります。同期 read() を1スレッドで回すとQDは常に1ですが、io_uring や非同期I/O、あるいは多数のスレッドで発行すれば、デバイスへの滞留数を増やして並列性を引き出せます。NVMeとblk-mqが用意した並列パスは、こうして発行側が深さを供給して初めて活きます。
「I/OパスはVFS → ページキャッシュ → ファイルシステム → ブロック層 → ドライバ → デバイス、キャッシュヒットならデバイスへ行かない」「blk-mqはper-CPUソフトウェアキューとハードウェアキューでロック競合を排す」「NVMeはコア毎のSQ/CQ対で競合なく多重発行、ドアベルとMSI-X・割り込み合体で通知」「IOPS = キュー深度 ÷ レイテンシ(リトルの法則)、飽和後は深度を上げてもレイテンシだけ増える」の4点が核心です。
まとめ
- アプリのI/OはVFS・ページキャッシュ・ファイルシステム・ブロック層・ドライバを順に下り、ページキャッシュにヒットすればデバイスへ達しない。ブロック層はbioをrequestへマージし並べ替える。
- 単一キュー+単一ロックの旧ブロック層はNVMe世代の並列性に追従できず、blk-mqがper-CPUソフトウェアキューとハードウェアキューでロック競合を解消した。
- NVMeはコア毎にSQ/CQ対を割り当て、ロックもキャッシュライン共有もなく多数のコマンドを並行発行する。ドアベルのバッチ化と割り込み合体が発行・完了のオーバーヘッドを抑える。
- 達成性能はリトルの法則 IOPS = キュー深度 ÷ レイテンシに従う。深度を上げるとIOPSは飽和まで伸び、飽和後はレイテンシだけが延びる。深さはスループットとレイテンシを秤にかけるツマミである。
並列化が頭打ちになる構造はアムダール/グスタフソンの法則とも通じ、デバイスへの最終段は割り込みと例外処理が完了を運びます。I/Oが「どの層で止まり、どこで並列化され、何が往復のコストになるか」を押さえると、ストレージ性能の数字が腑に落ちます。
CPU/メモリ/ディスク Article
ストレージスタックとI/Oパス ─ ブロック層・キュー深度・NVMe多重キューを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ストレージ
比較で見る軸
難易度: advanced / カテゴリ: CPU/メモリ/ディスク / タグ数: 6
導入後に効く点
blk-mqはコア毎のソフトウェアキューとハードウェアキューでロック競合を排し、NVMeはコア毎にSQ/CQ対を持ち多数の要求を並行発行する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- CPU/メモリ/ディスク
- タグ数
- 6
判断チェックリスト
- 自社の用途が「ストレージ / NVMe」に近いか確認する。
- 強みである「I/OはVFS・ページキャッシュ・ブロック層・ドライバを順に下り、各層でマージや並べ替え・アドレス変換を受けてデバイスへ届く。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。