こんぺいと.dev

一人で開発していると誰もレビューしてくれないので、未来の自分のために残しておく

Lambdaのコールドスタート対策で試したこと

個人開発のAPIをLambdaで動かしていて、しばらく間が空いた後のリクエストだけ妙に遅い、という現象に気づいた。いわゆるコールドスタートで、対策として試したことをまとめておく。

そもそも何が起きているか

Lambdaはリクエストが来るたびに実行環境を使い回すが、しばらくアクセスが無いと実行環境が破棄される。次のリクエストが来たとき、ゼロから実行環境を立ち上げる(コールドスタート)ので、その分だけ余計に時間がかかる。個人開発は常時アクセスがあるわけではないので、この現象に遭遇しやすい。

ランタイムとパッケージサイズを見直した

一番効いたのはここ。依存パッケージを減らすほど、起動が速くなる。

  • 使っていないライブラリを棚卸しして削除
  • 巨大なSDK全体ではなく、必要なモジュールだけをインポートする形に変更(AWS SDK v3はモジュール分割されているので、v2から乗り換えて効果があった)
  • ビルド時にTree Shakingが効くように、importの書き方を整理

パッケージサイズが小さくなるほど、実行環境が読み込む量が減って起動が速くなる。

初期化処理をハンドラの外に出した

DB接続の確立やSDKクライアントの生成を、リクエストごとのハンドラ関数の中に書いてしまうと、毎回律儀にやり直してしまう。ハンドラの外(モジュールのトップレベル)に出すことで、実行環境が使い回されている間は再利用されるようになる。

// 悪い例: 毎回作り直す
export const handler = async (event) => {
  const client = new DynamoDBClient({});
  // ...
};

// 良い例: 実行環境が生きている間は使い回す
const client = new DynamoDBClient({});
export const handler = async (event) => {
  // ...
};

基本的なことだが、意外と見落としがちだった。

Provisioned Concurrencyは個人開発では見送った

常にウォームな実行環境を確保しておく機能もあるが、これはAlways Free枠の対象外で、確保している間ずっと課金される。アクセスが少ない個人開発では費用対効果が悪いと判断して、今回は使わなかった。本当にレイテンシがシビアな用途が出てきたら検討する。

メモリ設定を上げてみた

Lambdaのメモリ設定はCPU性能とも連動しているため、メモリを上げると起動・実行の両方が速くなることがある。デフォルトの128MBから256MBに上げただけで、体感できる差があった。料金は使用時間×メモリ量で決まるが、実行時間が短くなる分、思ったほど値上がりしなかった。

それでも許容できない遅延には

API GatewayとLambdaの間に、定期的にダミーのリクエストを送って実行環境を起こしておく「ウォームアップ」という手もあるが、これは常にコストとリクエスト数を消費する対処療法なので、今回は見送った。ユーザー数がまだ少ない個人開発の段階では、コールドスタートの数百ミリ秒より、根本のパッケージサイズと初期化処理の見直しのほうが費用対効果が良かった。

まとめ

  • パッケージサイズを削るのが最も効果的だった
  • 初期化処理はハンドラの外に出して使い回す
  • Provisioned Concurrencyは個人開発の規模ではコスパが悪い
  • メモリを上げると起動・実行の両方が速くなることがある

派手な対策より、地味な基本の積み重ねで十分改善した。まず疑うべきはパッケージサイズと初期化処理の位置、というのが今回の教訓。