Lambda Layersを使うとデプロイは遅くなり、コールドスタートは高速化する?!Lambda Layersを使って巨大なLambda Functionを分割した場合の挙動の変化

はじめに

サーバーレス開発部@大阪の岩田です。 前回のブログでLambda Functionに紐付けるレイヤーのサイズを増加させていった場合の挙動について考察しました。

巨大なレイヤーを紐付けた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作成の所要時間が長くなるというのは説明がつきますが、なぜコールドスタートが高速化されたのかは不明です。。。

今後も色々なパターンを試しながら色々考察を進めていきたいと思います。 誰かの参考になれば幸いです。