分散ロック
1)分散ロックが必要な理由
分散ブロッキングは、複数のクラスタノード間の重要なセクションの相互排除を保証するメカニズムです。典型的なタスク:- バックグラウンドタスク/シェーダーのリーダー選挙。
- 共有リソース(ファイル移動、スキーママイグレーション、排他的支払いステップ)上の単一の実行者の制限。
- idempotency/orderを達成できない場合は、集計(ウォレット/オーダー)のシーケンシャル処理。
- idempotent upsertを作成できる場合、CAS (compare-and-set)またはキーごとの順序。
- リソースが可換演算(CRDT、 counters)を許可する場合。
- 問題が1つの店のトランザクションによって解決されれば。
2)脅威モデルとプロパティ
障害と合併症:- ネットワーク:遅延、パーティション、パケット損失。
- プロセス:GCの一時停止、世界停止、ロックキャプチャ後のクラッシュ。
- 時間:クロックドリフトと変位ブレークTTLアプローチ。
- 回収:ネットの後の「ゾンビ」プロセスは、それがまだ城を所有していると思うかもしれません。
- 安全:複数の有効な所有者(安全)。
- 生存可能性:所有者が失敗したときにロックが解放されます(生きている)。
- 正義:断食はありません。
- クロックの独立性:正しさはウォールクロック(またはフェンシングトークンによって補償)に依存しません。
3)主要なモデル
3.1リース(レンタルロック)
ロックはTTLで発行されます。所有者は有効期限(heartbeat/keepalive)の前にそれを更新する義務があります。
長所:自己吸収をクラッシュ。
リスク:所有者が「立ち往生」し、働き続けるが、延長を失った場合、二重所有権が発生する可能性があります。
3.2フェンシングトークン
キャプチャが成功するごとに、単調に増加する数が発行されます。リソース消費者(データベース、キュー、ファイルストレージ)は、トークンをチェックし、古い番号で操作を拒否します。
これはTTL/リースとネットワークパーティションにとって非常に重要です。
3.3クォーラムロック(CPシステム)
分散コンセンサスが使用されます(いかだ/Paxos;etcd/ZooKeeper/Consul)、レコードはコンセンサスログに関連付けられています→ほとんどのノードで分割された脳はありません。
プラス:強力なセキュリティ保証。
マイナス:クォーラムに対する感度(それが失われたとき、生存性はラメである)。
3.APロック×4(インメモリ/キャッシュ+レプリケーション)
たとえば、Redisクラスタです。高可用性と高速、しかしネットワークパーティションのための強力なセキュリティ保証なし。傷口の側面で囲うことを要求して下さい。
4)プラットホームおよびパターン
4.1 etcd/ZooKeeper/領事館(強いロックにおすすめ)
一時的なノード(ZK)またはセッション/リース(etcd):セッションが生きている間にキーが存在します。
セッションのkeepalive;quorum loss→セッションの期限切れ→ロックが解放されます。
waitキュー→fairのシーケンスノード(ZK 'EPHEMERAL_SEQUENTIAL')。
go cli, _:= clientv3. New(...)
lease, _:= cli. Grant(ctx, 10) // 10s lease sess, _:= concurrency. NewSession(cli, concurrency. WithLease(lease. ID))
m:= concurrency. NewMutex(sess, "/locks/orders/42")
if err:= m. Lock(ctx); err!= nil { / handle / }
defer m. Unlock(ctx)
4.2 Redis(きちんとした)
クラシック-'SETキー値NX PX ttl'。
問題:- レプリケーション/feiloverは同時所有者を許可する場合があります。
- 複数のインスタンスからのRedlockはリスクを軽減しますが、排除しません。信頼できないネットワークを持つ環境で物議を醸しています。
迅速な調整レイヤーとしてRedisを使用する方が安全ですが、常にターゲットリソースのフェンシングトークンを補完します。
例(Lua-unlock):lua
-- release only if value matches if redis. call("GET", KEYS[1]) == ARGV[1] then return redis. call("DEL", KEYS[1])
else return 0 end
4.3つのDBロック
PostgreSQLアドバイザリロック:Postgresクラスタ内のロック(プロセス/セッション)。
すべてのクリティカルセクションがすでに同じデータベースにある場合、それは良いことです。
sql
SELECT pg_try_advisory_lock(42); -- take
SELECT pg_advisory_unlock(42); -- let go
4.4ファイル/クラウドロック
S3/GCS+オブジェクトメタデータロック「If-Match」 (ETag)条件→本質的にCAS。
バックアップ/移行に適しています。
5)安全ロックの設計
5.1所有者のID
'owner_id'(#process#pid#start_time node)+ランダムトークンを格納してロック解除検証を行います。
繰り返されたロック解除は、他の誰かのロックを削除するべきではありません。
5.2 TTLと拡張
TTL <T_fail_detect(故障検出時間)およびp99 ≥の重要なセクション操作×スペア。
更新-定期的に(例えば、すべての'TTL/3')、期限付き。
5.3傷のフェンシングトークン
外部リソースを変更するセクションは'fencing_token'を渡す必要があります。
Sink (DB/cache/storage)は「last_token」を格納し、より小さなものを拒否します:sql
UPDATE wallet
SET balance = balance +:delta, last_token =:token
WHERE id =:id AND:token > last_token;
5.4待ち行列と正義
ZK-'EPHEMERAL_SEQUENTIAL'およびオブザーバー:クライアントは最も近い前任者のリリースを待っています。
etcdで-リビジョン/バージョン管理のキー;'mod_revision'で指定します。
5.5スプリットブレイン行動
CPアプローチ:クォーラムなしでは、ロックを取ることはできません-安全性を破るよりも立った方が良いです。
APアプローチ:分割された島で進捗が許可されます→フェンシングが必要です。
6)リーダー選挙
etcd/ZKでは「、リーダー」は排他的なエペメリック・キーです。残りは変更のためにサインアップされます。
リーダーはハートビートを書きます;損失-再選。
すべてのリーダー操作にフェンシングトークン(era/revision number)を同行します。
7)エラーとその処理
クライアントはロックを取りましたが、クラッシュして作業→標準、誰も苦しむことはありません。TTL/セッションを公開します。
作業中にロックが切れました:- 必須ウォッチドッグ:拡張に失敗した場合は、クリティカルセクションを中断し、ロールバック/補償します。
- No 「finish later」:ロックなしでは、クリティカルセクションを続行できません。
長い一時停止(GC/stop-the-world)→拡張は起こらなかった、他はロックを取りました。ワークフローは、所有権の喪失(キープアライブチャネル)を検出し、中止する必要があります。
8) Dedloki、優先順位と反転
分散世界のデドロキは珍しい(通常は1つの城がある)が、いくつかの城がある場合は、単一の受注(ロック注文)に準拠します。
優先度反転:優先度の低い所有者はリソースを保持し、優先度の高い所有者は待機します。ソリューション:TTL制限、プリエンプション(ビジネスが許可している場合)、リソースのシャーディング。
断食:フェアネスのために待ちキュー(ZK-sub-orderノード)を使用します。
9)観測可能性
メトリクス:- 'lock_acquire_total {status=ok' timeout 'error}'
- 'lock_hold_seconds {p50、 p95、 p99}'
- 'fencing_token_value'(単調)
- 'lease_renew_fail_total'
- 'split_brain_prevented_total'(クォーラム不足のために拒否された試行回数)
- 'preemptions_total'、 'wait_queue_len'
- 'lock_name'、 'owner_id'、 'token'、 'ttl'、 'attempt'、' wait_time_ms'、 'path' (ZK)、 'mod_revision' (etcd)。
- 「acquire→critical section→release」は結果と重なります。
- 成長'lease_renew_fail_total'。
- 'lock_hold_seconds {p99}'> SLO。
- 「孤児」ロック(ハートビートなし)。
- 膨らんだ待機リスト。
10)ケーススタディ
10.1フェンシング付きセキュアRedisロック(擬似)
1.信頼できるストア(Postgres/etcdなど)にトークンカウンターを保存します。
2.'SET NX PX'が成功すると、トークンを読み取り/増分し、データベース/サービスでトークンをチェックした状態でリソースにすべての変更を加えます。
python acquire token = db. next_token ("locks/orders/42") # monotone ok = redis. set("locks:orders:42", owner, nx=True, px=ttl_ms)
if not ok:
raise Busy()
critical op guarded by token db. exec("UPDATE orders SET... WHERE id=:id AND:token > last_token",...)
release (compare owner)
10.2 etcd Mutex+watchdog (Go)
go ctx, cancel:= context. WithCancel(context. Background())
sess, _:= concurrency. NewSession(cli, concurrency. WithTTL(10))
m:= concurrency. NewMutex(sess, "/locks/job/cleanup")
if err:= m. Lock(ctx); err!= nil { /... / }
// Watchdog go func() {
<-sess. Done ()//loss of session/quorum cancel ()//stop working
}()
doCritical (ctx )//must respond to ctx. Done()
_ = m. Unlock(context. Background())
_ = sess. Close()
10.3 ZKのリーダーシップ(Java、キュレーター)
java
LeaderSelector selector = new LeaderSelector(client, "/leaders/cron", listener);
selector. autoRequeue();
selector. start(); // listener. enterLeadership() с try-finally и heartbeat
10.4期限付きPostgresアドバイザリーロック(SQL+アプリ)
sql
SELECT pg_try_advisory_lock(128765); -- attempt without blocking
-- if false --> return via backoff + jitter
11)テストプレイブック(ゲーム日)
クォーラム損失:1-2 etcdノードを無効にする→ロックを取る試みは通過しないでください。
GC-pause/stop-the-world:オーナーのフローを人工的に遅らせる→ウォッチドッグが作業を中断していることを確認する。
スプリットブレイン:所有者と城の側の間のネットワーク分離のエミュレーション→新しい所有者はより高いフェンシングトークンを取得し、古いものは青で拒否されます。
クロックスキュー/ドリフト:所有者から時計を離します(Redis/リース用)→トークン/チェックによって正確さが確保されていることを確認します。
リリース前のクラッシュ:プロセスのクラッシュ→TTL/セッション経由でロックが解除されます。
12)アンチパターン
外部リソースにアクセスするときにフェンスなしでTTLロックをクリーンにします。
正確さのための現地時間に頼って下さい(HLC/囲うこと無し)。
feiloverとレプリカの確認なしの環境で1つのRedisマスタを介したロックの配布。
無限クリティカルセクション(TTL「年齢のための」)。
'owner_id '/tokenをチェックせずに他の誰かのロックを削除します。
バックオフの欠如+ジッタ→試みの嵐。
単一のグローバルロック「すべてのために」-紛争の袋。キーシャーディングの方がいい。
13)実装チェックリスト
- リソースタイプが定義され、CAS/queue/idempotencyをディスペンスできます。
- 選択されたメカニズム:etcd/ZK/Consul for CP;Redis/cache-フェンシングのみ。
- 実装:'owner_id'、 TTL+extension、 watchdog、正しいロック解除。
- 外部リソースはフェンショントークン(単調)をチェックします。
- リーダーシップ戦略とフェイルオーバーがあります。
- 設定されたメトリック、アラート、ロギングトークンおよびリビジョン。
- バックオフ+ジッタと取得タイムアウトが提供されます。
- ゲーム開催日:クォーラム、スプリットブレイン、GCは一時停止、クロックスキュー。
- 複数のロックを取るための手順のドキュメント(必要に応じて)。
- brownout plan-ロックが利用できない場合の対処方法。
14) FAQ
Q: 'SET NX PX' Redisロックで十分ですか?
A:リソースがフェンシングトークンをチェックする場合のみ。そうでなければ、ネットワークパーティショニングでは、2人の所有者が可能です。
Q: 何を「デフォルトで」選ぶためにか"?
A:厳密な保証のため-etcd/ZooKeeper/Consul (CP)。1つのデータベース内の簡単なタスクのために-アドバイザリはPostgresをロックします。レディス-フェンシングのみ。
Q:どのTTLを置くべきですか?
A: 'TTL ≥ p99クリティカルセクションの持続時間× 2'とすぐに"ゾンビをクリアするのに十分な短い。"リニューアル-すべての'TTL/3'。
Q:断食を避ける方法か?
A:順番待ちキュー(ZKシーケンシャル)または公平性アルゴリズム;試みおよび公正な計画の限界。
Q:私は時間の同期を必要としますか?
A:正しさのため-いいえ(使用囲うこと)。動作予測可能性については、はい(NTP/PTP)ですが、ロックロジックはウォールクロックに依存しません。
15)合計
信頼できる分散ロックは、リース+キープアライブでクォーラムレベル(etcd/ZK/Consul)に構築され、変更されるリソースのレベルでトークンをフェンシングすることによって必然的に補完されます。フェンシングなしのTTL/Redisアプローチは、スプリットブレインのリスクです。因果関係と偶像性について最初に考え、それがそれらなしで不可能であるロックを使用し、測定し、故障モードをテストします-そしてあなたの「重要なセクション」は、インシデントの数ではなく、意味でのみ重要です。