1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【実践!AWS Lambda】Javaで書いたLambdaの初回起動が遅い問題を解決する

Last updated at Posted at 2021-08-26

■ 概要

AWS Lambdaの開発において、必ず出てくる初回起動が遅い問題、、、
NodejsやPythonなどのスクリプト言語のLambdaであれば対策は比較的簡単なのですが、Javaなどのコンパイル型の言語の場合は難易度が上がります
JavaのLambdaの初回起動にかかる時間を10秒→1秒に短縮することに成功したので、ここに備忘録を残します

■ 目次

  1. 前提条件
  2. 設計要素の洗い出し
  3. コールドスタート対策
  4. DBコネクション対策
  5. クラスローディング対策

■ 前提条件

・ランタイムはJava 11を使用
・以下のような構成とする
①API GatewayをトリガーにLambdaが起動
②RDS Proxyを介してAuroraに接続
③ClientにResponceを返す
構成図-ページ2のコピー.png

■ 設計要素の洗い出し

時間がかかっている要因を洗い出していく

実行時間を計測

まずはPostmanからAPIを叩いて初回起動に要する時間を計測

実行時間
1回目 10245ミリ秒
2回目 683ミリ秒
3回目 364ミリ秒
4回目 537ミリ秒
5回目 366ミリ秒

明らかに初回が遅いことが分かる

コールドスタート問題

Lambdaの実行時間を短縮したい場合にはまず、コールドスタートの回避を図る必要がある
コールドスタートを理解するために、どのような仕組みでLambdaが実行されているかを整理してみる
Lambda実行時、裏側では以下のような処理が走っている

➀ 実行環境の作成(コンテナ立ち上げ)
↓
➁ デプロイパッケージのロード
↓
➂ デプロイパッケージの展開
↓
➃ ランタイム起動・初期化
↓
➄ 関数/メソッドの実行
↓
10分~30分くらい(どのタイミングでコンテナが破棄されるかは決まっていない)
↓ 
➅ コンテナの破棄

この➀~➄の全てを実行するのが、いわゆるコールドスタート
コンテナが破棄される前に再度実行すると、➀~➃を省けるウォームスタートとなる
初回起動がおそいのは、コールドスタートになっている為である

DBのConnection問題

今回の設計上、コールドスタートを回避できたとしても、DBとのConnectionが切れていればConnection作成に時間がかかってしまう
DB処理をさせるLambdaの場合は、この罠にも引っかからないように設計する必要がある

クラスローディング問題

ここはかなりはまりがち、、、Javaのようなコンパイル型言語ならではの罠

JavaのLambdaの場合コンテナ起動後に、
JVM(Javaを実行する仮想マシン)がクラスローディングを行いながら処理が走る仕様となっている

起動と実行の2段構えで時間がかかることになる(JavaのLambdaが遅いといわれる原因)
ちなみにJVMも同じコンテナで使いまわされるので、2回目以降はクラスローディング済みの為早くなる
回避するには、コンテナ起動と同時にクラスローディングを済ませておく必要がある

設計要素まとめ

設計要素は以下のようになる
①コールドスタートの回避
②DBのConnection維持
③クラスローディング対策

ここから一つずつ解決していく

■ コールドスタート対策

コールドスタートを回避するには、主に以下の2つの手段がある

1. Amazon EventBridgeでスケジュール実行させる
2. Provisioned Concurrencyで事前プロビジョニングしておく

それぞれ一長一短あるので、簡単に解説

「Amazon EventBridge」

メリット
 ・コンテナを起動→実行まで定期実行できる
 ・Lambdaの実行の料金しかかからない
デメリット
 ・スケジュール実行の合間にコンテナが破棄される可能性が否定できない(五分間隔でも稀に破棄される)

「Provisioned Concurrency」

メリット
 ・設定が簡単
 ・コンテナ数をプロビジョニングできるため、大量のリクエストに対応できる
デメリット
 ・Lambdaの実行とは別に料金がかかる
 ・コンテナを事前に立てておくだけの機能の為、DBのCnnectionが維持できない

DBのConnectionを維持する必要があるので、今回はAmazon EventBridgeでスケジュール実行させる方法を採用

構築のPoint

・今回は複数のAPIを作成するため、一つずつEventBridgeの設定をしていくのは大変
 → 全APIをたたくLambdaを新規作成し、そのLambdaのみスケジュール実行させる
・毎回ソース内の全ての処理が実行されると困る(毎回DBのデータが書き換えられたりする)
 → スケジュール実行のLambdaによって呼び出された場合は、通常実行時の処理を行う前にレスポンスを返すようにする

構成図は以下のようになる
構成図-ページ2.png

実行時間を計測

「schedued-execution-lambda」をスケジュール実行させた状態で動作確認

実行時間
1回目 3625ミリ秒
2回目 473ミリ秒
3回目 633ミリ秒
4回目 376ミリ秒
5回目 574ミリ秒

5~7秒ほど短縮されていて、コールドスタートは回避できていそう
それでもまだ目に見えて初回起動に時間がかかっているので、DB接続とクラスローディング対策を進めていく

■ DBコネクション対策

スケジュール実行時にはDB処理を行っていない為、DBのConnectionが維持できていない
単純だが、スケジュール実行された場合の処理に、DB接続して閉じるだけの処理を追加する(現在はスケジュール実行によるHTTPリクエストの場合、通常の処理を行わずにすぐReturnさせている)

実行時間を計測

「schedued-execution-lambda」をスケジュール実行させた状態で動作確認

実行時間
1回目 1846ミリ秒
2回目 634ミリ秒
3回目 356ミリ秒
4回目 472ミリ秒
5回目 533ミリ秒

さらに2秒程度短縮されていることが確認できた
ただ、これでもまだ実用できるほどの短縮にはなっていない為、クラスローディング対策も行う

■ クラスローディング対策

時間がかかっている処理を特定

ミリ秒単位で計測したいので、java.lang.SystemクラスのcurrentTimeMillis()メソッドを使用し、処理時間をログに出力させていく

Staticイニシャライザを使用

Staticイニシャライザとは、
クラスのロード時に一度だけ実行されるstaticで宣言されたコードブロックのこと
つまりここで一度読み込んでクラスをロード済みにしてしまえば、実行時のクラスローディングにかかる時間を短縮できる
初回起動で時間がかかっている処理をここにガシガシ書いていく
ここにDB接続処理も書いておけば、「Provisioned Concurrency」も使えそう

実行時間を計測

「schedued-execution-lambda」をスケジュール実行させた状態で動作確認

実行時間
1回目 749ミリ秒
2回目 637ミリ秒
3回目 745ミリ秒
4回目 465ミリ秒
5回目 585ミリ秒

ついに1秒を切った、、、!

■ まとめ

JavaのLambdaの初回起動が遅い問題ですが、
コールドスタート対策
DBコネクションの維持
クラスローディング対策
を行うことで解消することができました
他にもできることがあれば教えていただきたいです。

こういったチューニングを行う際には裏側の理解が必要なので、歯ごたえがあって楽しいです!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?