Skip to content

Stay Connected

チャットサービスにWebSocketを導入したとき、スケールの概念が変わった。

HTTPはステートレスだ。リクエストが来て、レスポンスを返して、接続を切る。サーバーはクライアントのことを覚えていない。だからロードバランサの後ろにサーバーを並べれば、どのサーバーが応答しても同じ結果になる。スケールアウトの前提がここにある。

WebSocketはその逆だ。接続を張りっぱなしにする。サーバーとクライアントの間に常時開いたパイプがある。サーバーはクライアントを覚えている。ステートフルだ。ロードバランサの後ろにサーバーを増やしても、Aさんの接続がサーバー1に張られている限り、Aさんへのメッセージはサーバー1を経由しなければならない。どのサーバーでもいい、というわけにはいかない。

WebSocketが来る前は、わたしたちはAjaxでループさせて擬似的にポーリングしていた。数秒おきにサーバーに問い合わせる。リアルタイムに見えるが、接続のたびにHTTPのオーバーヘッドがかかる。ユーザーが増えると負荷が洒落にならなかった。WebSocketが来たとき、ようやくまともな双方向通信ができると思った。

ところがインフラが追いつかない。当時のロードバランサはWebSocketの長時間接続を想定していなかった。アイドルタイムアウトで勝手に切られる。LBの再起動で全接続が吹き飛ぶ。接続数が増えればポートの枯渇も起きる。CDNはHTTPのキャッシュを前提にした仕組みで、常時接続とは相性が悪い。

いまではCloudflareもAWS ALBもWebSocketをサポートしている。Cloudflare Durable Objectsのようにエッジでステートフルな処理を動かす仕組みまである。ただし同時接続数の上限やコストの問題は残る。楽にはなったが、解決したわけではない。SSEも選択肢に加わり、用途によってはWebSocketを使わなくても済むようになった。

それでもステートフルな接続のスケーリングは本質的に難しい。接続を持っているサーバーにメッセージを届けるために、PubSubやRedisで中継する仕組みが要る。サーバーが落ちたら再接続先を探す必要がある。WebSocketを触るたびに、HTTPの「切れる前提」の潔さを思い知る。