服务跑着跑着报 Too many open files:一次 Linux 文件描述符限制排查复盘

一个网关服务流量爬升后开始大面积报 Too many open files,查遍代码的文件 close 却找不到泄漏。排查梳理:Linux 一切皆文件、socket 和管道都占文件描述符、压垮服务的是几千个网络连接而非磁盘文件;ulimit 软硬限制、/proc/PID/limits 看进程实际上限;systemd 服务不读 limits.conf 必须在 unit 配 LimitNOFILE;看 fd 曲线区分高并发与泄漏、CLOSE_WAIT 堆积实锤漏 close,以及一套文件描述符排查纪律。

2024 年,我们一个网关服务上线后,头几个小时一切风平浪静。可随着流量慢慢爬上来,日志里开始零星冒出一行扎眼的报错:Too many open files。一开始一小时几条,我没太当回事,后来越来越密,最后变成大面积的连接建立失败——新请求进不来,服务等于半死。我的第一反应是"文件没关好",于是去翻代码里所有读写文件的地方,逐个确认 close 调了没有,翻了半天,一个泄漏都没找到。可报错还是 Too many open files。真正让我愣住的是,当我顺着这个 "files" 往下查,才发现压垮服务的那"太多文件",压根不是什么磁盘上的文件——而是几千个网络连接。一个服务明明没怎么碰磁盘文件,却被 "open files" 太多给压垮了,这件事说明,我对 Linux 里 "文件" 这个词的理解,从一开始就太狭隘了。这件事逼着我把 Linux 的文件描述符、ulimit 资源限制、systemd 下的 limit 配置这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一个 Java 网关服务,被 systemd 管着
事故现象:
- 上线初期正常,流量爬升后开始报 Too many open files
- 越来越密,最后大面积连接建立失败
- 查代码,文件读写处的 close 都调了,没找到泄漏

现场排查:
# 1. 看服务进程当前打开了多少"文件"
$ ls /proc/9001/fd | wc -l
1024                                      # ★ 正好卡在 1024

# 2. ★ 看这个进程的文件描述符限制
$ cat /proc/9001/limits | grep -i 'open files'
Max open files            1024     1024     files   # ★ 软硬都是 1024

# 3. 看这 1024 个"文件"到底是什么
$ ls -l /proc/9001/fd | head
... 8 -> socket:[38211]                   # ★ 是 socket!
... 9 -> socket:[38215]                   # ★ 还是 socket!
... 10 -> socket:[38240]                  # ★ 全是网络连接
# ★ 占满 1024 的根本不是磁盘文件,是几千个网络连接

# 4. 我在 unit 文件里没配 limit
$ cat /etc/systemd/system/gateway.service
[Service]
ExecStart=/opt/gateway/run
# ★ 没有 LimitNOFILE —— 服务吃的是 systemd 给的默认值

根因(后来想清楚的):
1. ★ Linux 里"一切皆文件":磁盘文件是文件,
   一个网络连接(socket)、一个管道,也都算"打开的文件"。
2. 网关服务要同时维持大量并发连接,每个连接
   都占用一个【文件描述符(fd)】。
3. ★ 每个进程能同时打开的 fd 数量是有【上限】的,
   默认常常是 1024。连接数一爬过 1024,
   再开新连接(新 fd)就失败 -> Too many open files。
4. ★ 我以为限制的是"磁盘文件",拼命查文件泄漏 ——
   方向全错。真正爆的是"连接数 vs fd 上限"。
5. ★ 而且服务是 systemd 起的:它【不读】
   /etc/security/limits.conf,光改那个文件没用,
   要在 unit 文件里配 LimitNOFILE。
"open files"包含 socket,fd 上限要按并发连接量来调。

修复 1:文件描述符是什么——为什么连接也算"文件"

# === ★ 先破除误解:Linux 里的"文件",远不止磁盘文件 ===
# "一切皆文件"是 Linux 的核心设计:进程要访问任何资源,
# 都通过一个统一的东西 —— 文件描述符(fd)。

