百万WebSocket连接需突破三道硬门槛:一是ulimit -n调至1048576避免EMFILE;二是nf_conntrack_max设为2621440防丢包;三是换用gobwas/ws+epoll事件循环,复用缓冲区并消除goroutine风暴。

靠默认配置和通用库(比如 gorilla/websocket)根本撑不到百万连接——不是代码写得不够好,而是系统资源早被耗尽、内核机制直接拦在门外。真正卡脖子的从来不是业务逻辑,是文件描述符、连接跟踪表、缓冲区分配这三道硬门槛。
ulimit -n 不调到 1048576,accept() 就会持续返回 EMFILE
每个 WebSocket 连接对应一个 socket 文件描述符,Linux 默认 ulimit -n 是 1024,连 1 万都撑不住。不改这个,服务启动后新连接全被拒绝,日志里反复出现 accept: too many open files。
- 临时生效:运行前执行
ulimit -n 1048576,再启动 Go 或 Node.js 进程 - 永久生效:往
/etc/security/limits.conf加两行:* soft nofile 1048576和* hard nofile 1048576 - 注意:如果用 systemd 启动服务,还得在 service 文件里加
LimitNOFILE=1048576,否则 limits.conf 不生效 - 验证是否到位:启动后进进程里执行
cat /proc/$(pidof yourserver)/limits | grep "Max open files"
nf_conntrack_max 不设到 2621440,conntrack 表一满就丢包
Linux 内核用 nf_conntrack 模块跟踪每个 TCP 连接状态。WebSocket 长连接不会快速释放,但默认 nf_conntrack_max 通常只有 65536 左右。一旦打满,内核开始丢包,现象是客户端频繁重连、握手失败,dmesg 里刷出 nf_conntrack: table full, dropping packet。
- 临时改:执行
echo 2621440 > /proc/sys/net/netfilter/nf_conntrack_max - 永久改:在
/etc/sysctl.conf加一行net.netfilter.nf_conntrack_max = 2621440,再运行sysctl -p - 确认模块已加载:
lsmod | grep nf_conntrack,没输出就得先modprobe nf_conntrack - 别漏掉清理旧连接:
echo 1 > /proc/sys/net/netfilter/nf_conntrack_tcp_be_liberal可缓解 TIME_WAIT 占用
gorilla/websocket 换成 gobwas/ws,内存从 3.2GB 降到 860MB
gorilla/websocket 默认为每个连接 malloc 独立的读写缓冲区(各 4KB),100 万连接光缓冲区就吃掉 ~8GB 内存;更致命的是它强制配一个 goroutine 等着读,百万 goroutine 带来的是调度风暴,不是并发能力。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
-
gobwas/ws把缓冲区管理交还给应用层:用sync.Pool显式借还,连接之间复用同一块内存 - 关键差异:
ws.ReadFrame(conn, buf)的buf是你传进去的,用完必须put回池;而gorilla的ReadMessage完全隐藏了 buffer 生命周期 - 代价是 API 不兼容:要重写消息收发逻辑,不能直接套用现有
Upgrader流程 - 实测数据:100 万空连接下,goroutine 数量从百万级压到几百个,heap profile 显示对象分配锐减
epoll + 单线程事件循环才是百万连接的底层支撑
哪怕系统参数全调对、库也换了,如果还是“一个连接起一个 goroutine”,照样崩。Go 的 goroutine 虽轻量,但 100 万个仍占至少 200MB 栈空间,且调度器无法高效处理这种规模的抢占切换。
- 真正的解法是绕过阻塞 I/O:用
epoll(Linux)或kqueue(macOS)做多路复用,单 goroutine 监听数万连接的读写就绪事件 - 项目
1m-go-websockets/4_optimize_gobwas/epoll.go就是这么干的:syscall.EpollWait返回就绪列表,挨个处理帧,不阻塞、不挂起 - 这意味着你不能再依赖
conn.Read()这类同步调用,所有读写必须基于事件回调+缓冲区复用 - 别指望 runtime 自动优化——Go 的
net.Conn默认走阻塞 syscall,必须用底层 syscall 或封装好的异步库(如gobwas/ws配合自定义net.Conn实现)
最常被跳过的一步:调完 ulimit 和 nf_conntrack_max 后,忘了改 net.ipv4.ip_local_port_range。客户端发起连接时需要本地端口,范围太窄会导致 connect: cannot assign requested address,尤其在压测机上高频建连时必现。










