Snap Startだけでは速くならないJava＋AWS Lambda


## はじめに

Java＋AWS LambdaはSnap Startさえしておけばなんとかなる！って思ってた愚かな自分が、少しパフォーマンスチューニングしてみたお話です。

なお、今回検証に使ったソースは以下に格納しています。

https://github.com/SatoshiMoriyama/spring-lambda-warmup/tree/main

ソースのベースは、以下を利用させていただき、２つの改造をしております。

* DynamoDBへの接続
* Powertools for AWS Lambda (Java)のTracing適用

https://github.com/aws/serverless-java-container/tree/main/samples/spring/pet-store

## 何もしていない状態

DynamoDBの初期化などをハンドラ外に定義しているだけの状態で動作確認してみます。

### 初回起動時

トータル7.27秒でした。
起動に4.4秒、実際の処理は2.57秒くらいです。

初回だけとはいえ、流石に遅すぎです！

![スクリーンショット 2025-02-01 10.50.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/c0f1bb21-947a-8c5b-9037-a6e2b9bda367.png)

### ２回目以降

ちなみに２回目は29ミリ秒。さすがJavaさんです！

![スクリーンショット 2025-02-01 9.36.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/86dbe9f0-14a6-a9cd-24a1-41bd78dff234.png)

## Lambda SnapStart適用

次にLambda SnapStartを有効にしてみます。

Lambda SnapStartはコールドスタート時間を大幅に短縮するための機能で、初期化済み状態のスナップショットを作成・保存し、それを新しい実行環境のベースとして使用することで、起動時間を大幅に軽減する機能です。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/snapstart.html

デプロイの時間は少し長くなりましたが、初回起動時の性能はトータル7.27→3.73秒となり、半減しました。

起動時間が約80％程度まで削減されていることがわかります。
（`Init`->`Restore`になってる。）

![スクリーンショット 2025-02-01 10.47.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/4f0bab7d-f506-8df4-e414-7c569fe8eda2.png)

ただし、起動以降の処理時間は変化がなく、２回目以降のスピードが出ていません。

Snap Startを使えば、常に２回目以降のスピードが出るものとずっと思い込んでました。。

公式のページ通りですが、リストアにも803ミリ秒と結構時間かかってますね。

## Warm up（クラスローディング対応）

以下のベストプラクティスを参考に、初回起動時にクラスローディングされない部分を手動コールするようにしてみます。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/snapstart-best-practices.html

サンプルでは、実際にハンドラから呼び出しをしていますが、クラスローディングが主目的だと思うので、ハンドラのStaticイニシャライザから`PetsController`を手動でコールしてみました。

```java
public class StreamLambdaHandler implements RequestStreamHandler {
    private static final Logger logger = LoggerFactory.getLogger(StreamLambdaHandler.class);
    private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
    static {
        try {
            handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class);

registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*");
            });

            DynamoDbClient dynamoDbClient = DynamoDbClient.create();
            PetsController controller = new PetsController(dynamoDbClient);
            controller.listPets(Optional.of(1), null);

            logger.info("warm up！");
        } catch (ContainerInitializationException e) {
            e.printStackTrace();
            throw new RuntimeException("Could not initialize Spring Boot application", e);
        }
    }

```

これで初回起動時の性能がトータル約3.73→1.79秒となり、さらに半減しました！

![スクリーンショット 2025-02-01 10.41.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/62e06958-1cdc-683d-3485-21839a0337ea.png)

Javaは起動時には全てのクラスをローディング（メモリに格納し、実行可能状態にすること）するわけではなく、初回呼び出し時にローディングすることで起動時間やメモリの節約をしている模様です。

## AWS Lambda Power Tuning

最後にAWS Lambda Power Tuningを使って、Lambdaそのものの性能を上げてみます。

変更可能なのはメモリサイズだけですが、メモリサイズと共にCPU性能も上がる仕様になっています。

https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:451282441545:applications~aws-lambda-power-tuning

ここからは札束で殴る力技ですが、性能を上げると処理時間が減り、結果、コスト面も優位になったりするので、AWS Lambda Power Tuningを使って調整するのが良いです。

なお、ここまでの検証は`x86_64`の`512MB`でした。

### x86_64の検証結果

速度面で見ると、1024MBが性能面では最適。

![スクリーンショット 2025-02-01 10.16.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/6ab6366e-fcc0-3ec0-0162-1082aaffc9cc.png)

### arm64の検証結果

x86_64とほぼ同じ傾向が出るかなと思いきや、結構違いますね。

こちらは2048MBが良いみたい。

![スクリーンショット 2025-02-01 11.09.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/0adcb4f9-0225-ece3-2d04-a9a77bc26f4f.png)

### 比較

それぞれの結果を同時に表示してみます。

![スクリーンショット 2025-02-01 10.30.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/9f48aacb-fa09-fea9-b3f2-9cb17477f337.png)

コストを一旦無視すると、`arm64`の`2048MB`が良さげ！

`arm64`の`2048MB`で再測定すると、トータル約1.79->1.26秒となりました。

![スクリーンショット 2025-02-01 11.04.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/b4283d6a-9627-a359-eb6f-8e804ee1ac76.png)

なお、何も考えずメモリサイズを`10240MB`にしてみても、性能はあまり変わらないですね。

![スクリーンショット 2025-02-02 7.13.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3894042/252ee9b3-f261-b48d-52ab-c15af334c36a.png)


### まとめ

ここまでの対策で7.27→1.26秒まで短縮できました！

Snap StartはJavaを利用する上で必須の機能ですが、それだけでもダメなこともわかりました。クラスローディング等、Javaの言語知識を深める必要もありそうです！

そもそもSpringを使わないとか、Native化してしまうなどの本質的な対策もあるかもですが、今回はSpringで頑張ってみました！