# === 什么东西会占用一个 fd ===
# - 打开一个磁盘文件        -> 占 1 个 fd
# - ★ 建立一个网络连接(socket) -> 占 1 个 fd
# - 一个管道(pipe)        -> 占 fd
# - 标准输入/输出/错误      -> 就是 fd 0 / 1 / 2
# ★ 所以一个高并发网络服务,fd 的大头【几乎全是 socket】。

# === ★ 直接看一个进程都开了哪些 fd ===
$ ls -l /proc/9001/fd
lrwx------ 0 -> /dev/null
lrwx------ 1 -> socket:[38000]
lrwx------ 8 -> /opt/gateway/logs/app.log    # 这才是磁盘文件
lrwx------ 9 -> socket:[38211]               # ★ 这是网络连接
lrwx------ 10 -> pipe:[38300]                # 这是管道
# /proc/PID/fd/ 里每一个软链接 = 一个打开的 fd。

# === 数一数总共开了多少 ===
$ ls /proc/9001/fd | wc -l
1024

# === ★ 按类型拆开看,fd 都耗在哪了 ===
$ ls -l /proc/9001/fd | awk '{print $NF}' | \
    sed 's/\[.*\]//' | sort | uniq -c | sort -rn
    980 socket:                  # ★ 980 个是网络连接
     30 /opt/gateway/...         # 磁盘文件其实没几个
     ...
# ★ 这一拆,真相立刻清楚:压垮服务的是 socket,不是文件。

# === 一句话 ===
# "Too many open files" 里的 files,是【文件描述符】,
# 它把磁盘文件和网络连接算在【同一个】配额里。

修复 2:ulimit——看与改文件描述符上限

# === ★ 每个进程能开多少 fd,由 ulimit 的 "open files" 项决定 ===

# === 看当前 shell 的 fd 上限 ===
$ ulimit -n
1024                              # ★ 默认常常就是 1024

# === ★ 软限制 vs 硬限制 —— 必须分清 ===
$ ulimit -Sn                      # 软限制(soft):当前实际生效的值
1024
$ ulimit -Hn                      # 硬限制(hard):软限制能调到的天花板
4096
# - 软限制:真正生效的限制,普通用户可【自己调高】,
#   但【不能超过】硬限制。
# - 硬限制:天花板,普通用户只能【调低】不能调高,
#   调高硬限制需要 root。

# === 临时调高(只对当前 shell 及其子进程有效)===
$ ulimit -n 65536
$ ulimit -n
65536
# ★ 这个改动【不持久】:关掉这个 shell 就没了。
# ★ 而且只能调到不超过硬限制;要更高得 root 先抬硬限制。

# === 看其它资源限制 ===
$ ulimit -a                       # 列出所有资源限制
# -n 打开文件数  -u 进程数  -c core 大小  -s 栈大小 ...

# === ★ 关键认知:ulimit 是"谁启动进程,进程就继承谁的 ulimit" ===
# 你在 shell 里 ulimit -n 65536,然后在这个 shell 里
#   启动的程序,才会继承 65536。
# ★ 如果程序是别的方式拉起来的(systemd / 别的会话),
#   它继承的是【那个启动者】的 ulimit —— 跟你这个 shell 无关。
# 这正是下一节那个大坑的根源。

修复 3:limits.conf 与 systemd——这次踩的真正的坑

# === ★ 想让 fd 上限【持久】,传统办法是 limits.conf ===
$ vi /etc/security/limits.conf
*           soft    nofile      65536
*           hard    nofile      65536
appuser     soft    nofile      65536      # 也可只对某用户
appuser     hard    nofile      65536
# 格式:  域(用户/*)  soft|hard  nofile  数值
# 改完【重新登录】才生效(它由登录时的 PAM 模块加载)。

# === ★ 但这次的大坑:systemd 起的服务,根本不读 limits.conf ===
# /etc/security/limits.conf 是给【登录会话】用的 ——
# 它靠 PAM 在用户【登录】时加载。
# ★ 而 systemd 启动一个服务,【不经过登录、不经过 PAM】,
#   所以 limits.conf 里写的 nofile,对它【完全无效】!
# 我当时就是改了 limits.conf,重启服务,毫无效果 ——
#   方向又错了。

