3歳の息子がきらきら星を歌っていたので、きらきら星変奏曲(モーツァルト)を弾いたら、「それは違う」と否定されてしまった@phonypianistです。
AWS re:Invent 2022でLambdaのSnapStartが発表されました。サーバーレス界隈に衝撃が走っていますw
ということで、本ブログのアドベントカレンダー12/8の記事として、SnapStartを紹介します。
Lambda SnapStartとは
LambdaのInitフェーズに時間がかかる問題、いわゆるコールドスタート問題を解決するための機能です。
Lambdaのライフサイクルは大まかには、「初期化 (Init)」「起動 (Invoke)」「シャットダウン (Shutdown)」の3つのフェーズに分かれています。
最初の呼び出し、もしくはしばらく時間が経過したりスケールアウトしたりする際に「初期化」が行われますが、この部分はプログラムを実行するための環境準備やライブラリ読み込みなど、通常重い処理が行われます。 とりわけ、JavaではJava VMの起動があり、PythonやNode.jsに比べると「初期化」フェーズに時間がかかります。
SnapStartでは、Lambdaが実行されたときの状態のスナップショットを取っておき、それを復元することで、「初期化」フェーズの高速化を行います。 これにより、JavaではJava VMの起動が省略され、圧倒的に速くLambdaを起動することができるようになります。
実際にどれくらい高速になるのか、起動に時間がかかるSpringアプリケーション(Spring Cloud Function)を使って試してみました。 また、スナップショットのサイズはLambdaのメモリサイズに依存すると思われるため、Lambdaのメモリサイズにも影響するか検証してみました。
検証の概要
Spring Cloud Functionで作ったエコー(送られてきた文字をそのまま返す)アプリケーションをLambda上で動かし、CloudShellからリクエストを送って性能を検証します。
コールドスタートを確実に発生させるために、アプリケーションではリクエストを受けたときに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並列でリクエストを送ります。
検証結果
Lambdaに割り当てたメモリサイズ毎に、SnapStartの無効/有効時のコールドスタートにかかった時間は、以下のようになりました。
SnapStart無効 | SnapStart有効 | |||||
---|---|---|---|---|---|---|
メモリサイズ | 最小 | 最大 | 平均 | 最小 | 最大 | 平均 |
512MB | 4278.45 | 5049.41 | 4737.08 | 195.01 | 416.18 | 285.83 |
1024MB | 4534.51 | 5200.47 | 4764.89 | 267.76 | 398.31 | 326.71 |
2048MB | 3721.23 | 4659.76 | 4161.39 | 203.59 | 813.66 | 321.07 |
4196MB | 3084.84 | 3607.29 | 3222.51 | 212.74 | 768.53 | 353.76 |
SnapStart無効 | SnapStart有効 | |||||
---|---|---|---|---|---|---|
メモリサイズ | p50 | p90 | p99 | p50 | p90 | p99 |
512MB | 4717.68 | 4881.76 | 5049.41 | 283.59 | 359.71 | 416.18 |
1024MB | 4745.57 | 4906.42 | 5200.47 | 321.88 | 373.49 | 398.31 |
2048MB | 4132.39 | 4345.26 | 4659.76 | 345.51 | 406.48 | 813.66 |
4196MB | 3213.91 | 3354.72 | 3607.29 | 354.87 | 424.72 | 768.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では、キャリア採用を行っています。少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com
- ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
- Elasticsearch等を使ったデータ収集/分析/可視化
- マイクロサービス、DevOps、最新のOSSやクラウドサービスを利用する開発プロジェクト
- 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長