You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

进程文件句柄泄漏排查:Too many open files与WebSocket异常

排查Go WebSocket服务CLOSE_WAIT套接句柄泄漏问题

咱们直接针对你遇到的问题拆解分析——大量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,提问作者Данияр

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 08:22:47