DynamoDBで分散ロックを実現するsetddblockと現代版アクセスカウンター

こんにちは。今年からアナリティクスエンジニアを名乗ろうとしてる池田です。

こちらは、KAYAC Advent Calendar 2021 1日目の記事です。
この記事では、setddblockという分散ロックを実現するためのツールと、その動作試験のために現代版アクセスカウンターを作成した話をします。

setddblock

皆様がcronなどで定期実行のスクリプトを実行する場合、同時実行されては困るものはどうしていたでしょうか?
daemontoolsのsetlock というツールを使って同時実行を防いでいたことがある方も多いと思います。

http://cr.yp.to/daemontools/setlock.html

このsetlock はローカルのファイルシステム上にロックファイルを作成し、ロックファイルの作成が成功したら指定されたコマンドを実行するというツールです。
例えば1分ごとに起動するようなCronの場合は以下のように記述していたのではないでしょうか?

*/1 * * * * setlock -n /tmp/task.lock task.sh

本来の実行したいスクリプトをラップする形で使えて非常に便利でした。
ところが令和のいま現在においては、サーバーインスタンス上では実行せずにコンテナ環境で実行、あるいはサーバレスな実行環境で実行ということも多い時代になりました。
特にAWSをお使いの皆様、cronで行ってた定期的な処理をEventBridge(CloudWatch Events)経由でLambda関数やECS Taskを起動することで実現することも多くなってきたと思います。
このようなケースで、同時実行を防ぎたい場合はsetlockが適用できなくて、とても困ります。
そこで、setlock と同じような感覚で使える setddblock というものを作りました。

github.com

これは、ロックファイルの代わりにDynamoDBのテーブル上のアイテムを使ってロックを実現します。
setddblockの中身のアルゴリズムは以下の記事を参考に実装されています。

aws.amazon.com

setddblockをCLIのコマンドとして使う場合は、次のようになります。

$ setddblock -n ddb://lock_table/task task.sh

この場合、DynamoDBのlock_tableという名前のDynamoDBテーブルに task というIDでロックを取得しに行きます。
コマンドのオプション等はsetlockと同様になっているので、setlockと同じような感覚で使えると思います。
また、setddblockはGo言語で書かれており、Go言語でアプリケーションを書く場合はライブラリとしても使用できます。

l, err := setddblock.New("ddb://ddb_lock_table/lock_item_id")
if err != nil {
    // 初期化失敗時の処理
}
func (ctx context.Context) {
    granted, err := l.LockWithErr(ctx)
    if err != nil {
        // ロック取得時に発生したエラーに関する処理
    }
    if !granted {
        // 指定したタイムアウト等でロックが取得できなかったときの処理
    }
    defer func() {
        if err := l.UnlockWithErr(ctx); err != nil {
            //アンロック時に発生したエラーに関する処理
        }     
    }()
    // ロック取得中に行う処理
}(ctx)

もちろん、sync.Locker インタフェースを満たしていますので、単純にLock()Unlock()とだけ呼ぶこともできます。sync.Condと合わせて分散環境でS3を使ってProducer/Consumerパターンを実現するのも面白いかもしれませんね。
※ デフォルトの挙動においてはLock()Unlock()は、Lock取得時にNetworkエラーなどが発生した場合は、panicしてしまうので、通常の使用時はLockWithErr()UnlockWithErr() の方の使用をおすすめします。

ところで、このsetddblock、ちゃんと動くか心配ですよね?
当然のごとく動作試験をしたくなりましたので、動作試験用のアプリケーションとして現代版アクセスカウンターを作成しました。

現代版アクセスカウンター

ロック機構の動作試験方法は色々あると思います。
しかし、単純なCLIのスクリプトを作って試験するというのも味気がないので、アクセスカウンターを作成しました。
動作試験用のアクセスカウンターのリポジトリは以下にまとめてあります。

github.com

構成

このアクセスカウンターの構成図は次のようになっています。

f:id:ikeda-masashi:20211129155526p:plain
構成

このアクセスカウンターは、完全なるサーバレスですね。現代版アクセスカウンターと言っても過言ではないでしょう。

この現代版アクセスカウンターは、API Gatewayにより構成されており、アクセスが発生すると本体であるLambda関数を起動しリクエストを処理します。
リクエストの本体は、まずsetddblockをライブラリとして使用し、DynamoDBに対してロックを取得しに行きます。 その次に、環境変数で指定されたS3 Bucketにあるcounter.jsonをGetし、アクセスカウントをインクリメントしてからS3 BucketにPutして上書きします。
そして、インクリメントする前のアクセスカウントでHTMLを生成してレスポンスを返します。

setddblock動作試験

S3が強整合性をサポートしたので、setddblockが正しく動作している場合、並列でアクセスを行ったとしても正しくアクセスカウンターが更新されるはずです。
では、実際に試してみましょう。

f:id:ikeda-masashi:20211129155611p:plain

最初にアクセスすると、1430と表示されました。
ここで、Apache Benchというベンチマークソフトを用いて、総リクエスト数200回 並列度20のアクセスを行います。

ab -n 200 -c 20 https://<api gatewayのエンドポイント>

そして、再度アクセスすると以下のようになりました。 f:id:ikeda-masashi:20211129155632p:plain

2回目のアクセスが 1631 となっています。これはApache Benchが200回アクセスしたあとに、自分自身がもう一度アクセスしたため、最初から加えて201インクリメントされていることになります。
この結果からsetddblockがきちんと動作していることをがわかります。

これでsetddblockを安心して使えますね。

おわりに

この記事の内容としては以下でした。

  • setddblockという分散ロックツールを作成しました。
  • setddblockの動作試験のために現代版アクセスカウンターを作成しました。
  • 並列度20のアクセスを行ってもアクセスカウンターが正しくカウントできたので、setddblockはちゃんとロックできていました。

EventBridge経由でLambda関数やECS Taskを起動するとき、多重起動の考慮はつきものになります。 定期処理系を冪等に作るのが困難な場合や、簡単に同時実行制御をしたくなった場合にはsetddblockの活用を検討してみてください。

隙間家具を作りたいエンジニアも募集しています

中途採用も募集しています