# === ★ systemd 服务的 fd 上限,要在 unit 文件里配 ===
$ vi /etc/systemd/system/gateway.service
[Service]
ExecStart=/opt/gateway/run
LimitNOFILE=65536                 # ★ 就是这一行!
$ systemctl daemon-reload         # 改 unit 必须 reload
$ systemctl restart gateway

# === 也可以给所有 systemd 服务配一个默认值 ===
$ vi /etc/systemd/system.conf
DefaultLimitNOFILE=65536
# 改完要重启系统或 systemctl daemon-reexec 才全局生效。

# === ★ 改完一定要【验证生效】—— 别想当然 ===
$ systemctl restart gateway
$ cat /proc/$(pgrep -f gateway)/limits | grep 'open files'
Max open files       65536    65536    files     # ★ 生效了!
# ★ 永远去 /proc/PID/limits 确认进程【实际】拿到的值,
#   而不是看你改了哪个配置文件。

# === 一句话总结这个坑 ===
# 登录会话(ssh/su 进去手动跑)-> limits.conf 生效
# systemd 服务               -> 只认 unit 里的 LimitNOFILE
# ★ 改对地方,比改对数值更重要。

修复 4:查进程实际打开了多少 fd——定位泄漏

# === ★ 调高上限是"扩容";但要先确认是真不够、还是在泄漏 ===

# === 看某进程当前开了多少 fd ===
$ ls /proc/9001/fd | wc -l
1024

# === ★ 用 lsof 看某进程开的 fd 明细 ===
$ lsof -p 9001 | head
COMMAND  PID  USER  FD  TYPE  ...  NAME
java    9001  app   8u  REG   ...  /opt/gateway/logs/app.log
java    9001  app   9u  IPv4  ...  TCP 10.0.0.5:8080->10.0.0.9:51234 (ESTABLISHED)
# 看清楚每个 fd 是文件还是连接、连到哪。

# === 统计某进程开了多少 fd ===
$ lsof -p 9001 | wc -l

# === ★ 哪些进程开的 fd 最多(全局排查)===
$ for p in /proc/[0-9]*; do \
    echo "$(ls $p/fd 2>/dev/null | wc -l) ${p##*/}"; \
  done | sort -rn | head
9988 9001                         # ★ PID 9001 开了 9988 个
  88 1
  ...

# === ★ 区分"真不够"还是"泄漏"——看曲线 ===
# 反复采样某进程的 fd 数:
$ watch -n5 'ls /proc/9001/fd | wc -l'
# - fd 数随并发上下【波动、能回落】 -> 是真高并发,扩容上限
# - fd 数【只涨不降、一路爬高】     -> ★ 是泄漏!代码里
#   打开了 socket/文件却没 close,迟早撑爆,扩容只是拖延。

# === 系统级:看全系统 fd 使用 ===
$ cat /proc/sys/fs/file-nr
12832   0   2097152
# ① 已分配  ② 已分配但空闲  ③ 系统级上限
# ★ 系统级上限由 fs.file-max 控制(sysctl),也别忘了它。
$ sysctl fs.file-max

修复 5:fd 泄漏——开了不关,迟早撑爆

# === ★ 调高上限治标;如果是泄漏,不修代码迟早还会爆 ===

# === 泄漏的典型信号 ===
# 1. 进程 fd 数【单调上涨】,从不回落(上一节的曲线)。
# 2. lsof -p 里看到大量【同一个对端】或同一文件的 fd。
$ lsof -p 9001 | awk '{print $9}' | sort | uniq -c | sort -rn | head
   3201 10.0.0.50:3306            # ★ 三千多个连到 MySQL 的连接!
# 一个正常程序不该开三千个数据库连接 -> 连接池没复用/没关。

# === ★ 泄漏的常见来源 ===
# - 打开文件/连接后,异常路径上漏了 close
#   (Java 要用 try-with-resources;Python 用 with)
# - HTTP 客户端、数据库连接每次新建却不关、不放回池
# - 连接池配置错误,只借不还

