Python
AWS
lambda

[AWS] Lambdaの内部処理結果をキャッシュする(Python3.6)

やりたいこと

AWS Lambdaで何か処理をするとき、DynamoDBやS3などからデータを取得するケースがあると思いますが、変更されることが少ないマスタデータを参照する場合にDynamoDBやS3から毎回Getしていると処理コスト(速度・料金)の無駄が多くなります。
そこで、そのようなリアルタイムで最新化されている必要性が薄いデータをLambda内部でキャッシュし再利用することで、処理コストを削減します。

前提

言語バージョン:Python3.6

サンプルコード

# coding: utf-8

# キャッシュ変数(global)
cache = {}

# AWS Lambda メイン処理
def lambda_handler(event, context):
    count   =   event["count"]
    print("{0}: ".format(count), end="")

    # キャッシュに"test"があればキャッシュから取得、無ければ計算
    result  =   get_cache("test", (lambda:func_addition(10000, count)), 4)
    print(result)

# 結果をキャッシュしたい処理
def func_addition(a, b):
    result  =   a + b
    print("*", end="")
    return result

# キャッシュ処理
def get_cache(key, func, expiration_seconds = 60):
    import datetime
    global cache

    # キャッシュから取得
    if key in cache:
        now = datetime.datetime.now()
        if (cache[key]["updated"] + datetime.timedelta(seconds=expiration_seconds)) > now:
            return cache[key]["value"]

    # キャッシュ更新
    value   =   func()
    cache[key]  =   {
        "updated" : datetime.datetime.now(),
        "value"   : value
    }
    return value

# テスト用のMain処理
if __name__ == '__main__':
    for i in range(1, 16):
        lambda_handler({"count": i}, None)
        import time
        time.sleep(1)

実行結果

上記サンプルコードを AWS Lambda Function としての実行ではなく、ローカルで実行した場合。

$ python lambda_cache.py
1: *10001
2: 10001
3: 10001
4: 10001
5: *10005
6: 10005
7: 10005
8: 10005
9: *10009
10: 10009
11: 10009
12: 10009
13: *10013
14: 10013
15: 10013

こちらのサンプルでは4秒間キャッシュされます。
キャッシュしたい処理が実際に動いたときだけ、*付きで出力されています。

利用方法

  • グローバル変数「cache」を宣言
# キャッシュ変数(global)
cache = {}
  • キャッシュ処理ファンクションを定義
# キャッシュ処理
def get_cache(key, func, expiration_seconds = 60):
    ...
    (サンプルコード参照)
    ...
  • キャッシュ処理をコール
result  =   get_cache(<cache_key>, lambda:<function(args)>, <expiration_seconds>)

<cache_key> ... キャッシュキー。任意の値
<function(args)> ... キャッシュしたい処理
<expiration_seconds> ... キャッシュ有効期間。「0」でキャッシュ無効化。

ポイントは、get_cacheをコールするときにキャッシュしたい処理を lambda で定義すること。AWS の Lambda ではなく、Python の lambda式 です。
ここを lambda:function_name(args) ではなく function_name(args) にしてしまうと、get_cacheをコールする時点、つまりキャッシュを利用するかどうか判定する前に実行されてしまい、キャッシュ処理関数を通す意味が無くなります。

説明

  • キャッシュの持ち方。
    グローバル変数に入れているだけです。こちらの記事 で説明されている通り、Lambdaのインスタンスは再利用されています。
    同じインスタンスを使用している間はグローバル変数は初期化されませんので、次回に引き継げます。
    値と更新日をセットで保存し、キャッシュ有効期限を判定できるようにしています。

  • 指定した時間、必ずキャッシュが利用されるわけではない。
    別のインスタンスに切り替わるタイミングはAWS任せになるため、有効期限前でもインスタンスが切り替わることでキャッシュがクリアされることがあります。

  • キャッシュをクリアしたいときは。
    運用中のLambdaにおいて、キャッシュをクリアしたいケースもあると思います。そのときはlambdaを更新しましょう。
    何か1つ設定(たとえばlambda環境変数)を更新すれば、以降は新しいインスタンスで実行されるようです。(公式に謳われているわけではないので保証はできませんが)
    また、グローバル変数に保存しているだけですので、コード内で何か条件を付けて cache={} または del cache[key] とするだけでもキャッシュはクリアできます。

別案

DynamoDBの場合は『Amazon DynamoDB Accelerator (DAX) 』というサービスがありますので、そちらを利用したほうが良いかもしれません。
ただし個人で使うにはちょっと料金がちょっとお高めですが。