エムオーテックス株式会社が運営するテックブログです。

AWS Lambda SnapStart パフォーマンス改善に向けた取り組み

AWS Lambda  SnapStart にパフォーマンス改善 向けた取り組み

はじめに

こんにちは、アプリケーションチームの古山です。

LANSCOPE エンドポイントマネージャー クラウド版では、AWS Lambdaを用いたサーバーレスアーキテクチャをベースとして製品開発をしており、言語はScalaを採用しています。そのため、JVMでの実行となり、コールドスタートによる処理遅延の影響が大きく、SnapStartなどを使った対策を施しています。今回の記事では、そのSnapStartの導入にあたって効果を発揮させるために行った取り組みをご紹介します。

SnapStartとは

SnapStartとは、初期化フェーズを終えた実行環境のスナップショットを利用して、Lambdaのコールドスタートを短縮する機能です。Lambda関数は、一度実行された後は一定期間環境が保持されるために二度目以降の実行時は高速な応答を行います(ウォームスタート)。一方で、保持期間を超過してから実行されると環境の構築から行うことになり処理遅延が発生します(コールドスタート)。SnapStartでは、実行環境の構築をスナップショットのリストアに置き換えることで、コールドスタートの処理遅延の軽減を実現します。特に、JavaベースのLambdaではコールドスタートでJVMの初期化に時間がかかるためSnapStartが効果的です。

SnapStart実行フロー

SnapStart導入で見つかった課題

SnapStartは関数の初期化フェーズを事前実行してスナップショットを作成しますが、ハンドラー内部の処理は事前に実行されません。これにより、データベース接続や外部ライブラリとの接続などがキャッシュされずSnapStartの効果が減少します。そのため、単純なSnapStartの導入では初期化フェーズの実行時間の短縮はできましたが、実行時間に大きな変化はなく、コールドスタートの処理時間を1割程度短縮することが限度でした。

class App{
    val a: String = "a"   // ここは初期化フェーズで定義される。

    def handle(){   // エントリーポイント。ここは初期化フェーズで存在を定義されるが、実行はされない。
   // Lambdaでの処理。データベース操作など。
    }
}

2つの課題解消アプローチ

上記のように、既存のLambda関数にSnapStartの設定追加を行うだけでは期待通りの効果を得ることができているとは言い難い状況です。そこで、SnapStartをより効果的に使うために、Lambda関数に2つのアプローチを取り組みました。

暖機運転

まず、暖機運転を導入しました。暖機運転では、初期化フェーズの中にハンドラー内部の処理を組み込むことで、ハンドラー内部の処理を実行済みの状態のスナップショットを作成しておくという考え方になります。

SnapStart+暖機運転実行フロー

class App {
    def handle(){   // エントリーポイント
    }

    def warmUp(){   // 暖機運転用の関数。handleを動かす。
        handle()
    }

    warmUp()    // こちらで呼ぶことで、初期化フェーズで処理される。
}

この暖機運転は非常に大きな効果があり、処理時間を8割以上短縮することに成功しました。ただし、スナップショット取得のためにハンドラー内部の処理を実行する必要がある特性上、仮にデータベース操作の処理をハンドラーで行わせている場合、データベース上に不要なデータの読み書きが行われることになります。お客様データが存在するデータベースにノイズとなるようなデータを配置することを避けるため、データベース操作直前までの処理のみ暖機運転を行わせた結果、おおよそ半分の処理時間短縮にとどまりました。

Lambda関数の初期化における遅延評価

Lambdaの初期化フェーズでは、拡張機能の初期化、ランタイムの初期化、関数の初期化の3つの初期化が行われます。関数の初期化では静的コードの実行を行いますが、LANSCOPE エンドポイントマネージャー クラウド版では、静的コードの多くで遅延評価を実施していました。その結果、静的コードで遅延評価された変数やオブジェクトは、実際にハンドラー内部の処理が動作した時に初めて評価されることになり、スナップショットが作成されたタイミングでは評価されていません。つまり、ハンドラー処理中に静的コードの評価が発生するため、SnapStartの効果が減少してしまいます。

SnapStartにおける遅延評価について

class App{
    lazy val a: String = "a"   // ここは初期化フェーズで定義されない。aが呼ばれたタイミングで評価。

    val b: String = "b" // ここは初期化フェーズで定義される。

    def method() = ??? // ここは初期化フェーズで存在を定義されるが、実行はされない。

    def handle(){   // エントリーポイント
        method(a)   // ここで初めてaが使われ、評価される。
    }
}

上記の対策として、既存コードの不要な遅延評価を削除したところ、暖機運転ほど大きな効果は見られずとも、処理時間をおおよそ半分に短縮することができました。

採用手法とその効果

SnapStartの効果が小さいことに対し、2つのアプローチからの解決を試みました。効果と開発・運用コストのバランスから以下の二つを採用しました。

  • データベース操作までの処理のみの暖機運転
  • 静的コード内の不要な遅延評価を削除

これらの対策により、最終的に処理時間を7割程度短縮することに成功しました。 以下は、今回の調査での各対策の処理時間まとめになります。

性能検証 処理時間 初期起動時間 (コールドスタート時間)
ウォームスタート 475.93ms なし
コールドスタート 6411.94ms 1012.08ms
SnapStart 6342.52ms 357.30ms
SnapStart + 暖機運転 813.58ms 410.54ms
SnapStart + 暖機運転(データベース操作直前まで) 2505.01ms 333.93ms
SnapStart + 遅延評価削除 2829.23ms 431.65ms
SnapStart + 遅延評価削除 + 暖機運転(データベース操作直前まで) 1812.33ms 470.77ms

製品環境におけるLambdaの処理時間軽減効果の一例

参考

aws.amazon.com

docs.aws.amazon.com

おわりに

今回の記事では、SnapStartの効果増幅手法とその裏側についてご紹介しました。 SnapStartは、導入コストの低いコールドスタート対策として知られています。単純に導入するだけでは効果が低い場合もありますが、SnapStartの仕組みを知ることで、今回のように効果を引き上げることができます。 臨機応変に使い、ユーザー体験の向上に繋げていきたいと思います。

ここまでお読みいただきありがとうございました。 本内容がお役に立つことがあれば幸いです。