# === 看 socket 状态分布,辅助判断 ===
$ ss -s
TCP:   9300 (estab 9100, closed 100, ...)
# ★ estab 数量大得离谱,且持续涨 -> 连接没正常关闭。
$ ss -tn state established | wc -l        # 数已建立的连接

# === ★ CLOSE_WAIT 堆积:一种很典型的泄漏 ===
$ ss -tan state close-wait | wc -l
3150                              # ★ 大量 CLOSE_WAIT
# CLOSE_WAIT = 对方关了连接,但【我方代码没调 close】。
# 大量 CLOSE_WAIT 堆积,几乎实锤是代码漏了关连接。

# === 应急 vs 根治 ===
# 应急:LimitNOFILE 调高 + 重启,先把服务救活。
# ★ 根治:顺着 lsof / CLOSE_WAIT 的线索回到代码,
#   把漏关的 socket/文件补上 close。
#   ——上限再高,也扛不住一个只涨不降的泄漏。

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

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

# === 1. ★ "Too many open files" 不只是磁盘文件 ===
# fd 包含 socket、管道。高并发服务的 fd 大头是网络连接。
$ ls -l /proc/PID/fd | grep -c socket    # 看有多少是连接

# === 2. ★ 看进程实际的 fd 上限和用量,认准 /proc ===
$ cat /proc/PID/limits | grep 'open files'   # 实际上限
$ ls /proc/PID/fd | wc -l                    # 实际用量

# === 3. ★ 改限制要改对地方 ===
# 登录会话 -> /etc/security/limits.conf(重新登录生效)
# systemd 服务 -> unit 文件 LimitNOFILE(daemon-reload)
# 系统总上限 -> sysctl fs.file-max

# === 4. ★ 改完必须验证进程拿到的值 ===
$ cat /proc/PID/limits | grep 'open files'
# 看配置文件不算数,看进程实际继承到的值才算数。

# === 5. 区分"真不够"和"泄漏":看 fd 曲线 ===
$ watch -n5 'ls /proc/PID/fd | wc -l'
# 波动能回落=扩容;只涨不降=泄漏,要修代码。

# === 6. CLOSE_WAIT 堆积 = 代码漏了 close ===
$ ss -tan state close-wait | wc -l

# === 7. 排查 fd 问题的命令链 ===
$ ls /proc/PID/fd | wc -l                # ① 开了多少 fd
$ cat /proc/PID/limits | grep open       # ② 上限是多少
$ lsof -p PID | awk '{print $9}' | sort | uniq -c | sort -rn  # ③ 耗在哪
$ ss -s                                  # ④ 连接状态分布
$ ss -tan state close-wait | wc -l       # ⑤ 有没有泄漏迹象
# 按这个顺序,fd 问题基本能定位。

命令速查

