メインコンテンツまでスキップ

AWS LambdaのJavaは遅い?

· 約8分
moritalous
お知らせ

過去にQiitaに投稿した内容のアーカイブです。

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)処理内容
16200
2422
3217
4210
5315

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

検証2 Provisioned Concurrencyを有効化

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

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

回数レイテンシ(ms)処理内容
15500
2266
3274
4402
5304

初回が遅いままじゃないか。。 同時実行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;
}
}

結果

回数レイテンシ処理内容
1625msInitialization処理のみ
25600msS3 PUT(1回目)
3393msS3 PUT(2回目)
4401msS3 PUT(3回目)
5311msS3 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;
}
}

結果

回数レイテンシ処理内容
12400msInitialization処理 と S3Clientインスタンスの生成
22200msS3 PUT(1回目)
343msS3 PUT(2回目)
446msS3 PUT(3回目)
578msS3 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;
}
}

結果

回数レイテンシ処理内容
14000msInitialization処理 と staticメソッドによるS3 PUT(1回目)ダミーファイル
242msS3 PUT(2回目)
3125msS3 PUT(3回目)
442msS3 PUT(4回目)
544msS3 PUT(5回目)

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

検証6 検証5+Provisioned Concurrency

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

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

回数レイテンシ処理内容
180msInitialization処理
2370msS3 PUT(2回目)※Provisionedの際にstaticイニシャライザで1回実行済みのため
343msS3 PUT(3回目)
472msS3 PUT(4回目)
584msS3 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;
}
}
回数レイテンシ処理内容
1552msS3 PUT(2回目)※Provisionedの際にstaticイニシャライザで1回実行済みのため
2118msS3 PUT(3回目)
344msS3 PUT(4回目)
486msS3 PUT(5回目)
5146msS3 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/