Go 报错 too many open files 怎么解决?文件句柄泄漏排查定位与 ulimit、systemd LimitNOFILE 配置
报错现场
服务上线跑了几天,监控突然出现大量错误,日志里反复出现:
http: Accept error: accept tcp [::]:8080: accept4: too many open files; retrying in 5ms
与此同时,新连接建立失败、健康检查超时——这是 Linux 上最高频的 Go 线上故障之一:进程打开的文件描述符(fd)数量达到了系统上限。net/http 的 accept 循环遇到这种临时错误不会退出,而是按 5ms 起步指数退避重试(上限 1s),所以服务看起来「还在」,实际已经拒新连接了。本文按「确认上限 → 定位泄漏 → 修复代码 → 调大上限」四步给出完整排查路径。
第一步:确认当前 fd 上限和占用量
先拿三个数字:进程实际用了多少 fd、软限制是多少、硬限制是多少。
# 1. 进程当前打开的 fd 数量
ls /proc/<PID>/fd | wc -l
# 或
lsof -p <PID> | wc -l
# 2. 该进程的软/硬限制(Max open files 一行)
cat /proc/<PID>/limits | grep "open files"
# 3. 当前 shell 的默认限制(新进程从这里继承)
ulimit -n
如果第一项(已用 fd)长期贴着第二项(软限制)走,就是典型的句柄耗尽。系统级总开关 cat /proc/sys/fs/file-max 是整个机器所有进程的上限,一般远大于单进程限制,线上 99% 的情况是单进程 soft limit(常见默认 1024 或 4096)被打满,而不是系统级耗尽。
第二步:看 fd 都是什么,定位泄漏点
# 按 fd 类型统计分布
lsof -p <PID> | awk '{print $5}' | sort | uniq -c | sort -rn
典型分布对照表:
| lsof 里 TYPE 列的分布 | 说明 | 最常见的泄漏源 |
|---|---|---|
大量 TCP / IPv4 / IPv6 socket |
网络连接句柄堆积 | HTTP 客户端 resp.Body 没关闭、下游连接无限建 |
大量 REG(普通文件) |
文件句柄堆积 | os.Open 后没有 Close,循环里反复打开 |
大量 FIFO / pipe |
管道堆积 | os/exec 创建了子进程但没回收 |
can't identify protocol 且数量巨大 |
半关闭的旧 socket | TIME_WAIT 堆积(一般不占 fd,先排除前三种) |
也可以用一段最小代码亲手复现耗尽过程(Linux 下几秒就会报错):
package main
import (
"log"
"os"
)
func main() {
for i := 0; ; i++ {
f, err := os.Open("/etc/hostname")
if err != nil {
log.Fatalf("opened %d files, err: %v", i, err) // too many open files
}
_ = f // 故意不 Close:fd 只增不减
}
}
第三步:修复代码——三大高频泄漏源
3.1 HTTP 客户端:resp.Body 必须关闭
这是最常见的坑。http.Get / client.Do 返回的 resp.Body 不关闭,底层 TCP 连接就不会归还复用池,fd 只涨不回落:
// ❌ 错误:Body 不关闭,每个请求泄漏一个连接
resp, _ := http.Get("https://api.example.com/data")
data, _ := io.ReadAll(resp.Body)
_ = data
// ✅ 正确:defer 关闭,并读完剩余内容让连接复用
resp, err := http.Get("https://api.example.com/data")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, resp.Body) // 把 Body 读完,连接才会被复用池回收
只 Close 不读完 Body,连接同样进不了复用池(源码层面:未读完的 Body 关闭时会直接丢弃连接)。高频出站请求还应该显式调大每主机连接池,否则默认 MaxIdleConnsPerHost = 2,其余空闲连接照样被关掉重连,既耗 fd 又耗端口:
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100, // 全局空闲连接上限
MaxIdleConnsPerHost: 20, // 单主机空闲连接上限(默认仅 2)
// MaxConnsPerHost: 50, // 可选:单主机总连接数上限,超了会阻塞等待
},
}
3.2 文件句柄:defer 关闭,注意循环与早退
func readAll(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // 无论后续哪条 return,都保证关闭
return io.ReadAll(f)
}
循环内打开文件、error 分支提前 return 但没写 defer——是这类泄漏的经典组合。生产代码里可以用 gosec 或静态检查发现。
3.3 数据库与下游连接:设置池上限
用 database/sql 或 GORM 时,一定要给连接池设上限,否则突发流量会把 fd 顶满:
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal(err)
}
db.SetMaxOpenConns(50) // 最大打开连接数
db.SetMaxIdleConns(10) // 最大空闲连接数
db.SetConnMaxLifetime(time.Hour) // 连接最长存活时间
同时检查 Query 返回的 rows 是否全部 rows.Close() 了——和 HTTP Body 是同一类问题,排查方法也一样:看 lsof 里 Unix socket / TCP 到数据库端口的连接数是不是贴着 SetMaxOpenConns 走。
第四步:调大 ulimit——临时与永久两种姿势
临时生效(只对当前 shell 及其子进程有效,重启失效):
ulimit -n 65535
永久生效分两种场景:
- 用 systemd 托管的服务(线上标准做法):在 unit 文件的
[Service]段加一行,然后systemctl daemon-reload && systemctl restart yourapp:
[Service]
LimitNOFILE=65535
- 普通 shell / 非 systemd 启动的进程:编辑
/etc/security/limits.conf,追加:
* soft nofile 65535
* hard nofile 65535
注意:limits.conf 对 systemd 服务不生效(systemd 不经过 PAM 登录流程),所以线上服务务必用 LimitNOFILE,改完用 cat /proc/<PID>/limits 验证。
排查速查清单
- 报错先跑
ls /proc/<PID>/fd | wc -l和cat /proc/<PID>/limits | grep "open files",确认是贴着软限制。 lsof -p <PID> | awk '{print $5}' | sort | uniq -c看 fd 类型分布:socket 多查 HTTP Body / 下游连接,REG 多查文件关闭。- 代码侧三件套:
resp.Body.Close()+ 读完、os.Open配 defer、SetMaxOpenConns。 - 调大限制:systemd 用
LimitNOFILE=65535;普通进程用limits.conf或ulimit -n。 - 改完重启服务,再跑一次
cat /proc/<PID>/limits验证生效,并观察 fd 数量是否随流量回落。
相关阅读
- Go 报错 invalid memory address or nil pointer dereference 怎么解决?空指针 panic 排查与 5 种修复姿势
- Go 报错 cannot assign to entry in nil map 怎么解决?nil map 的正确初始化与 4 种修复姿势
- Go 报错 go mod tidy 失败怎么解决?六大常见报错的排查与修复(附国内镜像配置)
- Goroutine 泄漏检测实战:如何发现与修复泄漏问题?
- Go net/http 服务端底层实现全解析:每连接一个 goroutine、路由决策树与 HTTP/2 多路复用
版本说明
本文无第三方依赖,示例代码仅使用 Go 标准库(os、net/http、io、database/sql)。文中 Transport.MaxIdleConnsPerHost 默认值 2 及 MaxConnsPerHost 零值语义、net/http accept 循环的退避日志格式,均逐字对照 go1.26.0 src/net/http/transport.go 与 server.go 核实(2026-11-09 核对,官方源码:go.dev/src/net/http)。ulimit、limits.conf、LimitNOFILE 为 Linux 通用机制,与 Go 版本无关。
- 《Go 程序设计语言》(Go 语言圣经,机械工业出版社) —— Go 官方团队出品,公认入门第一书
- 《Go 语言实战》(异步图书,人民邮电出版社) —— 实战导向,面试前巩固工程手感
- 《Go 语言编程之旅:一起用 Go 做项目》(博文视点,电子工业出版社) —— 项目驱动入门,从零写出完整应用
- Keychron K1Max 矮轴机械键盘(Mac/Win 双系统,QMK/VIA 开源改键) —— 轻薄矮轴 + 开源固件,程序员桌面常客
- 罗技 MX Master 3S 无线鼠标(Flow 跨屏,静音电磁滚轮) —— 人体工学设计,写代码一整天手不酸
以上链接含京东联盟推广代码;通过链接购买本站可能获得少量佣金,不影响你的购买价格。
每周更新 Go + AI 应用开发实战:AI 面试官完整项目、MCP、Agent 工程化,完整源码随文章首发。
微信扫码,或搜索「GO语言入门之路」关注,第一时间获取系列更新与源码,不再错过任何一篇。