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