コードの上で考えるためのメンタルモデル
『Think Distributed Systems』の第1章を読みながら、かなり強い共感を覚えました。分散システムの具体的な技法が並ぶ前に、そもそも理解とは何か、モデルは何を残して何を捨てるのか、全体を見渡せない構成要素からどうやって全体の保証を作るのかが丁寧に置かれていたからです。
そもそも第1章のタイトルが「Thinking in distributed systems: Models, mindsets, and mechanics」です。中心に置かれている語は distributed systems ではなく Thinking です。少なくとも私には、分散システムについての知識を列挙する章というより、分散システムを題材にしながら、ほかの対象にも持ち運べそうな推論の仕方を扱う章だと読めました。モデル、ものの見方、仕組みを通して、何を知るかよりも、どのように考えるかへ重心を置いているように見えます。このため、第1章で示される考え方は、分散システムの内部だけで完結するものではないのではないか、と読み進めるうちに感じました。
私が日々の設計や技術的な議論で引っかかる場所も、だいたいこの周辺にあります。言葉の定義が曖昧なまま議論が進んでいる。具体例から一般概念へ飛ぶ途中が省略されている。設計者だけが持つ情報を、実行時のコンポーネントも知っているかのように扱っている。個々の部品の性質から、システム全体の性質を直接導いている。個別には違う問題に見えますが、どれも推論に使っているモデルの境界が見えなくなったときに起きる問題のように思われます。
この記事では、第1章の内容を紹介しながら、私がなぜそこに強く反応したのかを考えます。結論を先に短く書くなら、技術を理解するとは用語や実装例を保持することではなく、前提から可能な振る舞いと保証までを再構成できることです。その再構成を支える道具がメンタルモデル(mental model)です。良いメンタルモデルは、単に現実を分かりやすく縮めたものではありません。詳細を捨てても、いま答えたい問いについての推論が壊れないように作られた表現です。
もっとも、これからずいぶん偉そうなことを書きますが、私が「分かっている側にいる」という話ではありません。むしろ毎度「よく分からんなぁ」となり、何が分からないのかを切り分けるために、前提や言葉の射程を確認しています。この記事も、理解に到達した人間による講義というより、分からなさを扱うための足場を組んでみた記録として読んでいただければと思います。
知っていることと理解していること
技術の学習では、知っていることと理解していることが混ざりがちです。CAP定理、Raft、2相コミット(2PC)、直列化可能性(serializability)といった名前を知り、短い説明を言えれば、ひとまず会話には参加できます。しかし、前提が少し変わったときに同じ説明が使えるとは限りません。
私にとって理解とは、説明を記憶している状態よりも、その説明がどの前提から導かれ、どの範囲で有効なのかを再構成できる状態に近いものです。入力となる仮定を変えたとき、可能な振る舞いがどう変わり、どの保証が失われるのかを追えるなら、その知識は別の問題にも持ち運べます。
前提 → 起こり得る振る舞い → 保証
この連鎖の途中が抜けると、用語は結論だけを収納したラベルになります。ラベルは検索の入口としては便利ですが、設計判断の根拠には足りません。逆に、連鎖を再現できれば、知らない製品や未経験の構成を前にしても、何を調べるべきかを決められます。知識量の差が消えるわけではありませんが、未知の対象へ近づく足場になります。
メンタルモデルは推論のための内部表現
本書はメンタルモデルを、対象システムについて私たちが内部に持つ表現として扱います。重要なのは、モデルが現実の縮小コピーではないことです。モデルは必ず何かを捨てます。問いに必要な区別を残し、不要な詳細を落とすことで、考えられる大きさへ対象を変形します。
したがって、良いモデルを最も詳細なモデルとみなすことはできません。詳細を増やせば現実に近づくようには見えますが、推論に不要な状態まで抱え込めば、何が結論を左右しているのか分からなくなります。一方で、捨てすぎれば異なる振る舞いを同じものとして扱ってしまいます。モデルの品質は、写実性よりも、その問いに必要な区別を保存しているかで評価すべきなのでしょう。
ここでいう捨象は、単に情報量を減らして分かりやすくすることではありません。まず、何について答えたいのかを決めます。次に、その答えを左右しない詳細を捨てます。そして、詳細を捨てたモデル上で得た結論が、元の対象についても成り立つかを確かめます。少なくとも今回の問いについて、この対応が保たれていなければなりません。
現実の対象 → 必要な区別を残して捨象 → モデル → 推論 → 元の対象についての結論
たとえば、二つの実装が内部では異なっていても、いま調べたい性質に関して常に同じ振る舞いをするなら、その内部差は捨てられるかもしれません。反対に、捨てた詳細によって結論が変わるなら、その捨象は今回の問いには耐えていません。単純化によって別の問題へ置き換えてしまったか、モデルの射程を越えた問いを立てています。
ここで重要なのは、「現実を完全に再現したモデル」を求めることではありません。完全な再現ができたとしても、それはモデルというより現実の複製に近づき、考えるために対象を小さくする役目を果たせません。どこまで捨てられるかと、捨てたあとにもどの推論が保存されるかは、同じ問題の表裏です。
ただし、本書の著者もこれを絶対的で唯一の考え方として提示しているわけではありません。メンタルモデルは「対象は本当はこういうものだ」と宣言するものではなく、「今回は、この問いについて考えるために、こう捉えます」と置くものです。問いが変われば、残すべき区別も変わります。一つのモデルを対象そのものと同一視すれば、理解を助けるはずの道具が、別の見方を閉ざす枠にもなり得ます。
この見方は、私が言葉の不正確さに引っかかる理由も説明します。言葉は単なる名前ではなく、現実のどの差を残し、どの差を同一視するかを決める小さなモデルです。用語の射程が曖昧なままでは、同じ文章を読んでいても、参加者が異なる状態空間を想定しているかもしれません。議論が噛み合わないとき、意見の対立より先にモデルの不一致を疑う価値があります。
具体例が概念を狭くする
本書が競合状態(race condition)をコード片から説明しないという選択も印象に残りました。典型的な説明では、二つのスレッドが同じ変数を更新し、更新の消失(lost update)が起きます。例としては正しいのですが、その具体性が強いため、競合状態とは共有変数を同時更新する問題だという狭い像が残りやすくなります。
本書は代わりに、プロセスを不可分な操作(atomic action)の列として置きます。プロセスPが操作a、b、cを順に実行し、プロセスQが操作x、y、zを順に実行するとします。PとQを並行に実行したときの構成(concurrent composition)とは、それぞれの内部順序を保ったまま作れる、すべての操作順序の組み合わせ(interleaving)の集合です。その集合の一部が、こちらで定めた正しさの基準(correctness criterion)に反するなら、競合状態があります。
競合状態 = 起こり得る操作順序の一部が、正しさの基準に反する状態
このモデルには変数名もスレッドAPIもCPU命令もありません。それでも、単一コア、複数コア、複数マシン、データベーストランザクションへ同じ構造を持ち運べます。具体例を捨てたことで、かえって概念の適用範囲が見えるようになっています。
このモデルの価値は、コードの詳細を捨てたこと自体にはありません。それらを捨てても、「実行順序によって正しさが変わるか」という推論が壊れないことにあります。競合状態について考える限り、変数名やスレッドAPIは結論を左右しないため、モデルから外せます。捨象によって、複数の具体的な実装に共通する構造だけが残ります。
ただし、CPUによる命令の並べ替えや、ある書き込みが別のコアからいつ見えるかを調べたいなら、このモデルでは足りません。その問いでは、先ほど捨てたCPUのメモリモデルが結論を左右するからです。これは直ちにモデルが誤っているという意味ではありません。競合状態の構造を考えるには使えても、メモリ上の可視性まで考えるには情報が足りないという、モデルの射程の問題です。
私は技術解説を読むとき、例が何を示したのかと、その例からどこまで一般化できるのかを分けて考えたくなります。これは細部への執着に見えることもあります。しかし、例の成立条件を落としたまま一般化すると、別の環境で同じ説明が成立する保証はありません。具体例は理解の入口になりますが、概念の境界まで自動的に教えてくれるわけではありません。一般化の負担を読者だけに預けない説明は、それだけで価値があります。
解像度を変えることと視点を変えること
第1章の中で特に鮮やかだったのが、ブラックボックスとホワイトボックスの違いを解像度(resolution)の変更とし、大域的な視点(global view)と局所的な視点(local view)の違いを視点(perspective)の変更として分けた箇所です。
ブラックボックスの蓋を開けて内部のコンポーネントを見ても、観測者は依然としてシステム全体を見渡しています。見ている細かさが変わっただけです。
ところが、一つのコンポーネントの位置へ観測者を移すと事情が変わります。そのコンポーネントが直接知るのは、自分の状態と、そこまで届いたメッセージだけです。他のコンポーネントの現在状態やネットワーク内部の状態は見えません。これはズームの問題ではなく、情報へアクセスできる範囲の違いです。
たとえば、コンポーネント1がコンポーネント2へ要求を送ったのに、応答が返ってこないとします。全体を見られる運用者は、コンポーネント2が停止したのか、要求が失われたのか、処理が遅いのか、応答が遅延しているのかを調べられるかもしれません。しかしコンポーネント1が観測する事実は、応答がまだ届いていないことだけです。異なるシステム全体の状態が、コンポーネント1からは同じ観測結果に見えています。
異なるシステム全体の状態 → 同じ局所的な観測結果
設計図を見ている私たちは、つい全体の知識を実装へ密輸入します。「コンポーネント1は、コンポーネント2が停止したので処理を切り替える」と説明したくなります。しかしコンポーネント1が観測できるのは、多くの場合、一定時間応答がないことです。タイムアウトは事実の直接観測ではなく、時間に関する仮定を含む判断です。この差を落とすと、設計書の文章は自然でも、実装できないアルゴリズムが出来上がります。
全体の保証を局所的な行動から作る
分散システム設計の難しさは、全体について定義した性質を、局所的な知識しか持たないコンポーネントの行動から成立させることにあります。本書の表現を借りれば「大域的に考え、局所的に行動する(thinking globally while acting locally)」です。
スプリットブレイン(split brain)を例にすると、欲しい全体の保証は、リーダーが高々一つであることです。しかし各ノードは、全員の状態を直接確認してから自分の役割を決められません。ネットワークが分断され、双方が相手の不在を自分の昇格理由にすれば、それぞれの判断は局所的にはもっともらしくても、システム全体では二つのリーダーが生まれます。誰も自分の規則を破っていないのに、全体の正しさが壊れます。
ここでは、部品が正しければ全体も正しいという推論が成立しません。システムの性質は、部品の性質だけでなく、組み合わせ方と相互作用から生じます。信頼できない部品から信頼できるシステムを作れる一方で、信頼できる部品から壊れやすいシステムも作れます。違いを生むのは、構成要素をどう組み合わせるか(composition)です。
構成要素 + 相互作用 → 起こり得る全体の振る舞い → システムの性質
この構図は分散システムに限りません。組織、運用手順、認可、証拠管理にも似た問題があります。各担当者が自分の手順を守ったという事実から、組織全体として必要な保証が満たされたとは限りません。スクリーンショットを複数枚保存したとしても、それぞれの画像が読めることと、画像間の同一性や時系列が保証されることは別です。個々の証拠の品質から、証拠体系全体の性質は直接には導けません。
日々の設計判断に現れる同じ癖
振り返ると、私は普段から無意識にモデルの射程を確認しています。BigQueryを関係データベース(RDB)の代わりに使う設計へ違和感を持つとき、製品の優劣を言いたいのではありません。分析用途を中心に組み立てられたシステムの性質と、低遅延の更新やトランザクションを繰り返すオンライントランザクション処理(OLTP)の要求が、どこで一致し、どこでずれるのかを知りたいのです。
投稿を24時間で期限切れにする設計でも同じです。有効期限(TTL)という語を聞いて特定のデータストアへ飛ぶ前に、読み取り時に期限切れを除外すればよいのか、物理削除の時刻まで保証したいのか、掃除の遅延を許容できるのかを分ける必要があります。要求されるシステム全体の性質が違えば、選ぶ仕組みも変わります。
古いゲームが想定外の環境で高速動作した事象を不具合(bug)と呼ぶかという問いにも、似た構造があります。観測された振る舞いが不都合であることと、どの仕様に違反したため不具合と判定するかは同じではありません。サポート対象、当時の時間取得方法、現在の互換層という前提を置かずに名称だけを決めると、分類が説明の代わりになってしまいます。
これらの反応は、単に細かい言葉が気になるというより、結論を生成した推論を再実行したいという欲求から来ているように思われます。何を前提にし、何を観測し、どの差を無視し、どの保証を求めたのか。その経路が見えれば、賛成するにしても反対するにしても、議論する場所を選べます。
コードより上で考える意味
「コードより上で考える(Thinking above the code)」とは、コードを軽視する態度ではありません。静的なコード片だけでは表しにくい動的な振る舞いの集合を、別の表現で扱うという提案です。実装は一つの実行可能な答えですが、そのコードが取り得るすべての実行や、満たすべき保証を自動的に説明してくれるわけではありません。
コードから出発すると、たまたま使われている言語、API、データ構造へ注意が引かれます。モデルから出発すると、参加者、各参加者が局所的に知り得ること、可能な行動、ネットワークの振る舞い、求めるシステム全体の保証を先に置けます。その後でコードへ降りれば、各行が何の仮定を実装し、どの不正な実行を排除しているのかを確認できます。
もちろん、モデルだけで実装上の問題が消えるわけではありません。CPUのメモリモデル、データベースの分離レベル、クラウドサービス固有の制約など、抽象化で捨てた詳細が結論を変える場面はあります。必要なのは常に上へ逃げることではなく、問いに応じて抽象度を上下し、そのたびに何を捨てたかを把握することです。
したがって、抽象度を上げること自体が目的なのではありません。どの詳細を捨てても推論が保たれるかを見極めることが目的です。コードより上で考えたあとには、必要に応じて再びコードや実行環境へ降り、捨てた詳細が本当に結論へ影響しないかを確かめる必要があります。この往復まで含めて、メンタルモデルを使って考えるということなのでしょう。
設計時に問い直したいこと
第1章から持ち帰れるものを、自分用の確認項目としてまとめると次のようになります。
| 観測項目 | 問い |
|---|---|
| 対象 | いま一つのシステムまたはコンポーネントとして扱っている範囲はどこか |
| 前提 | ネットワーク、障害、時間、順序について何を仮定しているか |
| 局所的な知識 | その判断に必要な情報を、実行主体は本当に観測できるか |
| 可能な振る舞い | 正常系の一例ではなく、どのような実行が起こり得るか |
| 正しさ | どの振る舞いを不正と判定するのか。その基準は誰が決めるのか |
| 保証 | 安全性(safety)、活性(liveness)、拡張性(scalability)、信頼性(reliability)のどれを、どの条件下で約束するか |
| 抽象化 | モデルは何を捨てているか。捨てた詳細が今回の結論を変えないか |
これも万能のチェックリストではありません。問いを固定するためではなく、自分がいま使っているモデルを露出させるための仮置きです。対象や議論の段階によっては、別の観測項目が必要になるはずです。
理解は再構成できること
私はこの章を読んで、分散システムの知識が増えたというより、普段の引っかかりに名前が付いた感覚を持ちました。細部を気にしているようで、実際には推論がどのモデルの上で成立しているかを確認したかったのだと思います。
良いメンタルモデルは、正解を代わりに出してくれるものではありません。何が観測でき、何が起こり得て、何を正しいと呼ぶのかを分離し、推論を再実行できる形にします。そのために詳細を捨てますが、捨てることそのものを目的にはしません。対象としている問いについての推論が保存される範囲で捨てます。そこまでできれば、結論が変わったときにも、どの前提が変わったのか、あるいはモデルの射程を越えていないかを追えます。
分散システムでは、全体を知らない部品たちから全体の保証を作ります。私たちが技術を理解するときも少し似ています。手元に届くのは断片的な説明、具体例、コード、観測結果です。それらを相互作用まで含むモデルへ組み直し、どこまで言えるかを確かめる。その作業を自分で再現できることが、知っている状態から理解している状態へ移る一つの目安になりそうです。
そして本書が第1章で示している考え方は、分散アルゴリズムだけに閉じたものではないように思えました。何を一つの対象として扱うのか。各主体には何が見えているのか。構成要素の相互作用から、全体としてどのような振る舞いが生じるのか。どの詳細を捨象しても、考えたい性質についての推論は保たれるのか。こうした問いは、データベースやAPIの設計だけでなく、認可、運用手順、証拠の扱い、さらに組織の仕組みを考える場面にも持ち込めそうです。
もちろん、異なる分野を同じモデルで説明できるという意味ではありません。それぞれの対象には、それぞれ固有の制約があります。ただ、対象に合わせてモデルを作り、モデルの射程を確かめながら考えるという姿勢には、分野を越えて使える部分がありそうです。分散システムについての入門章を読んでいたはずが、いつの間にか自分が普段どのように物事を理解しようとしているのかまで照らされていた。そのことが、この章に強く惹かれた理由なのだと思います。
とはいえ、ここまで書いてきたものは、私個人が第1章を読んで考えたり感じたりしたことを並べた感想であり、いくらかポエムでもあります。この記事だけで本書の内容を伝えられたとは思っていませんし、私の読み方が本書の唯一の読み方であるはずもありません。
この記事のどこかが少しでも気になったなら、『Think Distributed Systems』を実際に読んでみてください。ひとまず第1章だけでもよいと思います。具体的な分散アルゴリズムへ進む前に、これから何をどのように考えるのか、そのための座標軸をかなり丁寧に用意してくれています。私自身も、答えを得たというより、今後「よく分からんなぁ」となったときに戻ってこられる場所を得たように感じています。