仕組みから学ぶ Web
応用読了目安 15#WebSocket#SSE#ポーリング#リアルタイム#コネクション

ポーリングと SSE と WebSocket — どこまで双方向が要るか

5秒ごとの更新確認を入れた翌週、DB の接続が枯れた。1000人が5秒ごとに聞きに来れば毎秒200回。要るのは本当に双方向か、それとも「届く」だけか。

この記事の進み方

What — 3つの方式は「誰が口を開くか」で分かれる

更新を届ける方式は、通信を誰が始めるかで分類できます。

Compare3つの方式

クライアントが定期的に聞きに行く。サーバーは普通の HTTP で答える。

クライアント ──▶ サーバー   「変わった?」
クライアント ◀── サーバー   「いいえ」
           (5秒待つ)
クライアント ──▶ サーバー   「変わった?」
クライアント ◀── サーバー   「いいえ」
接続の保持
しない
無駄な往復
多い
遅れ
最大で間隔ぶん
実装の重さ
最も軽い
  • 既存の HTTP の仕組みがそのまま使える。ロードバランサもプロキシも何もしなくてよい。
  • 「変わっていない」を確かめるためのリクエストが大半を占める。冒頭の事故はこれ。

ポーリングは「聞きに行く」、SSE は「送ってもらう」、WebSocket は「互いに話す」。要件が「送ってもらう」で足りるなら、WebSocket は過剰になる。

ポーリング
一定間隔で問い合わせること。変化が無くても往復が発生する。
ロングポーリング
変化があるまでサーバーが応答を保留する方式。往復は減るが接続は保持される。
SSE (Server-Sent Events)
HTTP のレスポンスを閉じずにイベントを流し続ける仕組み。サーバーからの一方向。
WebSocket
HTTP から切り替えて双方向の通信路を張るプロトコル。
Upgrade
HTTP の接続を別のプロトコルに切り替える手続き。WebSocket はこれで始まる。

「リアルタイム」の中身は2つに分かれる

「リアルタイムにしたい」と言われたとき、実際に求められているものはたいてい次のどちらかです。

  • 速く届いてほしい(サーバー → クライアントの一方向)
  • 速くやり取りしたい(双方向)

前者なら SSE で足ります。通知、進捗の表示、ダッシュボードの数値更新、ビルドのログ。これらは全部、サーバーから送るだけです。

後者が本当に要るのは、チャット、共同編集、ゲーム、カーソルの共有あたりです。クライアント側が頻繁に、かつ低遅延で送る必要がある場合に限られます。

冒頭の事故の要件は「新着の通知を出す」でした。送るのはサーバーだけです。WebSocket も要りませんでした。

Why — 開いたままの接続は、数えられる資源になる

ポーリングと、接続を保持する方式(SSE / WebSocket)の違いは、何を消費するかです。

ポーリングはリクエストの回数を消費します。1回ごとに接続を作って捨てるので、同時に保持しているものは少ない。

SSE と WebSocket は同時接続数を消費します。往復は減りますが、1人1本の接続が開きっぱなしになります。

Figure1000人が接続したときに何が起きるか
  1. 1ブラウザ
  2. 2ロードバランサ
  3. 3アプリ
  4. 4DB
1/4
ブラウザ1000人が管理画面を開いている

ポーリングは瞬間の同時実行数が少ない代わりに回数が多い。保持型は回数が少ない代わりに、常に接続を持ち続ける。どちらも上限にぶつかる場所が違う。

上限にぶつかる場所が違う

ポーリングが壊すのは、たいてい DB か外部サービスです。1リクエストごとに問い合わせが飛ぶので、間隔と人数を掛けた回数がそのまま負荷になります。

保持型が壊すのは、たいていアプリのプロセスか途中の箱です。1000 本の接続を同時に持つには、それを持てる作りになっている必要があります。プロセスあたりの上限、ファイルディスクリプタ、ロードバランサの接続数。

どちらが軽いかは、人数と更新頻度で逆転します。

  • 人数が少なく更新が多い → 保持型が有利
  • 人数が多く更新が稀 → ポーリングのほうが安いこともある(ただし間隔を長くすること)

冒頭の事故は「人数が多く更新が稀」なのに、間隔が5秒と短すぎました。1日数十件の更新に対して、毎秒 40 回聞いていた

保持型には、切れる前提が要る

開いたままの接続は必ず切れます

  • 回線が変わる(Wi-Fi から携帯網へ)
  • スマートフォンがスリープする
  • ロードバランサのアイドルタイムアウト(無言が続くと切る)
  • デプロイでアプリが入れ替わる

SSE はブラウザが自動で再接続します。 Last-Event-ID を送り返す仕組みもあり、取りこぼしの回復も規格に入っています。

WebSocket は自分で書きます。 再接続、その間に発生したイベントの取得、重複の除去。ここを書かずに済ませると、「たまに通知が来ない」という再現しにくい不具合になります。

演習 — まず自分で判断する

確認 — ここまで読めたか

社内の管理画面に「処理の進捗バー」を出したい。進捗はサーバー側が5〜10秒ごとに更新する。同時に見る人は最大20人。どれを選びますか?

まず選ぶ(解答例は a〜d の記号で説明します)

確認 — ここまで読めたか

500人が3秒ごとにポーリングしています。1リクエストで DB に1回、50ms のクエリを投げるとき、DB の接続は常時およそ何本埋まりますか?

まず選ぶ(解答例は a〜d の記号で説明します)

読み終わりましたか?

読了にすると、これを前提とする記事がロードマップで開放されます。