服务报 Too many open files:一次 Linux 文件描述符耗尽排查复盘

一个常年安稳的 Java 服务高峰期满屏报 Too many open files,df 看磁盘和 inode 却都很充足。排查梳理:这句报错用光的是文件描述符 fd 不是磁盘,Linux 一切皆文件连 socket 网络连接、管道都各占一个 fd;每个进程 fd 上限由 ulimit 控制很多系统默认只有 1024,ulimit 分软硬限制、改了只对当前 shell 有效管不到在跑的进程;ls /proc/PID/fd 数 fd、对比 limits 看上限,fd 只涨不跌是泄漏、大量 CLOSE-WAIT 是代码没 close 的实锤;systemd 服务不读 limits.conf 要配 LimitNOFILE,改完认准 /proc/PID/limits 确认生效,以及一套文件描述符排查纪律。

2024 年,一个跑了大半年都安稳的 Java 服务,开始在业务高峰期频繁报错。我扒开日志,满屏都是同一句话:Too many open files。我看到"open files"这个词,第一反应是磁盘满了——文件太多嘛。我赶紧 df -h 看了一眼,磁盘空间富裕得很,用了不到一半。我又怀疑是不是 inode 满了,df -i 一看,inode 也宽裕。这下我糊涂了:磁盘有空间,inode 也够,系统凭什么说我"打开的文件太多"?到底什么东西"太多"了?而且更怪的是,这个服务大半年都好好的,根本没改过代码,怎么突然就"文件太多"了?我盯着 Too many open files 这句报错想了很久,最后才反应过来,我从一开始就把它理解错了——它说的"open files",根本不是磁盘上的文件,也和磁盘空间毫无关系。它说的是另一样东西,一样我以前从来没正眼看过、却给每个进程都设了天花板的东西。这件事逼着我把 Linux 的文件描述符、ulimit 限制、fd 泄漏排查这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一个常年运行的 Java 服务,对外提供 HTTP 接口
事故现象:
- 业务高峰期,日志大量报 Too many open files
- 报错时新请求处理失败、数据库连接也建不了
- ★ 磁盘空间和 inode 都很充足,服务代码也没改过

现场排查:
# 1. 先排除磁盘 —— 空间和 inode 都够
$ df -h
/dev/vda1   100G  44G  56G  44%  /          # ★ 空间够
$ df -i
/dev/vda1   ...  18%  /                     # ★ inode 也够

# 2. ★ 那 "open files" 到底指什么 —— 看进程开了多少 fd
$ ls /proc/9200/fd | wc -l
1024                                        # ★ 正好卡在 1024!

# 3. ★ 看这个进程能开多少(它的 fd 上限)
$ cat /proc/9200/limits | grep 'open files'
Max open files   1024   1024   files        # ★ 上限就是 1024

# 4. ★ 看它开的这 1024 个 fd 都是些什么
$ ls -l /proc/9200/fd | awk '{print $NF}' | grep socket | wc -l
870                                         # ★ 870 个是 socket!
# 大量 socket 连接没关,把 fd 配额吃光了。

根因(后来想清楚的):
1. ★ "open files" 不是磁盘文件,是【文件描述符】(fd)。
   Linux 里,进程每打开一个文件、每建一条网络连接
   (socket)、每开一个管道,都会占用一个 fd。
2. ★ 每个进程能同时持有的 fd【有上限】,这个上限由
   ulimit 控制,很多系统默认就是 1024 —— 低得可怜。
3. 这个 Java 服务,每处理一个请求要连数据库、连下游
   服务,高峰期并发一上来,同时存在的 socket 连接
   就逼近 1024。
4. ★ 更要命的是代码里有 fd 泄漏:部分连接 / 流用完
   【没有 close】,fd 只增不减,慢慢就把 1024 耗尽。
5. 一旦 fd 用满,进程【再也开不了任何新文件 / 新连接】
   -> 新请求失败、连不上数据库 -> Too many open files。
6. 大半年没事、突然爆,是因为业务量在涨 + 泄漏在累积,
   两者叠加,这阵子刚好顶破了 1024 这条线。
Too many open files 是 fd 用满了,和磁盘空间毫无关系。

修复 1:Too many open files——用光的是文件描述符,不是磁盘

# === ★ 先纠正最核心的误解:这句报错和磁盘无关 ===

