# 為什麼我們不再自己架 WebSocket 即時通訊伺服器

> 我們自己架的即時伺服器一遇到訊息尖峰就塞住，還得自己追 PHP2Shell 這類漏洞的修補。我們踩過的坑、自己架需要持續投入的工作，以及託管服務替你承擔了什麼。

Source: https://hibira.io/zh-tw/blog/hosted-vs-self-hosted-realtime
Updated: 2026-10-26

在做 Hibira 之前，我們在過去的專案裡自己維運過 WebSocket 伺服器，當時直接採用現成的 PHP 開源方案，架成一個獨立服務，單一節點運行。即時通訊只是整個系統裡的一小塊，只要功能正常，平常也沒有人特別注意。直到訊息量變大，問題才開始浮現：伺服器忙著處理手上的訊息，新的請求在後面排隊，延遲一路往上爬。等訊息終於送到使用者手上，內容已經過時了。報價已經變了、比分早就更新，「對方正在輸入」的提示在對方送出訊息之後才出現。

這篇文章整理那次的經驗：即時伺服器為什麼可能在尖峰時塞住、自己架的話要補上哪些東西，以及什麼時候交給託管服務比較划算。回頭看，問題不在於用了開源方案，而是我們當初只確認功能能用，沒有規劃後續的監控、擴充和維護。自己架是可以做得很好的，也有成熟的開源伺服器可以用；要付出的是持續的工程工作，這篇談的就是這些工作。

## 塞住是怎麼發生的

即時伺服器做的事情看起來很單純：收到一則事件，送給所有訂閱的連線。問題在於這件事的工作量會被放大，而且只要有一段慢下來，後面的全部都得等。

```text
flow: 處理速度跟不上時，每一則新訊息都排在所有舊訊息後面
你的後端|尖峰時大量發布 --HTTP API--> *WebSocket 伺服器|處理不完，開始排隊 --越來越晚--> 使用者|收到已過時的訊息
```

### 進來的比處理的快，佇列只會變長

這是排隊的基本規則：只要訊息進來的速度超過伺服器處理的速度，佇列就會一直變長，每一則新訊息都得等前面所有的訊息處理完。

用一個簡單的數字算算看。假設伺服器每秒最多能寫出 5,000 則訊息給用戶端（這裡算的是實際送出的份數，不是發布次數，下一節會說明差別），尖峰時每秒有 6,000 則要送，持續 10 秒。這 10 秒會累積 10,000 則處理不完的訊息。假設尖峰結束後新訊息完全停止，伺服器也還要再花 2 秒才能清完，佇列最後面那則訊息會晚 2 秒才送出。如果尖峰後還有正常流量進來，清得更慢。

對網頁來說，慢 2 秒是體驗差一點；對即時功能來說，2 秒前的報價、位置或遊戲狀態，常常就是錯的資料。

### 一則事件，乘上每一位訂閱者

發布一次不等於送出一次。一個有 2,000 人訂閱的頻道，每發布一則事件，伺服器就要寫出 2,000 次。所以實際的工作量是「每秒發布次數 × 訂閱人數」：每秒發布 6,000 則、每則送給 2,000 人，最多就是每秒 1,200 萬次寫出。直播聊天室、比賽比分、營運儀表板這類一對多的情境，最容易在人數變多的那一刻突然超過負荷。

### 等待會互相傳染

伺服器一忙，負責發布事件的 HTTP 請求也開始變慢。你的後端在等它回應，自己的請求也跟著堆積；請求逾時之後如果自動重試，又在已經過載的伺服器上多加一份工作。原本只是即時伺服器慢，最後變成整個系統都慢。

### 擴充是最難的一步

我們當時的 WebSocket 伺服器是獨立部署的，和應用程式分開，不跟主程式共用同一台機器。真正卡住的是擴充：一台伺服器的處理量有上限，想加第二台，所有機器就得共享頻道訂閱和線上狀態，事件才送得到連在另一台上的使用者，前面還要有負載平衡器分配連線。我們那台只有一個節點，這些機制都沒有，所以根本沒辦法擴容，尖峰時只能讓它一台硬撐。而且因為只有一個節點，每次更新或重新啟動，所有 WebSocket 連線都會同時斷開，再一起重連。

### 看不見，只能等出事才知道

