Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

LambdaのSnapStartをSpring Cloud Functionで検証してみた

3歳の息子がきらきら星を歌っていたので、きらきら星変奏曲モーツァルト)を弾いたら、「それは違う」と否定されてしまった@phonypianistです。

AWS re:Invent 2022でLambdaのSnapStartが発表されました。サーバーレス界隈に衝撃が走っていますw
ということで、本ブログのアドベントカレンダー12/8の記事として、SnapStartを紹介します。

Lambda SnapStartとは

LambdaのInitフェーズに時間がかかる問題、いわゆるコールドスタート問題を解決するための機能です。

aws.amazon.com

Lambdaのライフサイクルは大まかには、「初期化 (Init)」「起動 (Invoke)」「シャットダウン (Shutdown)」の3つのフェーズに分かれています。

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

最初の呼び出し、もしくはしばらく時間が経過したりスケールアウトしたりする際に「初期化」が行われますが、この部分はプログラムを実行するための環境準備やライブラリ読み込みなど、通常重い処理が行われます。 とりわけ、JavaではJava VMの起動があり、PythonやNode.jsに比べると「初期化」フェーズに時間がかかります。

SnapStartでは、Lambdaが実行されたときの状態のスナップショットを取っておき、それを復元することで、「初期化」フェーズの高速化を行います。 これにより、JavaではJava VMの起動が省略され、圧倒的に速くLambdaを起動することができるようになります。

実際にどれくらい高速になるのか、起動に時間がかかるSpringアプリケーション(Spring Cloud Function)を使って試してみました。 また、スナップショットのサイズはLambdaのメモリサイズに依存すると思われるため、Lambdaのメモリサイズにも影響するか検証してみました。

検証の概要

Spring Cloud Functionで作ったエコー(送られてきた文字をそのまま返す)アプリケーションをLambda上で動かし、CloudShellからリクエストを送って性能を検証します。

SnapStart検証構成

コールドスタートを確実に発生させるために、アプリケーションではリクエストを受けたときに1秒スリープさせます。

準備

プロジェクトの生成

Spring Cloud Functionのコードを用意します。 Spring Initializrで依存関係に「Spring Cloud Function」を追加してプロジェクトを生成します。今回はMavenプロジェクトにしました。 注意点として、LambdaはJava 11しか対応していないため、最新のSpring Boot 3は選択できません。Spring Boot 2とJava 11を選択してください。

pom.xmlの修正

生成したプロジェクトにはAWS用のライブラリがないため、pom.xmlにspring-cloud-function-adapter-awsを追加します。

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-function-adapter-aws</artifactId>
</dependency>

Spring Bootが生成するJARファイルはそのままではLambdaから呼び出せないため、maven-shade-pluginでJARファイルを生成するように設定します。

<build>
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <dependencies>
        <dependency>
          <groupId>org.springframework.boot.experimental</groupId>
          <artifactId>spring-boot-thin-layout</artifactId>
          <version>1.0.28.RELEASE</version>
        </dependency>
      </dependencies>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
          <version>3.0.0</version>
        </dependency>
      </dependencies>
      <configuration>
        <createDependencyReducedPom>false</createDependencyReducedPom>
        <shadedArtifactAttached>true</shadedArtifactAttached>
        <shadedClassifierName>aws</shadedClassifierName>
        <transformers>
          <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.handlers</resource>
          </transformer>
          <transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer">
            <resource>META-INF/spring.factories</resource>
          </transformer>
          <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
            <resource>META-INF/spring.schemas</resource>
          </transformer>
        </transformers>
      </configuration>
    </plugin>
  </plugins>
</build>

これにより、 mvn clean package を実行することで、Lambdaで設定可能な snapstartdemo-0.0.1-SNAPSHOT-aws.jar ファイルが生成できます。

Javaコードの実装

簡易的に実装します。 メインクラスに echo メソッドを実装し、1秒待つ処理を追加します。

@SpringBootApplication
public class SnapStartDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(LambdademoApplication.class, args);
    }

    @Bean
    public Function<Flux<String>, Flux<String>> echo() {
        return flux -> flux.doOnNext(this::sleep);
    }

    private void sleep(String text) {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // Do nothing.
        }
    }

}

serverless.yml

今回はServerless Frameworkでデプロイします。 512MB/1024MB/2048MB/4096MBの4種類のメモリサイズ×SnapStartのON/OFFの組み合わせで計8個のLambda関数を作成します。 アプリケーションはすべて同じとします。

SnapStartを有効にするために、resources.extensionsでSnapStartのApplyOnに PublishedVersions を指定します。

