WebSocket长连接怎么防止连接数被耗尽
摘要:# 当WebSocket长连接变成“耗电狂魔”:你的服务器是怎么被拖垮的 前两天跟一个做在线教育的朋友聊天,他愁眉苦脸地说:“我们那个实时答题系统,一到高峰期就卡,用户老掉线。查了半天,CPU内存都正常,就是新用户死活连不上来。” 我问他:“你们用We…
当WebSocket长连接变成“耗电狂魔”:你的服务器是怎么被拖垮的
前两天跟一个做在线教育的朋友聊天,他愁眉苦脸地说:“我们那个实时答题系统,一到高峰期就卡,用户老掉线。查了半天,CPU内存都正常,就是新用户死活连不上来。”
我问他:“你们用WebSocket做长连接的吧?最大连接数设了多少?”
他愣了一下:“这个……没专门设过,服务器默认的。”
得,问题八成就在这儿了。 很多团队上WebSocket的时候,光顾着高兴“终于能实时推送了”,却忘了长连接这玩意儿,本质上是在服务器上开了个长期占着的“包间”。包间数量有限,来的人多了,后来的就只能排队——或者直接被拒之门外。
一、连接数耗尽,比你想象中来得快
先别急着翻文档查配置,咱们算笔简单的账。
假设你用的是一台常见的4核8G云服务器,跑着Nginx和Node.js(或者其他语言的服务)。理论上,单个进程能支撑的WebSocket连接数,受限于文件描述符限制和内存。
Linux系统默认给单个进程的文件描述符限制通常是1024。就算你调高了系统级限制,比如调到65535,你猜猜一个8G内存的服务器,纯维持WebSocket连接(不算业务逻辑),能撑多少?
一个空闲的WebSocket连接,大概占20-30KB内存。 听着不多对吧?那我们来算:
- 8G内存,留一半给系统和业务,剩4G(约4000MB)给连接。
- 4000MB / 30KB ≈ 13.3万连接。
看起来不少?但这是理想状态。实际上,一旦有消息收发,内存占用会涨,CPU要处理心跳、解码。而且,连接不是均匀来的——搞个活动,瞬间涌进来几万人,连接池一下就满了。更可怕的是,如果遇到恶意攻击,对方用脚本批量建连接,只连不发,专占坑位,你的正常用户瞬间就被挡在门外。
这感觉就像:你开了个网红餐厅,突然来了100个“顾客”,每人占一张桌,只点一杯白水坐一天。真正的食客?对不起,没位置了。
二、防耗尽,不是简单调个参数就行
很多人第一反应是:“那把最大连接数调大不就行了?”
真这么简单就好了。 盲目调大,就像为了应对洪水,把堤坝无限加高——最后洪水没来,堤坝自己先塌了(内存溢出,服务器崩溃)。你得有一套组合拳。
1. 设个“包间”使用规则(连接限制与超时)
首先,每个服务端必须设置明确的连接数上限。这个上限不是拍脑袋定的,要根据压测结果来。比如你的服务器压测到12万连接时响应明显变慢,那就设个10万的安全阈值。
其次,给连接加上“最长包厢时间”。也就是心跳超时机制。WebSocket本身没有强制心跳,但你需要自己实现。客户端每隔一段时间(比如30秒)发个心跳包(ping/pong),如果超时(比如90秒)没收到,服务端主动断开。这样能清理掉那些断了线但没正常关闭的“僵尸连接”。
// 一个简单的Node.js WS服务端心跳示例
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws, req) {
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true; // 收到pong,标记为活跃
});
// ... 你的业务逻辑
});
// 每30秒检查一次
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // 超时了,断开
}
ws.isAlive = false;
ws.ping(); // 发送心跳探测
});
}, 30000);
2. 别让“占座党”得逞(IP与Session限流)
针对恶意连接,光超时不够,得从源头控制。
- IP级连接数限制:同一个客户端IP,限制其最大并发连接数。比如,一个IP最多同时保持10个WebSocket连接。这能有效防止单IP用脚本疯狂建连。Nginx层面就可以做:
# 在Nginx配置中,使用limit_conn模块 limit_conn_zone $binary_remote_addr zone=perip:10m; limit_conn perip 10; # 每个IP同时最多10个连接 - 用户级连接数限制:如果用户已登录,可以用userId来限制。一个用户账号,在同一设备类型上,原则上只保持1个活跃连接。新连接建立时,踢掉旧连接。避免用户自己“左右互搏”。
3. 准备个“备用大厅”(水平扩展与负载均衡)
单台服务器总有瓶颈。真正的解决方案是别把鸡蛋放一个篮子里。
通过负载均衡(如Nginx的ip_hash或一致性哈希),将WebSocket连接分散到多台后端服务器上。这样,每台服务器的连接数上限就成了集群的总上限。假设单台限10万连接,5台服务器就能撑50万。
这里有个关键细节:会话粘滞(Session Affinity)。因为WebSocket是长连接,一旦连接建立,后续通信必须落到同一台后端服务器,否则会找不到连接状态。所以负载均衡策略要选对,不能单纯轮询。
4. 雇个“保安队长”(接入层防护与WAF)
在流量进入你的WebSocket服务器之前,在前面加一层防护。
- 高防IP/高防CDN:可以清洗掉大量协议畸形或流量异常的连接请求。很多DDoS攻击在协议层就被拦下了,到不了你业务服务器。
- Web应用防火墙(WAF):可以配置针对WebSocket握手阶段的规则。比如,检查Upgrade头、Origin头是否合法,拦截那些连握手包都发不对的恶意脚本。
- 慢连接攻击防护:有些攻击会建立连接后,以极慢的速度发送数据,耗尽你的线程池。在Nginx里可以配置
client_body_timeout和client_header_timeout,超时直接断。
三、那些“PPT里不提”的坑
说了这么多标准动作,我再分享几个实战中容易踩的坑,这些你在很多方案文档里可看不到。
坑1:心跳间隔太短,把自己“心跳”死了。 为了快速发现死连接,有人把心跳设成5秒一次。结果呢?连接数是稳了,但CPU开销暴涨,尤其是连接数大的时候,光处理心跳包就够忙了。心跳不是越短越好,要根据业务容忍度来。用户掉线30秒重连能接受吗?如果能,心跳设30-45秒完全没问题。
坑2:只防了建立,没防“保持”。 攻击者建立连接后,可以规规矩矩地发心跳,长期保持连接活跃,就是不干正事。这种“合法占用”更难防。这时候需要业务层监控:如果一个连接建立后,长时间(比如10分钟)没有任何业务消息交互,只有心跳,可以将其标记为“低活跃度连接”,在连接池紧张时优先回收其资源。
坑3:忽略客户端多样性。 移动端网络不稳定,用户进出电梯、切换WiFi/4G,都会导致连接断开重连。如果你的重连机制太激进(比如断开立即重连,无限重试),会在网络抖动时产生重连风暴,瞬间压垮服务器。好的做法是指数退避重连:第一次断,等1秒再连;再失败,等2秒、4秒、8秒……给服务器喘息之机。
四、说到底,这是一种“资源管理”思维
防止WebSocket连接数耗尽,归根结底是对有限服务器资源的一种精细化管理。它不像防CC攻击那样轰轰烈烈,更像是物业管理——得清楚有多少车位,谁在停车,停了多久,僵尸车怎么清理,访客怎么安排。
所以,下次当你设计一个实时应用时,别光盯着功能能不能跑通。在技术评审会上,多问一句:“咱们这个长连接,打算怎么管?”
方案再漂亮,真到流量洪峰来的那一刻,能稳稳接住的,永远是那些提前把“下水道”疏通好的团队。
行了,不废话了,如果你源站的长连接还在“裸奔”,我劝你今晚就加个班,把心跳和限流先配上。这玩意儿,平时感觉没用,真出了事,就是救命的。

