T-CREATOR

WebSocket を NGINX/HAProxy で終端する設定例:アップグレードヘッダーとタイムアウト完全ガイド

WebSocket を NGINX/HAProxy で終端する設定例:アップグレードヘッダーとタイムアウト完全ガイド

WebSocket は、クライアントとサーバー間でリアルタイム通信を実現する強力なプロトコルです。チャットアプリケーション、リアルタイムダッシュボード、オンラインゲームなど、双方向通信が必要な場面で広く使われています。しかし、本番環境で WebSocket を運用する際には、NGINX や HAProxy といったプロキシサーバーを介して通信を終端するケースが一般的です。

プロキシサーバーを介した WebSocket 通信では、「アップグレードヘッダー」の適切な設定と「タイムアウト」の調整が重要になります。これらの設定を誤ると、接続が確立できなかったり、予期せず切断されたりといった問題が発生してしまうでしょう。

本記事では、NGINX と HAProxy における WebSocket 終端の設定方法を基礎から実践まで丁寧に解説します。アップグレードヘッダーの役割、タイムアウトパラメータの意味、そして実際に使える設定例を通じて、安定した WebSocket 通信を実現する方法をお伝えしますね。

WebSocket とプロキシの基礎知識

WebSocket プロトコルの特徴

WebSocket は HTTP/1.1 をベースとして設計された通信プロトコルで、クライアントとサーバー間で永続的な双方向通信を可能にします。通常の HTTP 通信とは異なり、一度接続が確立されれば、サーバー側から能動的にデータを送信できる点が大きな特徴です。

WebSocket 通信は以下のような流れで確立されます。

mermaidsequenceDiagram
    participant Client as クライアント
    participant Proxy as プロキシ<br/>サーバー
    participant Backend as バックエンド<br/>サーバー

    Client->>Proxy: HTTP リクエスト<br/>Upgrade: websocket
    Proxy->>Backend: HTTP リクエスト転送<br/>Upgrade: websocket
    Backend->>Proxy: HTTP 101 Switching Protocols
    Proxy->>Client: HTTP 101 Switching Protocols

    Note over Client,Backend: WebSocket 接続確立

    Client->>Backend: WebSocket フレーム送信
    Backend->>Client: WebSocket フレーム送信

この図が示すように、WebSocket 通信は最初に通常の HTTP リクエストから始まります。クライアントは Upgrade: websocket ヘッダーを含むリクエストを送信し、サーバーが 101 Switching Protocols レスポンスを返すことで、プロトコルが HTTP から WebSocket へ切り替わるのです。

WebSocket プロトコルの主な特徴をまとめると以下のようになります。

#特徴説明
1双方向通信クライアント・サーバー双方から自由にデータ送信が可能
2低レイテンシ接続を維持するため、リクエストごとのオーバーヘッドがない
3プロトコルアップグレードHTTP から WebSocket へのプロトコル切り替えが必要
4フレームベース通信小さなフレーム単位でデータをやり取り

プロキシ終端が必要な理由

本番環境で WebSocket アプリケーションを運用する際、バックエンドサーバーを直接インターネットに公開することは推奨されません。代わりに、NGINX や HAProxy といったプロキシサーバーを前段に配置し、以下のような役割を担わせます。

セキュリティ層としての役割

プロキシサーバーは、外部からの直接アクセスを遮断し、TLS/SSL 終端を担当することで通信を暗号化します。DDoS 攻撃や不正なリクエストをフィルタリングする役割も果たすでしょう。

負荷分散とスケーラビリティ

複数のバックエンドサーバーへトラフィックを分散させることで、システム全体の可用性とパフォーマンスを向上させられます。特定のサーバーに負荷が集中するのを防ぎ、スムーズなスケールアウトを実現できますね。

以下の図は、プロキシサーバーを介した WebSocket 通信のアーキテクチャを示しています。

mermaidflowchart LR
    clients["クライアント群"] -->|WebSocket<br/>リクエスト| proxy["プロキシサーバー<br/>(NGINX/HAProxy)"]

    proxy -->|負荷分散| backend1["バックエンド<br/>サーバー 1"]
    proxy -->|負荷分散| backend2["バックエンド<br/>サーバー 2"]
    proxy -->|負荷分散| backend3["バックエンド<br/>サーバー 3"]

    subgraph security["セキュリティ層"]
        proxy
    end

    subgraph backend_pool["バックエンドプール"]
        backend1
        backend2
        backend3
    end

このアーキテクチャでは、プロキシサーバーが単一のエントリーポイントとなり、複数のバックエンドサーバーへ接続を振り分けます。クライアントはプロキシサーバーのアドレスのみを知っていればよく、バックエンドの構成変更にも柔軟に対応できるのです。

運用管理の一元化

アクセスログの集約、監視、トラフィック制御などを一箇所で管理できます。バックエンドサーバーの追加・削除もプロキシ設定の変更だけで対応可能です。

アップグレードヘッダーの役割

WebSocket 通信を確立するには、HTTP から WebSocket へのプロトコル切り替え、つまり「アップグレード」が必要になります。このプロトコル切り替えを実現するために、特定の HTTP ヘッダーが使用されるのです。

クライアントが WebSocket 接続を開始する際、以下のようなヘッダーを含む HTTP リクエストを送信します。

httpGET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

このリクエストの中で特に重要なのが UpgradeConnection ヘッダーです。

#ヘッダー名役割
1Upgradeプロトコルを websocket へ切り替えることを要求
2ConnectionUpgrade ヘッダーを適用することを指示
3Sec-WebSocket-Keyクライアントが生成するランダムな値(セキュリティ用)
4Sec-WebSocket-Version使用する WebSocket プロトコルのバージョン

サーバーがこのリクエストを受け入れる場合、以下のようなレスポンスを返します。

httpHTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

101 Switching Protocols というステータスコードが、プロトコル切り替えの成功を示しています。この時点で、HTTP 接続は WebSocket 接続へと変換され、以降は WebSocket フレームを使った双方向通信が行われるのです。

プロキシサーバーでの問題

ここで注意が必要なのは、多くのプロキシサーバーはデフォルトで通常の HTTP 通信を想定しており、Upgrade ヘッダーを適切に処理しないという点です。プロキシが Upgrade ヘッダーをバックエンドへ転送しなかったり、Connection ヘッダーを削除したりすると、プロトコル切り替えが失敗してしまいます。

