ulimit -n 是第一个要查的指标,因为Linux默认限制进程最多打开1024个文件描述符,而每个socket连接均占用一个FD,超限即触发“too many open files”错误,直接导致HTTP服务无法accept新连接。

为什么 ulimit -n 是第一个要查的指标
Go HTTP 服务在百万并发下崩掉,八成不是代码写错了,而是进程连打开文件的资格都没了。Linux 中每个 socket 连接、每个打开的文件、每个管道都占用一个文件描述符(FD),而默认 ulimit -n 是 1024 —— 意味着最多处理 1024 个并发连接,连测试环境都过不了。
你启动服务后,用 lsof -p $(pidof your-server) 查当前 FD 使用量,如果接近或超过 ulimit -n 的值,立刻就会出现 accept: too many open files 或 socket: too many open files 错误。
- 临时调高:运行前执行
ulimit -n 1048576 - 永久生效:在
/etc/security/limits.conf加两行:* soft nofile 1048576* hard nofile 1048576 - 注意:systemd 启动的服务需额外在 service 文件里加
LimitNOFILE=1048576,否则 ulimit 设置会被忽略
http.Server 必须显式限制连接数,不能依赖默认行为
标准库的 http.ListenAndServe 不做任何连接数限制,每个新连接就启一个 Goroutine,FD 耗尽只是时间问题。必须用自定义 http.Server 并配 MaxConns 和超时控制。
示例关键配置:
server := &http.Server{
Addr: ":8080",
Handler: myHandler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
// Go 1.19+ 支持,低于此版本需用第三方中间件或自定义 listener
MaxConns: 50000,
}
-
MaxConns是硬性上限,超出的连接会被内核直接拒绝(SYN DROP),不进应用层 -
IdleTimeout防止长连接空占 FD;若用 WebSocket 或 HTTP/2,这个值更要收紧 - 旧版 Go(MaxConns,得自己 wrap
net.Listener,用semaphore或sync.Pool+ 计数器控制 accept
连接池和复用比“开 Goroutine”更重要
很多人以为“Go 天然高并发” = “随便开 Goroutine 就行”,但真正压垮服务的,常是下游资源没节制:比如每个请求都新建 http.Client、直连数据库不走连接池、反复 os.Open 文件不关闭。
典型错误链:
1 个请求 → 1 个 Goroutine → 1 次 DB 查询 → 新建连接 → 占 1 个 FD → 查询完连接未归还 → FD 泄漏
- HTTP 出站调用:始终复用
&http.Client{Transport: &http.Transport{MaxIdleConns: 100, MaxIdleConnsPerHost: 100}} - 数据库:用
sql.DB.SetMaxOpenConns(50)显式限流,别依赖驱动默认值(常为 0,即无上限) - 文件操作:避免高频
os.Open;热文件用sync.Pool缓存*os.File,冷文件确保defer f.Close()
系统级 FD 限制和 Go 运行时的隐含消耗
别只盯着 ulimit -n。Go 程序自身也会悄悄吃掉几百个 FD:pprof 监听端口、trace 日志句柄、GC 堆栈采样文件、甚至 runtime.LockOSThread 绑定的线程都可能持有 FD。
更隐蔽的是:Goroutine 数量本身不直接消耗 FD,但每个活跃连接背后至少对应 1 个 socket FD,而每个 socket FD 在内核中还关联着接收/发送缓冲区、TCP 状态结构体等内存开销。所以当看到 lsof 显示用了 60000 个 FD,实际内存占用可能已超 4GB。
- 监控建议:用
cat /proc/<pid>/fd | wc -l实时看 FD 总数,比lsof更轻量 - 调试技巧:在 panic 或 SIGUSR1 时打印
runtime.NumGoroutine()和len(runtime.Frames()),交叉判断是 Goroutine 堆积还是 FD 耗尽 - 真正到百万并发时,单机靠调参数已不够——得切分连接(如用 SO_REUSEPORT)、引入代理层(nginx 限流)、或改用连接复用模型(如 QUIC 或自定义二进制协议)


















