靠默认“一连接一goroutine+gorilla/websocket”模式撑不过10万连接;百万级必须调操作系统限制(ulimit与syscall.Setrlimit)、换事件驱动模型(epoll/kqueue替代阻塞读)、改内存分配策略(换gobwas/ws或sync.Pool缓冲区)。

直接说结论:靠默认的“一连接一 goroutine + gorilla/websocket”模式,撑不过 10 万连接;百万级必须做三件事——调操作系统限制、换事件驱动模型、改内存分配策略。
ulimit -n 不够大,syscall.Setrlimit 必须在程序启动时调用
Linux 默认 ulimit -n 是 1024,连 1 万连接都卡死在 accept 阶段。光靠 shell 临时改(ulimit -n 1000000)不够稳定,Go 程序自己得主动申请:
-
syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)先读当前上限 - 把
rLimit.Cur设成rLimit.Max,避免软限制拖后腿 - 立刻
syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)生效 - 注意:这个调用必须在
http.ListenAndServe之前,否则部分连接会因 fd 不足被静默丢弃
别让每个连接都起 goroutine —— 用 epoll 或 kqueue 替代阻塞读
gorilla/websocket 的 conn.ReadMessage() 是阻塞的,100 万连接 = 100 万个常驻 goroutine,调度开销和栈内存直接爆掉。正确做法是把连接套接字设为非阻塞,用系统事件机制统一监听:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- Linux 下用
epoll(Go 标准库没直接暴露,需封装syscall.EpollCreate1等) - macOS/BSD 下用
kqueue - 每个连接不再起 reader goroutine,而是由单个 epoll loop 轮询所有 socket 的
EPOLLIN - 收到数据后,再派发到 worker goroutine 解析,数量可严格控制在几十个以内
缓冲区不能每连接 malloc —— 换 gobwas/ws 或手动池化
gorilla/websocket 每次升级连接都会 new 出独立的 bufio.Reader/Writer,百万连接就是百万份 4KB+ 缓冲区,光这部分就吃掉 4GB+ 内存。优化路径只有两条:
- 换库:
gobwas/ws的ws.UpgradeHTTP返回的Conn支持复用底层[]byte缓冲区,实测内存降 60%+ - 自己池化:用
sync.Pool管理[][]byte,但要注意避免跨 goroutine 误用导致数据错乱 - 关键点:缓冲区大小要对齐 CPU cache line(通常 64 字节),否则频繁 false sharing 会拖慢原子操作
nf_conntrack_max 不调,连接会随机断连
内核连接跟踪表(conntrack)默认只存 65536 条记录,远低于百万连接所需。不调它,新连接会触发 nf_conntrack: table full, dropping packet,表现为客户端 ping 不通、连接秒断:
- 查当前值:
cat /proc/sys/net/netfilter/nf_conntrack_max - 临时调高:
echo 2621440 > /proc/sys/net/netfilter/nf_conntrack_max - 永久生效需写入
/etc/sysctl.conf,加一行net.netfilter.nf_conntrack_max = 2621440 - 注意:这个值不是越大越好,超过物理内存能支撑的条目数会导致 OOM killer 干掉进程
真正卡住人的从来不是 Go 语法或 WebSocket 协议本身,而是你忘了 Linux 内核眼里——每个连接都是一个 socket、一个文件描述符、一条 conntrack 记录、一块内核缓冲区。漏调其中任意一项,百万连接就只是个启动失败的日志。

















