Help us understand the problem. What is going on with this article?

AWS LambdaのJavaは遅い?

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

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

テスト対象

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

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"));

検証1 普通に実行

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

ソース全体
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;
    }
}

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

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

検証2 Provisioned Concurrencyを有効化

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

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

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

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

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

検証3 処理の分離(Provisioned Concurrencyなし)

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

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

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

ソース全体
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;
    }
}

結果

回数 レイテンシ 処理内容
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(初回)に時間がかかっているようです。

検証4 初期化処理をstaticにする(Provisioned Concurrencyなし)

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

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

ソース全体
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;
    }
}

結果

回数 レイテンシ 処理内容
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回目以降も早くなってますがこれもなにか影響があるのでしょうか?

検証5 staticイニシャライザで1回やっちゃう(Provisioned Concurrencyなし)

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

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

    System.out.println(result);
}

ソース全体
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;
    }
}

結果

回数 レイテンシ 処理内容
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回目以降が速くなりましたよ~!

検証6 検証5+Provisioned Concurrency

検証5で早くなったので、Provisioned Concurrencyも組み合わせたら、1回目から速くなるのか?!

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

回数 レイテンシ 処理内容
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有効

ソース全体
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;
    }
}

回数 レイテンシ 処理内容
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/

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした