Lambda Layersを使うとデプロイは遅くなり、コールドスタートは高速化する?!Lambda Layersを使って巨大なLambda Functionを分割した場合の挙動の変化
2019/10/26追記
最近類似の検証を流した際は ・ サイズの大きなFunctionのパッケージ ・ サイズの小さなFunctionのパッケージ & Layer どちらもコールドスタートの所要時間に差異がありませんでした。
若干検証パターンが違うので一概に比較はできませんが、Layerを使うだけで常にコールドスタートが高速化するわけでは無さそうです。 ブログ執筆時以後にLambdaの内部的な挙動が変わった可能性もあります。 以後の内容は、あくまでブログ執筆時点における検証結果として参考にするに留めて下さい
はじめに
サーバーレス開発部@大阪の岩田です。 前回のブログでLambda Functionに紐付けるレイヤーのサイズを増加させていった場合の挙動について考察しました。
今回は
- Lambda Layersを使わずにLambda Function内に諸々のパッケージを詰め込んだ場合
- Lambda Layersを使ってLambda Functionと諸々のパッケージを分離した場合
の挙動の違いについて調べてみました。
やること
解凍後のサイズが250M程度かつZIPに圧縮すると50M以内に収まる適当なデータを使いLambda Layers有り・無しそれぞれのパターンで
- Lambda Function作成の所要時間計測
- X-Rayを使ったLambda Function実行の総所要時間計測
を実施します。
レイヤーの作成
計測の準備を進めていきます。
解凍後のサイズが250M程度かつ、ZIPに圧縮すると50M以内に収まる適当なデータを探します。 手元にあったMySQLのダンプが良い感じに使えそうだったのでそれを流用します。
$ du -ah 29M ./test.zip 240M ./test.sql
test.sqlが圧縮前、test.zipが圧縮後のファイルです。
レイヤーを作成します
aws s3 cp test.zip s3://<適当なS3バケット> aws lambda publish-layer-version --content S3Bucket=<適当なS3バケット>,S3Key=test.zip --layer-name 240mlayer
Lambda Function用のコードを準備
Lambda Function用のコードを準備します。 コード自体は何でも良いのでhello-worldのテンプレートをそのまま使います。
exports.handler = async (event) => { // TODO implement const response = { statusCode: 200, body: JSON.stringify('Hello from Lambda!'), }; return response; };
デプロイ用にZIPに圧縮します。まずはレイヤー有り用
zip hello.zip hello.js
次にレイヤー無し用
先ほど作成したレイヤーに詰め込んだMySQLのダンプもまとめてZIP化します。
zip hello2.zip hello.js test.sql
計測してみる
簡単なPythonのスクリプトを実行して計測してみます。 スクリプトはPython3.6で実装しました。
import boto3 from datetime import datetime, timedelta, timezone import time def filter_trace(x, fnc_name): resource_arns = x['ResourceARNs'] filterd_resource = [i for i in resource_arns if i['ARN'] == f'arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:{fnc_name}'] return len(filterd_resource) != 0 def main(): xray = boto3.client('xray') lambda_client = boto3.client('lambda') print('----------no layer function create duration----------') fnc_name = '240mfunction' for _ in range(10): before_create = time.time() lambda_client.create_function( FunctionName=fnc_name,Runtime='nodejs8.10',Handler='hello.handler', Role='arn:aws:iam::xxxxxxxxxxxx:role/lambda_basic_execution', Code={'ZipFile':open('hello2.zip','rb').read()}, TracingConfig={'Mode': 'Active'}) duration = time.time() - before_create print(duration) lambda_client.invoke(FunctionName=fnc_name) lambda_client.delete_function(FunctionName=fnc_name) print('----------use layer function create duration----------') fnc_name = '240mlayer_function' for _ in range(10): before_create = time.time() lambda_client.create_function( FunctionName=fnc_name,Runtime='nodejs8.10',Handler='hello.handler', Role='arn:aws:iam::xxxxxxxxxxxx:role/lambda_basic_execution', Code={'ZipFile':open('hello.zip','rb').read()}, TracingConfig={'Mode': 'Active'}, Layers=['arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:layer:240mlayer:1']) duration = time.time() - before_create print(duration) lambda_client.invoke(FunctionName=fnc_name) lambda_client.delete_function(FunctionName=fnc_name) # X-Rayに反映されるまでちょっと待つ time.sleep(30) JST = timezone(timedelta(hours=+9), 'JST') end_time = datetime.now(JST) start_time = end_time - timedelta(minutes=5) res = xray.get_trace_summaries(StartTime=start_time, EndTime=end_time) summaries = res['TraceSummaries'] print('----------no layer function invoke duration----------') traces = filter(lambda x :filter_trace(x, '240mfunction'), summaries) for trace in traces: print(trace['Duration'] * 1000) print('----------use layer function invoke duration----------') traces = filter(lambda x :filter_trace(x, '240mlayer_function'), summaries) for trace in traces: print(trace['Duration'] * 1000) if __name__ == '__main__': main()
結果
スクリプトを実行した計測結果です
Lambda Function作成の所要時間比較
No | レイヤー有り(単位:秒) | Functionのみ(単位:秒) |
---|---|---|
1 | 6.127879858 | 4.455529213 |
2 | 6.758473158 | 4.076428175 |
3 | 7.186372042 | 4.815581799 |
4 | 7.06692791 | 3.894051075 |
5 | 6.41276288 | 4.087101221 |
6 | 6.161081076 | 4.056248903 |
7 | 6.906441927 | 3.628862858 |
8 | 7.177785873 | 4.508531094 |
9 | 7.544185877 | 4.046594143 |
10 | 7.78280282 | 3.773734808 |
平均 | 6.912471342 | 4.134266329 |
レイヤーを使うと平均2.8秒程度遅くなるという結果が出ました。
レイヤーを使うことで
- デプロイパッケージをZIPに圧縮する処理
- 作成したZIPをS3にアップする処理
の高速化は期待できますが、肝心のLambda Function作成処理は遅くなってしまうようです。 Lambda Function10本で構成されるシステムを例に挙げるとシステム全体のデプロイ所要時間は10本 × 2.8秒で28秒遅くなる計算です。 たかだが250M程度のコードベースなので、いくらZIP圧縮・S3アップロードが高速化されても28秒以上の改善効果は見込めないでしょう。
「Lambda Layersを使うとLambda Functionのデプロイは遅くなる」と考えても差し支え無さそうです。
Lambda Function実行の総所要時間比較
No | レイヤー有り(単位:ミリ秒) | Functionのみ(単位:ミリ秒) |
---|---|---|
1 | 616 | 1740 |
2 | 457 | 1723 |
3 | 418 | 1729 |
4 | 1041 | 1856 |
5 | 454 | 1736 |
6 | 450 | 1698 |
7 | 431 | 1708 |
8 | 541 | 1747 |
9 | 380 | 1704 |
10 | 443 | 1837 |
平均 | 523.1 | 1747.8 |
レイヤーを使うと平均1.2秒程度早くなるという結果が出ました。 コード実行にかかる時間は1ms程度と無視できるレベルなので、「Lambda Layersを使うとコールドスタートが高速化する」と考えられそうです。
まとめ
Lambdaが裏でどういう動作をしているのかは不明ですが、今回計測したパターンだと
- Lambda Layersを使うとLambda Functionのデプロイは遅くなる
- Lambda Layersを使うとコールドスタートは高速化する
という結果になりました。
前回の記事で書きましたが、Lambda Function作成時に、Lambdaのサービス基盤の裏側でdocker build
相当の処理が動いてコード本体とレイヤーをマージしていると予想しています。
この仮説に基づくと、レイヤーを使うことでコード本体とレイヤーをマージする処理が追加で発生し、Lambda Function作成の所要時間が長くなるというのは説明がつきますが、なぜコールドスタートが高速化されたのかは不明です。。。
今後も色々なパターンを試しながら色々考察を進めていきたいと思います。 誰かの参考になれば幸いです。