■ 概要
AWS Lambdaの開発において、必ず出てくる初回起動が遅い問題、、、
NodejsやPythonなどのスクリプト言語のLambdaであれば対策は比較的簡単なのですが、Javaなどのコンパイル型の言語の場合は難易度が上がります
JavaのLambdaの初回起動にかかる時間を10秒→1秒に短縮することに成功したので、ここに備忘録を残します
■ 目次
- 前提条件
- 設計要素の洗い出し
- コールドスタート対策
- DBコネクション対策
- クラスローディング対策
■ 前提条件
・ランタイムはJava 11を使用
・以下のような構成とする
①API GatewayをトリガーにLambdaが起動
②RDS Proxyを介してAuroraに接続
③ClientにResponceを返す
■ 設計要素の洗い出し
時間がかかっている要因を洗い出していく
実行時間を計測
まずは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によって呼び出された場合は、通常実行時の処理を行う前にレスポンスを返すようにする
実行時間を計測
「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コネクションの維持
・クラスローディング対策
を行うことで解消することができました
他にもできることがあれば教えていただきたいです。
こういったチューニングを行う際には裏側の理解が必要なので、歯ごたえがあって楽しいです!