はじめに
Java+AWS LambdaはSnap Startさえしておけばなんとかなる!って思ってた愚かな自分が、少しパフォーマンスチューニングしてみたお話です。
なお、今回検証に使ったソースは以下に格納しています。
ソースのベースは、以下を利用させていただき、2つの改造をしております。
- DynamoDBへの接続
- Powertools for AWS Lambda (Java)のTracing適用
何もしていない状態
DynamoDBの初期化などをハンドラ外に定義しているだけの状態で動作確認してみます。
初回起動時
トータル7.27秒でした。
起動に4.4秒、実際の処理は2.57秒くらいです。
初回だけとはいえ、流石に遅すぎです!
2回目以降
ちなみに2回目は29ミリ秒。さすがJavaさんです!
Lambda SnapStart適用
次にLambda SnapStartを有効にしてみます。
Lambda SnapStartはコールドスタート時間を大幅に短縮するための機能で、初期化済み状態のスナップショットを作成・保存し、それを新しい実行環境のベースとして使用することで、起動時間を大幅に軽減する機能です。
デプロイの時間は少し長くなりましたが、初回起動時の性能はトータル7.27→3.73秒となり、半減しました。
起動時間が約80%程度まで削減されていることがわかります。
(Init
->Restore
になってる。)
ただし、起動以降の処理時間は変化がなく、2回目以降のスピードが出ていません。
Snap Startを使えば、常に2回目以降のスピードが出るものとずっと思い込んでました。。
公式のページ通りですが、リストアにも803ミリ秒と結構時間かかってますね。
Warm up(クラスローディング対応)
以下のベストプラクティスを参考に、初回起動時にクラスローディングされない部分を手動コールするようにしてみます。
サンプルでは、実際にハンドラから呼び出しをしていますが、クラスローディングが主目的だと思うので、ハンドラのStaticイニシャライザからPetsController
を手動でコールしてみました。
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秒となり、さらに半減しました!
Javaは起動時には全てのクラスをローディング(メモリに格納し、実行可能状態にすること)するわけではなく、初回呼び出し時にローディングすることで起動時間やメモリの節約をしている模様です。
AWS Lambda Power Tuning
最後にAWS Lambda Power Tuningを使って、Lambdaそのものの性能を上げてみます。
変更可能なのはメモリサイズだけですが、メモリサイズと共にCPU性能も上がる仕様になっています。
ここからは札束で殴る力技ですが、性能を上げると処理時間が減り、結果、コスト面も優位になったりするので、AWS Lambda Power Tuningを使って調整するのが良いです。
なお、ここまでの検証はx86_64
の512MB
でした。
x86_64の検証結果
速度面で見ると、1024MBが性能面では最適。
arm64の検証結果
x86_64とほぼ同じ傾向が出るかなと思いきや、結構違いますね。
こちらは2048MBが良いみたい。
比較
それぞれの結果を同時に表示してみます。
コストを一旦無視すると、arm64
の2048MB
が良さげ!
arm64
の2048MB
で再測定すると、トータル約1.79->1.26秒となりました。
なお、何も考えずメモリサイズを10240MB
にしてみても、性能はあまり変わらないですね。
まとめ
ここまでの対策で7.27→1.26秒まで短縮できました!
Snap StartはJavaを利用する上で必須の機能ですが、それだけでもダメなこともわかりました。クラスローディング等、Javaの言語知識を深める必要もありそうです!
そもそもSpringを使わないとか、Native化してしまうなどの本質的な対策もあるかもですが、今回はSpringで頑張ってみました!