进程文件句柄泄漏排查:Too many open files与WebSocket异常
咱们直接针对你遇到的问题拆解分析——大量TCPv6套接字卡在CLOSE_WAIT状态,部分永久残留导致文件描述符耗尽,核心原因肯定是服务端没有在所有场景下正确关闭WebSocket连接,结合你的代码来看,这些细节可能被忽略了:
一、先搞懂CLOSE_WAIT的本质
TCP连接进入CLOSE_WAIT,意味着客户端已经主动断开(发了FIN包),但服务端还没调用close()关闭套接字。所以咱们要找代码里哪些场景下,WebSocket连接没被正确关闭。
二、你的代码里的潜在问题点
1. PlayerListener异常场景未发送CloseMessage
当客户端断开连接时,ReadMessage()会返回错误,但你的代码里只是break跳出循环,然后靠defer来关闭WS。虽然defer会执行pc.WS.Close(),但WebSocket协议要求,服务端在收到客户端断开信号后,应该先主动发送CloseMessage,再关闭连接。如果跳过这一步,可能导致TCP连接的关闭流程不完整,套接字卡在CLOSE_WAIT。
2. PlayerWriter部分错误场景未处理关闭流程
比如当写消息出错时,你直接return,虽然defer会关闭WS,但同样没有先发送CloseMessage给客户端,这也可能导致连接关闭不彻底。
3. 多余的break语句容易误导(不影响逻辑但需注意)
你在select的每个case里都加了break,但Go里select的case执行完后,break只会跳出select块,不会跳出外层的for循环,这些break其实是多余的,容易让人误解逻辑。
4. 连接资源可能被goroutine泄漏持有
比如当PlayerWriter或PlayerListener退出后,如果Room还持有PlayerConn的引用,会导致GC无法回收连接资源,进而WebSocket的底层套接字一直没被关闭,停留在CLOSE_WAIT。
三、具体修复方案
1. 完善WebSocket关闭流程
在所有异常场景下,先发送CloseMessage再关闭连接:
修复PlayerListener:
for { _, command, err := pc.WS.ReadMessage() if err != nil { // 先发送标准关闭消息给客户端 _ = pc.WS.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) break } // ... 原有逻辑 }
修复PlayerWriter:
err := pc.WS.WriteMessage(websocket.TextMessage, message) if err != nil { _ = pc.WS.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "write failed")) return }
2. 移除多余的break语句
把select每个case里的break删掉,让逻辑更清晰:
for { select { case message, ok := <-pc.Ch: pc.WS.SetWriteDeadline(time.Now().Add(utils.WriteWait)) if !ok { pc.WS.WriteMessage(websocket.CloseMessage, []byte{}) return } err := pc.WS.WriteMessage(websocket.TextMessage, message) if err != nil { _ = pc.WS.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "write failed")) return } inst := &model.PlayerLeftInstruction{} _ = json.Unmarshal(message, inst) if inst.Instruction == utils.UtilAFK || inst.Instruction == utils.RoomMoneyLess { return } case <-ticker.C: pc.WS.SetWriteDeadline(time.Now().Add(utils.WriteWait)) if err := pc.WS.WriteMessage(websocket.PingMessage, nil); err != nil { _ = pc.WS.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "ping failed")) return } } }
3. 检查连接资源的清理逻辑
确保当PlayerConn退出时,Room的Leave通道能被正确处理,及时清理掉对PlayerConn的引用,避免goroutine泄漏:
- 检查
Room.Leave的处理逻辑,是否在收到消息后,从Room的玩家列表中移除对应的PlayerConn - 避免全局或长期持有PlayerConn的引用,确保GC能回收不再使用的连接资源
4. 验证TCP超时参数(临时缓解)
如果需要临时缓解问题,可以调整IPv6的TCP FIN超时(默认60秒):
# 临时生效 sysctl -w net.ipv6.tcp_fin_timeout=30 # 永久生效,编辑/etc/sysctl.conf echo "net.ipv6.tcp_fin_timeout=30" >> /etc/sysctl.conf sysctl -p
但这只是缓解,核心还是要修复代码里的连接关闭逻辑。
四、总结
最可能的根源是部分异常场景下,服务端没有遵循WebSocket协议的关闭流程,导致TCP连接的关闭不彻底,加上可能的连接资源泄漏,最终让套接字卡在CLOSE_WAIT状态。按照上面的修复点逐一调整后,应该能解决文件描述符持续增长的问题。
内容的提问来源于stack exchange,提问作者Данияр

