/note/tech

システムは「動く」だけでは足りない 実装編 - 非機能要件・分散システム・トレードオフをコードで見る

要約:

■ 1. 概要

  • 発表タイトル: 「システムは『動く』だけでは足りない 実装編 ― 非機能要件・分散システム・トレードオフをコードで見る」
  • 発表者: @nwiizo(3-shake)
  • 基礎編で示した「守るものが違うと設計が変わる」「分けると成功したか不明な場面が増える」「最後は何を守るために何を引き受けるかを決める」という原則を、小さなRustサンプルで具体的に再現する
  • 取り上げるテーマ:
    • やり直し(Retry)だけでは危ない理由
    • レプリカ(replica)を読むことがなぜ少し古い値を連れてくるのか
    • その判断をどう残すか(ADR)

■ 2. サンプルの全体像

  • サンプルは現実のシステムをかなり単純化したものであり、難しい仕組みを再現することではなく何を守ると何が増えるかを明示することを目的とする
  • 注文処理の系:
    • FakePaymentGateway(テスト用決済サービス)
    • CheckoutService(注文を進める役)
    • OrderRequest(注文内容)
  • 在庫参照の系:
    • InventoryStore(在庫を見る役)
    • primary(最新の在庫)
    • replica(少し前の在庫)
  • 前半は「やり直しで事故が起きる」話、後半は「速く読むと少し古いかもしれない」話

■ 3. Rustを読むための最低限の知識

  • struct(構造体): 関連するデータをまとめる箱
    • 例: OrderRequest(注文番号・金額)、ChargeRecord(課金記録)
  • enum(列挙型): 「この中のどれか1つが起きる」と表すための型
    • 例: GatewayStep::TimeoutAfterCommitTemporaryFailureSuccess
  • match(分岐): 状態ごとにどう振る舞うかを決める書き方
  • HashMap(辞書): 「キー → 値」で覚える辞書
  • 今回の目的はRustの文法を覚えることではなく、何を記録して、どこで分岐して、どう安全にしようとしているかを読めるようにすること

■ 4. テーマ1: やり直し(Retry)だけでは危ない

  • 問題の核心:
    • タイムアウトが返ってきたとき、呼び出し元からは「返事がない」としか見えない
    • 返事がない原因は3つあり区別できない: (a)リクエストが途中で消えた (b)相手が止まっていた (c)相手は処理したが返事が消えた
    • 今回のコードが再現するのは(c)のケースで、決済サービスは内部的に成功扱いの記録を残しているのに、呼び出し元には失敗に見える状態
  • retryのコード動作:
    • CheckoutServiceはタイムアウトを見たら素直にやり直す
    • 最大回数まで繰り返し → gateway.chargeで課金を依頼 → 成功(Ok)なら終了、タイムアウト(Timeout)なら次の回へ、回数を使い切ったら諦める
    • このコードだけ見ると自然だが、実際は成功済みだったら同じ課金をもう一度してしまう
  • 実行結果(冪等性なし):
    • charges: 2 / total_amount: 10000 → 5000円の注文なのに10000円引かれる(二重課金)
  • 問題の本質: 失敗したことよりも、成功したのか失敗したのかを決めきれないことが難しい
  • 解決策: 冪等性(べきとうせい / Idempotency)
    • retryをやめる工夫ではなく、成功したか失敗したか分からない場面でも事故を増やしにくくする工夫
    • 「このrequest_idはもう処理した」と覚えておけば、同じ依頼が再送されても2回目は捨てられる
  • 冪等性のコード実装:
    • 確認: self.processed.contains_key(&request.request_id) → 処理済みなら何もせず「成功」を返す
    • 記録: .entry(...).or_insert(...) → 初めての依頼なら課金してrequest_idを記録する
  • 実行結果(冪等性あり):
    • charges: 1 / total_amount: 5000 → 正しい金額のまま
  • 結論:
    • retryは止まりにくくするための道具(一時的な失敗から復帰しやすくなる)
    • 冪等性は「成功したか分からない場面」で事故を増やしにくくする道具(やり直しによる二重実行を抑える)
    • 分散では「止まりにくさ」と「二重実行しない」を組み合わせて守る

