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/