還有一個問題是看不見。我們當時是自己架設、自己維運，沒有視覺化的監控圖表，也沒有告警，連線數、訊息處理量和伺服器負載都無法即時掌握，更沒有盯著佇列長度、訊息延遲和被拖慢的連線，伺服器的實際狀況其實一直掌握不清楚。CPU 看起來還好，不代表訊息準時送到。結果往往要等到使用者受到影響、主程式也被拖慢，我們才發現：發布事件的請求卡住，主程式等著它回應，跟著一起變慢，就像上面「等待會互相傳染」說的那樣。

## 另一個原因：執行環境的安全更新

效能之外，讓我們決定不再自己維運的另一個原因是安全。我們當時也遇過 PHP 的安全漏洞，只能自己追著修補。自己架伺服器，作業系統、語言執行環境、函式庫的漏洞都得自己盯，一個都不能漏。

最近的一個例子是 PHP 官方在 2026 年 10 月修補的兩個嚴重漏洞，被稱為「PHP2Shell」（[加拿大網路安全中心通報 AL26-026](https://cyber.gc.ca/en/alerts-advisories/al26-026-critical-vulnerabilities-impacting-php-cve-2026-109618-cve-2026-109619)）：

- **CVE-2026-109618**：處理記憶體不足（OOM）時發生的記憶體損壞。
- **CVE-2026-109619**：請求之間殘留的狀態造成的 use-after-free。

兩者的 CVSS v4.0 分數都是 9.2，可能讓未經驗證的攻擊者遠端執行程式碼。受影響的是處理網頁請求的 PHP，例如 PHP-FPM、Apache mod_php 和 php-cgi。修補版本是 8.2.35、8.3.36、8.4.27 和 8.5.12，8.1 以前的版本已經沒有官方的例行更新。

要說清楚的是，這兩個漏洞出在處理網頁請求的 PHP 上，常駐的 WebSocket 程序不一定會受影響。我們舉它當例子，是為了說明例行的維護工作，而不是 WebSocket 本身的缺陷：不管伺服器用什麼語言寫，你用到的軟體一有漏洞公開，就得自己確認是否受影響、排時間升級、重新部署，而且越快越好。

## 自己架的話，要補上這些

這些問題都有解法，只是每一項都是持續的工作，不是設定一次就好：

- **和應用程式分開部署。** 讓即時伺服器可以獨立擴充、獨立部署，尖峰時才不會拖垮其他功能。
- **每條連線各自的有上限佇列。** 寫出不能互相等待。每條連線有自己的佇列，滿了就代表這個用戶端跟不上，應該中斷它，而不是讓它拖慢別人。
- **把收訊息和送訊息分開。** 發布用的 API 不應該等所有訂閱者都送到才回應，否則廣播一慢，你的後端也跟著慢。
- **水平擴充。** 單台伺服器總有上限。要加機器，就需要讓所有機器共享頻道和線上狀態，並在前面放負載平衡器。
- **規劃重連與漏掉的訊息。** 斷線、部署、網路切換之後，用戶端都會重新連線。重連要有退避，才不會全部同時湧回來；斷線期間發布的事件不會補送，所以應用程式重新連上後要再取得一次最新狀態。
- **以尖峰來測容量。** 用平均流量做的壓力測試會告訴你「沒問題」。要測的是尖峰的好幾倍，以及大量用戶端同時重新連線的情況。
- **監控佇列與延遲，而不只是 CPU。** CPU 看起來還有空間時，佇列可能已經在變長。要能看到訊息排了多久、有多少連線因為太慢被中斷，並在變差時收到告警。
- **有人值班。** 尖峰通常就是你最重要的時刻：活動開賣、比賽最後一分鐘、促銷開始。伺服器出事時要有人能處理。
- **追安全更新。** 作業系統、語言執行環境和函式庫的漏洞一公開，就要評估影響、升級並重新部署。

```text
flow: 解法的核心：每條連線各自排隊，慢的只影響自己
== 依序寫出
新事件 --發布--> 伺服器 --先寫給 B--> 連線 B|網路很慢 --等 B 寫完--> 其他所有連線|一起延遲
== 每條連線各自的佇列
新事件 --發布--> *伺服器 --各自的佇列--> 其他所有連線|準時收到
新事件 --發布--> *伺服器 --佇列滿了--> 連線 B|中斷後自動重連
```

## 託管服務替你承擔了什麼

使用託管的即時服務，上面這些工作就由服務商負責。以 Hibira 為例：

- **慢的用戶端不會拖慢別人。** 每條連線有自己的有上限佇列，寫入不會互相等待。佇列滿了或寫出逾時的用戶端會以 Pusher 協定的 4100 錯誤碼中斷，pusher-js 收到後會稍等一下再重新連線。中斷是為了保護其他連線，並不會讓那台裝置的網路變快；如果它還是很慢，重連後可能再次被中斷。
- **重新連上後由你的應用程式恢復狀態。** Pusher 協定不會補送用戶端斷線期間漏掉的事件。付費方案可以在應用程式設定中開啟頻道歷史，重新連線的用戶端就能用 `pusher:rewind` 補收最近幾分鐘的事件。不過報價、遊戲狀態這類一定要正確的資料，請把事件當成更新通知，連線恢復後向你的後端重新取得最新狀態；pusher-js 和 Laravel Echo 都會通知你連線已恢復。
- **你看得到發生了什麼。** 這類中斷會出現在應用程式的錯誤卡片上，標示為「過慢的用戶端被中斷」，顯示過去 24 小時，所有方案（包括免費方案）都有，你不用自己去翻伺服器記錄。
- **容量由我們規劃。** 伺服器組成叢集，共享頻道與線上狀態，前面有負載平衡器，並依 CPU、記憶體和最忙那台伺服器的連線數自動加開伺服器。新伺服器需要幾分鐘才能開始接連線，所以自動擴容應付的是持續成長的流量，不是瞬間的尖峰；在那之前，滿載的伺服器會用 4100 婉拒新連線，讓用戶端重試到其他伺服器。已經建立的連線不會被搬移。這是我們這一側的基礎架構；你的用量仍以方案的硬上限為準，不會在帳單上給你意外，而可用性 SLA 包含在 Enterprise 方案。
- **使用的是 Pusher 協定。** 你現有的 pusher-js、Laravel Echo 和伺服器端函式庫都能直接使用，換服務通常不需要重寫業務邏輯：改主機位址和憑證、重新設定 Webhook，再確認你用到的功能和限制。[遷移指南](https://hibira.io/zh-tw/migrate)列出支援範圍，以及怎麼不停機切換。
- **修補由我們負責。** WebSocket 伺服器和它底下執行環境的安全更新由我們處理。你仍然要更新自己的應用程式，但要盯的東西少了一項。

代價也要說清楚：超過免費額度後要付月費，事件會經過第三方，而且要接受方案的上限。

## 什麼時候自己架仍然合理

託管服務不是唯一的答案。以下情況，自己架可能更適合：

- 流量不大、穩定，而且沒有明顯的尖峰。
- 事件內容不能離開你的基礎架構。
- 你已經有維運團隊、監控和值班制度，多一個常駐服務不是負擔。

比較成本時要算整體：自己架的成本是伺服器，加上監控、擴容、修補和值班花掉的工程時間；託管服務則是依用量付的月費。流量小又穩定、已經有人在顧的話，自己架往往比較省；需要隨尖峰擴充、又沒有專人維運時，人力時間常常比伺服器還貴。

用 Laravel 的話，自己架 Reverb 還是用託管服務？這個比較我們下次再揭露。因為兩邊都使用 Pusher 協定，之後改變主意也只是改設定。

## 我們的結論

市面上已經有成熟的託管服務，直接採用可能更省事。但我們還是決定自己重建一遍：一方面想真正弄懂 WebSocket 和分散式即時通訊背後的運作，另一方面想實際量測不同架構的效能、資源消耗和營運成本，也藉此理解這類服務是怎麼定價的。

我們把即時通訊重新做成可以水平擴充的服務。前面放負載平衡器，每條連線有自己的有上限佇列，負載增加時自動加開伺服器，讓流量分散到更多伺服器上，而不是讓一台伺服器硬撐；連線數、訊息量和延遲都有監控和告警。我們先在內部使用，之後也開放給外部，這就是 Hibira。

自己架的 WebSocket 伺服器可以跑得很好。難的不是讓它跑起來，而是背壓控制、容量規劃、故障隔離、監控和安全更新這些持續的工作，讓它在最忙的那幾分鐘依然準時。那正好是使用者最在意、也最容易出事的時候。Hibira 把這些持續的工作做成託管服務，讓你的團隊把時間花在產品上。

Hibira 的免費方案不需要信用卡，足夠開發和小型應用使用。已經在用 Pusher 的話，通常只要更新連線設定，不需要重寫業務邏輯；步驟在[遷移指南](https://hibira.io/zh-tw/migrate)，我們自己的遷移經驗下次再揭露，敬請期待。