■ 5. テーマ2: 速さと最新性は両立しにくい

  • リーダーベースレプリケーションの仕組み:
    • 書き込みはLeader(元データ)に行き、変更内容がFollower(複製)に流れる
    • 読み取りはFollowerからもできるので速い
    • ただし流れが追いつくまでは古い値が返る可能性がある
  • サンプルのInventoryStore構造:
    • primary(プライマリ): まず更新される、元の在庫
    • replica(レプリカ): あとから追いつく、読むためのコピー
    • 書き込みはprimaryのみ、読み取りはprimaryまたはreplica
  • 1回の流れ:
    • 最初はprimaryreplicaも在庫3
    • purchase_on_primary()primaryだけ2になる
    • まだreplicaには反映されないので3のまま
    • replicate()を呼ぶとreplicaも2になる
  • 実行結果:
    • primary stock right after purchase: 2
    • replica stock before replication: 3(古い)
    • replica stock after replication: 2
  • replicaを使うことのトレードオフ:
    • 得られるもの: 読み取りを速くしやすい、混雑を分散しやすい
    • 失うかもしれないもの: 最新の値をすぐには見られない、場面によっては危険
  • 場面による使い分け:
    • 商品一覧画面: 多少古くてもよい(速さが大事)→ replicaでよい
    • 購入確定直前: 古い在庫は危険(最新性が大事)→ primaryを見たい
    • どちらが正しいかは技術の好みではなく、その場面で何を守りたいかで決まる
  • 同期・非同期レプリケーションのトレードオフ:
    • 同期レプリケーション(複製が終わるまで待つ方式): 待ちが増える
    • 非同期レプリケーション(待たずに進める方式): 古い値が見える
    • 速さを取りにいくほど、どこかで待つか、どこかで古さを受け入れるかの判断が必要

■ 6. テーマ3: コードを全部自分で書かない時代になぜ必要なのか

  • コーディングエージェントが速くできること:
    • 画面やAPIの雛形を作る
    • テストコードをたたき台から書く
    • リファクタリングを進める
    • 定型的な実装をつなぐ
  • 人が決めること(自動では決まらないこと):
    • どこまでretryをしてよいか
    • timeoutを本当に失敗とみなしてよいか
    • 冪等性が必要な操作はどこか
    • primaryreplicaをどの場面で使い分けるか
  • 難しさはコードを書く量の問題ではなく、どう壊れるかと何を守るかの問題として残る
  • 人に残る仕事は「決めること」と「確かめること」:
    • timeoutとsuccessが食い違う場面を想像できる
    • やり直しが二重実行を生む危険を説明できる
    • replicaの古さを許せる場面と許せない場面を分けられる
    • エージェントが書いた実装が、その考えに合っているか確かめられる
  • エージェント時代に価値が上がるのは、何を守るかを決めて、大きい事故を小さい不便に変えられているか確かめる力

■ 7. 判断を残すためのADR(Architecture Decision Record)

  • ADRとは: 大げさな設計書ではなく、何を決めて、なぜそうしたかを短く残すためのメモ
  • ADRが必要な理由: 設計は図だけ見ても理由が分からなくなる
    • 図だけ残る場合: 「なぜそうしたのか」があとで読めない
    • ADRがある場合: 背景、決めたこと、その結果をあとから追える
  • ADRの構成要素(4つ):
    • Title(題名): 何を決める話なのか
    • Context(前提): どんな困りごとがあるのか
    • Decision(決めたこと): 何を選ぶのか
    • Consequence(結果): 何がよくなり、何が増えるのか
  • ADRの記述例:
    • Title: Retry時の二重課金を防ぐためrequest_idを使う
    • Context: timeoutのあとにretryすると同じ課金が重なることがある
    • Decision: request_idで同じ依頼かどうかを見分ける
    • Consequence: 実装は少し増えるが、二重課金の事故を減らしやすくなる
  • ADRは問題と判断と結果を短くつなぐメモだと考えると扱いやすい
  • コードを自分で全部書かない時代ほど、その理由を短く残しておく価値が上がる

■ 8. まとめ・持ち帰ってほしいこと

  • コードは短くても、設計の問題は見える(大規模な本番システムでなくても、本質は小さなサンプルで観察できる)
  • 便利な仕組みにも別の困りごとがある(retryもreplicaも、それだけで安心とは限らない)
  • 設計は「大きい事故」を「小さい不便」に変える仕事(どう壊れるかを先に考えて、何を守るために何を引き受けるかを決める)
  • 基礎編との対応:
    • タイムアウト(Timeout): 相手が成功しても、こちらからは失敗に見えることがある
    • やり直し(Retry): 止まりにくくできるが、状態が読めない場面では事故も増える
    • 冪等性(Idempotency): retryを入れても二重実行しにくくする
    • 一貫性(Consistency): primaryは最新の値を返しやすい
    • 遅延(Latency): replicaは速いが、少し古いことがある