AWS LambdaのJavaは遅い？

AWS Lambdaで動作するJavaは初回が遅いですが、速くする方法がないか調べました。
末尾にある参考サイトの内容にたどり着いて、実際に試してみたのでその記録です。

レイテンシ情報はX-Rayにて取得しました。

# テスト対象

S3にファイルをPUTするだけのものです

```java
S3Client s3 = S3Client.builder().region(Region.AP_NORTHEAST_1).build();
PutObjectResponse result = s3.putObject(
        PutObjectRequest.builder().bucket(ENV_BUCKET).key("filename.txt").build(),
        RequestBody.fromString("contents"));
```

# 検証１　普通に実行

まずは普通に試してみます。

<details><summary>ソース全体</summary><div>

```java
package helloworld;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;

public class TestTarget0429 implements RequestHandler<Object, Object> {

    public Object handleRequest(final Object input, final Context context) {
        String ENV_BUCKET = System.getenv("BUCKET");

        S3Client s3 = S3Client.builder().region(Region.AP_NORTHEAST_1).build();
        PutObjectResponse result = s3.putObject(
                PutObjectRequest.builder().bucket(ENV_BUCKET).key("filename.txt").build(),
                RequestBody.fromString("contents"));

        System.out.println(result);

        return null;
    }
}
```
</div></details>

| 回数 | レイテンシ(ms) | 処理内容 |
| --: | --: | --- |
| 1|6200|
| 2 | 422|
| 3 | 217|
| 4 | 210|
| 5 | 315|

1回目だけ遅い、いわゆるコールドスタートが遅い状態ですね。
S3に1ファイル作成するだけで6秒は遅いですよねぇ。

# 検証２　Provisioned Concurrencyを有効化

では昨年末に登場した`Provisioned Concurrency`を使うとどうでしょう。
https://aws.amazon.com/jp/blogs/news/new-provisioned-concurrency-for-lambda-functions/

ソースコードは検証１と同じものです。

| 回数 | レイテンシ(ms) | 処理内容 |
| --: | --: | --- |
| 1|5500|
| 2 | 266|
| 3 | 274|
| 4 | 402|
| 5 | 304|

**初回が遅いままじゃないか。。**
同時実行1をプロビジョンドしただけでも月$14.42かかるのに、あんまりじゃないか。。。

なので、以降はProvisioned Concurrencyを無効にして検証を続けます

# 検証３　処理の分離（Provisioned Concurrencyなし）

初回に遅い原因を探るため、Lambda初回起動時と2回目起動時で処理を分けてみました。

staticな`count`変数を作って、初回呼び出し時のみ速攻returnしてみます。

```java
        if (count == 1) {
            count++;
            return null;
        }
```


<details><summary>ソース全体</summary><div>

```java
package helloworld;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;

public class TestTarget0429 implements RequestHandler<Object, Object> {

    private static int count = 1;

    public Object handleRequest(final Object input, final Context context) {
        if (count == 1) {
            count++;
            return null;
        }
        
        String ENV_BUCKET = System.getenv("BUCKET");

        S3Client s3 = S3Client.builder().region(Region.AP_NORTHEAST_1).build();
        PutObjectResponse result = s3.putObject(
                PutObjectRequest.builder().bucket(ENV_BUCKET).key("filename.txt").build(),
                RequestBody.fromString("contents"));

        System.out.println(result);

        return null;
    }
}
```
</div></details>

## 結果

| 回数 | レイテンシ | 処理内容 |
| --: | --: | --- |
| 1 | 625ms | Initialization処理のみ |
| 2 | 5600ms | S3 PUT（1回目） |
| 3 | 393ms | S3 PUT（2回目） |
| 4 | 401ms | S3 PUT（3回目） |
| 5 | 311ms | S3 PUT（4回目） |

Initialization処理が遅いわけじゃないことがわかりました。
S3 PUT（初回）に時間がかかっているようです。

# 検証４　初期化処理をstaticにする（Provisioned Concurrencyなし）

S3Clientを作る部分をstatic化してみます。

```java
private static String ENV_BUCKET = System.getenv("BUCKET");
private static S3Client s3 = S3Client.builder().region(Region.AP_NORTHEAST_1).build();
```

<details><summary>ソース全体</summary><div>

```java
package helloworld;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;

public class TestTarget0429 implements RequestHandler<Object, Object> {

    private static int count = 1;

    private static String ENV_BUCKET = System.getenv("BUCKET");
    private static S3Client s3 = S3Client.builder().region(Region.AP_NORTHEAST_1).build();

    public Object handleRequest(final Object input, final Context context) {
        if (count == 1) {
            count++;
            return null;
        }

        PutObjectResponse result = s3.putObject(
                PutObjectRequest.builder().bucket(ENV_BUCKET).key("filename.txt").build(),
                RequestBody.fromString("contents"));

        System.out.println(result);

        return null;
    }
}
```
</div></details>

## 結果

| 回数 | レイテンシ | 処理内容 |
| --: | --: | --- |
| 1 | 2400ms | Initialization処理 と S3Clientインスタンスの生成 |
| 2 | 2200ms | S3 PUT（1回目） |
| 3 | 43ms | S3 PUT（2回目） |
| 4 | 46ms | S3 PUT（3回目） |
| 5 | 78ms | S3 PUT（4回目） |