# === "open files" 字面骗了很多人 ===
# 看到 Too many open files,直觉是"磁盘上文件太多"。
# ★ 错。它说的 "open files" 是【文件描述符】(file
#   descriptor,简称 fd),是一个【运行时】的概念,
#   和磁盘上存了多少文件、磁盘还剩多少空间,毫无关系。

# === 什么是文件描述符(fd)===
# 进程每打开一样东西,内核就给它发一个小整数当"凭证",
#   这个凭证就是 fd。进程之后读写这样东西,都靠这个 fd。
# ★ 关键:在 Linux 的世界观里,"文件"是个广义概念 ——
#   不只磁盘文件,下面这些【全都占一个 fd】:
#  - 打开一个磁盘文件
#  - ★ 建立一条网络连接(socket)—— 这是大头
#  - 开一个管道(pipe)
#  - 标准输入/输出/错误,本身就是 fd 0/1/2
# "一切皆文件",所以"一切都占 fd"。

# === ★ 于是报错就说得通了 ===
# Too many open files = 这个进程同时持有的 fd 数量,
#   ★ 撞到了它的【fd 上限】,内核不再给它发新 fd。
# 进程于是开不了新文件、建不了新连接 —— 业务就崩了。

# === 看一个进程当前开了多少 fd ===
$ ls /proc/9200/fd | wc -l
1024
# ★ /proc/PID/fd 目录,里面每个条目就是该进程的一个 fd。
#   数一数,就知道它现在持有多少。

# === 看这个进程的 fd 上限是多少 ===
$ cat /proc/9200/limits | grep 'open files'
Max open files   1024   1024   files
# ★ 当前 fd 数 == 上限 == 1024 -> 实锤:fd 配额用满了。

# === 全系统层面的 fd 用量(另一个层次的上限)===
$ cat /proc/sys/fs/file-nr
12000  0  790000
# ★ 三个数:已分配 / 已分配未用 / 系统总上限。
#   注意区分:这是【整个系统】的 fd 总量;
#   而 Too many open files 通常先撞的是【单进程】上限。

修复 2:socket 也是 fd——为什么网络服务最容易中招

# === ★ 重点理解:每一条网络连接,都吃掉一个 fd ===

# === 网络服务的 fd,大头是 socket ===
# 一个对外提供服务的进程,它的 fd 通常【极少】是磁盘文件,
#   ★ 绝大部分是 socket —— 每一条 TCP 连接占一个 fd:
#  - 每个进来的客户端连接,占一个 fd
#  - 每条连到数据库的连接,占一个 fd
#  - 每条连到下游服务、Redis、MQ 的连接,占一个 fd
# 高并发时,这些 socket 数量很容易冲上千。

# === 把一个进程的 fd 按类型拆开看 ===
$ ls -l /proc/9200/fd | awk '{print $NF}' | sort | uniq -c | sort -rn
    870 socket:[...]            # ★ 870 个 socket —— 最大头
     90 /opt/app/logs/app.log   # 日志文件
     ...
# ★ 一眼看清:fd 主要被 socket 吃掉了,问题在网络连接。

# === 进一步看这些 socket 连到哪、什么状态 ===
$ ss -anp | grep 9200 | awk '{print $1}' | sort | uniq -c
   620 ESTAB                    # ★ 620 条已建立的连接
   210 CLOSE-WAIT               # ★ 210 条 CLOSE-WAIT —— 危险信号!
# ★ CLOSE-WAIT 多,几乎等于 fd 泄漏的实锤(见修复 4)。

# === ★ 一个常被忽略的点:CLOSE-WAIT 状态的连接也占 fd ===
# 对端已经关了连接,本端却迟迟没 close ——
#   连接停在 CLOSE-WAIT,★ 它仍然死死占着一个 fd。
# 这种"该关没关"的连接堆起来,就是在慢慢吃光配额。

# === 看系统级的 socket / 连接总览 ===
$ ss -s
Total: 1100
TCP:   980 (estab 620, closed 50, ...)
# ★ ss -s 给你一个连接数的大盘,判断整机网络连接压力。

# === 网络服务为什么"大半年才爆" ===
# fd 泄漏是【慢性病】:每次漏一两个,平时看不出来。
# ★ 业务量增长(连接基数变大)+ 泄漏长期累积,
#   两条曲线一起往上爬,某天就一起顶破了 1024。

修复 3:ulimit——进程能开多少 fd 的天花板

# === ★ fd 上限是怎么来的:ulimit ===

# === 看当前 shell 的 fd 上限 ===
$ ulimit -n
1024
# ★ -n 就是 "open files" 的上限。很多系统默认 1024,
#   对一个稍有并发的网络服务来说,低得离谱。

