为什么我们不再自己部署 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 迁移的步骤,不改代码就换过来。