Go标准库net/http默认WebSocket服务无法支撑10万连接,因其阻塞式模型导致goroutine泛滥、内存飙升、GC频繁且无法复用TCP连接;须改用gorilla/websocket等手动升级连接并配合I/O复用、连接生命周期管理及主动心跳检测。

为什么 net/http 默认 WebSocket 服务扛不住 10 万连接
Go 标准库的 http.Serve 是阻塞式模型,每个连接占一个 goroutine,而默认 runtime.GOMAXPROCS 和调度器在高并发下会因频繁切换、栈分配、GC 扫描导致内存飙升(常达 2–3 GB/10w 连接)。更关键的是,标准 http.ResponseWriter 没有暴露底层 TCP 连接控制权,无法对接 epoll 级别复用。
- 现象:
runtime.ReadMemStats显示Mallocs持续上涨,HeapInuse居高不下,GC 频次超过 5s/次 - 根本原因:没绕过
http.Server的中间层封装,没接管net.Conn生命周期 - 必须换用
gorilla/websocket或gobwas/ws手动升级连接,并配合自定义 listener
如何用 epoll 思路复用 goroutine 而不真用 syscall.epoll
Go 不暴露 epoll 接口,但可通过 net.Conn.SetReadDeadline + net.Conn.SetWriteDeadline 配合非阻塞读写 + runtime.Gosched() 让调度器自然挂起 idle goroutine,等同于“用户态 epoll 池”。核心是避免每个连接独占 goroutine,改用固定数量 worker 处理所有连接的 I/O 事件。
- 用
sync.Pool复用websocket.Conn的读写 buffer,避免每次ReadMessage分配新 slice - worker 数量建议设为
runtime.NumCPU() * 2,太少吞吐不足,太多反而增加调度开销 - 禁用
websocket.Conn.SetPongHandler默认实现(它会启动 goroutine),改用同步响应Ping帧 - 示例关键点:
conn.SetReadLimit(512 * 1024) conn.SetReadDeadline(time.Now().Add(30 * time.Second)) // 后续在 worker 中循环调用 conn.ReadMessage()
gorilla/websocket 升级后忘记关闭底层 net.Conn 的后果
调用 upgrader.Upgrade 后,HTTP 连接已转为裸 TCP 连接,但 http.Request.Body 和底层 net.Conn 仍被持有。若未显式关闭,连接不会真正释放,FD 泄露,netstat -an | grep :PORT | wc -l 持续增长,最终 hit EMFILE 错误。
- 正确做法:在 Upgrade 成功后立即
req.Body.Close(),并在连接断开时conn.UnderlyingConn().Close() - 容易漏掉的场景:panic 恢复路径、超时退出、
websocket.CloseMessage发送后未 Close underlying - 验证方式:用
lsof -p PID | grep TCP | wc -l对比连接数与活跃 conn map 长度
心跳和断连检测必须自己做,别信 Ping/Pong 自动机制
gorilla/websocket 的 SetPingHandler 只负责接收 Ping 并自动回 Pong,但不发 Ping,也不检查对方是否失联。客户端假死、NAT 超时、防火墙静默丢包都会让连接卡在 ESTABLISHED 状态,内存持续占用。
立即学习“go语言免费学习笔记(深入)”;
- 必须启动独立 goroutine 定期向每个 conn 发
Ping,并设置conn.SetWriteDeadline,写失败即标记断连 - 不要用
time.Ticker全局广播,应 per-conn 启动,避免单个慢连接拖垮全部心跳节奏 - 断连清理必须加锁操作 conn map,且要防止 double-close:先从 map 删除,再调
conn.Close()
最麻烦的其实是连接恢复逻辑——重连窗口、消息去重、状态同步这些,都得在应用层补全。底层优化只解决“能扛住”,不解决“怎么稳住”。