# === ★ 软限制 vs 硬限制,必须分清 ===
$ ulimit -Sn        # 软限制(soft):当前实际生效的值
1024
$ ulimit -Hn        # 硬限制(hard):软限制能调到的天花板
4096
# - 软限制:真正起作用的那个数。
# - 硬限制:软限制的"上限的上限"。
# ★ 普通用户可以把软限制往上调,但【不能超过硬限制】;
#   只有 root 能抬高硬限制。

# === 临时调大(只对当前 shell 及其子进程有效)===
$ ulimit -n 65535
# ★ 重点:这只在【当前这个 shell 会话】里有效,
#   关掉终端就没了,也【管不到已经在跑的那个服务进程】。
#   它适合临时验证,不是持久方案。

# === ★ 一个最常见的坑:改了 ulimit,服务却没吃到 ===
# 一个进程的 fd 上限,是它【启动的那一刻】从父进程
#   继承下来的,之后你在别的 shell 里改 ulimit,
#   ★ 对这个【已经在跑】的进程,完全没用。
# 要让服务用上新上限,必须【改好配置后,重启服务】。

# === 看一个【正在运行】的进程,真实的上限是多少 ===
$ cat /proc/9200/limits | grep 'open files'
Max open files   1024   1024
# ★ 别在别处 ulimit -n 看一个数就以为服务也是这个数 ——
#   认准 /proc/服务PID/limits,那才是它真正的上限。

修复 4:揪出 fd 泄漏的真凶

# === ★ 调大上限只是拖延,先确认是不是 fd 泄漏 ===

# === 区分两种"fd 不够" ===
# 情况 1:业务并发就是大,fd 是被【正常】用满的
#   -> 解法:把上限调大(修复 5),够用就行。
# 情况 2:★ fd 泄漏 —— fd 数量【只涨不跌】
#   -> 解法:必须修代码,调大上限只是推迟爆炸。

# === ★ 怎么分辨:盯着 fd 数量的走势看 ===
$ watch -n5 'ls /proc/9200/fd | wc -l'
# ★ fd 数随业务起伏、忙时高闲时回落 = 正常。
#   fd 数【只增不减】,业务空了也不降 = 泄漏实锤。

# === 用 lsof 看一个进程都开着什么 ===
$ lsof -p 9200 | wc -l                       # 该进程 fd 总数
$ lsof -p 9200 | awk '{print $5}' | sort | uniq -c | sort -rn
   870 IPv4                                  # ★ 870 个网络连接
    90 REG                                   # 90 个普通文件
# ★ lsof -p 把一个进程持有的所有 fd 列得清清楚楚。

# === ★ 泄漏最典型的指纹:大量 CLOSE-WAIT ===
$ lsof -p 9200 | grep CLOSE_WAIT | wc -l
210
# CLOSE-WAIT 意味着"对端关了,我方代码没调 close()"。
# ★ 大量 CLOSE-WAIT = 代码里有连接/流用完忘了关 ——
#   这就是 fd 泄漏在代码层面的直接证据。

# === 看泄漏的 fd 具体是什么,反推是哪段代码 ===
$ ls -l /proc/9200/fd | tail -20
# 看那些 fd 指向的文件/socket,结合连的是哪个下游,
#   ★ 就能反推出"是连数据库的没关,还是连某个 API 的没关"。

# === 全系统找"谁在猛开 fd"(不知道是哪个进程时)===
$ lsof | awk '{print $1,$2}' | sort | uniq -c | sort -rn | head
  3200 java 9200                             # ★ java(9200)开了 3200 个
# ★ lsof 不带参数列出全系统 fd,按进程一聚合,
#   开 fd 最多的进程立刻浮出来。

# === ★ 泄漏的根治方向(交给开发)===
# - 数据库连接、HTTP 连接、文件流:用完【必须 close】
# - 用语言的自动关闭机制:Java 的 try-with-resources、
#   Python 的 with、Go 的 defer Close()
# - 连接池要配上限和回收,别让连接只借不还
# ★ 运维能调大上限止血,但泄漏的洞,只有改代码能补上。

修复 5:怎么正确地把 fd 上限调大

# === ★ 持久调大 fd 上限,分两种服务启动方式 ===

