← ブログ

WebSocket サーバーの自前運用をやめた理由

· Hibira

自前のリアルタイムサーバーはメッセージが急増すると詰まり、PHP2Shell のような脆弱性の修正も自分で追うことになります。私たちの経験、自前運用に必要なこと、ホスティング型サービスが引き受けること。

Hibira を作る前、私たちは過去のプロジェクトで WebSocket サーバーを自前で運用していました。当時は PHP で構築し、アプリケーションと一緒にモノリスとしてデプロイしていました。普段は問題ありませんでしたが、メッセージ量が急増すると遅れ始めました。サーバーは手元のメッセージの処理に追われ、新しいリクエストはその後ろで待たされ、遅延はどんどん伸びていきました。メッセージがようやくユーザーに届いたときには、内容はもう古くなっていました。価格はすでに動き、スコアは更新され、「入力中…」の表示は相手がメッセージを送った後に出てきました。

この記事はそのときの経験をまとめたものです。リアルタイムサーバーがピーク時に詰まる理由、自前で運用するなら何を足す必要があるか、そしてホスティング型サービスに任せたほうが割に合うのはどんなときかを書きます。

詰まりはこうして起きる

リアルタイムサーバーの仕事は単純に見えます。イベントを受け取り、購読しているすべての接続に送るだけです。問題は、この仕事量が何倍にも膨らむこと、そしてどこか一か所が遅くなると、その後ろがすべて待たされることです。

バックエンドピーク時に大量に publish
HTTP API
WebSocket サーバー処理しきれず、行列ができる
どんどん遅れる
ユーザー古くなったメッセージが届く
処理が追いつかないと、新しいメッセージはすべて古いメッセージの後ろに並ぶ

処理より速く届けば、キューは伸び続ける

これは待ち行列の基本です。メッセージが届く速さがサーバーの処理速度を上回っている間、キューは伸び続け、新しいメッセージはその前のすべてが処理されるまで待つことになります。

数字で考えてみましょう。サーバーが毎秒 5,000 件を処理できるとして、ピーク時に毎秒 6,000 件が 10 秒間届いたとします。この 10 秒で、処理しきれないメッセージが 10,000 件たまります。ピークが終わって新しいメッセージが完全に止まっても、サーバーがそれを片付けるのにさらに 2 秒かかり、キューの最後のメッセージは 2 秒遅れて送られます。ピーク後も通常のトラフィックが届き続けるなら、片付くのはもっと遅くなります。

Web ページなら 2 秒の遅れは体験が少し悪くなる程度です。リアルタイム機能では、2 秒前の価格、位置、ゲームの状態は、多くの場合そのまま間違ったデータです。

1 つのイベントに、購読者の数を掛ける

1 回 publish しても、送信は 1 回では済みません。購読者が 2,000 人いるチャンネルでは、イベントを 1 つ publish するたびにサーバーは 2,000 回書き込みます。実際の仕事量は「毎秒の publish 数 × 購読者数」です。ライブチャット、試合のスコア、運用ダッシュボードのような 1 対多の場面ほど、人数が増えた瞬間に急に限界を超えやすくなります。

1 本の遅い接続が全員を止める

エレベーターや地下鉄の中のスマートフォンは、通信がとても遅くなったり、しばらくデータを受け取れなくなったりします。サーバーが接続に 1 本ずつ順番に書き込み、書き終わるのを待ってから次に進むなら、1 本の接続が詰まっただけで、その後ろのユーザー全員が遅れます。これが私たちの遭遇した「ブロッキング」でした。問題は大多数のユーザーではなく、少数の遅い接続がブロードキャスト全体を遅らせていたことでした。

待ちは連鎖する

サーバーが忙しくなると、イベントを publish する HTTP リクエストも遅くなります。バックエンドはその応答を待つので、自分のリクエストも積み上がっていきます。タイムアウトしたリクエストを自動で再試行すれば、すでに過負荷のサーバーにさらに仕事を足すことになります。リアルタイムサーバーが遅いだけだったはずが、最後にはシステム全体が遅くなります。

モノリスが問題を大きくした

私たちが遭遇した問題の多くは、すべてを 1 つのモノリスとしてデプロイしていたことから来ていました。WebSocket サーバーはアプリケーションのほかの部分と一緒に動き、一緒にデプロイされていました。リアルタイムのトラフィックが急増するとアプリケーション全体のリソースを使い切り、普通の API リクエストまで待たされました。逆に、ほかの機能が遅くなると、リアルタイムのメッセージも一緒に止まりました。リアルタイムの部分をスケールするには、アプリケーション全体のマシンを増やすしかありませんでした。さらにモノリスでは、アプリをデプロイするたびに、通常はすべての WebSocket 接続も切断されて再接続になります。

もう一つの理由:実行環境のセキュリティ更新

性能のほかに、自前の運用をやめたもう一つの理由はセキュリティです。当時は PHP のセキュリティ脆弱性にも遭遇し、自分たちで修正を追いかけるしかありませんでした。自前でサーバーを運用すれば、OS、言語ランタイム、ライブラリの脆弱性はすべて自分で追う必要があり、一つも見逃せません。

最近の例が、PHP プロジェクトが 2026 年 10 月 21 日に修正した「PHP2Shell」と呼ばれる 2 つの深刻な脆弱性です。

  • CVE-2026-109618:メモリ不足(OOM)の処理中に起きるメモリ破損。
  • CVE-2026-109619:リクエスト間に残った状態による use-after-free。

どちらも CVSS v4.0 で 9.2 と評価され、認証されていない攻撃者がリモートでコードを実行できるおそれがあります。影響を受けるのは PHP-FPM、Apache mod_php、php-cgi など Web リクエストを処理する PHP です。修正版は 8.2.35、8.3.36、8.4.27、8.5.12 で、8.1 以前には公式の定期更新がもうありません。

