ども。
今年は夏の暑さが長引いてるなーと思ってましたが、気づくと朝起きれば風邪をひいてる寒い季節になりました。
寝相が悪すぎる私は、掛布団があらぬところに行って冬の間は毎日風邪ひいてます。
寒くなってきたといえば、firebaes functionsのcold startはユーザー体験にすごく影響しますよね!
つなぎが少し無理やりな感じありましたが、本日はfirebaseのcloud functionのコールドスタートについて、実験をしましたのでそのご報告です。
firebase functionのコールドスタート問題に悩まされる
firebase functionでは、しばらく使われてない関数は、立ち上がっていたはずのインスタンスは削除されてしまい、再度関数の呼び出しがあったときにインスタンスを立て始めるので、処理時間がかなりかかってしまいます。これをコールドスタートと呼んでいます。
このコールドスタートについて、有識者の方が対応策などを発信してくださってますので、参考にして、私のcloud functionにも適用してどれほど性能が改善されるかをまとめました。
下記に参考にさせていただきましたサイトを記載させていただきます
Cloud Functionsのパフォーマンス問題にどう立ち向かうのか
計測する方法と環境
時間計測をする関数をフロントエンドに仕込む
タイムの計測方法は前回のブログと同じく、コード中に下記で時間を出力します。
console.time("addLike Execution Time"); // 処理時間の測定開始
console.timeEnd("addLike Execution Time"); // 処理時間の測定完了
【firebase】ボタンを押してからの「1秒」の処理とひたすら戦い続けて「0.3秒」にした【firestore , cloud functions】
前回はcloud functionの関数内に入れていましたが、今回はコールドスタート問題の立ち上がり含めて時間を図りたいので、フロントエンド側で出力します。
こうすることで、
- フロントエンドからcloud functionの関数を呼んだ
- cloud funcsionの関数の処理が終わって値が返された
の間の時間がはかれますので、コールドスタートの影響をダイレクト反映させた時間が取得できます。
const getFuncCallable = httpsCallable(functions, "getFunc");
try {
console.time("call cloud function"); //ここ
const result = await getFuncCallable ();
console.timeEnd("call cloud function"); //ここ
firebaseにデプロイした状態で計測(本番環境)
本番で実用しうる時間になるかを気にして計測したいので、検証の時間計測などは、firebaseにデプロイした環境で行います。ローカルではないです。
フロントエンドのほうにconsole.timeを仕込んでますので、ブラウザの開発者ツールで時間が確認できますので、勝手は良いです。
計測観点
サイトを参考に下記観点を計測することとしました。
- 使用していない依存モジュールを排除する
- 依存モジュールをアップデートする
- 割り当てメモリを増やしてみる
比較観点
コールドスタートは、インスタンスが削除された後に実行すると発生するものですので、連続で計測しているとコールドスタートは発生しません。
ですので、各設定で一度はコールドスタートの時間は取得しますが、回数が少なく信頼性が低いため、連続で関数を叩いた時、つまり、コールドスタートではない時にかかった時間を複数回とり、その平均同士でも比較をします。
1.使用していない依存モジュールを排除する
npm の depcheckで現在のディレクトリの使用していない依存関係を出力してくれます。
出力された依存ライブラリは使用してないことになるので、削除していきます。
ただし、これが絶対に正解とは限らないので、削除した後にきちんとアプリが動くかどうかは確認するようにお願いします
C:\tool\functions>npm install -g depcheck // インストール
C:\tool\functions>depcheck // 実行
Unused dependencies
* dotenv
* firebase
* ikoi-react
Unused devDependencies
* @types/dotenv
* firebase-functions-test
Missing dependencies
* @types/node: .\tsconfig.json
C:\tool\functions>npm remove dotenv //unsuedの依存を削除していきます
2.依存モジュールをアップデートする
npm outdated で現在のディレクトリのモージュールのバージョンと、現在の最新のバージョンを教えてくれるみたいです
C:\tool\functions>npm outdated
Package Current Wanted Latest Location Depended by
@types/dotenv 8.2.0 8.2.3 8.2.3 node_modules/@types/dotenv functions
@typescript-eslint/eslint-plugin 5.62.0 5.62.0 8.13.0 node_modules/@typescript-eslint/eslint-plugin functions
@typescript-eslint/parser 5.62.0 5.62.0 8.13.0 node_modules/@typescript-eslint/parser functions
eslint 8.57.1 8.57.1 9.14.0 node_modules/eslint functions
firebase-admin 12.6.0 12.7.0 12.7.0 node_modules/firebase-admin functions
firebase-functions 5.1.1 5.1.1 6.1.0 node_modules/firebase-functions functions
typescript 4.9.5 4.9.5 5.6.3 node_modules/typescript functions
C:\tool\functions>npm outdated
また、npm-check-updates というツールですべてのパッケージバージョンを最新にアップデートできるようです。
ただし、メジャーバージョンアップには特に、破壊的変更などが含まれてますのでバージョンアップによりアプリが動かなくなる可能性がありますので、よく調査をしてから対応をお願いします。
npm install -g npm-check-updates
C:\tool\functions>ncu
Checking C:\tool\functions\package.json
[====================] 10/10 100%
@typescript-eslint/eslint-plugin ^5.12.0 → ^8.13.0
@typescript-eslint/parser ^5.12.0 → ^8.13.0
eslint ^8.9.0 → ^9.14.0
eslint-plugin-import ^2.25.4 → ^2.31.0
firebase-admin ^12.4.0 → ^12.7.0
firebase-functions ^5.0.0 → ^6.1.0
typescript ^4.9.0 → ^5.6.3
Run ncu -u to upgrade package.json
C:\tool\functions>ncu -u
Upgrading C:\tool\functions\package.json
[====================] 10/10 100%
@typescript-eslint/eslint-plugin ^5.12.0 → ^8.13.0
@typescript-eslint/parser ^5.12.0 → ^8.13.0
eslint ^8.9.0 → ^9.14.0
eslint-plugin-import ^2.25.4 → ^2.31.0
firebase-admin ^12.4.0 → ^12.7.0
firebase-functions ^5.0.0 → ^6.1.0
typescript ^4.9.0 → ^5.6.3
Run npm install to install new versions.
ろくに調べずにすべてアップデートすると、ビルドエラーが大量に出るようになりました。
よく考えると、devDependenciesは動作に影響がありませんので、今回は、下記だけでした。。。
ほとんど効果は得られなさそうですね
firebase-admin ^12.4.0 → ^12.7.0
3.割り当てメモリを増やしてみる
これが効果覿面でしょうね。。。コールドスタートには本当に有効のようです。
前回は依存モジュールを集めてきてインスタンスが立ち上がった後の計測となったのであまり処理時間との因果関係はありませんでしたが、今回は立ち上げ時間込みの計測なので期待です。
一応、メモリの変更方法を下記に記しておきます。
// 共通のリソース制限設定
const apiLimitSetting = {
maxInstances: 3, // 最大インスタンス数の制限
timeoutSeconds: 30, // タイムアウトの制限
memory: "128MB" as const, // メモリの制限
};
// 各関数
export const getPictures = functions
.region("asia-northeast1")
.runWith(apiLimitSetting)
.https.onCall(async (data, context) => {
// 処理!!!
検証結果
コールドスタート時の呼び出し~値返却の時間
128MB | 不要依存削除 | モジュール更新 | 256MBへ | 512MBへ | 1GBへ |
12,975.00 | 9,832.33 | 10,727.00 | 5,444.00 | 3,139.00 | 1,856.00 |
コールドスタート以外の呼び出し~値返却の時間(それぞれ20 ~ 30回の平均値)
128MB | 不要依存削除 | モジュール更新 | 256MBへ | 512MBへ | 1GBへ |
1,372.06 | 1,279.09 | 1,332.85 | 958.79 | 810.39 | 758.12 |
メモリを128MB => 256MBに変更するだけで、コールドスタート時の処理時間が半分未満となりました!!
使用量を128MB => 1GBに変更すると、1/10程度の時間になりました!!
基本的に使用量は 処理時間×リソース(GB , GHz)ですので、これはメモリのサイズは1024 MB / 128MB = 8倍、処理時間は 1856 ms / 12975 ms = 0.15倍なので、コールドスタートについては、メモリと処理時間はトレードオフといった形で値段もさほど変わらなさそうですね。むしろ、値段変わらずで処理が早くなるのでメリットしかないですね。
ただし、コールドスタート以外の時は、メモリを8倍の1024MBにしても、処理時間は1/2なので、単純計算で、金額は4倍になってしまいそうです。
定期的にバッチで関数を呼び出すことに常にホットスタンバイ状態にしておくと256MBでもいい気がしますが、そちらの対応にも最小インスタンス数を指定するのに追加で課金がいるかもという情報がありますので、バランスをとって512MBで設定してみます。
結論
firebase functionsは初めて触って動かしたときのコールドスタートを見て「これはAPIの活用としてはかなり難しいぞ、、、」と思っていましたが、メモリのチューニングだけでもかなり改善できることがわかりました。
ただし、firebase functionsの無料枠は個人開発には相当太っ腹ですので、初期段階であれば、ある程度メモリを増やす対応だけで済ましてしまうのも金額を考慮しても有効な手段だと思います。
コールドスタートに悩んでる方は、是非お試しいただけますと幸いです。
参考(再掲)
Cloud Functionsのパフォーマンス問題にどう立ち向かうのか
【firebase】ボタンを押してからの「1秒」の処理とひたすら戦い続けて「0.3秒」にした【firestore , cloud functions】