Amazon DynamoDB Lock Client でクロスリージョン排他制御を実現してみる
Amaozon DynamoDB Lock Client って知ってる?
これです。
Amazon DynamoDBをバックエンドにした「汎用の分散ロックライブラリ」です。任意の文字列をキーにしてロックを掛けることが可能なためロックの粒度(ロックを掛ける範囲)は非常に柔軟に設計することができます。一方で、自由度が高いために慎重に設計しないと想定外にロックが解除されてしまったり、デッドロックが発生してしまったり、というリスクもはらんでいるためロックに関するロジックは一か所に集約して十分に統制を利かせながら利用する必要があるライブラリでもあります。
クロスリージョン排他制御を試した背景
その1。最近、DynamoDBのグローバルテーブルに強力な整合性がサポートされたことです。リージョンを跨いで常に最新の情報を扱えるようになったので、クロスリージョンでロック情報が共有可能になったのでは?という思い付きです。
その2。Aurora DSQLには排他制御の仕組みがないことです。Aurora DSQLは99.999%の高可用性と無限のスケーラビリティを提供する強力なRDBでが、トランザクションのコミット時に競合を検出する楽観的同時実行制御を採用しているため排他制御の仕組みが提供されていません。ならば、とりあえずAurora DSQLの外に排他制御の仕組みを作ってしまえばいいのでは?という発想です。
実行アーキテクチャ
今回お試しの実装を実行したAWSの構成は以下の通りです。
Aurora DSQLのマルチリージョンクラスターとDynamoDBグローバルテーブルはともに99.999%の高可用性を実現しています。加えてactive-active構成が可能な一貫性も提供されます。つまり、クライアントはどちらのリージョンにアクセスしても常に最新のデータにアクセスすることが可能という非常に信頼性の高い構成になっています。
なお、実際にはAurora DSQLとDynamoDBグローバルテーブルともにus-west-2リージョンも第三のデータ複製先として存在しています。しかし、アプリケーションからの読み書きは行わないため省略しました。
試してみてどうだったのか
で、結局のところクロスリージョン排他制御は実現できたのかどうかというと「出来ました」が結論です。ただし、いくつか対応が必要な内容があります。
ReplicatedWriteConflictExceptionのハンドリング
DynamoDBグローバルテーブルで強力な整合性を有効にするとReplicatedWriteConflictExceptionという例外が発生するようになります。Amazon DynamoDB Lock Clientはこの例外には未対応のためアプリケーション側で適切にハンドリングする必要があります。
再入可能ロックの実現
Amazon DynamoDB Lock ClientにはReentrantオプションはあります。このオプションをtrueにすると、現在のオーナーが対象のロックを既に保持している場合はロック取得処理をスキップしてくれます。ただし、このオプションはjava.util.concurrent.locks.ReentrantLockのようにロックの保持カウントをサポートしません。すなわち、複数回同じロックを取得したとしても、単一のロック解放操作によってロックは解放されてしまいます。この挙動が問題であり、ReentrantLockのようにロックの取得回数と同数の解放操作の後にロックが解放されるようにしたい場合はアプリケーション側に追加の実装が必要になります。
スレッドによるロック所有の実現
一部の処理がAmazonDynamoDBLockClientのインスタンス変数に依存しているため、スレッド単位でのロック所有を実現する場合はAmazonDynamoDBLockClientインスタンスをスレッドローカルにするのが良さそうです。また、ロックの所有者を識別するためのオーナー名の既定値がホスト名になっているため、ホスト名+スレッド名のようなスレッド単位でグローバルに一意になるようなオーナー名を設定するように設定値を変更する必要があります。
多数の短命なロックに最適化した設定
前述の通り、Amazon DynamoDB Lock Clientが発行するロックの有効期限の仕様はある程度の長い期間にわたってロックを保持し続けることを想定した内容になっています。具体的にはロックオーナーからハートビートを送信するスレッドをバックグラウンドで起動し、ハートビートが届く限りロックの有効期限を延長するような仕様になっています。また、ハートビートが届かなくなって一定期間(LeaseDurationで設定します)が経過すると、自動的に有効期限切れとして扱うことでロックを解放しないまま応答しなくなったロックオーナーが所有していたロックも解放できるように工夫されています。
しかし、数秒以内に終了するようなオンライン処理で利用する短命なロックの場合、このようなバックグラウンドでスレッドを起動してハートビートを送信する仕組みは過剰な負荷をシステムに強いることになります。そのため、許容される最大のロック保持期間をLeaseDurationに設定し、バックグラウンドのスレッド起動やハートビート送信は無効にしておくのが良いと思われます。
結局のところ排他制御は必要なのか
「Aurora DSQLには排他制御の仕組みがない」から「既存の仕組みを使って排他制御してみたらどうだろう」がこの試みの動機だったのですが、結局のところそのような排他制御が必要なのか、と冷静に考える必要はありそうです。というのも、非常に多くの同時並行処理が発生するようなワークロードにおいて排他制御を行うと、全ての処理が直列に待ち行列に入ってしまいます。すると処理時間は瞬く間に増加し、ある時点からほとんどの処理がタイムアウトするような事態に陥ることになるでしょう。すなわち、多数の同時並行処理を実現する必要がある場合、排他制御は避けるべきアーキテクチャになります。
また、排他制御によるボトルネックを受け入れ可能な程度のトラフィック量しかない場合、実際に処理が衝突するようなケースは少なくなる可能性が高いです。とすると、排他制御をせずとも衝突を検知した際にリトライをするアーキテクチャで問題なさそうです。
他に考慮すべき事項はあると思うので直ちに結論は出せませんが、スケーラブルなシステムを構築したいのであれば「排他制御をどうにか実現する方法」を考えるのではなく、「排他制御なしでどうにかする方法」を考える方が良さそうですね。
Discussion