AWS

再実行性を意識したLambdaの書き方

akira

Lambdaを書く際に再実行性をどの程度意識しているでしょうか。再実行性を高めるためには初期化フェーズ(メイン関数外の処理)にどのような内容を書くべきかが重要になってきます。

初期化フェーズでは感覚的なものとして、メインの処理で繰り返し使い回される処理(コネクション、環境変数など)をメインの処理から外に出しているという方もいるかと思います。ただ、それではより複雑な処理を実行する際にLambdaの良さを活かしきれないためサンプルを交えて初期化フェーズでの実行内容とそれにあわせたLambdaの書き方をご紹介したいと思います。

Lambda実行の流れ

Lambdaのライフサイクルは以下のようになっており、大きく3つのフェーズで実行されています。

https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html#runtimes-lifecycle より

実際のLambdaコードだと以下のイメージです。

# ============================================================
# [Init Phase]
# ============================================================
import json
import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

os.environ["TEST_ENV"] = os.environ.get("TEST_ENV")
client = boto3.client("aws-services")

# ============================================================
# [Invoke Phase] 使い回される部分
# ============================================================
def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

# ============================================================
# [Shutdown Phase-ish]
# ============================================================)

まず最初に各フェーズでの実行内容を簡単にふれていきます。

INIT

このフェーズでは以下の3つのタスクを実行し、環境の初期化を実行します。

  • 拡張機能の起動 ( Extension init)
  • ランタイムをブートストラップ ( Runtime init)
  • 関数の静的コード実行(Function init

このフェーズが完了するとLambdaはメイン関数の実行準備が完了となります。

注意点としてこのフェーズはLambdaのタイムアウトとは別に10秒のタイムアウトがあります(詳細は後続で触れます)。
Lambdaの拡張機能についての詳細はここでは割愛させていただきます

INVOKE

このフェーズで lambda_handler が実行されます。Lambdaのタイムアウト時間内にINIT + INVOKEの処理を完了する必要があります。

また、実行環境が再利用される場合はINVOKEフェーズのみが実行されます。

SHUTDOWN

外部への拡張機能を利用している場合、このフェーズでイベントを送信します。イベントの送信が完了した後、一定期間は実行環境を保持しますが、継続的に呼び出されていても実行環境を終了するためその場合は新しい実行環境でINITフェーズから実行されます。

再実行性を意識する

Lambdaの実行の流れを確認できたので再実行性を高めるために注意するべき箇所を記載していきます。

INITフェーズに何を含めるか

フェーズの処理内容の紹介から、INITフェーズには1度だけの実行で使い回せるものを実行する必要があります。例としては以下のようなものです。

import json
import boto3

# ログレベルの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# 環境変数読み込み
os.environ["TEST_ENV"] = os.environ.get("TEST_ENV")

# クライアント呼び出し
client = boto3.client("aws-services")

逆に含めてはならないものとしては実行時に動的に変化してほしいものです。

サンプルとして以下のような関数を実行します。INITフェーズとINVOKEフェーズで時刻を取得しています。

import json
from datetime import datetime

INIT_time = datetime.now()

def lambda_handler(event, context):
    INVOKE_time = datetime.now()
    print(f"INIT time: {INIT_time}")
    print(f"INVOKE time: {INVOKE_time}")

    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

このような関数を実行した場合、実行環境が使い回されると以下のようにINIT_timeとINVOKE_timeが大きくズレてしまいます。

このようにINVOKEのたびに変化してほしい処理はINITフェーズに含めてはならないことがわかります。

INITフェーズのタイムアウト

ブログの主旨とは若干ずれてしまいますが、INITフェーズのタイムアウトにも注意が必要となります。

前述の通り、INITフェーズには10秒のタイムアウトが設定されており、超過するとタイムアウトします。しかし、Lambdaのタイムアウトとは異なりタイムアウトしてもLAMBDAの実行時間内であれば再実行されます。

理解のために以下のようなLambdaを用意し、Lambdaのタイムアウトを30秒としました。

import json
import time

time.sleep(15) 
test = "sample_STRING"

def lambda_handler(event, context):
    print(f"test: {test}")

    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

init Status: timeout となっていますが、実行結果は成功となっています。

これは公式ドキュメントにも記載の仕様ですが、あまりにも多くの処理をINITフェーズに寄せてしまうのはバグにつながるため init Status: にも気をつける必要があります。

The Init phase ends when the runtime and all extensions signal that they are ready by sending a Next API request. The Init phase is limited to 10 seconds. If all three tasks do not complete within 10 seconds, Lambda retries the Init phase at the time of the first function invocation with the configured function timeout.

https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html

まとめ

Lambda実行時の各フェーズの役割や、再実行時の挙動を理解することで再実行性の高いLambdaとすることができます。最終的には Optimizing static initialization などを参考にしつつ、実際の要件や実行時間を鑑みながら処理の修正や分割を検討するのが良いと思われます。

このブログがどなたかの参考になれば幸いです。

AUTHOR
akira
akira
記事URLをコピーしました