需求                        命令
=============================================================
看当前 shell 的 fd 上限     ulimit -n
看软/硬限制                 ulimit -Sn / ulimit -Hn
看进程实际的 fd 上限        cat /proc/PID/limits | grep 'open files'
看进程开了多少 fd           ls /proc/PID/fd | wc -l
看进程 fd 明细              lsof -p PID
找开 fd 最多的进程          遍历 /proc/*/fd 计数排序
看系统级 fd 使用            cat /proc/sys/fs/file-nr
看 socket 状态分布          ss -s
数 CLOSE_WAIT 连接          ss -tan state close-wait | wc -l
systemd 服务调 fd 上限      unit 文件加 LimitNOFILE=65536

口诀:open files 含 socket,高并发服务的 fd 大头是连接
      systemd 服务不读 limits.conf,要在 unit 里配 LimitNOFILE

避坑清单

  1. Too many open files 里的 files 是文件描述符,包含 socket 和管道,不只是磁盘文件
  2. 高并发网络服务的 fd 大头是网络连接,排查方向别只盯磁盘文件
  3. ulimit 分软限制和硬限制,软限制是实际生效值,普通用户调不过硬限制
  4. ulimit 改动不持久,且进程继承的是启动者的 ulimit,不是你当前 shell 的
  5. systemd 启动的服务不读 /etc/security/limits.conf,必须在 unit 文件配 LimitNOFILE
  6. 改完限制一定去 /proc/PID/limits 验证进程实际拿到的值,别只看配置文件
  7. limits.conf 改完要重新登录才生效,它由登录时的 PAM 加载
  8. fd 数只涨不降是泄漏,波动能回落才是真高并发,看曲线区分
  9. 大量 CLOSE_WAIT 堆积几乎实锤代码漏了 close,扩容上限治标不治本
  10. 系统级 fd 总上限由 fs.file-max 控制,进程级上限再高也受它约束

总结

这次"网关服务被 Too many open files 压垮"的事故,纠正了我一个非常基础、却一直没真正想透的认知——Linux 里的"文件"这个词,到底指的是什么。在这次之前,我看到 "open files" 这两个词,脑子里浮现的画面是确凿无疑的:磁盘上的一个个文件,被程序用 open 打开。所以当报错说"打开的文件太多",我的全部排查动作,就顺理成章地锁死在一个方向上——去代码里翻所有读写磁盘文件的地方,一个个确认 close 有没有调。我翻得很认真,也确实没找到任何文件泄漏,可报错丝毫没有缓解。我卡在这里很久,卡的原因不是我不够仔细,而是我打从一开始,就在一个错误的地图上找路。复盘到根上,我才真正理解了"一切皆文件"这句被我背了无数遍、却从未真正消化的 Linux 设计哲学。它的意思,不是"很多东西长得像文件",而是说:在 Linux 看来,一个进程想访问的几乎所有资源——磁盘上的文件、一个网络连接、一根管道、甚至标准输入输出——都被统一抽象成了同一种东西,进程通过一个叫"文件描述符"的小整数去握住它。磁盘文件占一个文件描述符,而一个网络连接,一个 socket,同样、平等地占用一个文件描述符。Linux 不区分它们,它把它们扔进【同一个】配额里一起计数。而每一个进程,能同时握住的文件描述符数量,是有一个明确上限的——默认常常就是那个朴素的 1024。我的网关服务,本职工作就是同时维持成千上万个并发连接,每一个连接都是一个 socket,都实实在在地占着一个文件描述符。流量一爬升,并发连接数越过 1024 这道线,进程再想为新连接申请一个文件描述符时,系统就一口回绝——而它回绝时吐出的那句话,正是 Too many open files。压垮服务的"太多文件",从头到尾没有一个是磁盘文件,它们是几千个网络连接;我对着磁盘文件查了那么久,查的是一个根本不存在问题的地方。看清了这一层,后面那个 systemd 的坑也就顺理成章——我一度以为改了 /etc/security/limits.conf 就万事大吉,却忘了那个文件是给"登录会话"用的、靠登录时的 PAM 加载,而 systemd 拉起一个服务根本不走登录这条路,于是那份配置对它视而不见,真正该改的是 unit 文件里的 LimitNOFILE。这次从一个"文件没关好"的错误假设出发,我最大的收获,是把"文件"这个词在脑子里彻底地扩容了。当 Linux 说 "open files" 时,它说的从来不只是磁盘上那些有名有姓的文件,而是这个进程此刻正握在手里的一切资源句柄——文件、连接、管道,一视同仁。一个几乎不碰磁盘的网络服务,完全可能被 "open files" 太多给压垮,因为它握着的几千个连接,在 Linux 眼里,和几千个打开的文件,是同一回事。排查一个问题,最怕的不是不够努力,而是带着一张画错了的地图在拼命赶路;而这次,是 Too many open files 这句报错,亲手帮我把"文件"这块地图上的疆域,重新画对了。

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

磁盘明明满了 du 却找不到:一次 Linux 删了文件空间不释放排查复盘

2026-5-20 19:49:04

Linux教程

nginx 配置全对却 403:一次 Linux SELinux 安全上下文排查复盘

2026-5-20 19:57:34

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