個人開発の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は個人開発の規模ではコスパが悪い
- メモリを上げると起動・実行の両方が速くなることがある
派手な対策より、地味な基本の積み重ねで十分改善した。まず疑うべきはパッケージサイズと初期化処理の位置、というのが今回の教訓。