マルチスレッドディスクI/O設計

CPUの話ではない

[[並行処理と非同期]] では「待ち時間を無駄にしない」という一般論を扱いました。ディスク走査は、その中でも待ちの性質がはっきりしている種類の処理です。

数十万ファイルのサイズを集計する処理を考えます。1件あたりの計算はごくわずかで、時間の大半はストレージからの応答待ちに消えます。この状態を I/O バウンドと呼びます。

したがって並列度をCPUコア数に合わせるのは間違いです。 コアが8つでも、待ちが主なら8より多くの要求を同時に投げたほうが速くなります。逆に、投げすぎれば遅くなります。

適正な並列度はストレージで変わる

ストレージ特性適正な並列度
HDD磁気ヘッドの物理移動が要る低い(1〜2)。並列化すると遅くなる
SATA SSD移動が無く同時要求を捌ける中(4〜16)
NVMe SSDキューが深く並列前提の設計高い(16〜64)
ネットワーク越し往復遅延が支配的高い(遅延を隠すため多めに投げる)

HDD で並列度を上げると遅くなるのが直感に反する点です。複数のスレッドが別々の場所を要求すると、ヘッドが行ったり来たりして、順番に読むより遅くなります。

[[ストレージの種類]] によって最適解が逆向きになるため、並列度は設定可能にして、実測で決めるのが実務上の答えです。固定値を埋め込むと、別の環境で性能が落ちます。

走査と処理を分ける

ディレクトリを再帰的に辿る処理そのものは、並列化しても効きにくい部分です。構造上、親を読まないと子が分かりません。

現実的な構成は、発見と処理を分けることです。

[走査スレッド] --- キュー ---> [ワーカー×N]
 ディレクトリを辿り            見つかったファイルを
 対象を積む                    読む・ハッシュを計算する
  • 走査側 — 木構造を辿って対象をキューへ積む。ここは並列度を上げない
  • ワーカー側 — キューから取り出して実際の I/O を行う。ここを並列化する

こうすると、深い階層で走査が詰まっている間もワーカーは動き続けます。[[プロセスとスレッド]] の観点では、スレッドを都度作らずあらかじめ用意した数で回す(スレッドプール)のが基本です。数十万回のスレッド生成は、それ自体が無視できない負荷になります。

走査で必ず起きること

ファイルシステムの走査は、失敗する前提で書きます。

  • 権限が無いディレクトリがある — [[ファイルシステムとパーミッション]] により読めない場所は必ず存在します。1件の失敗で全体を止めない
  • 走査中にファイルが消える — 見つけた直後に削除されることがあります。「存在するはずのものが無い」は正常系として扱います
  • シンボリックリンクが循環する — 辿り続けると無限に潜ります。実体のパスで訪問済みを記録します
  • パスが長い・文字が特殊 — 環境によって上限や扱いが違います

途中で止まらず、最後に何が読めなかったかを報告するのが正しい振る舞いです。集計値だけを返すと、権限で読めなかった分が静かに欠落します。

実務での注意点

  • 進捗を出す — 数十万件の走査は分単位です。無反応だと止まったと判断されます
  • メモリに全件を溜めない — 結果を配列に積み上げると、件数によっては枯渇します。逐次書き出すか集計値だけを持ちます
  • 並列度と結果の順序は無関係 — 並列化すると完了順が入れ替わります。順序が要るなら最後に並べ替えます
  • [[OSの基礎]] のキャッシュに惑わされない — 2回目の測定はOSのページキャッシュが効いて速くなります。比較するなら条件を揃えます

関連技術とのつながり

  • [[並行処理と非同期]] — 待ち時間を無駄にしない一般論。本記事はその I/O バウンド版
  • [[プロセスとスレッド]] — スレッドプールで生成コストを抑える
  • [[ストレージの種類]] — 適正な並列度が逆向きになる理由
  • [[OSの基礎]] — ページキャッシュが測定結果を左右する
  • [[ファイルシステムとパーミッション]] — 読めない場所は必ずある前提で書く
Q: ディスク走査の並列度をCPUコア数に合わせるのが適切でない理由はどれ?
- [ ] コア数が取得できないから
- [x] 時間の大半がストレージの応答待ちで、CPUがボトルネックではないから
- [ ] スレッドの生成が遅いから
解説: I/Oバウンドの処理では、コア数より多くの要求を同時に投げたほうが速くなることがあります。

Q: HDDで並列度を上げると遅くなることがあるのはなぜ?
- [x] 複数スレッドが別々の場所を要求し、磁気ヘッドの移動が増えるから
- [ ] HDDはスレッドに対応していないから
- [ ] キャッシュが無効になるから
解説: NVMe SSDとは逆向きの結果になります。並列度は設定可能にして実測で決めます。

Q: ファイルシステム走査で正しい振る舞いはどれ?
- [ ] 権限エラーが出たら全体を中止する
- [x] 途中で止まらず、最後に読めなかった対象を報告する
- [ ] 読めない対象を集計に含める
解説: 集計値だけを返すと、権限で読めなかった分が静かに欠落します。