分散ロックの正しさとフェンシング
分散ロックが守ってくれると信じると二重書き込みで足をすくわれる。GC停止や遅延でロックが破れる理由、Redlock論争の本質、フェンシングトークンで遅延書き込みを確実に弾く原理がわかります。
- 1.分散ロックは「いつでも高々1人」を保証できない。プロセスのGC停止やネットワーク遅延で、ロックを失った旧保持者が失効に気づかず書き込みを再開しうるため。
- 2.Redlock論争の核心は時計依存。Kleppmannは「タイミング前提が崩れると安全性が壊れる」と批判し、antirezは「運用上の前提下では実用的」と反論した。正しさの保証としては前者が原理的に妥当。
- 3.決定打は単調増加するフェンシングトークン。ロックに連番を添え、保護リソース側が過去最大より小さいトークンを拒否する。判定を止まりうるクライアントから書き込みを受ける側へ移すのが要。
分散ロックは何を約束し、何を約束できないか
分散ロックは、複数のプロセスが同じリソース(ファイル・レコード・外部API)へ同時に手を出すのを防ぐ仕組みです。単一マシンの mutex を、ネットワーク越しの共有ストア(Redis・etcd・ZooKeeper など)で再現しようとするものだと考えてよいでしょう。
ところが分散ロックには、用途によってまったく異なる2つの目的が混ざっています。この区別を曖昧にすると設計を誤ります。
効率(efficiency)のためのロックは、同じ重い処理を二重に走らせて無駄を省くためのもの。たまにロックが破れて二重実行されても、結果は冗長なだけで壊れません。一方正しさ(correctness)のためのロックは、二重書き込みが起きるとデータ破壊や二重課金を招く。後者でロックが破れることは絶対に許されません。効率目的なら多少緩いロックで十分ですが、正しさ目的では本記事で述べる厳密さが要ります。
問題の本質は、ロックの取得は守れても、ロックの有効性は保持者の自己申告に委ねられている点にあります。「自分はまだロックを持っている」という信念と、ロックストアが認識する現実とが食い違いうるのです。
なぜロックは破れるのか:停止と遅延
破綻のシナリオは具体的です。クライアント1が有効期限(TTL)付きのロックを取り、リソースへ書き込もうとした瞬間に長いGC(ガベージコレクション)ポーズに入ったとします。プロセスは生きていますが、数秒〜十数秒のあいだ一切応答しません。
時刻 t0: クライアント1 がロック取得(TTL=10秒)
時刻 t1: クライアント1 が「書き込むぞ」と決めた直後にGCポーズ突入
(クライアント1の主観では一瞬。現実には15秒経過)
時刻 t2: ロックストアでTTL満了 → ロックが自動解放
時刻 t3: クライアント2 がロック取得 → リソースへ書き込み
時刻 t4: クライアント1 がポーズ復帰。失効に気づかず write を実行
→ クライアント1とクライアント2の書き込みが衝突(ロックが破れた)
ここで効くのが、プロセスの停止時間と時計の進みは独立だという事実です。クライアント1にとっては一瞬の出来事でも、現実世界の時計は容赦なく進み、TTLは満了している。原因はGCに限りません。ページフォールトでのスワップ、CPUスティール、SIGSTOP、VMのライブマイグレーション、そしてネットワーク遅延——いずれも「クライアントは生きているが、その認識が古びる」状況を作ります。
これはリーダー選出とスプリットブレイン回避で論じた問題と根を同じくします。非同期な世界では**「死んだノード」と「ただ遅いノード」を区別できない**ため、TTLという時間ベースの判定は本質的に脆いのです。
TTLでは守れない理由を一段深く
「TTLを長くすれば?」という発想は的を外しています。TTLを伸ばせば、本当にクラッシュした保持者のロックがなかなか解放されず、可用性が落ちます。逆に短くすれば誤失効が増えます。どちらに振っても、GCポーズがTTLを超えうるという根本は消えません。停止時間に上限を置けない以上、純粋に時間だけで相互排他を保証することは原理的にできません。
ロックストアが「TTL満了」と判断する時計と、クライアントが「まだ有効」と信じる時計は別物です。両者をNTPで揃えても、ずれ・ジッタ・うるう秒・VMのスケジューリング遅延は残ります。安全性の保証を物理時計の同期精度に賭ける設計は、その前提が崩れた瞬間に破れます。これが次節のRedlock論争の火種です。
Redlock論争:Kleppmann 対 antirez
Redis作者のSalvatore Sanfilippo(antirez)は、複数の独立したRedisノードを使う分散ロックアルゴリズムRedlockを提案しました。N 台(通常5台)のRedisに対して同じロックを取りに行き、過半数(⌊N/2⌋+1 台)で取得に成功し、かつ取得にかかった経過時間がTTLより十分短ければロック成立、とみなします。単一Redisのフェイルオーバーで起きるロック喪失を、多数決で緩和する狙いです。
これにMartin Kleppmann(『Designing Data-Intensive Applications』著者)が反論し、有名な論争になりました。批判の骨子は2点です。
| 論点 | Kleppmann(批判側) | antirez(反論側) |
|---|---|---|
| タイミング前提 | Redlockの安全性はクロックとプロセス停止の境界に依存し、GCポーズやクロック飛びで破れる | 実運用ではNTPと適切な余裕で前提は満たせる。理論的最悪は稀 |
| フェンシング | ロック単体では遅延書き込みを防げず、別途フェンシングトークンが必須 | Redisのインクリメント等でトークン相当を発行する運用は可能 |
| 位置づけ | 正しさ目的には不適。効率目的のロックなら単一Redisで十分 | 可用性と性能のバランスを取った実用的な選択肢 |
Kleppmannの中心的な主張は、「どんな分散ロックも、保護対象のリソース側でフェンシングを行わなければ正しさを保証できない」という点にあります。Redlockが過半数で頑健にロックを取れたとしても、取った後にクライアントがGCで固まれば、TTL満了後の遅延書き込みは止められない。多数決はロック取得の可用性を上げますが、取得後の停止には無力だからです。
両者の対立は実は噛み合っています。正しさ(correctness)が要るなら、ロックアルゴリズムの頑健さに関わらずフェンシングトークンが必要、というKleppmannの指摘は原理的に正しい。一方、効率(efficiency)目的で「たまに二重実行されても困らない」なら、Redlockどころか単一Redisの素朴なロックでも実用上は十分、というのが落とし所です。「分散ロックを使えば安全」という素朴な期待こそが危険なのです。
フェンシングトークン:書き込み側で止める
決定打は、ロックの取得時ではなく実際の書き込み時に有効性を検査することです。鍵が**フェンシングトークン(fencing token)**です。
ロックサービスは、ロックを付与するたびに単調増加する整数を一緒に発行します。クライアントが保護リソースへ書き込むときは、必ずこのトークンを添える。リソース側は、過去に受理した最大トークンより小さいトークンの書き込みを拒否します。
# ロック付与のたびにトークンは必ず増える
クライアント1 がロック取得 → token = 33
(クライアント1がGCポーズ。TTL満了)
クライアント2 がロック取得 → token = 34 # 必ず 33 より大きい
# 保護リソース側の検査
クライアント2 が write(token=34) → 受理。last_token = 34 を記憶
クライアント1 が復帰し write(token=33) → 33 が 34 未満なので拒否
旧保持者のクライアント1は生きていても、トークンが古いために書き込みそのものを跳ね返される。クライアント1が自分の失効に気づく必要はなく、リソースが機械的に弾きます。これが「フェンシング(柵で締め出す)」の意味です。
決定的に重要なのは、トークンを検査する主体が、書き込みを受けるリソースでなければならない点です。固まったクライアントに「お前は失効したか」と尋ねても正しく答えられません。判定を、止まりうる当事者から、書き込みを実際に受け取る側へ移す——これが分散ロックを正しくする核心です。トークンは時計のように「絶対時刻」を語るのではなく「世代の前後関係」だけを語るため、クロックのずれに一切影響されません。
フェンシングが効くには、トークンが全体で一意かつ単調でなければなりません。ZooKeeperのzxidや昇順znode、etcdのリビジョン番号は、過半数(クォーラム)合意のもとで単調増加を保証するため、フェンシングトークンの発行源として堅牢です。単一RedisのINCRでも発行できますが、フェイルオーバーで番号が巻き戻ると一意性が壊れるため、正しさ目的では合意ベースのストアが安全です。
べき等性という別の防御線
フェンシングトークンを保護リソース側に実装できない場合もあります。トークンを理解しない外部APIや、検査機構を持たないストレージが相手のときです。この場合の補完策が、書き込みを**べき等(idempotent)**に設計することです。
二重実行されても結果が一度きりと同じになるよう、操作にユニークキーを添えて重複排除する。詳細はべき等性とexactly-once配信の幻想に譲りますが、要点は「ロックで二重実行を防ぐ」のではなく「二重実行されても害がないようにする」発想の転換です。ただしべき等性も無限の重複排除記憶は持てないため、遅延した重複が排除窓を越えれば漏れます。フェンシングほど決定的ではない点に注意が要ります。
設計のまとめ
正しさが要る分散ロックは、ロックアルゴリズムの頑健さだけでは完成しません。耐故障やカオス実験での検証(カオスエンジニアリングでプロセス一時停止やネットワーク分断を注入する)まで含めて、層を重ねて初めて守れます。
- 分散ロックには効率目的と正しさ目的がある。前者は緩いロックで十分、後者は厳密なフェンシングが要る。両者の混同が事故の温床。
- ロックは取得は守れても取得後の有効性を保証できない。GCポーズ・遅延・スワップで停止時間と時計の進みが乖離し、旧保持者が失効に気づかず書き込むため。
- Redlock論争の本質はタイミング依存。多数決はロック取得の可用性を上げるが、取得後の停止には無力。正しさには別途フェンシングが要るというKleppmannの指摘が原理的に妥当。
- フェンシングトークンが決定打。単調増加する連番を書き込みに添え、保護リソース側が古いトークンを拒否する。判定を止まりうるクライアントから書き込みを受ける側へ移すのが核心。
- トークンを検査できない相手にはべき等性で二重実行を無害化する。ただし排除窓の限界があり、フェンシングほど決定的ではない。
DevOps/インフラ Article
分散ロックの正しさとフェンシングを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
分散システム
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 6
導入後に効く点
Redlock論争の核心は時計依存。Kleppmannは「タイミング前提が崩れると安全性が壊れる」と批判し、antirezは「運用上の前提下では実用的」と反論した。正しさの保証としては前者が原理的に妥当。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「分散システム / 分散ロック」に近いか確認する。
- 強みである「分散ロックは「いつでも高々1人」を保証できない。プロセスのGC停止やネットワーク遅延で、ロックを失った旧保持者が失効に気づかず書き込みを再開しうるため。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。