分散システムのテスト:決定論的シミュレーション
再現できないバグに悩む分散システムのテストを原理から。時刻・乱数・スケジュール・I/Oを単一スレッドの支配下に置き、シード固定で障害シナリオを完全再現する決定論的シミュレーションの仕組みを掴めます。
- 1.決定論的シミュレーションテスト(DST)は、時刻・乱数・スレッドスケジュール・I/Oといった非決定性の源をすべて単一スレッドの制御下に抽象化し、乱数シードを固定すれば実行が一意に再現されるようにする手法。
- 2.ネットワーク分断・遅延・パケットロス・ディスク障害をシミュレータが乱数で注入し、論理時刻を加速して数年ぶんのレアな事象を圧縮実行する。失敗したシードを再投入すれば同じバグが100%再現する。
- 3.FoundationDBやTigerBeetleが実装。実装にはOSやネットワークへの依存を注入可能な境界(決定論的境界)として設計する規律が要り、外部I/Oや実時間APIの直接呼び出しを禁じる必要がある。
再現できないバグという根本問題
分散システムのバグの多くは、特定のタイミングでのみ顔を出します。ノードAがメッセージを送った直後にネットワークが分断され、その最中にディスク書き込みが遅延し、さらに別ノードでGCが走った——という事象が重なって初めて壊れる。こうした並行性とタイミングに依存するバグは、テスト環境では数千回に1回しか起きず、しかも一度観測しても次に同じ条件を作れないため再現できません。スタックトレースを眺めても、その実行に至った時系列を復元できないのです。
決定論的シミュレーションテスト(Deterministic Simulation Testing, DST)は、この「再現できなさ」を根本から消し去ります。発想の核心は単純です——実行を非決定的にしている源をすべて特定し、それらを単一の乱数シードの支配下に置く。そうすれば、同じシードからは必ずビット単位で同一の実行が再生される。失敗したシードをメモしておけば、そのバグを何度でも、デバッガを当てながら再現できます。FoundationDB が先駆けとして実装し、TigerBeetle がさらに徹底した形で広めました。
非決定性の源を断つ
プログラムの実行を非決定的にする源は、突き詰めると数えられます。DST はそのすべてを「注入可能なインターフェース」の背後へ追い出します。
| 非決定性の源 | 通常の実装 | DSTでの抽象化 |
|---|---|---|
| 現在時刻 | OSの実時間APIを直接呼ぶ | シミュレータが持つ論理時計から取得 |
| 乱数 | OSのエントロピー源を使う | シードから決まる擬似乱数生成器(PRNG) |
| スレッドスケジュール | OSスケジューラが任意に切替 | 単一スレッド上で協調的に明示切替 |
| ネットワークI/O | 実ソケットで送受信 | メモリ内の仮想ネットワークを経由 |
| ディスクI/O | 実ファイルシステムへ書く | メモリ内の仮想ディスクへ書く |
| 並行処理の進行順 | プリエンプションで不定 | イベントキューの順序で一意に決定 |
肝は最後の二行です。通常のマルチスレッドプログラムは、OS スケジューラがいつどのスレッドへ CPU を割り当てるかを制御できず、プリエンプションの起きる箇所が実行ごとに変わります。これが並行バグの再現を阻む最大の壁です。DST はこれを単一スレッドへ畳み込むことで解きます。複数の論理ノードを協調的(cooperative)に動かし、「次にどのタスクを進めるか」をシミュレータが PRNG で決める。OS のプリエンプションを使わないので、スケジュールは完全にシードの関数になります。
真のマルチスレッド実行は、メモリの可視性順序やプリエンプション位置がハードウェアとOSに委ねられ、原理的に再現不能です。DSTはノード間・タスク間の並行性を「論理的な並行」として表現し、物理的には1スレッドで1イベントずつ処理します。並行性そのものは失われません——むしろシミュレータがスケジュールを支配するため、現実のOSなら滅多に踏まない「意地悪な順序」を意図的に試せます。物理的逐次・論理的並行、これがDSTの設計上の核です。
論理時計とイベント駆動の進行
DST の内部は**離散事象シミュレーション(discrete-event simulation)**です。実時間は一切使わず、「論理時刻」を持つイベントキューが進行を駆動します。
イベントキュー(論理時刻でソートされた優先度付きキュー):
t=0 ノードA: タイマー発火 → リクエスト送信
t=3 仮想ネット: メッセージ配送(遅延3を乱数で付与)
t=3 ノードB: リクエスト受信 → 応答をキューへ
t=7 仮想ネット: 応答配送
...
ループ:
while キューが空でない:
e = キューから最小論理時刻のイベントを取り出す
現在論理時刻 = e.time
e を実行(新たなイベントをキューへ積む)
ここから二つの強力な性質が出ます。第一に、論理時刻は実時間と切り離されているため、「イベントが無い区間」を一瞬で飛ばせます。30秒のタイムアウト待ちも、ネットワークがアイドルなら論理時刻を即座に t=30 へ進めるだけで済む。結果、数日〜数年ぶんの論理時間を数秒の実時間で圧縮実行でき、現実なら稀にしか起きないレアな事象の組み合わせを大量に踏ませられます。第二に、メッセージの配送遅延・到着順・喪失をすべて PRNG が決めるため、同じシードならイベントの取り出し順序まで完全に一致します。これが再現性の源泉です。
論理時刻が実時間と独立であることは、論理クロックの考え方(/devops/logical-clocks/)と通じます。DST のシミュレータが扱うのは因果の順序であって、壁時計の秒ではありません。
障害注入とシードの威力
シミュレータはネットワークとディスクを丸ごと仮想化しているので、現実の障害を確率的に注入できます。各イベント処理の前後で PRNG を引き、設定した確率に従って意地悪を仕込みます。
- ネットワーク:メッセージの遅延を乱数で増やす、順序を入れ替える、複製する、丸ごと落とす、特定ノード間を一定期間だけ分断する(/devops/leader-election-split-brain/ が問題にするスプリットブレインを再現)。
- ディスク:書き込みの遅延、fsync 前のクラッシュでの部分書き込み、読み出し時のビット化け、ディスクフルを模す。
- プロセス:ノードを任意の論理時刻でクラッシュ・再起動させ、永続化されていない状態が失われる挙動を試す。
これはカオスエンジニアリング(/devops/chaos-engineering/)と狙いは同じ「障害を先に起こして耐性を確かめる」ですが、決定的に違うのは完全な再現性とスピードです。カオスエンジニアリングが本番に近い環境で実時間に障害を注入するのに対し、DST はメモリ内で論理時間を加速し、失敗を見つけたら同じシードで何度でも再現します。
DSTの実用上の最大の価値はここです。テストが落ちたとき、出力されるのは「失敗した乱数シード」というたった一つの数値。このシードを再投入すれば、ネットワーク分断・遅延・クラッシュのタイミングまで含めて寸分違わず同じ実行が再生されます。printデバッグを足しても、デバッガでブレークしても実行は変わりません。「再現手順が分からない」というハイゼンバグ特有の地獄が、原理的に消えます。CIは毎回ランダムなシードで膨大な回数を回し、失敗シードだけをリグレッションテストとして固定すればよいのです。
シードを変えながら何百万回も実行すれば、シミュレータは膨大なスケジュールと障害パターンの組み合わせを探索します。論理時間の加速と相まって、人手では決して書けない数の障害シナリオを網羅的に踏ませられる——これが「網羅再現」の意味です。
実装に課される規律
DST は無料では手に入りません。コードベース全体に設計上の規律を要求します。核心は、非決定性に触れる操作をすべて注入可能な境界(インターフェース)の背後へ追い出すことです。
禁止される直接呼び出し(非決定性が漏れる):
現在時刻 … OSの実時間API
乱数 … OSのエントロピー源
ネットワーク … 実ソケットのread/write
ディスク … 実ファイルへのread/write
スレッド生成 … OSスレッド・プリエンプション
許される形:
これらを Clock / Random / Network / Storage といった
インターフェース越しに呼ぶ。
本番では実装A(実OS)、テストではシミュレータ実装Bを注入する。
つまり「コードは自分が実機で動いているのかシミュレータ内で動いているのかを知らない」状態に保ちます。アプリケーションロジックは抽象インターフェースだけを呼び、本番では実OS実装が、テストではシミュレータ実装が差し込まれる。この一線を破る箇所——たとえばどこか一か所でも実時間APIを直接叩く、ライブラリ内部が裏でスレッドを起こす——があれば、その瞬間に非決定性が漏れ、シードを固定しても再現が崩れます。
最大の落とし穴は、再現性が「全か無か」だという点です。99%決定的では意味がありません。ハッシュマップの反復順序がアドレス依存、ログ出力のタイムスタンプが実時間、サードパーティ製クライアントが内部でスレッドプールを使う——こうした小さな漏れ一つで、同じシードでも実行が分岐し、バグが再現しなくなります。だからDSTは「あとから足す」ことが極めて難しく、TigerBeetleのように設計初日からアーキテクチャ全体をこの制約の上に組む必要があります。依存ライブラリの選定すら、決定性を壊さないかで判断されます。
この制約は厳しい一方で、副次的な恩恵もあります。I/O を抽象化し並行性を単一スレッドへ寄せる設計は、状態機械(state machine)として書くことを自然に促し、コードの見通しとテスタビリティそのものを上げます。リトライやバックオフ(/devops/retry-backoff-jitter/)のような時間依存ロジックも、論理時計の上でなら決定的に検証できます。
DSTの限界と適用範囲
万能ではありません。DST が検証するのはシミュレータが忠実にモデル化した範囲だけです。仮想ネットワークや仮想ディスクが現実の障害モードを取りこぼしていれば、その障害はテストで踏めません。実機のハードウェアバグ、ファームウェアの癖、TCP スタックの実装差、ライブラリのネイティブコードといった「シミュレータの外」は守備範囲外です。だから DST は実機での負荷試験や本番でのカオスエンジニアリングを置き換えるのではなく補完します。
また、単一スレッドへ畳み込む以上、検証対象のコードは協調的に書かれていなければなりません。既存のブロッキングI/O前提・マルチスレッド前提のコードベースへ後付けするのは、ほぼ書き直しに等しい労力を要します。
DSTの本質は「非決定性の源(時刻・乱数・スケジュール・I/O)をすべて単一シードの支配下に置き、シード固定で実行を一意に再現する」こと。物理的には単一スレッドで逐次実行し、論理的並行性はイベントキューで表現する——この物理逐次・論理並行の対比を即答できること。再現性は全か無かで、決定性が一箇所でも漏れると崩れる。論理時刻が実時間と独立だからレアな組み合わせを圧縮実行できる。FoundationDBとTigerBeetleが代表例。カオスエンジニアリングとの違いは「完全再現性と速度」対「本番忠実度」。
まとめ
- **決定論的シミュレーションテスト(DST)**は、時刻・乱数・スレッドスケジュール・I/O という非決定性の源をすべて注入可能なインターフェースへ追い出し、単一の乱数シードの支配下に置くことで、実行をビット単位で再現可能にする手法。
- 内部は離散事象シミュレーション。論理時計とイベントキューで進行し、物理的には単一スレッドで逐次実行、論理的には並行を表現する。OSプリエンプションを使わないためスケジュールがシードの関数になる。
- 論理時刻が実時間と独立なので、アイドル区間を飛ばして数年ぶんを数秒で圧縮実行し、ネットワーク分断・遅延・喪失・ディスク障害・クラッシュを PRNG で確率的に注入できる。
- 失敗時に出るシードがバグの完全な再現手順になり、デバッガを当てても実行は変わらない。ハイゼンバグの再現地獄が原理的に消える。
- 代償は厳しい設計規律——非決定性の直接呼び出しを全面禁止し、決定性は全か無か。一箇所の漏れで再現が崩れるため、FoundationDB や TigerBeetle のように設計初日から組み込む必要がある。実機試験やカオスエンジニアリングを補完するもので、置き換えではない。
DevOps/インフラ Article
分散システムのテスト:決定論的シミュレーションを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
決定論的シミュレーション
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 6
導入後に効く点
ネットワーク分断・遅延・パケットロス・ディスク障害をシミュレータが乱数で注入し、論理時刻を加速して数年ぶんのレアな事象を圧縮実行する。失敗したシードを再投入すれば同じバグが100%再現する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「決定論的シミュレーション / 分散システム」に近いか確認する。
- 強みである「決定論的シミュレーションテスト(DST)は、時刻・乱数・スレッドスケジュール・I/Oといった非決定性の源をすべて単一スレッドの制御下に抽象化し、乱数シードを固定すれば実行が一意に再現されるようにする手法。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。