現在テスト中です: 完了後にGitHubのコードを公開する予定です。

動作原理

このページでは、クライアントとサーバー(クラスタ)が実際にやり取りするメッセージの流れに 沿ってTicketingの原理を説明します。ワイヤーを流れるバイトの正確な仕様は プロトコル (API)プロトコル (クラスタ) にあります。


全体構成

ユーザーリクエストを受けるサービス(例: チケット販売開始を処理するイベントサーバー群)が同じ キーでTicketingクラスタに並び、順番が回ってきたサーバーだけが在庫の減算や座席割り当てといった 作業を進める構造です。

groups
ユーザー
利用者のサービス(例: イベントサーバー群)
dns イベントサーバー 1
dns イベントサーバー 2
dns イベントサーバー 3
Ticketingクラスタ
confirmation_number ticketing-server 1
confirmation_number ticketing-server 2
confirmation_number ticketing-server 3

ロックの獲得と解放

基本的な流れです。空いているキーは即座に発行され、使用中のキーはFIFOキューで順番を待ちます。 保有者が解放すると、キューの先頭に再競合なしで直接引き継ぎされます — 新しいトークンと 共に即座に移るため、ロックが空く瞬間も、割り込みもありません。

クライアントX
クライアントY
サーバー
A · 獲得 (order-1234)
A · 発行 (token 41)
A · 獲得 (order-1234)
使用中 → FIFOキューに登録、応答保留
R · 解放
R · 解放済み
A · 直接引き継ぎ (token 42)
リクエストレスポンス
  • leaseは安全網です。 クライアントが落ちたり解放を忘れたりしても、leaseの時間が 経過すればサーバーがキーを回収し、次の待機者に引き継ぎます。クリティカルセクションの 最悪ケースの所要時間より余裕を持たせて設定してください。v1で許可されるのは1〜250秒のみで、 250秒を超える処理は明示的に未対応として拒否されます。
  • waitは待機時間の上限です。 その時間内に順番が来なければ、ロックを発行せずに T(タイムアウト)で応答するため、ロックが漏れることはありません。0はキューに入らない 1回だけの即時試行です。無限待機はありません。
  • 同じクライアントが保有中のキーを再度獲得しようとした場合も、他の待機者と同様に キューに並びます — 再入可能(reentrant)なロックではありません

キー状態ごとのサーバー動作の全ルールはリクエスト × キー状態マトリクスを、 フィールドの値の範囲はフィールド制約を参照してください。

フェンシングトークン

ロックサーバーが完璧であっても、クライアント側の時計までは制御できません。ロックを保持した ままGCの停止や過負荷でしばらく止まり、leaseが失効したことに気づかずに目覚めて作業を 続けてしまう古い保有者は、ロックサーバー単独では防げません。そのため獲得応答のtoken発行のたびに必ず前より大きくなるu64整数です。金融用途や永続DBでは、キーごとの high-waterの比較・更新と実際のbusiness writeを、同じDB transactionまたは1つの条件付き writeで原子的に実行する必要があります。fencing rowだけを先に更新し、実際のwriteを後で 行う方式は安全ではありません。

クライアントX
クライアントY
サーバー
保護対象リソース
A · 獲得
A · 発行 (token 7)
A · 獲得 → 待機
GC・過負荷で停止
lease失効 → 回収
A · 直接引き継ぎ (token 8)
書き込み (token 8)
token 8を記録 → 許可
遅れて目覚める
書き込み (token 7)
7 < 8 → 拒否
リクエストレスポンス

単調性はクラスタ内でも維持されます — トークンの発行番号自体が合意によって複製されるため、 リーダーが交代しても新しいリーダーは常にそれまでより大きい番号から続きを発行します。 トークンフィールドの正確な仕様はフェンシングトークン (スペック) を参照してください。

クラスタ — Raftコンセンサスによる無停止

クラスタモードでは、ノードがRaftコンセンサスによって1つのロック状態を共有します。 リーダーだけがクライアントのリクエストを処理し、すべてのロック状態変更(獲得・解放・失効)は 過半数のノードが記録した後にのみ確定されます。リーダーでないノードに接続すると、 M(移動)応答でリーダーへ案内されます。