# === 方式 A:服务由用户登录后手动 / 脚本启动 ===
# 改 /etc/security/limits.conf,给用户设上限:
$ vi /etc/security/limits.conf
appuser  soft  nofile  65535
appuser  hard  nofile  65535
# ★ nofile 就是 fd 上限。soft 和 hard 都要写。
# 注意:limits.conf 由 PAM 在【用户登录时】加载 ——
#   所以改完要【该用户重新登录】,新会话才带上新上限。

# === ★ 方式 B:服务由 systemd 管理(现在最常见)===
# 关键:systemd 启动的服务,★【根本不读 limits.conf】!
# 它的 fd 上限,由 unit 文件里的 LimitNOFILE 决定。
$ vi /etc/systemd/system/myapp.service
[Service]
LimitNOFILE=65535
# 改完走标准流程:
$ systemctl daemon-reload                    # 让 systemd 重读 unit 文件
$ systemctl restart myapp                    # 重启,服务才吃到新上限
# ★ 很多人改了 limits.conf 死活不生效,就是因为
#   服务是 systemd 拉起的,根本没走 limits.conf 那条路。

# === 系统级总上限也别忘了(通常够大,但要确认)===
$ cat /proc/sys/fs/file-max
790000
# ★ file-max 是整个系统的 fd 总上限。单进程上限
#   再大,也不能超过它。一般默认值够用,不用动。
# 真要调:
$ echo 'fs.file-max = 1000000' >> /etc/sysctl.conf
$ sysctl -p

# === ★ 改完务必验证:看服务进程真实的上限 ===
$ cat /proc/$(pgrep -f myapp)/limits | grep 'open files'
Max open files   65535   65535
# ★ 这一步不能省 —— 只有从 /proc/PID/limits 看到
#   新值,才算真的生效。看到了,才算调对了。

# === 调大上限的定位:止血,不是根治 ===
# ★ 如果是 fd 泄漏,调大上限只是把"爆炸"往后推几天。
#   上限调到够用 + 代码补上泄漏的洞,两件事都要做。

修复 6:文件描述符排查纪律

# === 这次事故暴露的认知盲区,定几条纪律 ===

# === 1. ★ Too many open files 是 fd 用满,和磁盘无关 ===
# 别去 df 看磁盘,去看进程的 fd 数和 fd 上限。

# === 2. ★ socket、管道都算 fd,网络服务的 fd 大头是连接 ===
$ ls -l /proc/PID/fd | awk '{print $NF}' | grep -c socket

# === 3. 看进程当前 fd 数 和 它的上限 ===
$ ls /proc/PID/fd | wc -l
$ cat /proc/PID/limits | grep 'open files'

# === 4. ★ 先分清:正常用满,还是 fd 泄漏 ===
$ watch -n5 'ls /proc/PID/fd | wc -l'   # 只涨不跌 = 泄漏

# === 5. ★ 大量 CLOSE-WAIT = 代码没 close,泄漏实锤 ===
$ lsof -p PID | grep -c CLOSE_WAIT

# === 6. ★ systemd 服务不读 limits.conf,要配 LimitNOFILE ===
# [Service] LimitNOFILE=65535,然后 daemon-reload + restart。

# === 7. 改完上限,认准 /proc/PID/limits 确认真生效 ===

# === 8. 排查 fd 问题的命令链 ===
$ 报错 Too many open files                  # ① 别去查磁盘
$ ls /proc/PID/fd | wc -l                    # ② 看当前 fd 数
$ cat /proc/PID/limits | grep 'open files'   # ③ 看 fd 上限
$ lsof -p PID | grep -c CLOSE_WAIT           # ④ 看是不是泄漏
$ 改 LimitNOFILE/limits.conf + 重启服务      # ⑤ 调大上限止血
# 同时:把泄漏交给开发改代码。按这个顺序基本能定位。

命令速查

需求                        命令
=============================================================
看进程当前开了多少 fd       ls /proc/PID/fd | wc -l
看进程的 fd 上限            cat /proc/PID/limits | grep 'open files'
看 fd 按类型分布            ls -l /proc/PID/fd | awk '{print $NF}' | sort | uniq -c
看进程持有的所有 fd         lsof -p PID
数 CLOSE-WAIT 连接          lsof -p PID | grep -c CLOSE_WAIT
持续盯 fd 数走势            watch -n5 'ls /proc/PID/fd | wc -l'
看当前 shell fd 上限        ulimit -n  (软 -Sn / 硬 -Hn)
看系统级 fd 上限            cat /proc/sys/fs/file-max
全系统按进程统计 fd         lsof | awk '{print $1,$2}' | sort | uniq -c | sort -rn
systemd 服务调 fd 上限      unit 文件 [Service] LimitNOFILE=65535

