アプリケーションの高速なパフォーマンスは、優れたユーザーエクスペリエンスを提供するために不可欠です!この記事では、サーバーレスアプリケーションにおけるコールドスタートとハンドラーのパフォーマンスを最適化するための落とし穴とベストプラクティスについて見ていきます。
目次
はじめに
FaaS(Functions-as-a-Service)を介したサーバーレスデプロイメントパラダイムは、開発者がスケーラブルかつ費用対効果の高い方法でアプリケーションを簡単にデプロイできるようにします。しかし、この利便性と柔軟性には、留意すべき一連の複雑さが伴います。
以前の、長時間稼働するサーバーを使用するデプロイモデルでは、サーバーが稼働している限り、実行環境は常に利用可能でした。これにより、アプリケーションは着信するリクエストに即座に応答できました。
新しいサーバーレスパラダイムでは、開発者として、関数が利用可能になり、できるだけ早くリクエストに応答する方法を見つける必要があります。
サーバーレス関数のパフォーマンスにおける落とし穴
サーバーレス環境では、関数はゼロにスケールダウンできます。これにより、運用コストを最小限に抑えることができますが、技術的なコストも発生します。リクエストに応答できる関数のインスタンスがない場合、新しいインスタンスがインスタンス化される必要があります。これはコールドスタートと呼ばれます。
注: コールドスタートとは何か、そしてPrisma ORMを使用する際にそれらを可能な限り短く保つためにどのように取り組んできたかについては、最近の記事「How We Sped Up Serverless Cold Starts with Prisma by 9x」を参照してください。
遅いコールドスタートは、ユーザーにとって非常に悪いエクスペリエンスにつながり、最終的には製品の体験を低下させます。これが問題1です。
コールドスタートの問題に加えて、実際のハンドラー関数のパフォーマンスも非常に重要です。サーバーレスアプリケーションは通常、HTTP、イベントバス、キューなどのプロトコルを介して互いにやり取りする、多くの小さな独立した関数で構成されています。
個々の関数間のこの相互通信は、各リクエストで依存関係の連鎖を作成します。これらの関数のいずれかが非常に遅い場合、チェーンの残りの部分に影響します。このため、ハンドラーのパフォーマンスが問題2となります。
FaaSにおけるパフォーマンス最適化のベストプラクティス
Prismaでは、過去数か月間、サーバーレス環境について深く掘り下げ、Prismaがそれらの環境でどのように動作するかを最適化してきました。その過程で、ご自身のアプリケーションでパフォーマンスを可能な限り高く保つために採用できる多くのベストプラクティスを見つけました。
この記事の残りの部分では、私たちが見つけたベストプラクティスのいくつかを見ていきます。
関数をデータベースと同じリージョンにホストする
従来のリレーショナルデータベースにアクセスする必要があるアプリケーションや関数をホストする場合、そのデータベースへの接続を開始する必要があります。これには時間がかかり、レイテンシが発生します。実行するクエリについても同様です。
目標は、その時間とレイテンシを最小限に抑えることです。現時点での最善の方法は、アプリケーションまたは関数がデータベースサーバーと同じ地理的リージョンにデプロイされていることを確認することです。

リクエストがデータベースサーバーに到達するために移動する距離が短いほど、接続はより速く確立されます。これを行わないことによる悪影響は非常に大きくなる可能性があるため、サーバーレスアプリケーションをデプロイする際には非常に重要なことです。
そうしないと、以下の処理にかかる時間に影響を与える可能性があります。
- TLSハンドシェイクを完了する
- データベースとの接続を保護する
- クエリを実行する
これらの要因はすべてコールドスタート時に活性化されるため、Prismaを使用してデータベースを使用することがアプリケーションのコールドスタートに与える影響に寄与します。
これがコールドスタートに与える影響を調査している際に、恥ずかしながら、AWS Lambdaのサーバーレス関数がeu-central-1
にあり、RDS PostgreSQLインスタンスがus-east-1
にホストされている状態で、最初の数回のテストを実行していたことに気づきました。すぐにそれを修正したところ、「修正後」の測定結果は、接続の作成と、実行されるすべてのクエリの両方において、データベースのレイテンシに計り知れない影響があることを明確に示しています。

修正前

