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

Ticketingとは?

Ticketingは分散ロックただ一つのために設計された専用ロックサーバーです。Rustで書かれた1つの サーバーバイナリと、同じバイナリプロトコルをバイト単位で同一に実装した言語ごとの公式クライアントで 構成されます。獲得(acquire)と解放(release)の2操作、キーごとのFIFOキュー、フェンシングトークン、 そして必要なときだけ有効にするRaftクラスタ — 分散ロックに必要なものだけを残し、それ以外は すべて削ぎ落としました。

ストアの上に実装されたロックとの違い

実運用の分散ロックの多くはキーバリューストアを利用します。Redis上でLuaスクリプトやRedlockを 動かしたり、ZooKeeperやetcdのセッション・リースの上にロックを実装したりします。Redisは優れた ストアですが、専用のロックシステムではなくキーバリューストアを転用して分散ロックを実装したもの であるため、構造的な限界があります。汎用プロトコルのパースコストと汎用データ構造のオーバーヘッドが 常につきまといます。

Ticketingは最初から分散ロック専用として設計されており、すべての層がロックを前提に作られて います。

  • プロトコル — テキストでも汎用シリアライズ形式でもない、固定長バイナリフィールドです。 パースなしでバイトオフセットからそのまま読み取り、応答を待たずに次のリクエストを送る パイプライニングにも対応します。
  • サーバー内部 — キーごとのFIFOキューとリースタイマーが状態のすべてです。リクエスト処理 経路ではヒープ確保すら行いません。
  • 運用 — サーバーバイナリ(またはコンテナ)を1つ起動し、いくつかの環境変数で設定すれば 終わりです。スキーマ設計も、別途のストアも、ロックと無関係な運用知識も必要ありません。

パフォーマンス

  • 毎秒100万件以上を処理しながら、メモリ使用量は10MB未満
最も広く使われている分散ロックシステムであるRedisson(Redis)との速度比較。
TicketingクライアントRedis(Redisson RLock)
JavaScript
210.2 ms · 190,295 ops/s
210.2 ms
Rust
225.9 ms · 177,069 ops/s
225.9 ms
Go
228.4 ms · 175,131 ops/s
228.4 ms
C#
286.9 ms · 139,421 ops/s
286.9 ms
Kotlin
443.5 ms · 90,192 ops/s
443.5 ms
Java
457.8 ms · 87,374 ops/s
457.8 ms
C/C++
659.7 ms · 60,634 ops/s
659.7 ms
Python
739.4 ms · 54,098 ops/s
739.4 ms
Ruby
1,682.4 ms · 23,776 ops/s
1,682.4 ms
Redis基準
4,909.1 ms · 8,148 ops/s
4,909.1 ms
バーの長さは全体処理にかかった時間(ms)— 短いほど高速です。
32 contended keys × 1,250 ops · concurrency 64 · mac mini m4 2024 basic (10 core), single local server, acknowledged release, 1s min-work budget

なぜTicketingなのか

🚦 順序の一貫性 — FIFOキューと直接引き継ぎ

先に待った側が先に受け取ります。保有者が解放すると、キューの先頭に再競合なしで直接引き継ぎ されます。ポーリングと再試行でロックを奪い合う方式で起きる飢餓(starvation)も、再試行の 暴走もありません。

🛟 クライアントが落ちてもロックは詰まらない — lease

ロックを保持していたプロセスが落ちたり、ネットワークが切断されたりしても、獲得時に設定した leaseの時間が経過すればサーバーが自動的に回収し、次の待機者に引き継ぎます。解放を忘れても システム全体が止まることはありません。

🔑 誤った作業順序を防ぐ — フェンシングトークン

ロックだけでは防げない事故が1つあります。leaseが期限切れになったことに気づかず遅れて目覚め、 作業完了を要求してくるケースです。Ticketingは獲得のたびに単調増加するフェンシングトークンを 併せて発行します。保護対象のリソースが「より低いトークンは拒否する」というルール1つだけを守れば、 この最後の穴もふさがれます。詳しい原理は動作原理で説明しています。

🗳️ Raftベースの無停止クラスタ

サーバー1台でも十分に動作しますが、無停止が必要な場合は3台(または5台)でクラスタを構成します。 すべてのロック状態変更は過半数のノードの合意(Raft)を経てから確定されるため、リーダーが 落ちたりネットワークが分断されたりしても、同じロックが2箇所で同時に発行されることはありません。 リーダー障害時は約1秒以内に新しいリーダーが選出され、クライアントは自動的に切り替わります。 クラウドインスタンスの再起動や順次メンテナンスのようにノードが1台ずつ落ちる状況でも サービスは継続します。

🔒 認証とTLSを標準搭載

すべての接続は接続直後にchallenge-responseトークン認証を経ており、トークンがワイヤー上に 平文で流れることはありません。必要であればTLSで通信全体を暗号化できます。

🌐 主要言語間でバイト単位まで同じプロトコル

Rust、Java/Kotlin、JavaScript/TypeScript、Python、C#、Go、Ruby、C/C++ — 公式クライアントは すべて同じバイナリプロトコルを実装しており、各言語のエコシステムに自然なAPI(非同期または ブロッキング)を提供します。どの言語で獲得したロックも、他言語のクライアントと同じキューで 公平に順番を待ちます。インストール方法と例はライブラリをご覧ください。

APIはたった2つ

ロック専用サーバーらしく、表面積も最小限です。

  1. 保護したいリソースにキーを付けます。例: order-1234account-77daily-batch
  2. 作業の前にそのキーを獲得(acquire)します — 誰かが使用中ならFIFOキューで順番を 待ちます。
  3. 作業が終わったら解放(release)します — 次の待機者が再競合なしですぐに引き継ぎます。

この2つの操作にlease(自動回収)とwaitTimeout(待機の打ち切り)というオプションが 付くだけで、APIはこれで全部です。

次に読むもの