[PR] あなたが Kindle で読みたいその本、Kindle に対応したら Twitter でお知らせします。

Lambda でアトミックなロック処理を実装する

Posted on

Lambda はステートレスで冪等性のある処理に向いていますが、処理によっては排他制御を必要とする場合があります。排他制御にはアトミックなロック処理が欠かせませんが、Lambda のアーキテクチャでは少し工夫が必要です。

というのも、Lambda 関数が実行されるインスタンスは常に再利用されるとは限らないため、ロックの状態は Lambda の外で管理しなければなりません。

DynamoDB をロックマネージャーとして使う

いろいろ調べたところ、DynamoDB の条件付き書き込みを使うのが良さそうです。

条件付き書き込みを使用すると、オペレーションが成功するのは、項目の属性が 1 つ以上の想定条件を満たす場合のみです。それ以外の場合は、エラーが返されます。

@imaifactory さんの Qiita 記事「DynamoDB をロックマネージャーとして使う」がとてもわかりやすいです。ロックの状態を DynamoDB に持つことでステートレスにしつつ、条件付き書き込みでアトミック性も保証します。

ロック処理の仕組みを作る

まずは DynamoDB にシンプルなテーブルを作ります。テーブル名は lambda_lock_manager、プライマリーキーは key としました。

DynamoDB のテーブル作成画面

Python と Boto 3 を使ってロック処理を実装してみるとこんな感じです。

import boto3
import time

client = boto3.client('dynamodb')

table_name = 'lambda_lock_manager'
item_name = 'lock_test'

def lock(table, item):
    try:
        client.put_item(
            TableName = table,
            Item = {
                'key': {'S': item}
            },
            Expected = {
                'key': {'Exists': False}
            }
        )
        return True
    except Exception, e:
        return False

def unlock(table, item):
    client.delete_item(
        TableName = table,
        Key = {
            'key': {'S': item}
        }
    )

def lambda_handler(event, context):
    result = lock(table_name, item_name)
    print result

    if not result:
        print "Locked. Nothing to do."
        return

    time.sleep(15)
    unlock(table_name, item_name)

ロックを取って、15 秒間スリープして、ロックを解除するという単純なスクリプトです。

ロックの詳細は lambda_lock_manager テーブルに lock_test というキーを Put して、

  • すでに存在していたら、他の誰かがロック済み。例外を投げて False を返す
  • 存在していなければ、誰もロックしていない。キーを Put して True を返す

という仕組みになっています。

テストしてみる

この Lambda 関数に S3 の Put イベントトリガーを設定して、同時に複数ファイルをアップロードしてみます。

START RequestId: 3dcfa1a0-532d-11e6-9605-xxxxxxxxxxxx Version: $LATEST
True
END RequestId: 3dcfa1a0-532d-11e6-9605-xxxxxxxxxxxx
REPORT RequestId: 3dcfa1a0-532d-11e6-9605-xxxxxxxxxxxx Duration: 15315.15 ms Billed Duration: 15400 ms  Memory Size: 128 MB Max Memory Used: 34 MB

CloudWatch Logs のログを確認すると、True が出力されているのは常に 1 つです。実行時間も約 15 秒です。

START RequestId: 3df2e1a6-532d-11e6-85b1-xxxxxxxxxxxx Version: $LATEST
False
Locked. Nothing to do.
END RequestId: 3df2e1a6-532d-11e6-85b1-xxxxxxxxxxxx
REPORT RequestId: 3df2e1a6-532d-11e6-85b1-xxxxxxxxxxxx Duration: 427.45 ms Billed Duration: 500 ms  Memory Size: 128 MB Max Memory Used: 34 MB

他は False が出力されており、正しくロックされていることが確認できました。

まとめ

DynamoDB の条件付き書き込みを使えば、Lambda でもアトミックなロック処理が実装できます。これで活用の幅がまたひとつ広がりそうです。