- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに — 二つのノードの間にはいつもネットワークがある
- リトライとバックオフ — メッセージを連投しない
- タイムアウト — いつまで待つかをあらかじめ決めておく
- ハートビート — 「今日はどうだった?」という小さなping
- 一貫性モデル — 私たち、今おなじページにいますか
- バックプレッシャー — 受け取れる分だけ流す
- グレースフルデグラデーション — つらい日はコア機能だけ
- 冪等性 — 謝罪は二度言っても二倍にはならない
- 二人の将軍問題 — その言葉、本当に届いた?
- TTL付きキャッシュとしての信頼 — 更新しなければ期限切れになる
- おわりに — 完璧なネットワークは存在しない
- 参考資料
はじめに — 二つのノードの間にはいつもネットワークがある
分散システムが難しい理由は、はっきりしています。ネットワークは信頼できず、メッセージは失われたり遅れて届いたりし、相手のノードが生きているかどうかすら確信できません。そして何より決定的なのは、相手のノードの内部状態を自分の目で直接には決して見られない、という事実です。私たちは相手が送ってくるメッセージからだけ、相手を推測します。
この一文をもう一度読むと、それは誰かと近づくことの説明にもなっています。二人の人間はそれぞれ独立して動くシステムであり、その間にはいつも信頼できないチャネルが横たわっています。表情、口調、返信の速さ、沈黙。私たちはこの不完全な信号だけを頼りに、相手の心という観測できない状態を推定して生きています。
長く分散システムと向き合ううちに、その中の解法がなぜか恋愛のアドバイスのように聞こえ始めました。完璧なネットワークを前提にしたシステムは必ずいつか倒れます。完璧な相手を前提にした関係も同じです。二つの世界の答えはまったく同じです。完璧さではなく、回復力(レジリエンス)、そして明示的な確認と、少しの寛容さ。この記事では、その重なりを九つに分けて読み解いていきます。
リトライとバックオフ — メッセージを連投しない
リクエストが失敗したとき、すぐにそのまま送り直してはいけません。相手のサーバーが過負荷で倒れている状況なら、失敗したクライアントたちが一斉に再試行を浴びせて、サーバーを完全に殺してしまうリトライストームが起きます。だからよくできたシステムは指数バックオフを使います。最初の再試行は1秒後、次は2秒後、その次は4秒後。失敗が続くほど間隔を広げ、相手に回復する余地を与えます。そこにランダムなジッターを足して、全員が同じ瞬間に殺到するのを防ぎます。
返信がないときの態度は、まさにこれであるべきです。既読なのに返事がないからと、すぐに「?」「起きてる?」「何してるの?」を立て続けに撃ち込むのはリトライストームです。相手は今、会議中かもしれないし、疲れているかもしれないし、ただ考えを整理している最中かもしれません。そんなときに必要なのは、もっと強く叩くことではなく、間隔を広げることです。一度送ったら待つ。その次はもっと長く待つ。
返事がないときの答えは、もっと頻繁にではなく、もっとまばらに。バックオフは無関心ではなく、相手が回復するための余地だ。
ジッターの教訓もあります。毎回きっかり同じ時刻に、同じトーンで連絡しないこと。予測できる圧力は、それ自体が負担になります。
タイムアウト — いつまで待つかをあらかじめ決めておく
タイムアウトのないリクエストは、システムの中で最も危険なものの一つです。応答を無限に待つスレッドは、リソースを握ったまま永遠に止まり続け、それが積み重なるとシステム全体がじわじわと首を絞められていきます。だから成熟したシステムは、すべての外部呼び出しに上限を置きます。「この時間内に返事がなければ失敗とみなして次に進む」。待つことには必ず終わりがなければなりません。
関係においても、無限の待ちは止まったシステムです。「いつかは心を開いてくれる」「いつかは変わってくれる」を期限もなく待ち続ける状態は、リソースを握ったまま何も処理できないスレッドと同じです。その間、自分の人生の他のリクエストはすべてキューの後ろに押しやられ、静かに飢え死にしていきます。
タイムアウトを設定するのは、相手を急かすことではありません。自分自身に正直になることです。どれくらい待つのか、何を確認したいのかを、自分であらかじめ決めておくこと。その時間が過ぎても信号がなければ、それは相手を恨む理由ではなく、次の状態へ移ってよいという静かな許可です。
終わりの定まっていない待ちは、献身ではなく、ハングだ。
ハートビート — 「今日はどうだった?」という小さなping
分散システムのノードたちは、互いが生きているかを知るために、定期的に小さな信号をやり取りします。これをハートビートと呼びます。重いデータではなく、「私はここにいる」という軽いpingです。ハートビートが何度か連続で届かないと、相手のノードは死んだと判断して対応を始めます。接続が生きているという確信は、大きな出来事ではなく、この小さな信号の絶え間なさから生まれます。
関係も、まったく同じように保たれます。関係を支えているのは、大きなイベントや記念日ではありません。「ご飯食べた?」「今日はどうだった?」「無事に着いた?」という、情報量はほとんどないけれど絶え間ない ping が支えています。この小さな信号が途切れ始めたとき、実は接続はもう静かに弱まっているのです。
大事なのは頻度であって、大きさではありません。ハートビートはもともと軽くあるべきです。毎回、深い会話をする必要はありません。ただ、規則的であること。そして相手のハートビートが数日届いていないなら、腹を立てる前に、まず気づく感度が必要です。システムは沈黙を障害の兆候として読みます。人もそうです。
一貫性モデル — 私たち、今おなじページにいますか
分散システムにおける**強い一貫性(strong consistency)とは、すべてのノードが常に同じ値を見ることを意味します。誰がいつ読んでも、最新の状態が保証されます。その代わり高くつきます。調整に時間がかかり、可用性を削ります。逆に結果整合性(eventual consistency)**は、一時的な不一致を許します。今はノードごとに値が違っていても、新しい更新がなければ、いつかはすべて同じ値に収束します。よい設計は、すべてのデータに強い一貫性を強制しません。どこに強い一貫性が本当に必要かを選びます。
関係もそうです。すべてを一瞬一瞬、完璧に一致させようとすると、互いに疲れ果てます。「私たち、今つき合ってるってことでいいの?」「お互いにだけ本気ってことでいいの?」といったものは、強い一貫性が必要な項目です。ここを結果整合性に任せてしまうと、二人が別々の関係を生きていたという事実を後になって知り、大きく壊れます。こういうものは明示的に合意して、すぐに同期しなければなりません。
一方で「今週末に何を観るか」「どの店に行くか」といったものは、結果整合性で十分です。今すぐ完璧に一致していなくても、会話を何度か交わせば自然に収束します。ここまで強い一貫性を求めると、関係が会議だらけになります。
あるものは今すぐ同じ値でなければならず、あるものはいつか同じになればいい。この二つを見分けることが、成熟だ。
バックプレッシャー — 受け取れる分だけ流す
速いプロデューサーが遅いコンシューマーにデータを浴びせると、コンシューマーのバッファがあふれ、やがてシステムは崩壊します。だからよく設計されたパイプラインにはバックプレッシャーがあります。コンシューマーがプロデューサーに「今はこれだけしか受け取れない」と逆向きに信号を送り、プロデューサーはその速度に合わせて流れを緩めます。処理能力は受け手が決め、送り手がそれに合わせる構造です。
人にも処理容量があります。つらい一週間を過ごしている相手に、自分の心配ごとや計画、不満、要求を一度に浴びせれば、相手のバッファはあふれます。どんなに正しい言葉でも、受け取れない瞬間に押し込めば失われます。愛が足りないからではなく、キューがいっぱいだからです。
バックプレッシャーの核心は、コンシューマーに速度の主導権を渡すことです。関係ではこう表れます。「今この話、する余裕ある?」と先に尋ねること。そして相手が「今日はちょっとしんどい」と信号を送ってきたとき、それを拒絶ではなくフロー制御として読むこと。よいパートナーは、相手の容量を観測しながら自分の送信速度を調整します。愛を減らすのではなく、相手が受け取れるリズムで渡すのです。
グレースフルデグラデーション — つらい日はコア機能だけ
大きなサービスは、一部が壊れたからといって全体を落としたりしません。レコメンドエンジンが死んだら、レコメンドだけ一時的に止めて、決済やログインといったコアは動かし続けます。これがグレースフルデグラデーションです。すべてを失う代わりに、最も重要なものを守るために、重要でない機能をあえて手放す戦略です。
人も、よい日ばかりを生きているわけではありません。体調が悪く、疲れ果て、心が崩れる日があります。そんな日に関係へ完全な稼働を求めるのは、現実的ではありません。気の利いた冗談、細やかな気遣い、長い会話といった付加機能は、一時的にオフになることがあります。それでもコアサービス、つまり「私はあなたの味方だ」という信号さえ生きていれば、関係はその日を持ちこたえます。
これは自分にも、相手にも当てはまります。自分がつらい日には、完璧なパートナーになろうと無理をする代わりに、コアだけを守ればいい。相手がつらい日には、いつもほど細やかでないことを障害として処理しない寛容さが必要です。今フロントエンドが少し無骨なだけで、バックエンドはちゃんと生きている、と知っていること。
崩れる日の目標は、完璧な稼働ではなく、コアサービスを生かしておくことだ。
冪等性 — 謝罪は二度言っても二倍にはならない
**冪等性(idempotency)**とは、同じ操作を何度適用しても、結果が一度適用したのと同じになる性質です。決済リクエストに冪等キーを付ければ、ネットワークエラーで同じリクエストが二度届いても、請求は一度きりです。状態を変えるのは最初の適用であり、その後の再送は状態をそれ以上変えません。
謝罪がまさにそれです。本物の謝罪は、一度、きちんと届いたときに状態を変えます。ところが同じ謝罪を十回くり返せば、関係が十倍に回復するでしょうか。しません。心のこもった謝罪が一度すでに状態を動かしているなら、その後のくり返しは何も変えられません。むしろ「こんなに謝ったじゃないか」というくり返しは、謝罪を、相手の回復のためではなく自分の罪悪感を軽くするための要求へと変えてしまいます。それはもう冪等ではない、副作用を積み上げる再送です。
逆のわなもあります。相手はすでに受け入れて状態が変わっているのに、その事実を確認できず、同じ謝罪を送り続けること。システムなら、相手の ACK を信頼して再送を止めるべきです。関係では、相手が「大丈夫、ちゃんと伝わったよ」と応えたら、そこで止めることが、その信頼です。
ごめんと二度言っても、二倍申し訳ないわけではない。きちんとした謝罪が一度、状態を変える。その後のくり返しは、自分のためだ。
二人の将軍問題 — その言葉、本当に届いた?
二人の将軍問題(Two Generals' Problem)は、分散コンピューティングで最も有名な不可能性の結果の一つです。谷を挟んだ二人の将軍が、敵陣を横切る伝令だけを頼りに、攻撃の時刻を合意しなければなりません。問題は、どのメッセージも途中で失われうる、ということです。将軍Aが「夜明けに攻撃」と送ります。将軍Bが確認の返信を送ります。しかしAは、その返信が届いたかどうかを知りません。だからBは、Aが返信を受け取ったかを確認してほしくなります。この確認の確認は果てしなく続き、信頼できないチャネルでは完全な合意は理論的に不可能です。
関係の最も静かな誤解は、まさにここで生まれます。自分は確かに気持ちを伝えたと思っています。ところがそのメッセージが相手に届いたのか、届いたとして自分の意図どおりに理解されたのか、100パーセントの確信は持てません。「当然わかっているはず」は、検証されていない仮定です。表現しなかった気持ちは送信されていないパケットであり、送った気持ちさえ、誤解という損失を被りうるのです。
理論上、完全な合意が不可能だとしたら、実務の答えは何でしょうか。システムの答えと同じです。ACK を明示的にやり取りし、大事なことは確認のラウンドをもう一度回すこと。「これ、こう理解したけど合ってる?」と聞き返すこと。相手の言葉を自分の言葉に置き換えて確かめること。完全な確信には決してたどり着けませんが、明示的な確認を一ラウンド多く回すほど、誤解の確率は確実に下がります。
相手がわかってくれたはず、という仮定は、到達確認なしで送ったメッセージだ。大事な言葉ほど、確認をもう一度回せ。
TTL付きキャッシュとしての信頼 — 更新しなければ期限切れになる
キャッシュは、高価な照会を毎回くり返さないために結果を保存しておく仕組みです。しかしたいていTTL(Time To Live)、つまり有効期限が付きます。その時間が過ぎると、キャッシュされた値は期限切れになり、もう一度もとの情報源に確認しなければなりません。TTLがある理由は単純です。世界は変わり、古いキャッシュはいつか現実とずれていくからです。
信頼が、まさにこのキャッシュです。私たちは一瞬ごとに相手を最初から検証したりはしません。これまで積み重ねた経験をもとに「この人は信頼できる」という値をキャッシュしておき、そのおかげで毎回疑うという高価な演算を飛ばします。信頼が関係を速く軽くする理由です。ただ、このキャッシュにはTTLがあります。何の更新もなく長く放置された信頼は、静かに古びていきます。だから信頼は、絶え間ない相互作用でずっと更新し続けなければなりません。
そして裏切りは、この視点では**キャッシュの無効化(cache invalidation)**です。信頼を裏切る一度の出来事が、それまでキャッシュしておいた信頼の値を丸ごと無効にします。無効化のあとは、ふたたび高価な照会の時代に逆戻りです。毎回もとの情報源を確認し直さなければならず、キャッシュを再構築するには、最初に作ったときよりはるかに長くかかります。信頼というキャッシュは、作るのは遅く、無効化は一瞬で、再構築が最も高くつきます。
信頼はキャッシュされた値であって、永久に保存された真実ではない。更新しなければ期限切れになり、裏切りはそのキャッシュを丸ごと無効にする。
おわりに — 完璧なネットワークは存在しない
ここまでの九つは、別々の話に見えて、根は一つです。分散システムが難しい理由と、関係が難しい理由が、まったく同じだからです。私は相手の内部状態を直接には見られず、私たちの間のチャネルは信頼できず、相手は私の思いどおりには動かない独立した存在です。この条件の上で、私たちは協力しなければなりません。
この条件の上で、完璧を目標にする設計は必ず失敗します。メッセージが決して失われないと信じるプロトコル、相手がいつも完璧に理解すると信じる関係。どちらも最初の障害で崩れます。だからよいシステムとよい関係は、同じものを目標にします。失敗をなくすことではなく、失敗を耐え抜くこと。すなわち回復力、明示的な確認、そして相手の不完全さを障害ではなく正常な状態として受け入れる寛容さです。
メッセージを連投する代わりにバックオフし、無限に待つ代わりにタイムアウトを置き、小さなハートビートを絶え間なく送り、大事なことは強く合意し、相手の容量に合わせて流し、つらい日にはコアだけを守り、謝罪はきちんと一度し、大事な言葉は確認をもう一ラウンド回し、信頼というキャッシュをまめに更新すること。これはよいエンジニアリングであり、そしておそらく、よい愛です。
参考資料
- Designing Data-Intensive Applications (Martin Kleppmann): https://dataintensive.net/
- 二人の将軍問題: https://en.wikipedia.org/wiki/Two_Generals%27_Problem
- 冪等性(Idempotence): https://en.wikipedia.org/wiki/Idempotence
- Timeouts, retries, and backoff with jitter (AWS Builders' Library): https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/
- 一貫性モデル(Consistency model): https://en.wikipedia.org/wiki/Consistency_model