登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go 服务出现 too many open files 怎么查:/proc/fd、ulimit 与连接泄漏

来源:17golang原创

时间:2026-08-10 21:11:21 119浏览 收藏

凌晨告警里跳出一串 open /tmp/cache-1842: too many open files,通常不是磁盘占满了,而是进程能同时持有的文件描述符已经耗尽。Go 服务里最容易被忽略的诱因有三个:HTTP 响应体没关干净、短连接场景下 socket 越积越多,以及只调大了 shell 的 ulimit 却没真正修复泄漏点。

要点速览

  • 先查进程级 FD 总量和类型分布,再判断是普通文件、socket 还是管道在异常增长。
  • 客户端发请求时必须消费完响应体或者直接关闭它,不然 HTTP 连接池根本没法稳定复用。
  • 拉高 LimitNOFILE 只能争取缓冲时间,完全替代不了泄漏点的修复。
  • 修复之后要同时观察 FD 曲线、错误率和连接复用情况,确认没有把问题延后到下一次版本发布。

告警出现时,先把“文件太多”拆成可验证的信号

别一上来就把所有实例全重启。先挑一台还在报错的进程,记下它的 PID、启动时间和当前 FD 数量,方便后续把处理前后的结果对应上。下面列的命令只会读取系统信息,完全不会改动服务的运行状态:

pid=$(pgrep -n order-api)
printf 'pid=%s\n' "$pid"
cat /proc/$pid/limits | grep 'open files'
printf 'fd_count='
find /proc/$pid/fd -maxdepth 1 -type l | wc -l
ls -l /proc/$pid/fd | sed -n '1,12p'

如果 fd_count 已经接近进程配置的上限,再接着看资源的类型分布。要是 socket 数量涨得很快,优先排查 HTTP 出站请求和数据库连接;普通文件占比很高,就去查临时文件、日志轮转或者文件上传流程;管道和 eventfd 数量异常增长,就把注意力放到 goroutine 的生命周期管理上。

Go 服务 too many open files 告警下,/proc/fd 与 open files 上限的证据排查

用 /proc/fd 和 lsof 定位是哪一类资源在泄漏

只看 FD 总数远远不够。Linux 会把每个打开的资源都映射成 /proc//fd 下面的符号链接,可以按照链接的目标做个粗略的分类统计:

ls -l /proc/$pid/fd 2>/dev/null \
  | awk '{print $NF}' \
  | sed -E 's#^socket:\[[0-9]+\]#socket#; s#^pipe:\[[0-9]+\]#pipe#' \
  | sort | uniq -c | sort -nr | head

生产机有 lsof 时,下面两条命令的输出会更直观:

lsof -nP -p "$pid" | awk 'NR>1 {print $5}' | sort | uniq -c | sort -nr
lsof -nP -a -p "$pid" -iTCP | sed -n '1,20p'

看到大量 CLOSE_WAIT,先查对端主动关闭之后,本地是不是还在持有未释放的响应;大量 ESTABLISHED 而且数量迟迟不回落,就去检查连接池上限和请求超时配置。这里别急着把 ulimit -n 改成特别大的数字,先把当前的证据留存下来,后面才能区分开“容量确实不够”和“资源根本没释放”两种情况。

Go HTTP 客户端最常见的坑:响应体没有关闭

Go 的 http.Client 会通过 Transport 复用 TCP 连接,但连接复用的前提是响应体被正确收尾。下面这种写法在错误分支里风险特别高:

resp, err := client.Do(req)
if err != nil {
    return err
}
if resp.StatusCode != http.StatusOK {
    return fmt.Errorf("upstream status=%d", resp.StatusCode)
}
defer resp.Body.Close()

一旦状态码不符合预期,函数会在 defer 注册前就直接返回。更稳妥的写法是拿到响应对象之后,立刻注册关闭动作,再去判断要不要读取响应正文:

resp, err := client.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
    io.CopyN(io.Discard, resp.Body, 4

读取一小段错误正文有助于后续问题复盘,同时用 LimitReader 限制读取长度,防止异常上游返回超大内容拖慢服务。如果请求是由多个 goroutine 并发发起的,再给 Transport 配置合理的空闲连接数和请求超时规则,避免依赖故障时程序无限等待:

transport := &http.Transport{
    MaxIdleConns:        100,
    MaxIdleConnsPerHost: 20,
    IdleConnTimeout:     90 * time.Second,
}
client := &http.Client{
    Transport: transport,
    Timeout:   8 * time.Second,
}
Go HTTP 响应体先关闭再读取,连接数量从持续增长恢复到可复用状态

处理步骤:先止血,再修复,最后留好回滚预案

1. 给故障实例降流量

如果只有少数几个实例的 FD 接近上限,把它们从负载均衡摘出来或者降低权重,留一台作为观测样本。别上来就批量重启,不然泄漏的现场会被直接清空,下一次出问题还是会复现。

2. 修复资源释放路径

全局搜索 client.Doos.Openos.Create 和数据库查询相关逻辑,确认成功分支和失败分支都在拿到资源后立刻安排好关闭动作。遇到流式响应,不要为了“关闭资源”就提前把全部内容读进内存,应该让消费方在处理结束或者收到取消信号时负责收尾。

3. 用单测和短压测验证

给所有出站请求写覆盖异常场景的测试,模拟返回 500、请求超时、返回超大正文的情况,连续跑几百次之后检查 FD 是不是能回到初始基线。压测的时候要记录 process_open_fdsprocess_max_fds、请求错误率和 p95 延迟,不能只盯着吞吐量看。

回滚与告警确认:怎么判断问题真的解决了

如果修复版本上线之后 FD 还在持续增长,直接回滚到上一个稳定版本,同时保留一台问题实例的采样数据。临时提高 systemd 的 LimitNOFILE 可以避免服务立刻全量不可用,但要把这个操作标注为临时缓冲措施,后续还要跟进检查替换。

systemctl show order-api -p LimitNOFILE
systemctl reload order-api
watch -n 5 "ls /proc/$(pgrep -n order-api)/fd | wc -l"

告警完全恢复至少要满足三个条件:请求量稳定的时候 FD 曲线不再单向持续上涨;too many open files 在日志里不再出现;socket 的状态分布回到发布前的正常范围。最后再把全部流量切回来,并且把“FD 使用率超过 80% 持续 5 分钟”和“错误率升高”两条告警关联上,后续排查能直接跳转相关面板。

常见问题:too many open files 处理中的几个判断

只调高 ulimit 能解决 Go 服务的 FD 报错吗?

通常不行。它只是拉高了容量上限,资源泄漏还是会继续增长;只有确认资源总数量完全合理、业务确实需要更高的并发承载能力时,才适合调整这个上限。

http.Response.Body 明明关了,连接数怎么还在涨?

还要排查是不是存在请求超时、Transport 被频繁新建、上游主动断开连接,或者连接池参数和实际并发量不匹配的情况。FD 的类型分布和 socket 状态,比单一的计数结果更有参考价值。

重启之后 FD 数量恢复正常,是不是说明修复成功了?

不能。重启会直接释放进程持有的所有资源,只能证明止血操作生效了。要在同等请求量下观察一段时间,还要用压测覆盖错误分支和请求取消路径,才能确认泄漏点已经被彻底堵住。

这类故障的处理顺序可以固定下来:先确认上限配置和资源类型,再找到没有释放的代码路径,最后用资源曲线、错误率和回滚结果做最终验收。这样就算下次遇到的不是 HTTP 连接泄漏,而是临时文件或者管道耗尽的场景,也能沿着同一套证据链快速定位收敛。

[] []
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>