真正卡住go程序百万级长连接的不是go本身,而是linux内核限制:必须同步配置fs.file-max、nf_conntrack_max、systemd的limitnofile及go进程内syscall.setrlimit,缺一不可。

单机跑百万级长连接,too many open files 不是 Go 写得不够好,而是 Linux 内核根本没给你开这个口子——fs.file-max、nf_conntrack_max、进程级 RLIMIT_NOFILE 三者任意一个卡住,连接数就断在十万级甚至更低。
systemd 启动的服务必须配 LimitNOFILE
你在终端里 ulimit -n 1048576 再运行 go run 是有效的,但生产环境几乎不用这种方式。用 systemctl start myapp 启动的服务,完全不读 /etc/security/limits.conf,也不继承 shell 的 ulimit。
- 编辑
/etc/systemd/system/myapp.service,在[Service]段下加:LimitNOFILE=1048576(同时设 soft 和 hard) - 改完必须执行:
sudo systemctl daemon-reload && sudo systemctl restart myapp - 验证是否生效:
cat /proc/$(pidof myapp)/limits | grep "Max open files",确认两列值都 ≥ 1048576 - 容器内运行时,若用 systemd 启动(如某些 Alpine 镜像),同样要配;否则优先靠
docker run --ulimit nofile=1048576:1048576
Go 程序启动时必须调 syscall.Setrlimit
systemd 配了只是“给了权限”,Go 进程默认仍按旧限制初始化。尤其在容器或非标准 init 环境中,getrlimit 返回的仍是 1024。必须在 main() 最早位置主动申请:
import "syscall"
func main() {
var rlim syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim); err == nil {
// 若当前 soft 已足够,可跳过;否则设为硬限或目标值
if rlim.Cur
- 别只设
Cur不设Max:软限制不能超过硬限制,否则Setrlimit静默失败 - 建议先
Getrlimit查当前值,避免重复设置或越权失败 - 该调用必须在任何 goroutine 启动前完成,否则新 goroutine 可能已继承旧限制
别漏掉内核级参数 fs.file-max 和 nf_conntrack_max
进程能开 1048576 个 FD,不代表内核允许整个系统开这么多。两个关键参数常被忽略,一满就丢包、断连、SYN 被 DROP:
-
fs.file-max:系统级最大文件句柄总数,百万连接至少设为2097152(2×连接数,留余量)。执行:sysctl -w fs.file-max=2097152,并写入/etc/sysctl.conf -
net.netfilter.nf_conntrack_max:连接跟踪表上限,默认仅 65536。百万连接需设为2621440或更高,否则日志出现nf_conntrack: table full, dropping packet,现象是连接秒断、ping 不通却无明显错误 - 顺手调大端口范围:
net.ipv4.ip_local_port_range="1024 65535"(虽不影响服务端 listen,但影响客户端 outbound 连接复用)
http.Server 或 websocket 库本身会吃光 FD
即使所有系统参数拉满,代码层不节制,FD 一样会提前耗尽。这不是配置问题,是设计缺陷:
-
http.Server必须设MaxConns(Go 1.19+)或自定义 listener 做 accept 限流,否则 SYN flood 就能把 FD 耗尽 - 用
gorilla/websocket时,每个连接独占 goroutine + 读写缓冲区,百万连接 ≈ 百万 goroutine + 数 GB 内存;换gobwas/ws可复用缓冲区,goroutine 数量压到几百个 - 务必注册
ConnState回调,用原子计数器监控真实连接数,而不是依赖netstat—— 后者可能包含已关闭但未回收的 socket - 所有超时(
ReadTimeout、WriteTimeout、IdleTimeout)必须显式设值,空闲连接不释放,FD 就永远卡在那里
真正卡住百万连接的,从来不是 Go 的 goroutine 调度,而是你没看到的那几行 sysctl 和 LimitNOFILE。它们不出错、不报 panic,只在连接数爬到 80 万时悄悄开始丢包、重连、GC 卡顿——那种“一切正常但就是撑不住”的状态,八成是 nf_conntrack_max 溢出了。











