上周五晚上,正当我准备美滋滋地过周末时,线上突然炸了。用户反馈聊天消息发不出去,实时数据不更新,客服电话被打爆。我赶紧上服务器看日志,好家伙,WebSocket连接数直接飙到十几万,内存占用爆炸。
这不是我第一次被WebSocket坑了,但绝对是死得最惨的一次。今天就把这些年踩过的坑整理一下,希望你们别重蹈覆辙。
第一个坑:以为WebSocket是长生不老的
很多人以为,建立WebSocket连接之后就可以躺着睡大觉了。朋友,你太天真了。
网络世界比你想象的脆弱多了。网络波动、负载均衡器超时、NAT会话过期、还有各种奇奇怪怪的网络设备,都可能导致连接悄无声息地断掉。用户还以为在聊天,实际上消息全发给了空气。
解决方案:心跳检测+自动重连。这俩是WebSocket的保命套装,缺一不可。
// 心跳检测示例
const HEARTBEAT_INTERVAL = 30000; // 30秒发一次心跳
const HEARTBEAT_TIMEOUT = 5000; // 5秒没响应就判定为挂了
let heartbeatTimer = null;
let heartbeatTimeoutTimer = null;
function startHeartbeat() {
clearInterval(heartbeatTimer);
clearTimeout(heartbeatTimeoutTimer);
heartbeatTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: ping }));
// 等待pong响应
heartbeatTimeoutTimer = setTimeout(() => {
console.warn("心跳超时,连接可能已死");
ws.close();
}, HEARTBEAT_TIMEOUT);
}
}, HEARTBEAT_INTERVAL);
}
ws.onmessage = (event) => {
if (event.data === "pong") {
clearTimeout(heartbeatTimeoutTimer);
}
};
第二个坑:把所有消息都塞进一个连接
有些同学喜欢在一个WebSocket连接里传输所有类型的数据:聊天消息、实时通知、系统报警、文件传输......结果呢?一个大文件传半天,其他小消息全被堵死在队列里。
更骚的操作是,有人居然用WebSocket传GB级别的文件。我不知道该说你胆子大还是脑子有坑。
解决方案:分通道、按优先级。不同类型的数据走不同的连接,或者干脆用专门的协议。文件传输这种大块头,老老实实用TCP直连或者S3预签名URL吧。
第三个坑:不考虑后端水平扩展
单机WebSocket玩得溜,上了集群就抓瞎。用户连上了Server A,消息发到了Server B,两边谁也不认识谁。
我见过最离谱的方案是把所有WebSocket请求都路由到同一台机器。恭喜你,成功把分布式系统变成了单机系统,顺便把扩展性也一起埋葬了。
解决方案:Redis发布/订阅 + 消息队列。用户连接状态存Redis,消息通过消息队列广播,所有服务器节点都能收到。这样用户无论连到哪台机器,都能收到消息。
# Redis Pub/Sub 架构简图
用户A --WS--> Server A --publish--> Redis Channel:room_001
用户B --WS--> Server B --subscribe---/
用户C --WS--> Server C --subscribe---/
当用户A发消息时,Server A发布到Redis,所有订阅的服务器都能收到,然后转发给各自连接的用户
第四个坑:没有优雅关闭的概念
很多团队部署新版本时,直接kill -9把服务停掉。结果呢?用户手里的连接全部断开,正在填的表单、正在传输的文件,全部打水漂。用户一脸懵逼,投诉电话响个不停。
解决方案:平滑关闭四部曲。
- 先把负载均衡器从池子里摘掉,停止接收新连接
- 给所有连接发送关闭通知,让客户端开始重连
- 等待一段时间(比如30秒),让现有连接完成手头工作
- 最后才真正停掉服务
// 平滑关闭示例(Node.js)
process.on("SIGTERM", async () => {
console.log("收到SIGTERM,开始平滑关闭...");
// 1. 通知所有连接准备关闭
wss.clients.forEach((client) => {
client.send(JSON.stringify({
type: "server_closing",
message: "服务器即将重启,请做好准备"
}));
client.close();
});
// 2. 等待一段时间让连接关闭
await new Promise(resolve => setTimeout(resolve, 30000));
// 3. 真正退出
process.exit(0);
});
第五个坑:忽视安全
WebSocket因为是持久连接,很容易成为攻击者的目标。连接数耗尽、恶意大量消息、跨站WebSocket劫持......这些问题不重视,分分钟被人按在地上摩擦。
解决方案:
- 限制单个IP的最大连接数
- 消息频率限制
- 使用WSS(WebSocket Secure)
- 验证Origin头
- 对消息内容做校验和过滤
写在最后
WebSocket是个好技术,但它不是银弹。用好了是神器,用砸了是灾难。
我的建议是:能不用WebSocket就不用,如果你的场景可以用轮询解决,那就轮询。毕竟轮询虽然low,但胜在稳定、简单、好排查问题。等真正遇到实时性需求了,再上WebSocket。
记住,最好的架构不是最先进的那个,而是最适合当前业务规模和团队能力的那一个。别为了技术而技术。
好了,我去修bug了,周末什么的,不存在的。