- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
アプリの内部を1つのまとまりとして見てきました。今回はそこに線を1本引き、何が変わるかを見ます。
まず結論
1つのプロセスを2つに分けると、無料で付いてきた3つの枠が消えます。同一トランザクション・呼び出しが必ず返る確実さ・型検査です。
得られるとされるのは「別々に配れること」。分割は交換であり、何を払うのかを名指しできれば、払う価値があるかを自分で判断できます。
数値を期待する受け手に、線の向こうから文字列の "100" や null を送ると、どうなるでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
無料で付いてきていた3つの枠
1つのプロセスの中で関数を呼ぶとき、気づかないまま3つのものを受け取っています。
同一トランザクション — 複数の更新をまとめ、途中で失敗したら全部を無かったことにできる。呼び出しが必ず返る確実さ — 値が返るか例外が飛ぶかで、向こうだけが宙に浮くことはない。型検査 — TypeScript などの静的型検査があれば、数値を期待する引数に文字列を渡すと実行前に止まる。
この3つは、同じプロセス・同じ言語・同じ接続の中にいるから成り立っているものです。線を1本引いた瞬間に提供者がいなくなります。
同じ受け手に4通りの値を送ってみる
実測です。Docker Desktop の Linux 仮想マシン上、aarch64 の Alpine Linux コンテナ(Node v24.21.0 / TypeScript 5.9.3)で 2026年9月18日に実行。通信はコンテナ内のループバック宛のみ。
まず同じプロセスの中で数値の変数に文字列を代入します。
types.ts(4,7): error TS2322: Type 'string' is not assignable to type 'number'.tsc exit=2
1行も実行される前に止まりました。 次に同じ受け手を線の向こうに置き、コードを変えずに4通り送ります。
{"amount":100} -> HTTP 200 {"received":100,"typeof":"number","doubled":200}{"amount":"100"} -> HTTP 200 {"received":"100","typeof":"string","doubled":200}{"amount":null} -> HTTP 200 {"received":null,"typeof":"object","doubled":0}{} -> HTTP 200 {"typeof":"undefined","doubled":null}
4通りすべてが HTTP 200 です。 型が合わないことを報告する場所がありません。特に null の行は例外にも異常応答にもならず、静かに 0 になっています。
同じ論理を2つに分け、途中で失敗させたとき何が起きましたか。
失敗したときに、誰が戻すのか
型は「入ってくる値」の話でした。次は出ていった変更です。
同じ接続の中なら、何も書かなくても途中の失敗で全部を巻き戻せます。接続が枠を持っているからです。線を越えるとこの枠が届きません。向こう側は自分の枠を開き、自分で閉じます。手前が後から失敗しても、向こうにとっては終わった話です。
- ・分けても原理的に戻せないのではない。自動で効いていたものが書くべきものに変わる
- ・枠を線の向こうへ伸ばす道具の代償は分散編が扱う
巻き戻しが届かないところを見る
同じ論理を2通りで動かし、同じ地点で失敗させます。まず1プロセス・1トランザクションの中。
例外: 2つ目の更新の直前で失敗 -> ROLLBACK した1プロセス・1TXの結果: [{"name":"A","bal":100},{"name":"B","bal":0}]
A は 100 に戻りました。次に別プロセス・別DB・線の向こうへ割ります。
サービスAから100を引いた(ここまで成功)例外: サービスBを呼ぶ直前で失敗BEGIN/ROLLBACK はサービスAの中だけの話なので、ここから巻き戻す手段は無いA: [{"name":"A","bal":0}]B: [{"name":"B","bal":0}]
A も B も 0 です。100 がどこにも無くなりました。
呼び出しが「返ってこなかった」とき、確実に言えるのはどれですか。
結末が2つから3つに増える
1プロセスの中の呼び出しには結末が2つしかありません。値が返るか例外が飛ぶか。どちらも制御が呼び出し側に戻ります。無限ループなどで返らないときは、呼び出し側も一緒に止まります。呼び出し側だけが先へ進む形はありません。
線を越えると返ってこないが加わります。呼び出し側だけが待つのをやめ、向こうの結末は不明のまま残ります。前の2つは向こうで何が起きたかを伝えますが、3つ目は何も伝えません。
返ってこなかった呼び出しの、向こう側を覗く
同じ操作を分けない場合と分けた場合で並べます。まず1つのプロセスの中。
call(100) -> RETURNED {"ok":true,"charged":100} (0ms)call(-1) -> THREW RangeError: negative amount (0ms)---- 呼び出し側だけが先へ進む結末は無い: 制御が戻るのは上の2行のどちらか ----
次に線の向こうへ。3件目だけ、向こう側が処理を確定させたあと応答を返さないようにしてあります。
charge(100) -> RETURNED status=200 body={"ok":true,"charged":100} (7ms)charge(-1) -> ERROR-RESPONSE status=400 body={"error":"negative amount"} (1ms)charge(777) 応答なし -> NO ANSWER (timeout) status=- body=- (2003ms)
呼び出し側が 777 について知るのは「返ってこなかった」だけ。向こう側の台帳ファイルの中身を見ます。
100777
777 は記録されています。処理は完了済み。 呼び出し側だけが知りません。
線を越えた呼び出しが返ってこない
呼び出し側には応答がありません。しかし向こう側の台帳には値が記録されていました。手前は空、向こうは完了です。
見る場所 記録されたもの そこから言えること呼び出し側 返ってこなかった 受け取れなかった、だけ向こう側の台帳 777 処理は完了している
受け手が null を 0 として扱う
型が合わない値が届いても受け手は正常応答を返します。送った側から見れば成功。しかし向こうで起きたのは意図と違う計算でした。
見る場所 記録されたもの そこから言えること送った側 HTTP 200 通った、だけ向こう側の計算 0 として扱った 意図と違う値になった
「返ってこない」と「200 で通ったが値が違う」。表に出る形は正反対ですが、共通する構造は何でしょうか。
手前に残った記録から、向こう側の状態をどこまで言えるかを両方で考えてみてください。
この章のまとめ
- 分けると同一トランザクション・呼び出しが必ず返る確実さ・型検査の3つが消える。同じプロセスの中にいたから成り立っていたもの。
- 型検査が届かない: 4通り送って全部が HTTP 200。null は静かに 0 に。
- 巻き戻しが届かない: 分けると A も B も 0。ただし原理的に戻せないのではなく、逆向きの操作を自分で書くことになる。
- 結末が2つから3つに増える。返ってこなかった呼び出しの値が向こう側の台帳に記録済みだった。「返ってこない」は「行われなかった」ではない。
- 2003ms は自分で設定した上限であって測定された遅延ではない。測ったのは HTTP だけ。
- 得られるとされるのは「別々に配れること」。出力で示せないので一段弱い格で扱う。割増の費用そのものは既存のクイズ群「アーキテクチャIII」が扱っている。
- 消えた枠を取り戻そうとすると、そこから先は第12期「分散システムの土台」の13本が全部効く。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
分けたときに失われる3つとして、本レッスンが名指ししたのはどれですか。
用語集(4語)
- 同一トランザクション
- 複数の更新をまとめ、途中で失敗したら全部を無かったことにできる枠。別プロセスの更新はこの枠に入らない。
- 補償
- 既に確定した操作を打ち消すため、逆向きの操作を自分で実行すること。線の向こうの更新は自動では戻らない。
- 3つ目の結末
- 呼び出しが「値が返る」「例外が返る」のどちらでもなく「返ってこない」まま呼び出し側だけが先へ進み、向こうの結末が不明で残ること。1プロセスの中では呼び出し側も一緒に止まるので、この形にはならない。
- 待ち時間の上限
- 応答を待つのをやめる時刻。自分で決める値で、向こう側の処理時間を測った値ではない。