口诀:Too many open files 是 fd 用满不是磁盘满,socket 连接也吃 fd
      fd 只涨不跌是泄漏要改代码,systemd 服务认 LimitNOFILE 不读 limits.conf

避坑清单

  1. Too many open files 用光的是文件描述符 fd,和磁盘空间、inode 毫无关系
  2. Linux 一切皆文件,网络连接 socket、管道也都各占一个 fd,不只磁盘文件
  3. 网络服务的 fd 大头是 socket 连接,高并发时连接数很容易冲上千
  4. 每个进程的 fd 上限由 ulimit 控制,很多系统默认只有 1024,对服务太低
  5. ulimit 分软限制和硬限制,软限制是实际生效值,普通用户不能调超过硬限制
  6. ulimit -n 改的只对当前 shell 有效,管不到已经在跑的进程,要重启服务才生效
  7. fd 数随业务起伏是正常,fd 数只涨不跌是泄漏,泄漏只能靠改代码补
  8. 大量 CLOSE-WAIT 连接是代码用完没调 close 的直接证据,也照样占着 fd
  9. systemd 启动的服务不读 limits.conf,fd 上限要在 unit 文件配 LimitNOFILE
  10. 改完 fd 上限要看 /proc/服务PID/limits 确认真生效,别只在别的 shell 看 ulimit

总结

这次"服务报 Too many open files、磁盘却空着一大半"的事故,纠正了我一个由"望文生义"带来的、根深蒂固的误解。Too many open files——"打开的文件太多了"——这句报错的字面意思如此直白,直白到我看一眼就自以为读懂了它:文件太多,那不就是磁盘里塞满了文件吗?于是我的排查,从第一秒起就跑偏到了一条完全错误的赛道上。我去 df 看磁盘空间,看 inode,翻有没有哪个目录堆了海量小文件——我所有的动作,都在围着"磁盘"这个被报错的字面意思硬塞给我的方向打转。可磁盘空间富富有余,inode 也宽宽松松,每一项检查都回报我一个"正常",每一个"正常"都让我更困惑:既然什么都正常,系统凭什么对我喊"文件太多"?现在回头看,真正困住我的,从来不是这个技术问题本身有多深,而是我太轻易地相信了"open files"这个词的表面意思,以至于压根没去想,它会不会指着另一样东西。复盘到根上,我才真正理解,这里的"file",是 Linux 一个独特世界观下的"file"。在 Linux 眼里,"文件"是个被极度拓宽了的概念——它不只是躺在磁盘上的那些数据块。一条打开的网络连接是文件,一个进程间通信的管道是文件,连标准输入输出本身都是文件。"一切皆文件"。而每当一个进程"打开"这样一个广义的文件,内核就会发给它一个叫"文件描述符"的小凭证。Too many open files 真正在说的,是这个进程手里攥着的凭证太多了——多到撞破了内核给它设的那个天花板。我那个服务,它持有的"文件"里,真正的磁盘文件寥寥无几,绝大多数是 socket,是一条又一条它建立起来、却用完之后忘了亲手关掉的网络连接。这些"该关没关"的连接,像没被收走的餐盘一样,在桌上越堆越高,一张张占着位置,最终把那个名叫 1024 的天花板,死死顶住了。它和磁盘空间,从头到尾就是两件毫不相干的事——我对着磁盘查了那么久,等于一直在错误的房间里找钥匙。这次最大的收获,是我对技术世界里的"名字",多了一份必要的戒心。一个报错信息、一个术语,它的名字是给人看的方便标签,但这个标签和它背后真正的机制之间,常常隔着一层我想当然就跨过去、却其实跨错了的鸿沟。"open files"听上去是磁盘的事,可它的战场在内存里、在内核的进程表里、在那一个个运行时才存在的描述符上。下一次再遇到一个我"一看就懂"的报错,我会强迫自己慢下来,先别急着顺着字面意思冲出去——而是先问一句:这个词,在它所属的那个系统里,到底被定义成了什么?读懂一个名字,有时恰恰要从"不轻信这个名字"开始。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

SSH 登录前要卡 30 秒:一次 sshd 反向 DNS 排查复盘

2026-5-20 20:32:23

Linux教程

删了大文件磁盘空间却没回来:一次 Linux 已删除文件占用排查复盘

2026-5-20 20:39:05

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索