お！少し1回目の処理時間がかかるようになって、2回目が少し早くなりましたね。
3回目以降も早くなってますがこれもなにか影響があるのでしょうか？

# 検証５　staticイニシャライザで1回やっちゃう（Provisioned Concurrencyなし）

staticで処理をすれば早くなることがわかりました。
一旦staticイニシャライザでダミーファイルを作成してみます。

```java
static{
    PutObjectResponse result = s3.putObject(
            PutObjectRequest.builder().bucket(ENV_BUCKET).key("dummy.txt").build(),
            RequestBody.fromString("contents"));

    System.out.println(result);
}
```

<details><summary>ソース全体</summary><div>

```java
package helloworld;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;

public class TestTarget0429 implements RequestHandler<Object, Object> {

    private static int count = 1;

    private static String ENV_BUCKET = System.getenv("BUCKET");
    private static S3Client s3 = S3Client.builder().region(Region.AP_NORTHEAST_1).build();

    static{
        PutObjectResponse result = s3.putObject(
                PutObjectRequest.builder().bucket(ENV_BUCKET).key("dummy.txt").build(),
                RequestBody.fromString("contents"));

        System.out.println(result);
    }

    public Object handleRequest(final Object input, final Context context) {
        if (count == 1) {
            count++;
            return null;
        }

        PutObjectResponse result = s3.putObject(
                PutObjectRequest.builder().bucket(ENV_BUCKET).key("filename.txt").build(),
                RequestBody.fromString("contents"));

        System.out.println(result);

        return null;
    }
}

```
</div></details>

## 結果

| 回数 | レイテンシ | 処理内容 |
| --: | --: | --- |
| 1 | 4000ms | Initialization処理 と staticメソッドによるS3 PUT（1回目）ダミーファイル |
| 2 | 42ms | S3 PUT（2回目） |
| 3 | 125ms | S3 PUT（3回目） |
| 4 | 42ms | S3 PUT（4回目） |
| 5 | 44ms | S3 PUT（5回目） |

めでたく2回目以降が速くなりましたよ～！

# 検証６　検証５＋Provisioned Concurrency

検証５で早くなったので、Provisioned Concurrencyも組み合わせたら、１回目から速くなるのか？！

ソースは検証５と同じものです。

| 回数 | レイテンシ | 処理内容 |
| --: | --: | --- |
| 1 | 80ms | Initialization処理 |
| 2 | 370ms | S3 PUT（2回目）※Provisionedの際にstaticイニシャライザで1回実行済みのため |
| 3 | 43ms | S3 PUT（3回目） |
| 4 | 72ms | S3 PUT（4回目） |
| 5 | 84ms | S3 PUT（5回目） |

**やりましたよ！**
期待してたのはこれです。

# 最終結果

最終形はこうなりました。

* staticメソッドでダミーファイル作成を一回やっちゃう
* Provisioned Concurrency有効

<details><summary>ソース全体</summary><div>

```java
package helloworld;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;

public class TestTarget0429 implements RequestHandler<Object, Object> {

    private static String ENV_BUCKET = System.getenv("BUCKET");
    private static S3Client s3 = S3Client.builder().region(Region.AP_NORTHEAST_1).build();

    static{
        PutObjectResponse result = s3.putObject(
                PutObjectRequest.builder().bucket(ENV_BUCKET).key("dummy.txt").build(),
                RequestBody.fromString("contents"));

        System.out.println(result);
    }

    public Object handleRequest(final Object input, final Context context) {
        PutObjectResponse result = s3.putObject(
                PutObjectRequest.builder().bucket(ENV_BUCKET).key("filename.txt").build(),
                RequestBody.fromString("contents"));

        System.out.println(result);

        return null;
    }
}
```
</div></details>

| 回数 | レイテンシ | 処理内容 |
| --: | --: | --- |
| 1 | 552ms | S3 PUT（2回目）※Provisionedの際にstaticイニシャライザで1回実行済みのため  |
| 2 | 118ms | S3 PUT（3回目） |
| 3 | 44ms | S3 PUT（4回目） |
| 4 | 86ms | S3 PUT（5回目） |
| 5 | 146ms | S3 PUT（6回目） |

めでたし、めでたし。

# 考察

どうも、Javaのクラスローダーは初めてクラスが呼ばれたタイミングでクラスを読み込むようで、クラスの初期ロードに時間がかかるらしいです。
なので、一回読み込んじゃって、クラスをロード済みにしてしまえば次から速いということのようです。

呼ばれたタイミングじゃなくて、はじめに全部クラスをロードしてくれたらいいんですが、そんなことはできないのですかねぇ。


参考サイトはこちらです。

クラスメソッドさんのブログ
https://dev.classmethod.jp/articles/report-best-practive-for-java-on-lambda/

re:Invent 2019でのセッション資料
https://d1.awsstatic.com/events/reinvent/2019/REPEAT_1_Best_practices_for_AWS_Lambda_and_Java_SVS403-R1.pdf
https://youtu.be/ddg1u5HLwg8

他に見つけたブログ
https://pattern-match.com/blog/2020/03/14/springboot2-and-aws-lambda-provisioned-concurrency/