【ふーん、”エッジ”じゃん】Cloudflare Workersが0ms Cold Startsを実現するカラクリ
はじめに
CDNやキャッシュ周りの知見を深めていく中で、個人的に最近はエッジ関連がめっっっっっっちゃくちゃ熱いと感じました。
と言いますのも、Cloudflare Workersをはじめとした様々なインフラサービスにおいて、いろんな「エッジで動作する系」のものが登場しています。
有名どころでいうとこんな感じになります。
- Cloudflare workers
- Vercel Edge Functions
- Supabase Edge Functions
- Netlify Edge Functions
- Deno Deploy
- Fastly Compute@Edge
- AWS Lambda@Edge
今回は、一度基礎に立ち返って"エッジ"とはなんぞや?というところから学び直しつつ、僕がとても気になっているCloudflare Workersの最大の特徴である0ms Cold Startsについて深掘りしていこうと思います。
そもそもエッジとは
まず初めにエッジについて理解する前に、エッジコンピューティングという概念について理解する必要があります。
エッジコンピューティングとは
遅延と帯域幅の消費を低減するために、演算処理を可能な限りデータのソースに近づけることに焦点を当てたネットワークの基本方針
要するに、本来データの配信元のサーバであるオリジンサーバで実行されるべきプロセスをエッジサーバなどのエンドユーザにより近いコンピュータに移すことでクライアントとサーバー間で発生する長距離通信の量が最小限に抑えられるというものです。
ではエッジコンピューティングという概念を知ったところで、エッジ、すなわちネットワークエッジに深掘っていきます。
ネットワークエッジとは、インターネットの場合、エンドユーザーのデバイスまたはそれを含むローカルネットワークがインターネットと通信する場所を指します。エッジでとても重要なことは、通信しあうデバイス同士の距離が長い場合(例えば、エンドユーザーと配信元のサーバやクラウドサーバなど)に、エンドユーザーにとって物理的に近くに存在していることです。
エッジで動くと何が嬉しい?
エッジで動くことで得られる一番のメリットはやはりパフォーマンスです。
エッジサーバー上で任意の処理を動作させるということは、アプリケーションサーバーに伝達する前にリクエストを処理することができます。
特にアプリケーションの物理サーバとエンドユーザのデバイスが通信する場合レイテンシーが発生します。
エッジでリクエストを扱い、エッジのキャッシュなどを利用して、そのままレスポンスを返すことができればそのレイテンシーを削減し高速にエンドユーザに必要なデータを届けることができます。
サーバレス環境のコールドスタート問題について
Lambda@Edgeのようなサーバーレス環境は、基本的に仮想コンテナが使用されています。
サーバーレス環境ではコードが実行されるたびにコンテナが自動的に起動するため、サーバーの管理やスケーリングについて心配する必要がなく、コスト削減や運用の簡素化に役立ちます。
しかし、サーバーレス環境ではコールドスタートと呼ばれる問題が発生することがあります。
コールドスタートは、サーバーレス環境で初めて関数を呼び出したときに、その関数を実行するために必要なコンテナが起動されるまでに時間がかかる問題です。
このため、初回の呼び出しは遅延が発生し、パフォーマンスが低下することがあります。
また、サーバーレス環境では、起動状態のコンテナを維持することができないため、一定時間アクセスがない場合にはシャットダウンされます。そのため、再度関数が呼び出された場合には、再びコールドスタートが発生する可能性があります。
コンテナを使用しないサーバレス環境
Cloudflare Workersではコンテナが使用されていません。
その代わりにWorkersのランタイムとしてJSのV8エンジンを使用しています。
さらにV8エンジンのisolateという機能を使い、実行環境の軽量化を実現しています。
isolateとは?
isolateは、Google Chromeチームが作成した技術で、JavascriptエンジンであるV8を強化するために使用されています。これは、非常に軽量なプログラムのコンテキストを作成するための技術です。
isolateは異なるJavaScript実行コンテキストを分離・管理するための仕組みであり、この仕組みにより、Cloudflare Workersは各リクエストに対して独立した実行環境を提供できます。
V8エンジンのドキュメントではisolateについて次のように説明されています。
IsolateはV8エンジンの分離されたインスタンスを表す。V8は完全に分離された状態を保持し、あるIsolateのオブジェクトはほかのIsolateから使われることはない。(V8エンジンの)組み込み元は、複数のIsolateを作成し、それらはマルチスレッドによって並列に利用できる。
またCloudflareのブログでも以下のように説明されています。
JavaScriptのランタイムのオーバヘッドは実行時の一度だけで済み、実質的にはいくらでも多数のスクリプトを個別のオーバーヘッドなしに実行可能だ。
自分のマシン上では、それぞれのIsolate空間はNodeのプロセスが起動するのに比べて百倍は速く起動できる。それ以上に重要なのは、そのプロセスよりも何ケタも少ないメモリしか消費しないということだ。
引用: https://developers.cloudflare.com/workers/learning/how-workers-works/
isolateは非常に速くインスタンスを起動することができ、メモリ消費量が少ないため、高速に処理を実行することができます。
ただしWorkers Runtimeの起動自体はマックス5msはかかるらしいです。
→ あれ0msじゃなくね......???
Cloudflare Workersの0ms Cold Startsを実現するカラクリ
HTTPSリクエストを送信する前に、クライアントはサーバーとのセキュアなチャンネルを確立する必要があります。これはTLS(Transport Layer Security)プロトコルでの「ハンドシェイク」と呼ばれるプロセスです。
クライアントは、ハンドシェイクの段階ででホスト名(例: cloudflare.com, example.com)を送信します。
これをSNI(Server Name Indication)と呼びます。
サーバー(今回で言うとCloudflare)はハンドシェイクを受信し、証明書を返送し、クライアントは暗号化された元のリクエストを送信できるようになります。
ホスト名がハンドシェイクの段階で送信されているため、CloudflareがTLSネゴシエーション*1中の最初のパケット「ClientHello」を受信すると、そのホスト名のWorkers Runtime起動させます。
(下記の図の1枚目を参照)
*1 TLSネゴシエーションとは
TLSネゴシエーションとは、Transport Layer Security (TLS) プロトコルを使用して、クライアントとサーバー間で安全な通信チャンネルを確立するためのプロセスです。TLSは、インターネット上でデータを暗号化して送受信するためのプロトコルで、安全な通信を実現するために広く使用されています。
TLSネゴシエーションでは、以下のステップが行われます。
- クライアントは、サーバーに対して「ClientHello」メッセージを送信します。このメッセージには、クライアントがサポートする暗号スイートやTLSバージョンなどの情報が含まれています。
- サーバーは、「ServerHello」メッセージをクライアントに送信します。このメッセージには、クライアントとサーバーの間で使用される暗号スイートやTLSバージョンなどが決定されています。
- サーバーは、自身の証明書(公開鍵を含む)をクライアントに送信します。
- クライアントは、この証明書を使用してサーバーの識別を検証し、通信の途中でデータが改ざんされていないことを確認します。
- 必要に応じて、クライアントはサーバーに自身の証明書を送信し、サーバーもこれを検証します(クライアント認証)。
- クライアントは、サーバーの公開鍵を使用して一時的な共有鍵(セッションキー)を暗号化し、サーバーに送信します。サーバーは、自身の秘密鍵でこれを復号化します。この時点で、クライアントとサーバーは両方ともセッションキーを持っており、これを使用して通信データを暗号化および復号化します。両者は、「Finished」メッセージを交換して、ネゴシエーションプロセスが終了したことを確認します。
TLSネゴシエーションが完了すると、クライアントとサーバーは安全な通信チャンネルを確立し、データのやり取りを行うことができます。TLSネゴシエーションは、インターネット上で安全な通信を実現するための重要なプロセスです。
引用: https://blog.cloudflare.com/eliminating-cold-starts-with-cloudflare-workers/
ハンドシェイクが完了すると、Workers Runtimeはウォーム状態でリクエストを受信する準備ができています。(めっちゃあったまってる状態)
Workers Runtimeの起動自体には5ミリ秒かかりますが、上記の画像のクライアントとCloudflare間の平均待ち時間がそもそもそれ以上であり、リクエストがCloudflareに到達する前のハンドシェイクの段階でWorkers Runtimeは起動されるためコールドスタートはゼロになります。
それゆえにWorkers Runtimeは、クライアントからのリクエストをCloudflare経由で受信した瞬間に処理の実行を開始することができます。
これは、0ms Cold Starts実現以前と比較するとわかりやすいかもしれません。
引用: https://blog.cloudflare.com/eliminating-cold-starts-with-cloudflare-workers/
0ms Cold Starts実現以前はCloudflareにリクエストが到達してから、Workers Runtimeの起動を行なっていました。
なので簡単に流れを説明すると、
- TLSの接続確立
- クライアントがリクエストを送信
- Cloudflareがリクエストを受け取る
- Workers Runtimeを起動(一定時間待機) ← ここが0ms Cold Startsにより削減
- Workers Runtimeで処理・レスポンス
といった感じで、Workers Runtimeの起動を待つ時間がありましたが、0ms Cold Startsの実現によりそれがなくなったと言うわけですね!
補足
現時点では、この0ms Cold Startsと言う機能はルートのホスト名(例:example.com)にデプロイされたWorkersでのみ利用可能であり、example.com/path/to/something
のような特定のパスにはまだ対応していません。
今後、特定のパスのプリロードできるようにする最適化も導入予定らしいです!
まとめ
まだまだその性質上、Cloudflare Workersは重い処理が絡むようなユースケースではなかなか採用されにくいかもしれません。しかし、逆に言えば簡易的なAPIやちょっとした処理などはめちゃくちゃ重宝されるかなと思います。
個人開発のような小規模開発のケースでは、個人的には開発からデプロイまでのスピード感とパフォーマンスの側面から、Denoflareを使用してDeno + Cloudflare Workers
が結構おすすめかなと思います!
参考情報
Discussion