したがって、NGINX や HAProxy で WebSocket を扱う際には、これらのヘッダーを正しく処理するための明示的な設定が必要になるのです。次のセクションでは、具体的な設定方法を見ていきましょう。

NGINX での WebSocket 終端設定

NGINX は高性能な Web サーバー兼リバースプロキシとして広く利用されており、WebSocket のプロキシ終端にも対応しています。ここでは、NGINX で WebSocket 接続を適切に処理するための設定方法を段階的に解説します。

基本設定:アップグレードヘッダーの構成

NGINX で WebSocket 通信を有効にするには、UpgradeConnection ヘッダーをバックエンドサーバーへ適切に転送する必要があります。まずは基本的なヘッダー設定から見ていきましょう。

map ディレクティブによる Connection ヘッダーの動的設定

NGINX の設定ファイルの http ブロック内に、以下の map ディレクティブを追加します。

nginx# http ブロック内に配置
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

この設定は、クライアントから送られてくる Upgrade ヘッダーの値に応じて、$connection_upgrade という変数の値を動的に変更します。Upgrade ヘッダーが存在する場合は upgrade を、存在しない場合は close をセットするのです。

この仕組みにより、WebSocket 接続とそうでない通常の HTTP 接続を同じプロキシ設定で扱えるようになります。

location ブロックでのヘッダー転送

次に、server ブロック内の location ディレクティブで、実際にヘッダーをバックエンドへ転送する設定を行います。

nginxlocation /ws {
    # バックエンドサーバーのアドレス
    proxy_pass http://backend_server:8080;

    # HTTP バージョンを 1.1 に設定(WebSocket に必要)
    proxy_http_version 1.1;

    # Upgrade ヘッダーをバックエンドへ転送
    proxy_set_header Upgrade $http_upgrade;

    # Connection ヘッダーを動的に設定
    proxy_set_header Connection $connection_upgrade;
}

この設定の各ディレクティブの役割を整理します。

#ディレクティブ役割
1proxy_passリクエストを転送するバックエンドサーバーのアドレス
2proxy_http_version 1.1HTTP/1.1 を使用(WebSocket は HTTP/1.1 が必須)
3proxy_set_header Upgradeクライアントの Upgrade ヘッダーをそのまま転送
4proxy_set_header Connectionmap で定義した変数を使って Connection ヘッダーを設定

proxy_http_version 1.1 は特に重要です。NGINX はデフォルトで HTTP/1.0 を使用しますが、WebSocket は HTTP/1.1 以降でしか動作しないため、この設定が必須となります。

追加の推奨ヘッダー設定

WebSocket 通信を安定させるために、以下のヘッダーも併せて設定することをお勧めします。

