エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

AWS Lambdaにおける「ステートレス」なキャッシュ利用

AWS Lambda は、AWSが提供するイベントドリブンなプログラム実行環境であり、昨今ではサーバーレスアーキテクチャの実現手段として注目を集めている。

AWSの公式ドキュメントによると、AWS Lambda の関数はステートレスな実装にする必要がある。一方でステートレスな実装を追求すると、外部サーバからのデータ取得処理が増え、結果として処理パフォーマンスが悪化することが多い。

本稿では、データの性質に応じたAWS Lambda での状態保持(キャッシュ利用)の可否について考察し、キャッシュに関する実装や運用の一例を示す。

1. Lambda 関数に求められる「ステートレス」の意味

AWSの公式ドキュメンテーションによると、AWS Lambdaの関数は、ステートレスな実装にする必要がある。

コードは必ず「ステートレス」なスタイルで書く必要があります。
~中略~
関数をステートレスにすることにより、AWS Lambda は必要なだけ関数を迅速に実行し、イベント受信の回数にスケールすることができます。AWS Lambda のプログラムモデルはステートレスですが、コードはAmazon S3 または Amazon DynamoDB など他のウェブサービスを呼び出すことでステートフルなデータにアクセスできます。

よくある質問 - AWS Lambda | AWSより

一方で、コンテナ(Lambda関数インスタンス)が再利用された場合、前回実行時に生成したデータを引き継げるとされている。ただし、再利用されるかどうかは不定であり、確実に再利用されるようにする方法は、この記事の執筆時点では提供されていない。

パフォーマンス向上のため、AWS Lambda は新しく関数のインスタンスを作成するのではなく、関数のインスタンスを保持してその後のリクエストに対応することがあります。ただし、常にインスタンスを再利用するわけではありません。
~中略~
各コンテナには、/tmp ディレクトリに多少のディスク領域があります。ディレクトリのコンテンツはコンテナが停止された際に維持され、複数の呼び出しに使用できる一時的なキャッシュを提供します。

よくある質問 - AWS Lambda | AWSより

以上から、AWSの公式ドキュメンテーションが求める「ステートレス」な実装とは、言葉通り「Lambda関数はあらゆる状態を持たない実装にすべき」ことを示すのではなく、「外部にマスタがある静的なデータはLambda関数上のキャッシュとして保持し、次回リクエスト時に再利用して良い」と言える。

2. Lambda におけるキャッシュ利用の例

例として、S3に格納されるHTMLテンプレートにAPIサーバから取得した情報を適用して返却するLambda関数を考える。

f:id:enterprisegeeks:20170417001812p:plain
図1:キャッシュ実装前の処理フロー

ここで、外部アクセスは 2. および 4. で発生している。2. は動的レスポンスを返すAPIのため、結果のキャッシュ対応はすべきでない。一方で 4. のレスポンスは静的であり、マスタはS3側にあるため、以下のようなキャッシュの実装が可能である。

  1. Lambda関数のデプロイ、またはコンテナ破棄の後の、当該テンプレートへの初回アクセスの時は、S3から取得する
  2. それ以外の場合は、当該テンプレートをLambda関数側に保持したキャッシュから取得する

このような仕組を構築することで、毎回S3アクセスすることによる応答速度の低下と課金発生の抑制を実現できる。

f:id:enterprisegeeks:20170417001849p:plain
図2:キャッシュ実装後の処理フロー(キャッシュにヒットした場合)

以下は、キャッシュを実現するLabmda関数のPythonコードの例である。

class ResponseConverter():

    # HTMLテンプレートのキャッシュ。
    # ここではクラス変数にキャッシュすることで、このクラスの
    # インスタンスをまたがって保持・利用する実装にしている。
    # テンプレートのサイズや数が多い場合はメモリが枯渇する
    # 恐れがあるため、/tmpディレクトリを利用する方が望ましい。
    template_cache = {}

    def get_template(self, template_id):

        # キャッシュからのテンプレート取得を試行
        template = self.template_cache.get(template_id)

        if not template:
            # キャッシュに無いテンプレートのため、S3から取得する
            template = self.get_template_from_s3(template_id)

            # キャッシュとして保持
            self.template_cache[template_id] = template

        return template

3. Lambda のキャッシュを破棄する手段

上述のコードを運用する場合、S3側のテンプレートを更新した時にキャッシュ側も更新する仕組が必要になる。

一定時間経過によるキャッシュ破棄など、プログラム実装による手段を採用しない場合、AWS CLIAWSコンソール操作によるコンテナの破棄・再作成をすることになる。しかし、執筆時点のAWSの公式ドキュメンテーションでは、純粋にコンテナ破棄だけを指示する手段の記述は見当たらなかった。

当然、Lambda関数のコードエントリを更新すればコンテナは破棄・再作成されるが、例え更新前と同じコードエントリによる更新であっても、システム運用の現場では極力避けたいオペレーションと言えるだろう。

より抵抗感の少ないオペレーションとして、Lambda関数の「説明」や「環境変数」の更新により、コンテナを破棄・再作成できることを、実際の動作により確認した。これらに「直近のLambda関数再作成時刻」を設定することによるキャッシュ更新は、採用しやすい運用手段のひとつと言えるだろう。

f:id:enterprisegeeks:20170417001919p:plain
図3:「説明」の更新例

なお、S3上のリソース更新からコンテナ再作成操作の間、Lambda関数全体として、新旧どちらのリソースも返却し得る状態になる。これは、既存コンテナはキャッシュ上の変更前リソースを、新規コンテナではS3上の変更後リソースを利用するためである。それが許容されないシステム要件の場合、別途何らかの考慮が必要となる。

4. AWS Lambda のコンテナ再利用の詳細な考察

最後に参考として、「コンテナの自動破棄までの時間」など、コンテナのライフサイクルに関する詳細を動作検証から考察した記事を紹介しておく。

AWS Lambda Container Lifetime and Config Refresh (英語)

※この記事にも記載があるが、AWSの仕様として保証されるものではない点に注意が必要である。

[熊坂 洋輔]