查清当前进程真实fd限制值须以cat /proc/$(pidof yourapp)/limits | grep "max open files"输出的软限为准,而非ulimit -n;go启动时仅读取一次该值,后续动态调整无效,且http.server未设超时或os.open/response.body未及时关闭是主要泄漏源。

查清当前进程真实的 fd 限制值
别信 ulimit -n 输出——它查的是启动 Go 进程的 shell 的软限,不是 Go 进程自己读到的值。容器里尤其容易误判:你看到 ulimit -n 是 65536,但 Go 启动时从 /proc/self/limits 读到的仍是 1024。
进容器后执行:cat /proc/$(pidof yourapp)/limits | grep "Max open files"
看 “Max open files” 行的两个数字(软限 / 硬限),这才是 Go 实际受控的上限。
更稳妥的做法是在 main() 开头主动读取:unix.Getrlimit(unix.RLIMIT_NOFILE, &rlim),取 rlim.Cur 做校验。若 rlim.Cur ,直接 <code>log.Fatal("fd soft limit too low"),别等运行中崩。
http.Server 是 fd 泄漏最常被忽略的源头
没设超时的 HTTP 服务,一个慢客户端或代理 keep-alive 失配,就能让 fd 卡在 ESTABLISHED 或 CLOSE_WAIT 状态几分钟。这不是“并发高”,是连接没被及时驱逐。
必须显式配置以下三项:
• ReadTimeout 和 WriteTimeout 设为 ≤30s,防止单请求长期占着 fd
• IdleTimeout 设为 60–120s,比 Nginx 的 keepalive_timeout 小 10s,避免半开连接堆积
• Go 1.19+ 可用 MaxConns 硬限总连接数(注意:它不区分活跃/空闲,适合突发压制)
漏掉其中任意一项,lsof -i :yourport 都会看到 ESTABLISHED 数持续上涨,最终触发 too many open files。
os.Open 和 http.Response.Body 漏关是最常见的泄漏点
defer f.Close() 看似万能,但实际有多个失效场景:
• 放在 for 循环里:5000 次 os.OpenFile + 5000 个 defer,函数返回前才批量执行,中间就可能超限
• 函数 panic 且未 recover,或调用 os.Exit,defer 根本不执行
• http.Response.Body.Close() 没写,底层 TCP 连接不会释放,fd 一直挂着
正确做法:
• 文件操作封装成独立函数,打开 → 使用 → 关闭,作用域收窄
• 优先用 os.ReadFile / os.WriteFile,它们内部自动开闭,无泄漏风险
• HTTP 请求务必手动 response.Body.Close(),不要只依赖 defer(尤其在循环中)
• 临时文件用 os.CreateTemp,业务逻辑结束立即 os.Remove,不等 GC
容器和 systemd 中 ulimit 生效必须盯住 PID 1
Go runtime 启动时只读一次 /proc/self/limits,后续容器动态调高 ulimit 它完全感知不到。K8s 的 securityContext.ulimits 或 docker run --ulimit 若没作用到 PID 1 进程,就等于没设。
验证方式:
• 进容器执行 ps aux | grep yourapp 找到主进程 PID
• 再执行 cat /proc/<pid>/limits</pid>,确认 Max open files 匹配预期
• 若用 sh -c "exec yourapp" 启动,shell 会重置限制;改用 exec yourapp 直接替换 PID 1
• systemd 下需在 /etc/systemd/system.conf 加 DefaultLimitNOFILE=65535,并 systemctl daemon-reload
真正难处理的不是“怎么开更多 fd”,而是“哪些 goroutine 正在持有 fd 却迟迟不释放”——比如卡在 os.File.Close 或 syscall.Read 的协程,得靠 /debug/pprof/goroutine?debug=2 抓堆栈定位。