クライアント
フォロワーB
リーダーA
フォロワーC
A · 獲得
M · リーダー案内
A · 獲得
複製提案
複製提案
応答
応答
過半数応答 → コミット
A · 発行 (token)
リクエストレスポンス合意形成RPC(Raft)
  • リーダーが落ちると、現在の標準設定では約2.3〜2.5秒で新しいリーダーを選出します。 リクエストのバイトをまだ1つも送っていない場合だけ、残りのwait予算内で同じ論理acquireを 続行できます。送信後に応答を確認できないacquireはIndeterminateであり、自動再送しません。
  • leaseの失効すら合意を経由します。 リーダーが失効コマンドをコミットして初めて ロックが消えるため、ノードごとに時計が多少ずれていてもロック状態が食い違うことは ありません。
  • 過半数を失うと(3台中2台が障害になるなど)、誤ったロックを発行するくらいなら 書き込みを止める側(安全優先) を選びます。3台中2台(または5台中3台)の揮発性process stateを失った場合、そのcluster/fencing domainは復旧せず、同じidentityで自動bootstrap してはいけません。

過半数こそが要であるため、ノード数は3または5のみ許可されます。 偶数はコストが増えるだけで 耐障害性は変わらないため、サーバーは起動を拒否します。

ノード数無停止同時障害耐性備考
1tokenの単調性はprocess lifetime内だけ保証。再起動可能なpersistent DBの保護には未対応
31無停止の標準構成。 ほとんどの場合これで十分
521台メンテナンス中でも冗長性を維持

ピアRPCのフレーム・ポートの規則はプロトコル (クラスタ)を、ノードを 1台ずつ交換する手順は無停止の新しいNodeId learner交換 を参照してください。

クライアントの動作

公式クライアントは共通して次のように動作します(言語別のAPIはライブラリを参照)。

  • アドレスごとに永続接続を1つバックグラウンドで維持します。アドレスを1つだけ指定した 場合も同じノードへの接続を2本維持するため、片方のソケットが一時的に切れてもサービスは 継続します。
  • リクエストを送る接続はリーダー優先、その次はラウンドロビンで選びます。M(移動)で 案内されたリーダーを記憶しておき、以降のリクエストを直接リーダーへ送ります。
  • リクエストはパイプライン化されます。 応答を待たずに次のリクエストを送れます — 対応付けのルールは応答マッチングルールに あります。
  • 切断された接続は指数バックオフ(0.1秒 → 最大3.2秒)で再接続を試み続けます。接続が 切れてもブローカーオブジェクトが壊れることはなく、自動的に回復します。
  • 同じ論理acquireを内部で再試行するのは、リクエストバイトを1つも送信できなかった場合だけです。 対応付け可能なBは確定したBusy(未獲得)として内部再試行せず即時にcallerへ返します。Mは 次の接続用のleader hintにすぎず、owner/keyを持たないため送信済みpendingを再送する根拠には なりません。送信後に応答を確認できないacquireはIndeterminateとして通知し、critical section に入ってはいけません。
  • 明示的releaseと既知tokenの補償はowner/token/keyの完全一致とbounded専用queueを使います。 再試行は残りleaseではなく、request/enqueue時点からの絶対5秒deadlineで終了します。成功応答を 得られない明示的releaseを成功として報告・仮定せず、最終的にはserver-side leaseが安全網になります。

接続とセキュリティ

すべての接続(クライアント・ピアともに)はTCP → (TLS) → challenge-response認証の順で 確立されます。サーバーが送った乱数をトークンと合わせてハッシュ化して応答するため、トークンが そのままワイヤー上に流れることはなく、接続のたびに乱数が変わるためリプレイ攻撃もできません。 正確な仕様は認証ハンドシェイクを参照してください。

整合性のまとめ

  • 排他制御は合意によって保証されます。 すべての発行が過半数コミットを経るため、 ネットワーク分断やリーダー交代の最中でも、同じキーのロックが2箇所で同時に発行されることは ありません。
  • 永続DBではフェンシングを必ず適用してください。 lease失効後に遅れて目覚めたクライアントは、 保護writeと同じtransaction/条件付きwriteで原子的に行うキー別high-water条件だけが防げます。 ロックは順番を整え、DB fencingが最後のstale writeを遮断します。