修正後
関数からできるだけ近くにないデータベースを使用すると、コールドスタートの時間が直接増加するだけでなく、ウォームリクエストの処理中に後でクエリが実行されるたびに同じコストが発生します。
ハンドラーの外で可能な限り多くのコードを実行する
以下のサーバーレス関数を検討してください
AWS Lambdaは、特定の状況において、関数の実行環境の初期起動時に仮想環境により多くのメモリとCPUを割り当てます。その後、ウォーム状態の関数の呼び出し中には、関数に利用可能なメモリとCPUは、実際に関数設定で構成された値であることが保証されます。そして、それは関数の外部よりも少なくなる可能性があります。
注: ご興味があれば、上記のリソース割り当ての違いを説明するいくつかの資料があります
この知識は、ハンドラーのスコープ外にコードを移動することで、関数のパフォーマンスを向上させるために使用できます。これにより、環境により多くのリソースが利用可能な間に、ハンドラーの外のコードが実行されることが保証されます。
例えば、サーバーレス関数で次のようなことを行っている場合があります
上記のハンドラー関数は、フィボナッチ数列の40番目の数を計算します。その計算が完了すると、関数はリクエストの処理を続行し、最終的に応答を返します。
ハンドラーの外に移動すると、環境により多くのリソースが利用可能な間にその計算が行われ、呼び出しごとに実行されるのではなく、一度だけ実行されるようになります。
更新されたコードは次のようになります
もう1つ覚えておくべきことは、AWS Lambdaがトップレベルのawaitをサポートしていることです。これにより、非同期コードをハンドラーの外で実行できます。
Prisma Clientの$connect
関数をハンドラーの外で明示的に実行すると、関数のパフォーマンスに良い影響があることがわかりました。
関数を可能な限りシンプルに保つ
サーバーレス関数は、非常に小さく、独立したコードの断片であることを意図しています。関数のJavaScriptと依存関係ツリーが大きく複雑であったり、多くのファイルにまたがっていたりすると、ランタイムがそれを読み込み、解釈するのに時間がかかることがわかります。
以下は、起動パフォーマンスを向上させるためにできることです
- 関数がそのタスクを実行するために実際に必要なコードのみを含める
- 不要なものを多くロードするライブラリやフレームワークを使用しない
ここでの一般的な考え方は、解釈すべきコードが少なく、依存関係ツリーが単純であるほど、リクエストの処理が速くなるということです。
必要以上の作業をしない
関数の呼び出しごとに再利用される可能性のある値の計算やコストのかかる操作は、ハンドラーのスコープ外で変数としてキャッシュする必要があります。そうすることで、関数が呼び出されるたびにそれらのコストのかかる操作を実行するのを避けることができます。
データベースに保存されている値で、設定可能なリダイレクトのようにあまり頻繁に変わらない値をフェッチする状況を考えてみましょう。
このコードは動作しますが、リダイレクトを見つけるためのクエリは関数が呼び出されるたびに実行されます。これは理想的ではありません。なぜなら、前回の呼び出しですでに発見した値を見つけるためにデータベースへのアクセスが必要となるからです。
これを記述するより良い方法は、まずハンドラーの外部でキャッシュされた値をチェックすることです。見つからなかった場合は、クエリを実行して結果を次回のために保存します。
これで、クエリは関数が最初に呼び出されたときにのみ実行されます。それ以降の呼び出しでは、キャッシュされた値が使用されます。
プロビジョンド同時実行
最後に考慮すべきことは、AWS Lambdaを使用している場合、ラムダをウォームアップするためにプロビジョンド同時実行を使用することです。
AWSのドキュメントによると
注: プロビジョンド同時実行は、リクエストされた数の実行環境を初期化し、関数の呼び出しに即座に応答できるように準備します。プロビジョンド同時実行の設定には、AWSアカウントに料金が発生することにご注意ください。
これにより、コールドスタートなしでリクエストに応答できる、指定された数の利用可能な実行環境を維持できます。
これは素晴らしいことのように聞こえますが、いくつか重要な点を覚えておく必要があります。
- プロビジョンド同時実行を使用すると追加費用がかかる
- アプリケーションがゼロにスケールダウンすることはない
これらの追加費用が特定のシナリオにとって価値があるとは限らないため、これらは重要な考慮事項です。この措置を採用する前に、それがアプリケーションにもたらす価値を検討し、追加費用が理にかなっているかどうかを考慮することをお勧めします。
結論
この記事では、Prisma ORMを使用してサーバーレス関数を構築・デプロイする開発者におすすめするベストプラクティスについていくつか見てきました。この記事で述べた改善点やベストプラクティスは、網羅的なリストではありません。
簡単にまとめると、以下をお勧めします。
- データベースをデプロイされた関数のできるだけ近くにホストする
- ハンドラーの外で可能な限り多くのコードを実行する
- 可能な限り再利用可能な値と計算結果をキャッシュする
- 関数を可能な限りシンプルに保つ
- 経済的なトレードオフを許容できる場合は、プロビジョンド同時実行の使用を検討する
お読みいただきありがとうございます。この情報がお役に立てれば幸いです!
次の投稿をお見逃しなく!
Prismaニュースレターに登録する