こんにちは。今年からアナリティクスエンジニアを名乗ろうとしてる池田です。
こちらは、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
というものを作りました。
これは、ロックファイルの代わりにDynamoDBのテーブル上のアイテムを使ってロックを実現します。
setddblock
の中身のアルゴリズムは以下の記事を参考に実装されています。
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のスクリプトを作って試験するというのも味気がないので、アクセスカウンターを作成しました。
動作試験用のアクセスカウンターのリポジトリは以下にまとめてあります。
構成
このアクセスカウンターの構成図は次のようになっています。
このアクセスカウンターは、完全なるサーバレスですね。現代版アクセスカウンターと言っても過言ではないでしょう。
この現代版アクセスカウンターは、API Gatewayにより構成されており、アクセスが発生すると本体であるLambda関数を起動しリクエストを処理します。
リクエストの本体は、まずsetddblockをライブラリとして使用し、DynamoDBに対してロックを取得しに行きます。
その次に、環境変数で指定されたS3 Bucketにあるcounter.jsonをGetし、アクセスカウントをインクリメントしてからS3 BucketにPutして上書きします。
そして、インクリメントする前のアクセスカウントでHTMLを生成してレスポンスを返します。
setddblock動作試験
S3が強整合性をサポートしたので、setddblock
が正しく動作している場合、並列でアクセスを行ったとしても正しくアクセスカウンターが更新されるはずです。
では、実際に試してみましょう。
最初にアクセスすると、1430
と表示されました。
ここで、Apache Benchというベンチマークソフトを用いて、総リクエスト数200回 並列度20のアクセスを行います。
ab -n 200 -c 20 https://<api gatewayのエンドポイント>
そして、再度アクセスすると以下のようになりました。
2回目のアクセスが 1631
となっています。これはApache Benchが200回アクセスしたあとに、自分自身がもう一度アクセスしたため、最初から加えて201
インクリメントされていることになります。
この結果からsetddblock
がきちんと動作していることをがわかります。
これでsetddblock
を安心して使えますね。
おわりに
この記事の内容としては以下でした。
setddblock
という分散ロックツールを作成しました。setddblock
の動作試験のために現代版アクセスカウンターを作成しました。- 並列度20のアクセスを行ってもアクセスカウンターが正しくカウントできたので、
setddblock
はちゃんとロックできていました。
EventBridge経由でLambda関数やECS Taskを起動するとき、多重起動の考慮はつきものになります。
定期処理系を冪等に作るのが困難な場合や、簡単に同時実行制御をしたくなった場合にはsetddblock
の活用を検討してみてください。