また、SnapStartはLambdaのバージョン指定での実行が必要です。Serverless FrameworkのデフォルトではAPI Gatewayから呼ばれるLambdaのバージョンは $LATEST になり、SnapStartのオプションを有効にしていてもSnapStartが機能しません。そのため、serverless-plugin-canary-deploymentsプラグインを用いて、API GatewayからLambdaを呼び出す際にLambdaバージョンが指定されるようにします。

service: snapstartdemo

provider:
  name: aws
  stage: dev
  region: ap-northeast-1
  runtime: java11
  timeout: 15
  logRetentionInDays: 7
  iamRoleStatements:
    - Effect: Allow
      Action:
        - codedeploy:*
      Resource:
        - '*'

package:
  # mvn clean packageで生成される、AWS用のjarファイルを指定する
  artifact: target/snapstartdemo-0.0.1-SNAPSHOT-aws.jar

functions:

  # これ以降は、SnapStart有効/無効の順番に、メモリサイズが異なるLambda関数を定義する
  # SnapStart有効/無効の設定は、下のresourcesに定義する

  SpringCloudFunction512MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 512
    events:
      - http:
          method: get
          path: /snapstart/512/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction512MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 512
    events:
      - http:
          method: get
          path: /snapstart/512/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction1024MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 1024
    events:
      - http:
          method: get
          path: /snapstart/1024/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction1024MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 1024
    events:
      - http:
          method: get
          path: /snapstart/1024/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction2048MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 2048
    events:
      - http:
          method: get
          path: /snapstart/2048/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction2048MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 2048
    events:
      - http:
          method: get
          path: /snapstart/2048/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction4096MBSnapStartEnabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 4096
    events:
      - http:
          method: get
          path: /snapstart/4096/enabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

  SpringCloudFunction4096MBSnapStartDisabled:
    handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
    memorySize: 4096
    events:
      - http:
          method: get
          path: /snapstart/4096/disabled
    deploymentSettings:
      type: AllAtOnce
      alias: Live

resources:
  - extensions:
      # SnapStartを有効にする設定を行う(デフォルトは無効)
      SpringCloudFunction512MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions
      SpringCloudFunction1024MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions
      SpringCloudFunction2048MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions
      SpringCloudFunction4096MBSnapStartEnabledLambdaFunction:
        Properties:
          SnapStart:
            ApplyOn: PublishedVersions

plugins:
  # Lambdaのバージョンを設定するためのプラグインを指定する
  - serverless-plugin-canary-deployments

デプロイ

deployコマンドでデプロイします。

serverless deploy

検証方法

クラスメソッドさんの以下の記事でまとまっていますので、こちらを参考にさせていただきました。 CloudShellから、API Gatewayに対して ab コマンドで100並列でリクエストを送ります。

dev.classmethod.jp

検証結果

Lambdaに割り当てたメモリサイズ毎に、SnapStartの無効/有効時のコールドスタートにかかった時間は、以下のようになりました。

メモリサイズ毎のコールドスタートにかかった時間(最小/最大/平均)
 SnapStart無効SnapStart有効
メモリサイズ最小最大平均最小最大平均
512MB4278.455049.414737.08195.01416.18285.83
1024MB4534.515200.474764.89267.76398.31326.71
2048MB3721.234659.764161.39203.59813.66321.07
4196MB3084.843607.293222.51212.74768.53353.76
メモリサイズ毎のコールドスタートにかかった時間(パーセンタイル)
 SnapStart無効SnapStart有効
メモリサイズp50p90p99p50p90p99
512MB4717.684881.765049.41283.59359.71416.18
1024MB4745.574906.425200.47321.88373.49398.31
2048MB4132.394345.264659.76345.51406.48813.66
4196MB3213.913354.723607.29354.87424.72768.53

90パーセンタイル値をグラフにすると、以下のようになりました。

割り当てメモリ毎のコールドスタート時間

どのメモリサイズでも、SnapStart有効の場合はSnapStart無効の場合に比べて約10倍も時間が短縮しています。

なお、Lambdaのメモリサイズを大きくすればするほど、SnapStartなしでの起動時間は短くなっていますが、SnapStart有効の場合はわずかに長くなっています。これはおそらく、スナップショットのサイズがメモリサイズに依存して大きくなっているものと思われますが、気にするほどではなさそうです。

まとめ

SnapStartを有効にすることで、JavaのLambdaコールドスタートの時間を大幅に短縮することができました。 今後はLambda関数をJavaで実装する、という選択もありかと思います。

ただ、X-Rayが使用できない、512MBより大きなエフェメラルストレージが使用できない等の制限があるため、注意が必要。 加えて、LambdaではJava 11(Amazon Corretto 11)しか選択できないのが残念なところ。 早くJava 17に対応してもらえないかなー。

では。

Acroquest Technologyでは、キャリア採用を行っています。
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com