モジュール 10ターム3 · 信頼性とスケール12 ポスター項目
分散システムの正しさ
ログインすると、進捗を複数のデバイスで保存できます。ログインするまでは、このブラウザに保存されます。
なぜ重要なのか
並行処理のバグはデモでは表に出ません。表に出るのは、顧客が二重に支払った、クーポンが 3 回使われた、1 つの座席が 2 人に売れた、月末に残高が合わない、といった形です。フィンテックでは、これらは単なるバグではなく規制上の事案です。
学習目標
- race condition を見抜き、アトミックな操作、制約、ロックで修正できる。
- レプリケーション、CAP、結果整合性を、それを前提に UX を設計できる程度まで理解している。
- 分散ロックとリーダー選出が必要な場面と、その落とし穴を把握している。
- デッドロックを回避できる。
- クロックスキューがあっても時刻を正しく扱える。
- メモリリークとガベージコレクションによる停止を診断できる。
レッスン
10.1 race condition(39, 43)
典型的な形は check-then-act です: 値を読み、判断し、書き込む。読み取りと書き込みの間に、別の誰かが動きます。 修正方法(優先度順):
- アトミックな文:
UPDATE accounts SET balance = balance - 100 WHERE id = $1 AND balance >= 100を実行し、影響を受けた行数を確認します。 - データベース制約: ユニークインデックスにより、2 回目の挿入が失敗します。
- 競合が少ない場合は 楽観的ロック(バージョン列)。
- 競合が多い場合は 悲観的ロック(
SELECT ... FOR UPDATE)。 アプリのコードでは: 長時間動く Node プロセス内の共有された可変状態はリクエスト間で共有されます。モジュールレベルの変数はユーザーごとではありません。スレッドセーフティ(43) は Go、Java、Python の async、ワーカーでも重要です。
10.2 デッドロック(40)
2 つのトランザクションが、それぞれ相手の必要とするロックを保持している状態です。リソースを常に一貫した順序でロックする(例: ID の昇順)、トランザクションを短く保つ、ロックタイムアウトを設定する、といった方法で防ぎます。Postgres はデッドロックを検知して片方を中断するので、コード側でリトライしなければなりません。
10.3 レプリケーション、CAP、結果整合性(32, 34, 35, 95)
- レプリケーション(32): 耐久性と読み取りのスケールのために、複数のノードにコピーを持ちます。非同期レプリカには遅延があります。
- ネットワーク分断(95) は起こります。分断の最中、CAP によれば、システムは一部のリクエストを拒否する(一貫性)か、古い可能性のあるデータで応答する(可用性)かを選ばなければなりません。
- 結果整合性(35): レプリカは時間をかけて収束します。設計上の示唆: read-your-own-writes(ユーザーが書き込んだ直後の読み取りはプライマリに向ける)、「処理中」の状態を表示する、遅延しているレプリカでお金に関わる判断をしない。
- 台帳と残高: 強い一貫性、唯一の信頼できる情報源、追記専用の複式記帳レコード。
10.4 分散ロックとリーダー選出(33, 38)
- 複数のインスタンスが同じことを同時に行ってはならない場合に必要です(cron の実行者は 1 つ、パーティションのコンシューマーは 1 つ)。
- まずは DB ネイティブの手段を優先します: Postgres の advisory lock、
SKIP LOCKEDを使ったジョブキュー。 - Redis のロックには有効期限(TTL)と fencing token が必要です。一時停止していたプロセスがロックの期限切れ後に目覚め、それでも書き込んでしまうことがあるからです。ストレージ層は古いトークンを拒否しなければなりません。
- リーダー選出(Raft、etcd、ZooKeeper、Kubernetes leases)は、自分で作るものではなく、ほとんどの場合インフラから利用するものです。
10.5 時刻とクロックスキュー(96)
- マシンの時計はずれていきます。異なるサーバーの壁時計時刻でイベントを順序付けてはいけません。
- 順序付けには DB が生成したタイムスタンプやシーケンス番号を使い、経過時間の計測にはモノトニッククロックを使います。
- トークンの有効期限や webhook のタイムスタンプチェックには、わずかなずれの許容幅が必要です。
10.6 メモリリークとガベージコレクション(41, 42)
- マネージド言語でのリークは、たいてい自分が保持し続けている参照です: 上限なく大きくなるキャッシュ、削除されないイベントリスナー、グローバル配列、閉じられていないストリーム。
- 症状: メモリが着実に増え続けた末に OOM で再起動する。長い GC 停止によるレイテンシのスパイク。
- ツール: ヒープスナップショット(Node なら Chrome DevTools)、
--inspect、インスタンスごとのメモリ指標、上限付きの LRU キャッシュ。
Lab 10: 二重支払いチャレンジ
- ウォレットを作ります: チャージ、送金、出金。200 件の送金を同時に発生させるテストを書きます。素朴な AI 版がお金を失ったり、生み出したりする様子を観察します。
- アトミックな更新と制約で修正し、全エントリの合計が常にゼロになる複式記帳の台帳として実装します。
- 逆順の 2 つの送金でデッドロックを発生させ、ロック順序で修正します。
- 遅延しているレプリカ(遅延をシミュレート)から読み取り、read-your-writes のバグを示して修正します。
- 仕込まれたメモリリークをヒープスナップショットで見つけます。
完了の定義
AI プロンプトパック
- 「このコードベース内の check-then-act のシーケンスをすべて見つけてください。それぞれについて、起こり得る競合とアトミックな修正を示してください。」
- 「この残高ロジックを、不変条件のテスト付きの複式記帳の台帳として書き直してください。」