AWS Lambdaのコールドスタートはコンテナイメージを使うと悪化するのか
こんにちは。株式会社GA technologies、Advanced Innovation Strategy Center(AISC)の三田です。
AISCではAWS Lambdaを使用することが非常に多いです。Lambdaはスケーラビリティやコスト効率が高くメリットが多いためです。
しかし一方でLambdaにはインスタンスを初回起動する際は関数の実行までに時間がかかる「コールドスタート」と呼ばれる問題も存在し、Lambdaとどう付き合っていくのがいいのかについては部内でもよく話題に上がります。
部内で出ていた仮説のひとつに
という説があります。
ファイルサイズで比較すれば、コンテナイメージとソースコードのzipでは非常に大きな差があります。AWSの公式Python3.12イメージ はベースイメージの時点で182MBあり、ソースコードのzipは小さいものなら1キロバイト未満です。
単純に考えればイメージによるデプロイは遅くなりますが、それはAWS側も予想したうえで対策しており、ファイルサイズのみでは単純に予測できない部分があると思われます。
そこで、デプロイ方法によってコールドスタートはどのくらい変わるのか実験して確かめてみました。
方法
ランタイム
以下のように検証していきます。まず、zipによりデプロイする際はPython 3.12ランタイムを使用します。厳密には以下のランタイムバージョンを使用します。
Runtime Version: python:3.12.v30
Runtime Version ARN: arn:aws:lambda:ap-northeast-1::runtime:acd6500d0e3f6a085fb07933e3472ed6e58360d19ec5dd91bc7c7e8ad119de42
コンテナイメージによるデプロイはAWS公式のPython 3.12イメージをベースに使用し、以下のDockerfileを使用してbuildします(app.pyは検証用のスクリプトです)。
FROM public.ecr.aws/lambda/python:3.12
COPY app.py requirements.txt ./
RUN python3.12 -m pip install -r requirements.txt -t .
CMD ["app.lambda_handler"]
なおmemorySizeの多寡は今回の実験には関係ないと思われますが、念のためすべて128MBに統一します。
計測方法
コールドスタート時間の計測方法は、Tracingを有効にしたLambdaでCloudWatch Logs Insightsの@initDurationの項目を計測する方法で行います。
検証に使用するコード(app.py)は以下のようにシンプルなものにし、handlerの外でライブラリをimportするPEP8準拠の標準的な書き方を行います(※ @initDurationにはhandler関数外の処理、つまりimportにかかる時間も含まれます)
import time
import pandas as pd
def lambda_handler(event, context):
time.sleep(1)
パッケージ
パッケージのファイルサイズの違いで結果が変わりそうなので、次の6つのパターンで試してみます。
外部パッケージなし
requests (ver. 2.32.3) のみを追加する
pillow (ver. 10.4.0) のみを追加する
boto3 (ver. 1.35.5) のみを追加する
pandas (ver. 2.2.2) のみを追加する
scipy (ver. 1.14.1) のみを追加する
デプロイ方法
デプロイの方法は以下の3パターンを試します。
image:コンテナイメージでデプロイする
zip:zipでデプロイし、外部パッケージもソースコードと同梱する
zip+layer:zipでデプロイし、外部パッケージはLayerに入れる
(ただし「外部パッケージなし」のときはlayerを使う必要がないため、imageとzipのみ実験します)
呼び出し間隔
LambdaはEventBridgeで一定間隔で呼び出します。間隔は次の3パターンです。
(もし一定時間で消えるような一時的なキャッシュが使われているなら、低頻度で呼び出される関数はコールドスタートが長くなったりするのかな~?という意図で間隔を変えて検証します)
15分ごと
1時間ごと
1日ごと
なお、モニタリングする期間はLambda関数のデプロイから5日間とします。
結果
外部パッケージがあれば、基本的にimageが最速
デプロイ後の各回のinit durationをパッケージごとに図にしたものが以下になります。それぞれの点が観測値で、横棒は平均値を表しています。(ここではすべての呼び出し間隔のサンプルをまとめて扱っています)
この図から以下のことがわかります。
外部パッケージ「なし」と「pillow」は、平均値は zip < image の関係になる(僅差ですが)
それ以外のパッケージの平均値は image < zip < zip+layer の順になる
imageデプロイは実行に極端に時間がかかった外れ値が存在する(詳しくは後述)
呼び出し間隔ごとに分けてプロットしたものは次の図になります。いずれの間隔でも、平均としては同様の順序関係が見られます。
平均的にはzip < zip+layerの関係にある、つまり「layerを使わずzipだけでデプロイしたほうがinit durationをわずかに削減できる」というのは個人的には興味深いです。layerはLambda Function間で共通するデータが多いはずなので、もしかするとキャッシュするシステムが組まれていたりするのかも?と思っていたのですが、そういった工夫はなさそうです。
layerを使わずzipでデプロイするほうが速いというのは個人的には朗報だったりします。AWS SAMを使ってデプロイする場合、layerを自分で作成してデプロイするよりもzipだけで(コードとライブラリをzipに同梱する形で)デプロイするほうがコード量やビルドの手間(layerを作るためにパッケージをローカルストレージに一旦ダウンロードする等)が減って楽なためです。今後は胸を張ってzip単体デプロイが使えます。
imageはデプロイ直後のコールドスタートが遅い
毎回の実行時のinit durationの推移を呼び出し間隔ごとに図にすると、次のようになります。
15分ごとの実行間隔の場合、imageによるデプロイを行った関数の初回呼び出し時のみinitDurationが長くなることが多いようです。一方で1時間ごとや1日ごとの実行ではそのようなことはなく、初回からimageのinitDurationが高速でした。
ここからは私の仮説ですが、もしかするとデプロイの直後(数分~十数分後)にアクセスするとキャッシュ作成などの準備が整う前にアクセスしてしまうケースが発生するのかもしれません。コンテナイメージでLambda関数をデプロイした場合、イメージを512KiBごとのチャンクに分割してS3にキャッシュを作るようなのですが(Brooker et al., 2023)、そのキャッシュ作成が終わる前にアクセスするとinitDurationが遅くなるのかもしれません。
パッケージサイズは関係しそうだが一概に言えない
今回実験に使用した外部パッケージのサイズの違いと平均init Durationの関係を図にすると、次のようになります(全ての呼び出し間隔のデータ全体で平均をとったものになります)。
pillowとscipy以外についてはパッケージのサイズとzipデプロイ時のinitDurationが強く相関するように見えますが、scipyは215MBあるにもかかわらずboto3と同程度のinitDurationなので、パッケージのサイズに応じた単調な関係があるわけではないようです。
(なお上記は圧縮前のパッケージのサイズですが、zipでデプロイした関数の圧縮済みのコードのサイズを確認した場合もパッケージごとの大小関係は上記と同じでした)
pillowとscipyのinitDurationが全体的に短くなる理由はいまのところ私にはわかっておりません。zipによりデプロイされた関数はimageデプロイと違ってブロックごとのキャッシュは作られず、現在も昔と変わらずS3からzipファイルが丸々ダウンロードされる運用になっているという認識なのですが(外部パッケージがある場合にimageよりもzipのほうがinitDurationが長い点についてはこれで説明がつくと思うのですが)、実際には違うのかもしれません。
まとめ
以上の結果をまとめますと、コールドスタートを速く(init durationを短く)するためには
1️⃣ 外部パッケージを使わない場合 (or pillowのような一部の軽量なパッケージ)
→ zipによるデプロイが最速になりそう
ただし、imageでデプロイしても大差ないので実用上はimageでも良さそう
2️⃣ 外部パッケージを利用する場合
→ 基本的には imageによるデプロイが最速になりそう
ただし、デプロイ直後はzipより遅い
となりました。よって冒頭の
という仮説については、「外部パッケージによる」が現状の結論になりますが、コールドスタートの観点ではコンテナイメージでのデプロイによるデメリットは非常に少なく、気軽に使用しても問題ないように思われます。