ブロックI/O層とマルチキュー(blk-mq)の構造
NVMeの数百万IOPSをカーネルが取りこぼさない理由を構造から理解。bioとリクエストの正体、単一キューが頭打ちになった必然、二段キューの設計までが一本でつながります。
- 1.ブロックI/O層はread/writeをbioに変え、隣接bioをマージしてrequestへ束ね、最終的にデバイスへ発行する。bioが入力、requestが発行単位。
- 2.旧来の単一requestキュー+1ロックは全コアが奪い合う直列点となり、SSD/NVMeの並列性を活かせない。これがblk-mq移行の根本理由。
- 3.blk-mqはCPU単位のソフトウェアキューとデバイス実キューに対応するハードウェアキューに分離し、ロック競合と完了割り込みのキャッシュ移動を同時に潰す。
ブロックI/O層を「データ構造」で捉える
read/write はシステムコールでカーネルに入り、ページキャッシュを抜けると最終的にブロックデバイスへの要求になります。この変換を担うのがブロックI/O層ですが、本記事はスケジューラの並べ替えアルゴリズムではなく、層を流れるデータ構造とキューの構造に焦点を当てます。並べ替え方式の系譜(エレベータ・Deadline・CFQ)はI/OスケジューラとブロックI/O層で扱っているので、ここでは「bioがどうrequestになり、どのキューを通ってデバイスへ届くか」を構造図として言葉で描きます。
要点を先に置くと、層の中心には2つの構造体があります。
bio:I/Oの入力単位。1回の論理I/Oを表し、「どのセクタ範囲へ/どのメモリページ群(bio_vec配列)の内容を/読むか書くか」を持つ。ファイルシステムが論理オフセットを物理ブロック(LBA)へ翻訳した結果がここに詰まる。request:デバイスへの発行単位。隣接・連続する複数のbioをマージして束ねたもの。実際にドライバがコマンド化して投げる粒度。
read/write ─→ bio ─→ (マージ) ─→ request ─→ ディスパッチ ─→ ドライバ ─→ デバイス
入力単位 発行単位
つまり層の仕事は「多数の小さな bio を、できるだけ大きく連続した request に束ね、適切なキューを経由してデバイスへ流す」ことです。
bio から request へ:マージという束ね
新しい bio が届くと、層はまず既存の request に合体できないかを探します。これがマージで、コマンド発行回数とデバイス側のオーバーヘッドを直接減らす最も効く最適化です。
- バックマージ:新
bioのセクタが、既存requestの末尾の直後に連続する場合。最も頻度が高い。 - フロントマージ:新
bioが既存requestの先頭の直前に連続する場合。 - マージ不可:連続しない、あるいはサイズ上限(
max_sectors)や境界制約に触れる場合は、新たなrequestを起こす。
既存request: セクタ[100..107]
新bio: セクタ[108..111] → バックマージ → [100..111] へ拡張
新bio: セクタ[ 96.. 99] → フロントマージ → [ 96..107] へ拡張
新bio: セクタ[500..503] → 連続しない → 新しいrequestを生成
隣接候補を高速に探すため、層は最近のリクエストをハッシュやコア局所のキャッシュ(plug、後述)で管理します。マージは要求数そのものを減らすため、並べ替えで移動距離を縮める以上に効く場面が多いのが実務上のポイントです。
プロセスが連続I/Oを出すと予想できる区間では、カーネルは要求を即座にデバイスへ流さず、タスク固有の小さなリスト(plug list)に一時的に溜めます。区間の終わり(unplug)でまとめてマージ・ディスパッチすることで、マージ機会を最大化します。blk_start_plug/blk_finish_plug がその境界です。コア局所のリストなのでロックも要りません。
なぜ単一キューが頭打ちになったのか
blk-mq以前のレガシーブロック層は、デバイスごとに1本の request キューを1つのスピンロック(queue_lock)で保護する設計でした。HDD時代はこれで十分でした。デバイス自体が直列(ヘッドは1つ)なので、要求を直列に並べること自体が仕事だったからです。
ところがSSD、とりわけNVMeが前提を壊します。SSDにはシークも回転待ちもなく、性能の源は内部の多数チャネルを同時に叩く並列性へ移りました。NVMe規格は最大65535本のサブミッションキュー、各深さ最大65536を許します。この並列デバイスに対し、単一キュー+単一ロックは構造的な欠陥になります。
- 全CPUコアが1本のロックを奪い合う。コア数が増えるほど競合が悪化し、ロック取得待ちがCPUを飽和させる。
- せっかく並列に受けられるデバイスへ、要求を直列にしか注げない。デバイスの実力の前にソフト側が律速する。
- 完了割り込みを処理するコアが、要求を発行したコアと異なると、
request構造へのアクセスがキャッシュライン越しに飛び回る(キャッシュ局所性が崩れる)。
数十万〜数百万IOPSの世界では、この単一直列点が決定的なボトルネックでした。「デバイスは速いのにカーネルが追いつかない」を解くには、キューの構造そのものを変える必要があったのです。
blk-mq:二段キューという答え
blk-mq(マルチキューブロック層)は、1本だったキューを2段に分離します。役割が明確に分かれている点が設計の核です。
- ソフトウェアキュー(
ctx):CPU(コア)ごとに1本。要求はまず自コアのキューに入る。コア局所なのでロック競合が原理的に消える。マージや並べ替えはここで完結する。 - ハードウェアディスパッチキュー(
hctx):デバイスが持つ実キューに1対1で対応。ソフトウェアキューの要求をここへ流し込み、ドライバがデバイスへ発行する。
CPU0 CPU1 CPU2 CPU3 ← ソフトウェアキュー ctx(コア毎・競合なし)
\ | | /
[ hctx 0 ] [ hctx 1 ] ← ハードウェアディスパッチキュー(実キューに対応)
| |
NVMe SQ/CQ0 NVMe SQ/CQ1 ← デバイスの実キュー(並列に発行)
ctx から hctx への対応づけ(マッピング)が肝です。コアとハードウェアキューを揃えれば、そのコアで発行した要求の完了割り込みを、同じコアで受けられる。request 構造が同じCPUのキャッシュに留まり、コア間のキャッシュライン移動が消えます(割り込みの上半分・下半分の完了処理がここに乗ります)。NVMeドライバは典型的にコア数ぶんのハードウェアキューを作り、ctx と hctx をほぼ1対1に張ることで、発行から完了までを1コアで閉じます。
blk-mqは起動時に固定数の request を確保し、各々に**タグ(整数ID)**を振っておきます。I/Oのたびに request を動的確保するのではなく、空きタグを1つ取って使い、完了で返す方式です。これによりホットパスからメモリ確保が消え、タグはそのままデバイスのコマンド識別子(NVMeのCID等)にも使えます。タグ枯渇=キュー深さ上限、という関係も直感的です。
構造の対比:レガシー vs blk-mq
両者の差は「並べ替えアルゴリズム」ではなくキューとロックの構造にあります。
| 観点 | レガシー(単一キュー) | blk-mq(マルチキュー) |
|---|---|---|
| requestキュー | デバイスごとに1本 | ソフトウェア=コア毎/ハードウェア=実キュー毎の二段 |
| ロック | 1本のqueue_lockを全コアで共有 | ソフトキューはコア局所で競合なし |
| requestの確保 | I/O毎に動的確保 | 起動時にタグ付きで事前確保 |
| 完了割り込みの局所性 | 発行コアと別コアになりがち | 発行コアと同コアで受けやすい |
| 並列デバイス適性 | 直列発行で律速 | 実キューへ並列に発行 |
| 想定デバイス | 回転HDD・低IOPS | SSD・NVMe・高IOPS |
Linuxは段階的に移行し、最終的にレガシー層を撤廃してblk-mqへ一本化しました。現在のスケジューラ(none/mq-deadline/kyber/bfq)はすべてblk-mqの上に載る実装であり、この二段キュー構造が共通の土台です。
NVMe時代のスケーラビリティが生まれる仕組み
なぜこの構造が「スケールする」と言えるのか。スケーラビリティはコアを増やしたとき性能が頭打ちにならず伸びることを指し、その条件はコア間で共有する書き込みポイントを無くすことです。blk-mqはそれを構造で満たします。
- ロック競合の消滅:要求は自コアのソフトウェアキューに入るため、ホットパスにコア共有ロックが無い。コアを足しても競合が増えない。
- キャッシュ局所性の維持:発行と完了が同コアに閉じ、
request構造のキャッシュライン移動が起きない。コアごとに独立して回る(NUMA局所性とも同じ発想)。 - デバイス並列性の解放:ハードウェアキューが実キューに1対1で張られ、複数コアが互いに干渉せず同時にコマンドを発行できる。NVMeの数万キューに対し、ソフト側がボトルネックにならない。
理想:性能 ∝ コア数(各コアが独立に発行・完了)
Core0 → ctx0 → hctx0 → SQ0 ┐
Core1 → ctx1 → hctx1 → SQ1 ├ 共有点なし=線形に近くスケール
Core2 → ctx2 → hctx2 → SQ2 ┘
完全な線形ではありません。タグ空間やデバイス側キュー数が有限なので、コア数がハードウェアキュー数を超えると複数 ctx が1つの hctx を共有し、そこに軽い同期が戻ります。それでも「全コアが1ロックを奪い合う」旧来の構造に比べれば、競合点は桁違いに小さく抑えられます。
none は ctx からデバイスへほぼ素通しで、最も競合が少なくNVMe向きです。一方 mq-deadline や bfq は並べ替えや公平制御のために共有状態を持つため、その分の同期が入ります。高IOPSのNVMeで none が推奨されがちなのは、シーク削減の利得が無いことに加え、スケジューラ自体が新たな共有点になり得るからです。遅延保証や公平性が要る場合に限り、コストを承知で挟みます。
「なぜblk-mqが必要だったか」を構造で答えられるかが分かれ目です。要点は3つ——(1) 旧来の単一キュー+単一ロックが全コア共有の直列点となりSSD/NVMeの並列性を殺していた、(2) blk-mqはコア毎ソフトキューでロック競合を消し、(3) ハードウェアキューを実キューに1対1で張り完了割り込みを発行コアに局在させてキャッシュ移動も消す。「キューを増やした」ではなく「共有書き込みポイントを無くした」と言えると強いです。
まとめ
ブロックI/O層は read/write を入力単位の bio に変え、隣接するものをバックマージ/フロントマージで束ねて発行単位の request を作り、デバイスへ流します。回転HDD時代は単一キュー+単一ロックで足りましたが、SSD/NVMeが並列性を主役にすると、その1ロックが全コアの奪い合う直列点となり律速しました。blk-mqはキューをコア毎のソフトウェアキューと実キューに対応するハードウェアキューに二段分離し、ロック競合の消滅・完了割り込みのコア局在・デバイス並列性の解放を同時に達成します。タグによる事前確保とplugによる束ねがホットパスを軽くし、NVMeの数百万IOPSをカーネルが取りこぼさない。並べ替え方式の系譜はI/Oスケジューラ、土台のシステムコールコストや割り込み処理の局所性まで遡ると、現代ストレージの速さが一本の線でつながります。
OS Article
ブロックI/O層とマルチキュー(blk-mq)の構造を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
blk-mq
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
旧来の単一requestキュー+1ロックは全コアが奪い合う直列点となり、SSD/NVMeの並列性を活かせない。これがblk-mq移行の根本理由。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「blk-mq / ブロックI/O」に近いか確認する。
- 強みである「ブロックI/O層はread/writeをbioに変え、隣接bioをマージしてrequestへ束ね、最終的にデバイスへ発行する。bioが入力、requestが発行単位。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。