WebSocket の使用

このページでは、Cloud Run で WebSocket などのストリーミング サービスを実行し、これらのサービス用のクライアントを作成するためのガイダンスとベスト プラクティスについて説明します。

WebSocket は、追加の構成を必要とせずに Cloud Run でサポートされています。ただし、WebSocket ストリームは HTTP リクエストであり、Cloud Run サービスに対して構成されたリクエスト タイムアウトが引き続き適用されるため、クライアントへの再接続の実装などで WebSocket を使用した際にこの設定が適切に機能することを確認する必要があります。

Cloud Run では、セッション アフィニティを使用できないため、組み込みの負荷分散により、WebSocket リクエストが別のコンテナ インスタンスに送信される可能性があります。この問題を解決するには、コンテナ インスタンス間でデータを同期する必要があります。

Cloud Load Balancing を使用している場合は、Cloud Run の WebSocket もサポートされます。

サンプルの WebSocket アプリケーションをデプロイする

Cloud Run に、WebSocket を使用して実装したサンプルのホワイトボード アプリケーションをデプロイします。

このサンプル アプリケーションを手動でデプロイするには、次の操作を行います。

  1. git コマンドライン ツールを使用して、Socket.IO リポジトリのクローンをローカルに作成します。

    git clone https://github.com/socketio/socket.io.git
    
  2. サンプル アプリケーションのディレクトリに移動します。

    cd socket.io/examples/whiteboard/
    
  3. gcloud コマンドライン ツールを使用して、ソースコードからアプリケーションをビルドし、新しい Cloud Run サービスをデプロイします。

    gcloud beta run deploy whiteboard --allow-unauthenticated --source=.
    
  4. サービスがデプロイされた後、2 つのブラウザタブを開き、サービスの URL に移動します。クライアントが WebSocket 経由で同じコンテナ インスタンスに接続されているため、1 つのタブに表示した内容が他のタブに伝播されます。

ベスト プラクティス

Cloud Run で WebSocket アプリケーションを作成する際に最も難しい点は、複数の Cloud Run コンテナ インスタンス間でデータを同期することです。コンテナ インスタンスの自動スケーリングとステートレスな特性、同時実行リクエスト タイムアウトの制限のため、実装が難しくなっています。

リクエスト タイムアウトとクライアントの再接続を処理する

WebSocket リクエストは、Cloud Run で長時間実行される HTTP リクエストとして扱われます。アプリケーション サーバーがタイムアウトを適用しない場合でも、リクエスト タイムアウトの影響を受けます(最大 60 分、デフォルトは 5 分)。

クライアントとの接続時間が Cloud Run サービスに構成されたタイムアウトより長くなると、リクエストがタイムアウトしたときにクライアントとの接続が切断されます。

Cloud Run に接続する WebSocket クライアントは、リクエストのタイムアウト時またはサーバーの切断時にサーバーへの再接続を処理する必要があります。この処理をブラウザベースのクライアントで行うには、reconnecting-websocket などのライブラリを使用します。SocketIO ライブラリを使用している場合は、接続解除イベントを処理します。

WebSocket の使用で発生する料金

WebSocket との接続がオープン状態の Cloud Run インスタンスはアクティブとみなされ、課金対象となります。

最大同時実行数の引き上げ

WebSocket アプリケーションは通常、多くの接続を同時に処理するように設計されています。Cloud Run は同時接続数をサポートしています(コンテナあたり 250 まで)。サービスが特定のリソースの負荷を処理できる場合は、コンテナの最大同時実行数をデフォルト値よりも大きくすることをおすすめします。

スティッキー セッション(セッション アフィニティ)について

WebSocket 接続はステートフルであるため、クライアントは接続が終了するまで Cloud Run 上の同じコンテナに接続された状態を保持します。これにより、単一の WebSocket 接続のコンテキスト内でセッションを持続できます。

ただし、Cloud Run は、コンテナ インスタンスを自動的にスケーリングし、使用可能なコンテナ インスタンス間でリクエストを負荷分散するため、セッションは持続されません。したがって、同じクライアントからの後続のリクエストは、コンテナ インスタンス間でランダムに負荷分散され、別のコンテナ インスタンスに対して WebSocket が開かれる可能性があります。

コンテナ インスタンス間の負荷分散のため、Cloud Run サービスに接続するクライアントが、データの調整や共有を行わない別のコンテナ インスタンスによって処理される可能性があります。これを軽減するには、次のセクションで説明するように、外部データ ストレージを使用して Cloud Run インスタンス間で状態を同期する必要があります。

コンテナ インスタンス間でのデータの同期

Cloud Run コンテナ インスタンスのステートレスな性質と自動スケーリングにより、Cloud Run サービスに接続するクライアントは、WebSocket 接続から同じデータを受信できない場合があります。

たとえば、WebSocket を使用してチャットルーム アプリケーションを構築し、最大同時実行の設定を 250 に設定した場合、250 を超えるユーザーがこのサービスに同時に接続すると、ユーザーが別のコンテナ インスタンスによって処理され、チャットルーム内で同じメッセージを表示できないことがあります。

Cloud Run コンテナ インスタンス間でデータを同期するには(たとえば、すべてのインスタンスでチャットルームに送信されたメッセージを受信するなど)、データベースやメッセージ キューなどの外部データ ストレージ システムが必要です。

Cloud SQL などの外部データベースを使用する場合は、データベースにメッセージを送信し、データベースから定期的にポーリングを実施できます。ただし、コンテナがリクエストを処理していない場合、Cloud Run インスタンスには CPU が割り当てられていないことに注意してください。サービスが主に WebSocket リクエストを処理している場合は、少なくとも 1 つのクライアントが接続されていることを条件としてコンテナに CPU が割り当てられます。

外部メッセージ キューは各コンテナ インスタンスのアドレスを解決してデータを push できないため、メッセージ キューを使用すると、Cloud Run コンテナ間のデータ同期をリアルタイムで行うことができます。アプリケーションは、メッセージ キューとの接続を確立して、メッセージ キューから新しいメッセージを pull する必要があります。

Google では、Redis Pub/SubMemorystore)や Firestore リアルタイム更新などの外部メッセージ キュー システムを使用することをおすすめします。これらのキューシステムにより、コンテナ インスタンスによって開始された接続を介して、すべてのインスタンスに更新情報を配信できます。

チャットルーム サービスの設計

WebSocket チャットルーム サービスのアーキテクチャ

Cloud Run で WebSocket ベースのリアルタイム チャット ルーム サービスを設計する場合は、クライアントが接続するすべてのコンテナ インスタンス間でデータを同期する必要があります。外部メッセージ キューを使用して pull ベースのサブスクリプションを使用すると、Cloud Run アプリケーション モデルと連携できます。

この目的のために、Memorystore から Redis インスタンスを作成することで Redis Pub/Sub メカニズムを使用できます。WebSocket 用の Socket.IO ライブラリを使用している場合は、ライブラリの redis アダプタを使用できます。

この Redis ベースのアーキテクチャでは、各 Cloud Run コンテナ インスタンスが、受信したチャット メッセージを含む Redis チャネルとの長時間接続を確立します(SUBSCRIBE コマンドを使用します)。コンテナ インスタンスがチャネルで新しいメッセージを受信すると、WebSocket を経由してリアルタイムでクライアントに送信できます。

同様に、クライアントが WebSocket を使用してチャットルームにメッセージを投稿すると、メッセージを受信したコンテナ インスタンスが PUBLISH コマンドを使用して Redis チャネルにメッセージをパブリッシュし、このチャネルに登録された他のコンテナ インスタンスに同じメッセージが送信されます。