最終更新:

分散ロックの仕組み — 同じ仕事を、2台が重ねて始めないために


期限の後に戻ったAを、書き込み先で止める

担当A番号 1長く停止担当B番号 2新しい取得Aの期限が切れる書き込み先が番号を見る✓ 2:Bの結果を保存× 1:遅いAは拒否検査と更新を、原子的に行うロックの期限だけでは、処理は止まらない
本文のフェンシングのモデルです。番号は取得順で信頼できるよう発行し、書き込み先が古い番号を拒む必要があります。
ひよこ ひよこ
2台が同じ注文を処理したら、どうするの?
ペンギン先生 ペンギン先生
共通の場所で「今は誰がこの仕事を使えるか」を調整するのが分散ロックだよ。各サーバーの中で別々にmutexを取るだけでは、お互いを待たせられないんだ。
ひよこ ひよこ
先に見て、空いていたら名前を書けばいい?
ペンギン先生 ペンギン先生
確認と記入の間に別の人が入ることがあるよ。取得の判断を原子的に行う仕組みが必要。RedisならSETのNXと期限を同じ操作で指定する方法があるんだ。
ひよこ ひよこ
期限を付ければ、止まったサーバーも片付く?
ペンギン先生 ペンギン先生
ロックの記録が期限で解放されるけれど、そのサーバーの処理まで強制停止するわけではないよ。長い停止の後で古い処理が戻ると、新しい所有者と重なることがあるんだ。
ひよこ ひよこ
終わったら、キーを削除すればいい?
ペンギン先生 ペンギン先生
自分が取ったロックかを値で確かめて、確認と削除を原子的に行う。期限後に別の所有者が取ったキーを、古い処理が消すと困るよ。DELだけでは足りないんだ。
ひよこ ひよこ
古い処理の書き込みは、どう防ぐ?
ペンギン先生 ペンギン先生
取得の順で増えるフェンシングトークンを渡し、書き込み先が古い番号を拒む方法がある。ロック側で番号を作るだけでなく、データを守る側の検査が必要なんだ。
ひよこ ひよこ
Redisを5台にすれば、いつでも安全?
ペンギン先生 ペンギン先生
Redlockは独立した複数ノードと過半数、取得にかかった時間や期限等の条件を使うよ。通信や時計、処理の停止への仮定もある。台数だけで正しさを保証する方式ではないんだ。
ひよこ ひよこ
ZooKeeperは、接続が一瞬切れたらすぐ解放?
ペンギン先生 ペンギン先生
セッションが有効な間は維持されることがあるよ。エフェメラルノードの削除はセッション終了・期限切れ等に結び付く。etcdもリースやロックの所有キーを使うけれど、外の書き込み先の保護まで自動ではないんだ。
ひよこ ひよこ
注文を二重に処理しないなら、ロックだけ使えばいい?
ペンギン先生 ペンギン先生
DBの制約やトランザクション、冪等な処理で解ける場合もあるよ。ロックを取ったことと業務が一度だけ完了することは別。失敗した後の再試行も含めて、必要な保証から選ぼう。

まずは、期限が切れた後の場面を考える

注文を担当するAがロックを取った後、長く止まります。期限が切れてBが新しいロックを取り、書き込みを終えます。その後にAが動き出したら、古い処理の書き込みを拒む仕組みが必要になることがあります。

下のPython 3の例は、書き込み先が番号を確認する練習です。fence-demo.pyへ保存してpython fence-demo.pyで試します。

last_token = 0
value = None

def write(token, new_value):
    global last_token, value
    if token < last_token:
        return False
    last_token = token
    value = new_value
    return True

print(write(2, "Bの結果"))
print(write(1, "遅れて戻ったAの結果"))
print(value)
True
False
Bの結果

この例は判定のモデルです。本番ではトークンを信頼できる取得順で発行し、書き込み先で検査と更新を原子的に行います。同じ番号の処理の再試行もどう扱うか決めます。Pythonのこの変数だけで複数サーバーを調整できるわけではありません。

取得・解放・書き込み先の3か所を見る

場所確認すること
取得誰が使えるかを原子的に決める
解放自分の取得かを確認して解放する
書き込み先期限後に戻った古い処理を拒む

Redisの単一インスタンスの方式では、NXと期限を同じSETで指定し、取得ごとの一意な値を保存します。解放は値の比較と削除を原子的に行い、他の所有者のキーを消さないようにします。これは、Redisの障害や非同期複製への切替時も排他が保たれるという保証とは別です。

方式ごとの条件を確認する

Redlockは独立した複数のRedisと、過半数の取得・期限から取得時間等を差し引いた有効時間を使います。時計や停止への前提があり、公式資料も整合性について注意と議論を案内しています。ノードを増やしただけで、あらゆる停止に安全になるとは説明できません。

ZooKeeperのロックのレシピは、エフェメラルな連番ノード等を使います。通信が一瞬途切れたこととセッション失効は別です。etcdのロックもリース・所有キーに結び付きます。いずれも外部のストレージへの古い書き込みを、名前だけで防げるわけではありません。

業務の正しさから選ぶ

重複注文なら、DBの一意制約やトランザクション、処理済みID等が適する場合があります。ロックが取れても、応答を失って再試行する場面や、処理が一部だけ終わる場面は残ります。守りたい資源と失敗時の振る舞いから、排他・冪等性・フェンシングを組み合わせます。

🐧 ペンギン先生のまとめ:「分散ロック」って出てきたら「離れたプログラム同士で、同じ仕事を使う順番を調整する仕組み」と思えばだいたいOK!

重複の扱いはメッセージキューの仕組み、データの更新はSQL入門へ進めます。

参考資料