為什麼我們不再自己架 WebSocket 伺服器
· Hibira
自己架的即時伺服器一遇到訊息尖峰就會塞住,還得自己追 PHP2Shell 這類漏洞的修補。我們踩過的坑、自己架要補上的東西,以及託管服務替你承擔了什麼。

在做 Hibira 之前,我們在過去的專案裡自己維運過 WebSocket 伺服器,當時是用 PHP 架的,而且和應用程式一起單體部署。平常一切正常,訊息量一大就出事:伺服器忙著處理手上的訊息,新的請求在後面排隊,延遲一路往上爬。等訊息終於送到使用者手上,內容已經過時了。報價已經變了、比分早就更新,「對方正在輸入」的提示在對方送出訊息之後才出現。
這篇文章整理那次的經驗:即時伺服器為什麼會在尖峰時塞住、自己架的話要補上哪些東西,以及什麼時候交給託管服務比較划算。
塞住是怎麼發生的
即時伺服器做的事情看起來很單純:收到一則事件,送給所有訂閱的連線。問題在於這件事的工作量會被放大,而且只要有一段慢下來,後面的全部都得等。
進來的比處理的快,佇列只會變長
這是排隊的基本規則:只要訊息進來的速度超過伺服器處理的速度,佇列就會一直變長,每一則新訊息都得等前面所有的訊息處理完。
用一個簡單的數字算算看。假設伺服器每秒能處理 5,000 則訊息,尖峰時每秒進來 6,000 則,持續 10 秒。這 10 秒會累積 10,000 則處理不完的訊息。就算尖峰結束、新訊息完全停止,伺服器也還要再花 2 秒才能清完,佇列最後面那則訊息會晚 2 秒才送出。如果尖峰後還有正常流量進來,清得更慢。
對網頁來說,慢 2 秒是體驗差一點;對即時功能來說,2 秒前的報價、位置或遊戲狀態,常常就是錯的資料。
一則事件,乘上每一位訂閱者
發布一次不等於送出一次。一個有 2,000 人訂閱的頻道,每發布一則事件,伺服器就要寫出 2,000 次。所以實際的工作量是「每秒發布次數 × 訂閱人數」。直播聊天室、比賽比分、營運儀表板這類一對多的情境,最容易在人數變多的那一刻突然超過負荷。
一條慢的連線,拖住所有人
手機在電梯裡、在捷運上,網路會變得很慢,甚至暫時收不到資料。如果伺服器是一條一條連線依序寫出、而且每次都等寫完才換下一條,只要有一條連線卡住,排在它後面的每一位使用者都會跟著延遲。這就是我們遇到的「阻塞」:問題不在大多數使用者,而在少數幾條慢連線,把整個廣播拖慢了。
等待會互相傳染
伺服器一忙,負責發布事件的 HTTP 請求也開始變慢。你的後端在等它回應,自己的請求也跟著堆積;請求逾時之後如果自動重試,又在已經過載的伺服器上多加一份工作。原本只是即時伺服器慢,最後變成整個系統都慢。
單體部署讓問題放大
我們當時遇到的狀況,主要是因為採用單體部署:WebSocket 伺服器和應用程式的其他部分跑在一起、一起部署。即時訊息一多,吃掉的是整個應用程式的資源,連一般的 API 請求也跟著排隊;反過來,其他功能一慢,即時訊息也一起被拖住。想擴充即時的部分,只能把整個應用程式一起加機器。而在單體部署裡,每次部署應用程式,通常也會讓所有 WebSocket 連線斷開重連。
另一個原因:執行環境的安全更新
效能之外,讓我們決定不再自己維運的另一個原因是安全。我們當時也遇過 PHP 的安全漏洞,只能自己追著修補。自己架伺服器,作業系統、語言執行環境、函式庫的漏洞都得自己盯,一個都不能漏。
最近的例子是 2026 年 10 月 21 日 PHP 官方修補的兩個嚴重漏洞,被稱為「PHP2Shell」:
- 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 程序。但道理是一樣的:漏洞公開的那一刻,你得自己確認哪些伺服器受影響、排時間升級、重新部署,而且越快越好。
自己架的話,要補上這些
這些問題都有解法,只是每一項都是持續的工作,不是設定一次就好:
- 和應用程式分開部署。 把即時伺服器從單體中拆出來,讓它可以獨立擴充、獨立部署,尖峰時才不會拖垮其他功能。
- 每條連線各自的有上限佇列。 寫出不能互相等待。每條連線有自己的佇列,滿了就代表這個用戶端跟不上,應該中斷它,而不是讓它拖慢別人。
- 把收訊息和送訊息分開。 發布用的 API 不應該等所有訂閱者都送到才回應,否則廣播一慢,你的後端也跟著慢。
- 水平擴充。 單台伺服器總有上限。要加機器,就需要讓所有機器共享頻道和線上狀態,並在前面放負載平衡器。
- 以尖峰來測容量。 用平均流量做的壓力測試會告訴你「沒問題」。要測的是尖峰的好幾倍,以及大量用戶端同時重新連線的情況。
- 監控佇列與延遲,而不只是 CPU。 CPU 看起來還有空間時,佇列可能已經在變長。要能看到訊息排了多久、有多少連線因為太慢被中斷,並在變差時收到告警。
- 有人值班。 尖峰通常就是你最重要的時刻:活動開賣、比賽最後一分鐘、促銷開始。伺服器出事時要有人能處理。
- 追安全更新。 作業系統、語言執行環境和函式庫的漏洞一公開,就要評估影響、升級並重新部署。
依序寫出
每條連線各自的佇列
託管服務替你承擔了什麼
使用託管的即時服務,上面這些工作就由服務商負責。以 Hibira 為例:
- 慢的用戶端不會拖慢別人。 每條連線有自己的有上限佇列,寫入不會互相等待。佇列滿了或寫出逾時的用戶端會以 Pusher 協定的 4100 錯誤碼中斷,pusher-js 收到後會自動重新連線。
- 你看得到發生了什麼。 這類中斷會出現在應用程式的錯誤卡片上,標示為「過慢的用戶端被中斷」,你不用自己去翻伺服器記錄。
- 容量由我們規劃。 伺服器組成叢集,共享頻道與線上狀態,可以水平擴充。方案的上限是明確的硬上限,不會在帳單上給你意外。
- 使用的是 Pusher 協定。 你現有的 pusher-js、Laravel Echo 和伺服器端函式庫都能直接使用,換服務只是改主機位址和金鑰。
- 修補由我們負責。 WebSocket 伺服器和它底下執行環境的安全更新由我們處理。你仍然要更新自己的應用程式,但要盯的東西少了一項。
代價也要說清楚:超過免費額度後要付月費,事件會經過第三方,而且要接受方案的上限。
什麼時候自己架仍然合理
託管服務不是唯一的答案。以下情況,自己架可能更適合:
- 流量不大、穩定,而且沒有明顯的尖峰。
- 事件內容不能離開你的基礎架構。
- 你已經有維運團隊、監控和值班制度,多一個常駐服務不是負擔。
如果你用 Laravel,自己架 Reverb 還是用託管服務有更具體的比較。因為兩邊都使用 Pusher 協定,之後改變主意也只是改設定。
我們的結論
所以我們決定重建:把即時通訊從單體中拆出來,做成獨立的服務。前面放負載平衡器,依負載自動橫向擴容,流量瞬間湧入時就加開伺服器分擔,而不是讓一台伺服器硬撐。我們先在內部使用,之後也開放給外部,這就是 Hibira。
自己架 WebSocket 伺服器,難的不是讓它跑起來,而是讓它在最忙的那幾分鐘依然準時。那正好是使用者最在意、也最容易出事的時候。如果你的團隊想把時間花在產品上,而不是佇列和值班上,託管服務是比較省力的選擇。
Hibira 的免費方案不需要信用卡,足夠開發和小型應用使用。已經在用 Pusher 的話,可以照著從 Pusher Channels 遷移的步驟,不改程式就換過來。