この 2 つは Web リクエストを処理する PHP の問題なので、常駐する WebSocket プロセスを直接狙うとは限りません。それでも教訓は同じです。脆弱性が公開された瞬間から、どのサーバーが影響を受けるかを調べ、アップグレードを計画し、再デプロイしなければなりません。それもできるだけ早く。

自前で運用するなら、これを足す

どの問題にも解決策はありますが、どれも一度設定すれば終わりではなく、継続的な仕事です。

  • アプリケーションとは別にデプロイする。 リアルタイムサーバーをモノリスから切り出し、単独でスケールとデプロイができるようにすれば、ピークでほかの機能まで巻き込まずに済みます。
  • 接続ごとに上限付きのキュー。 書き込み同士を待たせてはいけません。接続ごとに専用のキューを持たせ、いっぱいになったらそのクライアントは追いつけていないということなので、ほかを遅らせる前に切断します。
  • 受け取りと送信を分ける。 publish 用の API は、すべての購読者に届くまで応答を待つべきではありません。そうしないと、ブロードキャストが遅れたときにバックエンドまで遅くなります。
  • 水平スケール。 1 台のサーバーには必ず上限があります。マシンを増やすには、すべてのマシンでチャンネルとプレゼンスを共有し、前段にロードバランサーを置く必要があります。
  • ピークを基準に容量をテストする。 平均的なトラフィックでの負荷テストは「問題なし」と言うだけです。テストすべきはピークの何倍もの負荷と、大量のクライアントが一斉に再接続する状況です。
  • CPU だけでなくキューと遅延を監視する。 CPU にまだ余裕があるように見えても、キューは伸びているかもしれません。メッセージがどれだけ待ったか、遅すぎて切断された接続がいくつあるかを見られるようにし、悪化したらアラートを受け取れるようにします。
  • オンコールの担当者。 ピークはたいてい一番大事な瞬間です。チケットの販売開始、試合の最後の 1 分、セールの開始。サーバーに問題が起きたとき、対応できる人が必要です。
  • セキュリティ更新を追う。 OS、言語ランタイム、ライブラリの脆弱性が公開されたら、影響を評価し、アップグレードして再デプロイします。

1 本ずつ順番に書き込む

新しいイベント
publish
サーバー
先に B へ
接続 B通信が遅い
B を待つ
ほかのすべての接続まとめて遅延

接続ごとのキュー

新しいイベント
publish
サーバー
それぞれのキュー
ほかのすべての接続時間どおり届く
新しいイベント
publish
サーバー
キューが満杯
接続 B切断して自動再接続
解決策の核心:接続ごとに並ぶので、遅い接続は自分にしか影響しない

ホスティング型サービスが引き受けること

ホスティング型のリアルタイムサービスを使えば、上の仕事はサービス側が担います。Hibira を例にすると:

  • 遅いクライアントがほかを遅らせない。 接続ごとに上限付きのキューがあり、書き込み同士が待ち合うことはありません。キューがいっぱいになったり書き込みがタイムアウトしたりしたクライアントは、Pusher プロトコルの 4100 エラーコードで切断され、pusher-js はそれを受け取ると自動で再接続します。
  • 何が起きたか見える。 こうした切断はアプリのエラーカードに「遅いクライアントを切断」として表示されるので、サーバーのログを掘る必要はありません。
  • 容量は私たちが計画する。 サーバーはクラスターとして動き、チャンネルとプレゼンスを共有し、水平にスケールします。プランの上限は明確なハードリミットなので、請求で驚くことはありません。
  • Pusher プロトコルで話す。 いまの pusher-js、Laravel Echo、サーバーライブラリはそのまま使えます。サービスの切り替えはホストとキーを変えるだけです。
  • 修正は私たちが担当する。 WebSocket サーバーとその下の実行環境のセキュリティ更新は私たちが対応します。自分のアプリの更新は引き続き必要ですが、見張るものが一つ減ります。

コストもはっきり書いておきます。無料枠を超えると月額料金がかかり、イベントは第三者を経由し、プランの上限を受け入れる必要があります。

自前で運用するほうが合うとき

ホスティング型サービスだけが答えではありません。次のような場合は、自前の運用が合うかもしれません。

  • トラフィックが少なく安定していて、はっきりしたピークがない。
  • イベントの内容を自社のインフラの外に出せない。
  • すでに運用チーム、監視、オンコール体制があり、常駐サービスが 1 つ増えても負担にならない。

Laravel を使っているなら、Reverb を自前で動かすか、ホスティング型サービスを使うかでより具体的に比べています。どちらも Pusher プロトコルを話すので、あとで考えが変わっても設定を変えるだけです。

私たちの結論

そこで私たちは作り直すことにしました。リアルタイム通信をモノリスから切り出し、水平にスケールできる独立したサービスにして、まず社内で使い、その後外部にも提供しました。前段にロードバランサーを置き、負荷に応じて自動で水平にスケールアウトするので、急に流量が押し寄せても 1 台が耐えるのではなく、サーバーを増やして分担できます。それが Hibira です。

WebSocket サーバーを自前で運用するとき、難しいのは動かすことではありません。一番忙しい数分間にも時間どおりに届け続けることです。それはまさに、ユーザーが一番気にしていて、一番問題が起きやすい時間です。チームの時間をキューやオンコールではなくプロダクトに使いたいなら、ホスティング型サービスのほうが手間のかからない選択です。

Hibira の無料プランはクレジットカード不要で、開発や小規模なアプリには十分です。すでに Pusher を使っているなら、Pusher Channels からの移行の手順で、コードを変えずに乗り換えられます。