nginxlocation /ws {
    proxy_pass http://backend_server:8080;
    proxy_http_version 1.1;

    # WebSocket 用ヘッダー
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # クライアント情報をバックエンドへ伝達
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

これらの追加ヘッダーにより、バックエンドサーバーはクライアントの実際の IP アドレスやホスト名を把握できます。アクセスログの記録やセキュリティ判定に役立つでしょう。

タイムアウト設定の詳細

WebSocket は長時間接続を維持する性質があるため、NGINX のデフォルトのタイムアウト設定では接続が途中で切断されてしまう可能性があります。適切なタイムアウト設定を行うことで、安定した通信を実現しましょう。

主要なタイムアウトパラメータ

NGINX で WebSocket に関連する主なタイムアウトパラメータは以下の 3 つです。

nginxlocation /ws {
    proxy_pass http://backend_server:8080;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # バックエンドからの応答待ち時間
    proxy_read_timeout 3600s;

    # バックエンドへのリクエスト送信タイムアウト
    proxy_send_timeout 3600s;

    # バックエンドへの接続確立タイムアウト
    proxy_connect_timeout 60s;
}

各パラメータの意味と推奨値を詳しく見ていきます。

proxy_read_timeout(最重要)

このパラメータは、バックエンドサーバーから次のデータを受信するまでの最大待ち時間を指定します。WebSocket では、データが送信されない期間が長く続くことがあるため、この値を十分に大きく設定する必要があるのです。

nginx# デフォルト値: 60s
# WebSocket 推奨値: 3600s(1時間)以上
proxy_read_timeout 3600s;

この設定を行わないと、60 秒間データの送受信がない場合に NGINX が接続を切断してしまいます。チャットアプリのように、ユーザーが長時間メッセージを送らない可能性がある場合は、さらに長い値(例: 86400s = 24 時間)を設定することもあるでしょう。

proxy_send_timeout

バックエンドサーバーへのデータ送信が完了するまでのタイムアウトを設定します。proxy_read_timeout と同様に、長時間の接続を維持するために大きな値を設定します。

nginx# デフォルト値: 60s
# WebSocket 推奨値: proxy_read_timeout と同じ値
proxy_send_timeout 3600s;

通常、proxy_read_timeout と同じ値を設定することで、送信・受信の両方向で一貫したタイムアウト動作が実現できます。

proxy_connect_timeout

バックエンドサーバーへの初期接続を確立するまでのタイムアウトです。WebSocket の接続確立自体は通常数秒以内に完了するため、デフォルト値(60s)のままでも問題ないケースが多いでしょう。

nginx# デフォルト値: 60s
# WebSocket 推奨値: 60s(デフォルトのまま)
proxy_connect_timeout 60s;

ただし、バックエンドサーバーの応答が遅い場合や、ネットワークレイテンシが高い環境では、この値を調整する必要があるかもしれません。

タイムアウトの関係性

以下の図は、各タイムアウトパラメータがどのタイミングで適用されるかを示しています。

mermaidsequenceDiagram
    participant N as NGINX
    participant B as バックエンド

    Note over N,B: proxy_connect_timeout
    N->>B: 接続要求
    B->>N: 接続確立

    Note over N,B: proxy_send_timeout
    N->>B: リクエスト送信

    Note over N,B: proxy_read_timeout
    B->>N: レスポンス受信

    Note over N,B: WebSocket 通信開始<br/>proxy_read_timeout が継続的に適用
    N->>B: WebSocket フレーム
    B->>N: WebSocket フレーム
    N->>B: WebSocket フレーム
    B->>N: WebSocket フレーム

WebSocket 接続が確立された後は、proxy_read_timeout が最も重要なパラメータとなります。データの送受信がない期間でも接続を維持するために、十分に長い値を設定しましょう。

実践的な設定例

これまでに解説した内容を統合して、実際の運用で使える完全な NGINX 設定例を示します。ここでは、TLS/SSL 終端、複数バックエンドへの負荷分散、そして適切なタイムアウト設定を含む構成を紹介しますね。

アップストリーム定義

まず、バックエンドサーバーのプールを定義します。

nginx# http ブロック内に配置
upstream websocket_backend {
    # ラウンドロビンで負荷分散(デフォルト)
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;
    server 192.168.1.103:8080;

    # 接続の維持設定
    keepalive 32;
}

keepalive ディレクティブは、NGINX からバックエンドサーバーへの接続をキープアライブ状態で維持する数を指定します。これにより、接続の再利用が可能になり、パフォーマンスが向上するのです。

map ディレクティブの定義

nginx# http ブロック内に配置
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

完全な server ブロック設定

nginxserver {
    # HTTPS リスニング設定
    listen 443 ssl http2;
    server_name example.com;

    # SSL/TLS 証明書設定
    ssl_certificate /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # WebSocket エンドポイント
    location /ws {
        # アップストリームへプロキシ
        proxy_pass http://websocket_backend;

        # HTTP/1.1 を使用
        proxy_http_version 1.1;

        # WebSocket 用ヘッダー設定
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        # クライアント情報をバックエンドへ伝達
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # タイムアウト設定(1時間)
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_connect_timeout 60s;
    }

    # 通常の HTTP エンドポイント(参考)
    location / {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;

        # 通常の HTTP 用ヘッダー
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

この設定では、​/​ws パスへのアクセスが WebSocket として処理され、それ以外のパスは通常の HTTP として処理されます。

HTTP から HTTPS へのリダイレクト設定

セキュリティのため、HTTP アクセスを HTTPS へリダイレクトする設定も追加しましょう。

nginxserver {
    listen 80;
    server_name example.com;

    # すべてのリクエストを HTTPS へリダイレクト
    return 301 https://$server_name$request_uri;
}

設定ファイルの構成例

実際の運用では、設定を複数のファイルに分割すると管理しやすくなります。

nginx# /etc/nginx/nginx.conf(メイン設定ファイル)
http {
    # WebSocket 用の map 定義
    include /etc/nginx/conf.d/websocket-map.conf;

    # アップストリーム定義
    include /etc/nginx/conf.d/upstream.conf;

    # サーバー設定
    include /etc/nginx/sites-enabled/*.conf;
}
nginx# /etc/nginx/conf.d/websocket-map.conf
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}
nginx# /etc/nginx/conf.d/upstream.conf
upstream websocket_backend {
    server 192.168.1.101:8080;
    server 192.168.1.102:8080;
    server 192.168.1.103:8080;
    keepalive 32;
}
nginx# /etc/nginx/sites-enabled/websocket.conf
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/nginx/ssl/example.com.crt;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;

    location /ws {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

このように設定を分割することで、変更が必要な箇所を素早く特定でき、チームでの管理も容易になります。

設定の反映

設定ファイルを編集した後は、構文チェックを行ってから NGINX を再起動します。

bash# 設定ファイルの構文チェック
sudo nginx -t

# 構文に問題がなければ再起動
sudo systemctl reload nginx

nginx -t コマンドで構文エラーを事前に検出することで、サービスの停止を防げます。問題がなければ reload でグレースフルに設定を反映させましょう。

HAProxy での WebSocket 終端設定

HAProxy は高性能なロードバランサーとして知られ、TCP/HTTP レベルでの柔軟な負荷分散が可能です。WebSocket のプロキシ終端にも対応しており、NGINX とは異なるアプローチで設定を行います。ここでは HAProxy を使った WebSocket 終端の設定方法を解説しますね。

基本設定:アップグレードヘッダーの構成

HAProxy で WebSocket を扱う際の基本的な考え方は、HTTP リクエストを解析して WebSocket 接続かどうかを判断し、適切なバックエンドへルーティングすることです。

frontend セクションの設定

まず、クライアントからの接続を受け付ける frontend セクションを定義します。

haproxy# HAProxy 設定ファイル(/etc/haproxy/haproxy.cfg)

frontend http_front
    # HTTP/HTTPS リスニング設定
    bind *:80
    bind *:443 ssl crt /etc/haproxy/certs/example.com.pem

    # デフォルトバックエンド
    default_backend http_back

    # WebSocket 接続の判定
    acl is_websocket hdr(Upgrade) -i websocket
    acl is_websocket_path path_beg /ws

    # WebSocket 接続を専用バックエンドへルーティング
    use_backend websocket_back if is_websocket is_websocket_path

この設定では、acl(Access Control List)を使って WebSocket 接続を識別しています。

#ACL 名条件説明
1is_websockethdr(Upgrade) -i websocketUpgrade ヘッダーに websocket が含まれる
2is_websocket_pathpath_beg /wsパスが /ws で始まる

両方の条件を満たす場合に、websocket_back バックエンドへリクエストをルーティングします。この二重チェックにより、誤って通常の HTTP リクエストを WebSocket バックエンドへ送ることを防げるのです。

backend セクションの設定

次に、WebSocket リクエストを処理する backend セクションを定義します。

haproxybackend websocket_back
    # サーバー負荷分散設定
    balance roundrobin

    # WebSocket 用のオプション設定
    option http-server-close
    option forwardfor

    # バックエンドサーバーの定義
    server ws1 192.168.1.101:8080 check
    server ws2 192.168.1.102:8080 check
    server ws3 192.168.1.103:8080 check

HAProxy では、WebSocket 通信のために特別なヘッダー設定を明示的に行う必要はありません。UpgradeConnection ヘッダーはデフォルトでバックエンドへ転送されるためです。

option http-server-close の意味

option http-server-close は、サーバー側の接続をリクエストごとにクローズする設定ですが、WebSocket 接続の場合は自動的に無効化されます。つまり、WebSocket のアップグレードが成功した後は、接続が維持されるのです。

通常の HTTP バックエンド

WebSocket 以外の通常の HTTP リクエスト用のバックエンドも定義しておきましょう。

haproxybackend http_back
    balance roundrobin
    option http-server-close
    option forwardfor

    # クライアント情報をヘッダーに追加
    http-request set-header X-Forwarded-Proto https if { ssl_fc }

    server web1 192.168.1.101:8080 check
    server web2 192.168.1.102:8080 check
    server web3 192.168.1.103:8080 check

より詳細な WebSocket 判定

より厳密に WebSocket 接続を判定したい場合は、以下のような ACL を追加できます。

haproxyfrontend http_front
    bind *:80
    bind *:443 ssl crt /etc/haproxy/certs/example.com.pem

    # WebSocket 判定のための複数条件
    acl is_upgrade hdr(Upgrade) -i websocket
    acl is_connection hdr(Connection) -i upgrade
    acl is_ws_path path_beg /ws

    # すべての条件を満たす場合のみ WebSocket として扱う
    use_backend websocket_back if is_upgrade is_connection is_ws_path

    default_backend http_back

この設定では、Upgrade: websocketConnection: Upgrade の両方のヘッダーが存在することを確認しています。より安全な判定が可能になるでしょう。

タイムアウト設定の詳細

HAProxy でも、WebSocket の長時間接続を維持するために適切なタイムアウト設定が必要です。HAProxy のタイムアウトは NGINX とは異なる概念を持っているため、それぞれの意味を理解しましょう。

グローバルセクションとデフォルトセクション

タイムアウト設定は通常 defaults セクションに記述します。

haproxydefaults
    mode http

    # ログ設定
    option httplog

    # 基本的なタイムアウト設定
    timeout connect 10s
    timeout client 30s
    timeout server 30s

しかし、WebSocket 用には別のタイムアウト値が必要になるため、backend セクションで個別に設定します。

WebSocket バックエンドのタイムアウト設定

haproxybackend websocket_back
    balance roundrobin
    option http-server-close
    option forwardfor

    # WebSocket 用タイムアウト設定
    timeout tunnel 3600s
    timeout server 3600s
    timeout client 3600s

    server ws1 192.168.1.101:8080 check
    server ws2 192.168.1.102:8080 check
    server ws3 192.168.1.103:8080 check

HAProxy における主要なタイムアウトパラメータを詳しく見ていきます。

timeout tunnel(最重要)

timeout tunnel は、WebSocket や CONNECT メソッドなど、双方向通信のトンネル接続に適用される特別なタイムアウトです。WebSocket 接続が確立された後は、このタイムアウトが他のタイムアウト設定を上書きします。

haproxy# デフォルト値: なし(設定されていない場合は timeout client/server が適用)
# WebSocket 推奨値: 3600s(1時間)以上
timeout tunnel 3600s

この設定を省略すると、timeout clienttimeout server の値が使われますが、明示的に timeout tunnel を設定することで、WebSocket 専用のタイムアウトを指定できるのです。

timeout client

クライアントが非アクティブな状態(データを送受信しない状態)を許容する最大時間です。WebSocket 接続では、この値も十分に長く設定する必要があります。

haproxy# デフォルト値: 設定必須
# WebSocket 推奨値: timeout tunnel と同じ値
timeout client 3600s

timeout server

バックエンドサーバーが非アクティブな状態を許容する最大時間です。timeout client と同様に、長時間の接続を維持するために大きな値を設定します。

haproxy# デフォルト値: 設定必須
# WebSocket 推奨値: timeout tunnel と同じ値
timeout server 3600s

timeout connect

バックエンドサーバーへの接続確立を待つ最大時間です。WebSocket の接続確立は通常短時間で完了するため、デフォルト値のままで問題ないでしょう。

haproxy# デフォルト値: 設定必須
# WebSocket 推奨値: 10s(通常の HTTP と同じ)
timeout connect 10s

タイムアウトの優先順位

HAProxy では、WebSocket 接続が確立された後、以下の優先順位でタイムアウトが適用されます。

mermaidflowchart TD
    start["WebSocket 接続確立"] --> check_tunnel{"timeout tunnel<br/>設定あり?"}
    check_tunnel -->|はい| use_tunnel["timeout tunnel<br/>を適用"]
    check_tunnel -->|いいえ| use_cs["timeout client と<br/>timeout server を適用"]

    use_tunnel --> end_state["タイムアウト監視"]
    use_cs --> end_state

timeout tunnel を設定している場合、WebSocket 通信中はこの値が優先的に使われます。設定していない場合は、timeout clienttimeout server の小さい方の値が実質的なタイムアウトとなるのです。

タイムアウトの実践的な設定値

用途に応じた推奨タイムアウト値を以下の表にまとめます。

#用途timeout tunneltimeout clienttimeout servertimeout connect
1チャットアプリ7200s(2 時間)7200s7200s10s
2リアルタイムダッシュボード3600s(1 時間)3600s3600s10s
3オンラインゲーム1800s(30 分)1800s1800s10s
4通知システム86400s(24 時間)86400s86400s10s

チャットアプリやダッシュボードのように、ユーザーが長時間ページを開いたままにする可能性がある場合は、2 時間以上のタイムアウトを設定すると良いでしょう。

実践的な設定例

これまでの内容を統合して、本番環境で使える完全な HAProxy 設定例を示します。SSL/TLS 終端、ヘルスチェック、ログ設定を含む実践的な構成を見ていきましょう。

完全な HAProxy 設定ファイル

haproxy# /etc/haproxy/haproxy.cfg

#---------------------------------------------------------------------
# グローバル設定
#---------------------------------------------------------------------
global
    log /dev/log local0
    log /dev/log local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

    # SSL/TLS 設定
    ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
    ssl-default-bind-options ssl-min-ver TLSv1.2

#---------------------------------------------------------------------
# デフォルト設定
#---------------------------------------------------------------------
defaults
    log global
    mode http
    option httplog
    option dontlognull

    # 基本タイムアウト(WebSocket 以外)
    timeout connect 10s
    timeout client 30s
    timeout server 30s

    # エラー時の再試行
    retries 3
    option redispatch

#---------------------------------------------------------------------
# フロントエンド設定
#---------------------------------------------------------------------
frontend http_front
    # HTTP リスニング(HTTPS へリダイレクト)
    bind *:80
    redirect scheme https code 301 if !{ ssl_fc }

    # HTTPS リスニング
    bind *:443 ssl crt /etc/haproxy/certs/example.com.pem

    # WebSocket 接続の判定
    acl is_upgrade hdr(Upgrade) -i websocket
    acl is_connection hdr(Connection) -i upgrade
    acl is_ws_path path_beg /ws

    # WebSocket 接続をルーティング
    use_backend websocket_back if is_upgrade is_connection is_ws_path

    # 通常の HTTP 接続
    default_backend http_back

#---------------------------------------------------------------------
# バックエンド設定:WebSocket
#---------------------------------------------------------------------
backend websocket_back
    # 負荷分散アルゴリズム
    balance roundrobin

    # オプション設定
    option http-server-close
    option forwardfor

    # WebSocket 用タイムアウト(1時間)
    timeout tunnel 3600s
    timeout server 3600s
    timeout client 3600s

    # クライアント情報をヘッダーに追加
    http-request set-header X-Forwarded-For %[src]
    http-request set-header X-Forwarded-Proto https if { ssl_fc }

    # バックエンドサーバー定義
    # check: ヘルスチェック有効
    # maxconn 100: 最大100接続まで
    server ws1 192.168.1.101:8080 check maxconn 100
    server ws2 192.168.1.102:8080 check maxconn 100
    server ws3 192.168.1.103:8080 check maxconn 100

#---------------------------------------------------------------------
# バックエンド設定:通常の HTTP
#---------------------------------------------------------------------
backend http_back
    balance roundrobin
    option http-server-close
    option forwardfor

    # 通常の HTTP タイムアウト
    timeout server 30s
    timeout client 30s

    # ヘッダー設定
    http-request set-header X-Forwarded-For %[src]
    http-request set-header X-Forwarded-Proto https if { ssl_fc }

    # バックエンドサーバー定義
    server web1 192.168.1.101:8080 check
    server web2 192.168.1.102:8080 check
    server web3 192.168.1.103:8080 check

#---------------------------------------------------------------------
# 統計情報ページ(オプション)
#---------------------------------------------------------------------
listen stats
    bind *:8404
    stats enable
    stats uri /stats
    stats refresh 30s
    stats admin if TRUE

ヘルスチェックの詳細設定

バックエンドサーバーの健全性を監視するために、ヘルスチェックを細かく設定できます。

haproxybackend websocket_back
    balance roundrobin

    # ヘルスチェックの詳細設定
    option httpchk GET /health HTTP/1.1
    http-check send hdr Host example.com

    # タイムアウト設定
    timeout tunnel 3600s
    timeout server 3600s
    timeout client 3600s

    # サーバー定義(詳細なヘルスチェック設定)
    # inter 5s: 5秒ごとにチェック
    # rise 2: 2回成功で復帰
    # fall 3: 3回失敗でダウン判定
    server ws1 192.168.1.101:8080 check inter 5s rise 2 fall 3
    server ws2 192.168.1.102:8080 check inter 5s rise 2 fall 3
    server ws3 192.168.1.103:8080 check inter 5s rise 2 fall 3

この設定では、各バックエンドサーバーの ​/​health エンドポイントに対して 5 秒ごとにヘルスチェックを実行します。3 回連続で失敗したサーバーはダウンと判定され、2 回連続で成功すれば復帰するのです。

スティッキーセッションの設定(必要に応じて)

一部のアプリケーションでは、同じクライアントからのリクエストを常に同じバックエンドサーバーへ送る必要があります。

haproxybackend websocket_back
    balance roundrobin

    # Cookie ベースのスティッキーセッション
    cookie SERVERID insert indirect nocache

    timeout tunnel 3600s
    timeout server 3600s
    timeout client 3600s

    # サーバー定義(cookie オプション追加)
    server ws1 192.168.1.101:8080 check cookie ws1
    server ws2 192.168.1.102:8080 check cookie ws2
    server ws3 192.168.1.103:8080 check cookie ws3

この設定により、HAProxy は各サーバーに対して一意の Cookie を設定し、同じ Cookie を持つクライアントは同じサーバーへルーティングされます。

設定の検証と適用

設定ファイルを編集した後は、構文チェックを行ってから HAProxy を再起動しましょう。

bash# 設定ファイルの構文チェック
sudo haproxy -c -f /etc/haproxy/haproxy.cfg

# 構文に問題がなければリロード
sudo systemctl reload haproxy

# ステータス確認
sudo systemctl status haproxy

-c オプションで構文チェックを実行し、エラーがないことを確認してから reload でグレースフルに設定を反映させます。これにより、既存の接続を切断することなく新しい設定を適用できるのです。

タイムアウト設定の最適化

NGINX と HAProxy の基本設定を理解したところで、タイムアウト設定をさらに深く掘り下げます。アプリケーションの特性や運用環境に応じて、最適なタイムアウト値を選択することが、安定した WebSocket 通信を実現する鍵となるでしょう。

各種タイムアウトパラメータの解説

WebSocket 通信におけるタイムアウトは、単に「長ければ良い」というものではありません。タイムアウト値が長すぎると、障害発生時の検知が遅れたり、リソースが無駄に保持されたりする問題が生じます。逆に短すぎると、正常な接続が切断されてしまうでしょう。

タイムアウトの種類と役割

NGINX と HAProxy では異なるタイムアウトパラメータが使われますが、それぞれの役割を対応付けて理解しましょう。

#役割NGINXHAProxy説明
1接続確立proxy_connect_timeouttimeout connectバックエンドへの接続確立を待つ時間
2データ受信proxy_read_timeouttimeout serverバックエンドからデータを受信するまでの時間
3データ送信proxy_send_timeouttimeout clientバックエンドへデータを送信するまでの時間
4トンネル通信(なし)timeout tunnelWebSocket などの双方向通信のタイムアウト

タイムアウトの相互作用

以下の図は、WebSocket 通信におけるタイムアウトの適用タイミングを示しています。

mermaidstateDiagram-v2
    [*] --> Connecting: 接続開始
    Connecting --> Connected: 接続確立成功
    Connecting --> Failed: タイムアウト<br/>(connect_timeout)

    Connected --> Upgrading: プロトコル<br/>アップグレード
    Upgrading --> WebSocket: 101 Switching<br/>Protocols
    Upgrading --> Failed: タイムアウト<br/>(read_timeout)

    WebSocket --> Receiving: データ受信待ち
    Receiving --> Sending: データ受信
    Sending --> Receiving: データ送信
    Receiving --> Failed: タイムアウト<br/>(read_timeout/<br/>tunnel)

    Sending --> Failed: タイムアウト<br/>(send_timeout/<br/>tunnel)
    Failed --> [*]

接続確立後は、データの送受信がない期間でも read_timeout(NGINX)または timeout tunnel(HAProxy)がタイムアウトを監視し続けます。この値が最も重要なパラメータとなるのです。

キープアライブとの関係

多くの WebSocket クライアントライブラリは、接続を維持するために定期的に「Ping/Pong」フレームを送信します。この仕組みを理解することで、適切なタイムアウト値を設定できるでしょう。

mermaidsequenceDiagram
    participant C as クライアント
    participant P as プロキシ
    participant B as バックエンド

    Note over C,B: WebSocket 接続確立済み

    C->>P: データ送信
    P->>B: データ転送

    Note over C,B: 30秒間通信なし

    C->>P: Ping フレーム
    P->>B: Ping フレーム
    B->>P: Pong フレーム
    P->>C: Pong フレーム

    Note over C,B: Ping/Pong により<br/>タイムアウトリセット

    Note over C,B: さらに30秒間通信なし

    C->>P: Ping フレーム
    P->>B: Ping フレーム

Ping/Pong フレームは、実際のアプリケーションデータがなくても接続が生きていることを証明します。プロキシサーバーのタイムアウトは、この Ping/Pong の間隔よりも長く設定する必要があるのです。

例えば、クライアントが 30 秒ごとに Ping を送信する場合、プロキシのタイムアウトは少なくとも 60 秒以上(できれば 90 秒以上)に設定すべきでしょう。これにより、ネットワーク遅延などで Ping が遅れた場合でも接続が切断されません。

タイムアウトとリソース管理

タイムアウト値を長く設定すると、サーバーリソース(メモリ、ファイルディスクリプタ)を長期間保持することになります。同時接続数が多い環境では、これが問題になる可能性があるでしょう。

mermaidflowchart LR
    timeout["タイムアウト設定"] --> resource["リソース保持時間"]
    resource --> memory["メモリ使用量"]
    resource --> fd["ファイル<br/>ディスクリプタ"]

    connections["同時接続数"] --> memory
    connections --> fd

    memory --> capacity["サーバー容量"]
    fd --> capacity

同時接続数とタイムアウト値のバランスを考慮して、サーバーのリソース上限を超えないように設計する必要があります。

用途別の推奨値

アプリケーションの特性に応じた、実践的なタイムアウト設定の推奨値を紹介します。これらの値は出発点として使用し、実際のトラフィックパターンやサーバーリソースに応じて調整してください。

チャットアプリケーション

ユーザーが長時間ページを開いたままにする可能性があり、メッセージの送受信頻度は低い場合があります。

nginx# NGINX の場合
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # 2時間のタイムアウト
    proxy_read_timeout 7200s;
    proxy_send_timeout 7200s;
    proxy_connect_timeout 10s;
}
haproxy# HAProxy の場合
backend websocket_back
    timeout tunnel 7200s
    timeout server 7200s
    timeout client 7200s
    timeout connect 10s
#パラメータ推奨値理由
1読み取りタイムアウト7200s(2 時間)ユーザーが長時間メッセージを送らない可能性
2送信タイムアウト7200s(2 時間)読み取りタイムアウトと同じ値で統一
3接続タイムアウト10s通常の接続確立は数秒で完了

リアルタイムダッシュボード

データの更新頻度が比較的高く、定期的にサーバーからプッシュ通知が送られます。

nginx# NGINX の場合
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # 1時間のタイムアウト
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_connect_timeout 10s;
}
haproxy# HAProxy の場合
backend websocket_back
    timeout tunnel 3600s
    timeout server 3600s
    timeout client 3600s
    timeout connect 10s
#パラメータ推奨値理由
1読み取りタイムアウト3600s(1 時間)定期的なデータ更新があるため中程度の値
2送信タイムアウト3600s(1 時間)読み取りタイムアウトと同じ値で統一
3接続タイムアウト10s通常の接続確立は数秒で完了

オンラインゲーム(リアルタイムマルチプレイ)

頻繁にデータの送受信が発生し、レイテンシが重要な用途です。

nginx# NGINX の場合
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # 30分のタイムアウト
    proxy_read_timeout 1800s;
    proxy_send_timeout 1800s;
    proxy_connect_timeout 5s;
}
haproxy# HAProxy の場合
backend websocket_back
    timeout tunnel 1800s
    timeout server 1800s
    timeout client 1800s
    timeout connect 5s
#パラメータ推奨値理由
1読み取りタイムアウト1800s(30 分)頻繁な通信があるため比較的短い値
2送信タイムアウト1800s(30 分)読み取りタイムアウトと同じ値で統一
3接続タイムアウト5s低レイテンシが要求されるため短く設定

IoT デバイス監視システム

デバイスからの定期的なステータス送信があり、非常に長時間の接続維持が必要です。

nginx# NGINX の場合
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # 24時間のタイムアウト
    proxy_read_timeout 86400s;
    proxy_send_timeout 86400s;
    proxy_connect_timeout 30s;
}
haproxy# HAProxy の場合
backend websocket_back
    timeout tunnel 86400s
    timeout server 86400s
    timeout client 86400s
    timeout connect 30s
#パラメータ推奨値理由
1読み取りタイムアウト86400s(24 時間)デバイスが長時間データを送らない可能性
2送信タイムアウト86400s(24 時間)読み取りタイムアウトと同じ値で統一
3接続タイムアウト30sIoT デバイスは接続が遅い場合がある

タイムアウト設定の決定フロー

以下のフローチャートを参考に、用途に応じた適切なタイムアウト値を選択しましょう。

mermaidflowchart TD
    start["タイムアウト設定の検討"] --> freq{"データ送受信<br/>の頻度は?"}

    freq -->|高頻度<br/>数秒〜数分| short["短いタイムアウト<br/>1800s〜3600s"]
    freq -->|中頻度<br/>数分〜数十分| medium["中程度のタイムアウト<br/>3600s〜7200s"]
    freq -->|低頻度<br/>数十分以上| long["長いタイムアウト<br/>7200s〜86400s"]

    short --> resource{"同時接続数と<br/>リソースは?"}
    medium --> resource
    long --> resource

    resource -->|多い接続数<br/>限られたリソース| reduce["タイムアウトを<br/>短く調整"]
    resource -->|適切な範囲| keep["推奨値を<br/>そのまま使用"]

    reduce --> monitor["運用監視開始"]
    keep --> monitor

    monitor --> adjust["必要に応じて<br/>値を調整"]

初期設定では推奨値から始め、実際の運用データ(接続切断の頻度、リソース使用量)を見ながら最適化していくアプローチが効果的です。

トラブルシューティング

WebSocket のプロキシ終端では、設定ミスやネットワーク環境の問題により、さまざまなエラーが発生する可能性があります。ここでは、よくある問題とその解決方法を具体的に解説しますね。

よくあるエラーと解決方法

Error 400: Bad Request

WebSocket の接続リクエストが正しく処理されない場合に発生します。

vbnetError 400: Bad Request
The plain HTTP request was sent to HTTPS port

原因と解決方法

#原因解決方法
1HTTP/1.1 が使用されていないNGINX: proxy_http_version 1.1; を設定
2Upgrade ヘッダーが転送されていないproxy_set_header Upgrade $http_upgrade; を設定
3Connection ヘッダーが正しくないproxy_set_header Connection $connection_upgrade; を設定
4map ディレクティブが定義されていないmap $http_upgrade $connection_upgrade を追加

NGINX の場合、以下の設定が正しく記述されているか確認しましょう。

nginx# http ブロック内
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

# location ブロック内
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;  # これが必須
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
}

Error 101: Switching Protocols が返されない

クライアントが 101 Switching Protocols レスポンスを受け取れず、接続がアップグレードされません。

デバッグ方法

ブラウザの開発者ツールでネットワークタブを開き、WebSocket 接続リクエストのヘッダーを確認します。

makefile# クライアントのリクエストヘッダー(正常な場合)
GET /ws HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

# サーバーのレスポンスヘッダー(正常な場合)
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

レスポンスが 101 ではなく 200 OK404 Not Found などの場合、プロキシ設定またはバックエンドサーバーの設定に問題があります。

原因と解決方法

nginx# NGINX の場合:バックエンドへの接続を確認
location /ws {
    proxy_pass http://backend;  # バックエンドのアドレスが正しいか確認
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # デバッグ用にアクセスログを有効化
    access_log /var/log/nginx/websocket_access.log;
    error_log /var/log/nginx/websocket_error.log;
}

ログファイルを確認して、バックエンドサーバーへの接続が成功しているか、エラーが発生していないかをチェックしましょう。

Error 502: Bad Gateway

プロキシサーバーがバックエンドサーバーから有効なレスポンスを受け取れない場合に発生します。

javascriptError 502: Bad Gateway

主な原因

  1. バックエンドサーバーがダウンしている
  2. バックエンドサーバーへのネットワーク経路に問題がある
  3. バックエンドサーバーが WebSocket をサポートしていない
  4. ファイアウォールが WebSocket 通信をブロックしている

解決方法

bash# バックエンドサーバーの稼働確認
curl http://backend_server:8080/health

# ネットワーク経路の確認
ping backend_server

# ポートの疎通確認
telnet backend_server 8080

バックエンドサーバーが正常に応答する場合は、WebSocket のサポート状況を確認します。Node.js の場合、以下のような実装が必要です。

javascript// Node.js + Express + ws ライブラリの例
const express = require('express');
const http = require('http');
const WebSocket = require('ws');

const app = express();
const server = http.createServer(app);

// WebSocket サーバーを作成
const wss = new WebSocket.Server({ server });

// 接続イベントのハンドリング
wss.on('connection', (ws) => {
  console.log('WebSocket client connected');

  ws.on('message', (message) => {
    console.log('Received:', message);
  });
});

server.listen(8080, () => {
  console.log('Server listening on port 8080');
});

Error 504: Gateway Timeout

プロキシサーバーがバックエンドからのレスポンスを待っている間にタイムアウトが発生します。

javascriptError 504: Gateway Timeout

原因と解決方法

タイムアウト設定が短すぎることが主な原因です。WebSocket 接続では長時間の通信が想定されるため、タイムアウト値を増やしましょう。

nginx# NGINX の場合
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # タイムアウトを長く設定
    proxy_read_timeout 3600s;  # 1時間
    proxy_send_timeout 3600s;  # 1時間
    proxy_connect_timeout 60s;
}
haproxy# HAProxy の場合
backend websocket_back
    timeout tunnel 3600s  # 1時間
    timeout server 3600s  # 1時間
    timeout client 3600s  # 1時間
    timeout connect 60s

接続が切断される場合の対処法

WebSocket 接続が予期せず切断される問題は、タイムアウト設定やネットワーク環境に起因することが多いです。段階的に原因を特定していきましょう。

切断のタイミングを特定する

まず、どのくらいの時間が経過すると切断されるかを測定します。

javascript// クライアント側のデバッグコード
const ws = new WebSocket('wss://example.com/ws');
const startTime = Date.now();

ws.onopen = () => {
  console.log(
    'WebSocket connected at',
    new Date().toISOString()
  );
};

ws.onclose = (event) => {
  const duration = (Date.now() - startTime) / 1000;
  console.log(
    `WebSocket disconnected after ${duration} seconds`
  );
  console.log('Close code:', event.code);
  console.log('Close reason:', event.reason);
};

ws.onerror = (error) => {
  console.error('WebSocket error:', error);
};

切断までの時間が一定(例えば常に 60 秒後)である場合、プロキシサーバーのタイムアウト設定が原因である可能性が高いです。

Close コードによる原因特定

WebSocket の切断理由は、Close コードによって識別できます。

#Close コード意味対処法
11000正常なクローズ問題なし(アプリケーションが意図的に切断)
21001サーバーがダウンまたは移動バックエンドサーバーの状態を確認
31006異常なクローズプロキシのタイムアウト設定を確認
41009メッセージが大きすぎるメッセージサイズの上限を調整
51011サーバー内部エラーバックエンドのログを確認

Close コード 1006 の対処

Close コード 1006 は、TCP 接続が予期せず切断されたことを示します。これは最も一般的なタイムアウト関連の問題です。

nginx# NGINX の場合:タイムアウトを段階的に増やして確認
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # まず60秒→120秒→300秒→3600秒と段階的に増やす
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

タイムアウトを増やしても切断される場合は、ネットワーク経路上の別の機器(ファイアウォール、ロードバランサーなど)がタイムアウトを設定している可能性があります。

キープアライブ(Ping/Pong)の実装

タイムアウトを防ぐ最も確実な方法は、定期的に Ping フレームを送信して接続が生きていることを通知することです。

javascript// クライアント側のキープアライブ実装
const ws = new WebSocket('wss://example.com/ws');
let pingInterval;

ws.onopen = () => {
  console.log('WebSocket connected');

  // 30秒ごとに Ping を送信
  pingInterval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(JSON.stringify({ type: 'ping' }));
      console.log('Sent ping');
    }
  }, 30000);
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);

  if (data.type === 'pong') {
    console.log('Received pong');
  } else {
    // 通常のメッセージ処理
    console.log('Received:', data);
  }
};

ws.onclose = () => {
  // Ping の送信を停止
  clearInterval(pingInterval);
};

サーバー側でも Ping に対する Pong を返す実装が必要です。

javascript// Node.js サーバー側の Ping/Pong 実装
wss.on('connection', (ws) => {
  console.log('Client connected');

  // 定期的に Ping を送信(WebSocket API の組み込み機能)
  const pingInterval = setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.ping();
    }
  }, 30000);

  ws.on('message', (message) => {
    const data = JSON.parse(message);

    if (data.type === 'ping') {
      // Ping に対して Pong を返す
      ws.send(JSON.stringify({ type: 'pong' }));
    } else {
      // 通常のメッセージ処理
      console.log('Received:', data);
    }
  });

  ws.on('close', () => {
    clearInterval(pingInterval);
    console.log('Client disconnected');
  });

  // WebSocket API の Pong イベントハンドリング
  ws.on('pong', () => {
    console.log('Received pong from client');
  });
});

自動再接続の実装

接続が切断された場合に自動的に再接続する仕組みを実装することで、ユーザー体験を向上させられます。

javascript// 自動再接続機能付き WebSocket クラス
class ReconnectingWebSocket {
  constructor(url) {
    this.url = url;
    this.reconnectDelay = 1000; // 初期再接続遅延:1秒
    this.maxReconnectDelay = 30000; // 最大再接続遅延:30秒
    this.reconnectAttempts = 0;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('WebSocket connected');
      this.reconnectAttempts = 0;
      this.reconnectDelay = 1000;
      this.onopen?.();
    };

    this.ws.onmessage = (event) => {
      this.onmessage?.(event);
    };

    this.ws.onclose = () => {
      console.log('WebSocket disconnected');
      this.scheduleReconnect();
      this.onclose?.();
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error);
      this.onerror?.(error);
    };
  }

  scheduleReconnect() {
    this.reconnectAttempts++;
    const delay = Math.min(
      this.reconnectDelay *
        Math.pow(2, this.reconnectAttempts),
      this.maxReconnectDelay
    );

    console.log(
      `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
    );
    setTimeout(() => this.connect(), delay);
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    } else {
      console.warn(
        'WebSocket is not open. Message not sent:',
        data
      );
    }
  }
}

// 使用例
const rws = new ReconnectingWebSocket(
  'wss://example.com/ws'
);

rws.onopen = () => {
  console.log('Connection established');
};

rws.onmessage = (event) => {
  console.log('Received:', event.data);
};

rws.send(JSON.stringify({ message: 'Hello' }));

この実装では、接続が切断された場合に指数バックオフ(exponential backoff)アルゴリズムを使って再接続を試みます。最初は 1 秒後、次は 2 秒後、その次は 4 秒後…という具合に遅延を増やしていき、最大 30 秒まで到達したらその間隔で再接続を繰り返すのです。

ログによる診断

プロキシサーバーとバックエンドサーバーの両方でログを有効にし、切断のタイミングでどのようなエラーが記録されるかを確認しましょう。

nginx# NGINX のログ設定
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    # 詳細なログを記録
    access_log /var/log/nginx/websocket_access.log combined;
    error_log /var/log/nginx/websocket_error.log debug;
}

ログには、接続時間、転送データ量、エラーメッセージなどが記録されます。これらの情報から切断の原因を特定できるでしょう。

まとめ

WebSocket を NGINX や HAProxy で終端するには、アップグレードヘッダーの適切な設定とタイムアウトの調整が不可欠です。本記事で解説した内容を振り返りましょう。

重要なポイント

  1. アップグレードヘッダーの必須設定

    • HTTP から WebSocket へのプロトコル切り替えには UpgradeConnection ヘッダーの転送が必要
    • NGINX では map ディレクティブと proxy_set_header を使用
    • HAProxy では ACL を使った判定とルーティング
  2. HTTP/1.1 の使用

    • WebSocket は HTTP/1.1 以降が必須
    • NGINX では proxy_http_version 1.1 を明示的に設定
  3. タイムアウトの調整

    • WebSocket は長時間接続を維持するため、デフォルトのタイムアウトでは不十分
    • 用途に応じて 1800s〜86400s の範囲で設定
    • NGINX: proxy_read_timeoutproxy_send_timeout
    • HAProxy: timeout tunneltimeout servertimeout client
  4. キープアライブの実装

    • 定期的な Ping/Pong フレームでタイムアウトを防ぐ
    • クライアント・サーバー双方で実装することが理想的
  5. 自動再接続

    • 接続切断時の自動再接続により、ユーザー体験を向上
    • 指数バックオフアルゴリズムで過負荷を防ぐ

設定のチェックリスト

WebSocket プロキシを設定する際は、以下の項目を確認しましょう。

#項目NGINXHAProxy
1HTTP/1.1 の使用✓ proxy_http_version 1.1✓ デフォルトで有効
2Upgrade ヘッダー転送✓ proxy_set_header Upgrade✓ デフォルトで転送
3Connection ヘッダー設定✓ map + proxy_set_header✓ デフォルトで転送
4読み取りタイムアウト✓ proxy_read_timeout✓ timeout server
5送信タイムアウト✓ proxy_send_timeout✓ timeout client
6トンネルタイムアウト-✓ timeout tunnel
7SSL/TLS 終端✓ ssl_certificate✓ bind *:443 ssl crt
8負荷分散✓ upstream✓ backend + balance
9ヘルスチェック✓ upstream + check✓ server + check

トラブルシューティングの手順

問題が発生した場合は、以下の順序で確認していきます。

  1. ブラウザの開発者ツールでリクエスト/レスポンスヘッダーを確認
  2. プロキシサーバーのログを確認(error.log
  3. バックエンドサーバーの動作確認(直接接続テスト)
  4. タイムアウト設定を段階的に増やして検証
  5. Ping/Pong による キープアライブの実装

本記事で紹介した設定例は、多くの WebSocket アプリケーションで使用できる汎用的なものです。しかし、実際の運用では、トラフィックパターン、同時接続数、サーバーリソースなどの要因に応じて調整が必要になるでしょう。初期設定をベースとして、ログやモニタリングデータを参考に最適化を進めていくことをお勧めします。

WebSocket のプロキシ終端は、適切に設定すれば非常に安定した双方向通信を実現できます。本記事が、皆さんの WebSocket アプリケーションの構築に役立てば幸いです。

関連リンク