ulimit 改了还是 Too many open files:一次 Linux 文件描述符限制排查复盘

ulimit -n 改成 65535 重启服务,依然 Too many open files。排查梳理:文件描述符是有限资源、ulimit 的 soft/hard 与作用域、systemd 服务不读 limits.conf 只认 LimitNOFILE、用 lsof 与 /proc 数 fd 找泄漏、内核全局 fs.file-max,以及一套 fd 排查纪律。

2024 年,一个稳定跑了大半个月的服务,突然开始大面积报错:Too many open files。新的请求连不进来,日志里刷满了这一行。我当时对这个错误的理解很朴素——不就是打开的文件太多了嘛,把上限调大不就行了。我登上机器,ulimit -n 一看是 1024,确实小;我利落地敲下 ulimit -n 65535 把它改大,然后重启服务,心想这事儿稳了。结果服务起来没几分钟,Too many open files 又一字不差地刷了出来。我盯着屏幕愣住了:我命令行里 ulimit -n 明明白白显示 65535 了,这服务怎么还在 1024 的老限制上撞墙?这件事逼着我把 Linux 的文件描述符、ulimit、limits.conf、systemd 资源限制这一整套彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,一个用 systemd 管理的网络服务
事故现象:
- 服务报 Too many open files,新连接全部失败
- ulimit -n 改成 65535 后重启服务,依然报错
- 命令行里 ulimit -n 明明显示 65535

现场排查:
# 1. 当前 shell 的 ulimit —— 看起来已经改大了
$ ulimit -n
65535                              # ★ 我以为这就生效了

# 2. ★ 但服务进程【真实生效】的限制,要看 /proc
$ ps -ef | grep myapp
myapp     8123  ...
$ cat /proc/8123/limits | grep 'open files'
Max open files    1024    1024    files    # ★ 服务还是 1024!
# 我改的 ulimit,根本没作用到这个服务进程上。

# 3. 这个服务是 systemd 起的
$ systemctl status myapp
   Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
$ systemctl show myapp -p LimitNOFILE
LimitNOFILE=1024                   # ★ systemd 给它的限制就是 1024

# 4. 看这个进程到底开了多少个 fd
$ ls /proc/8123/fd | wc -l
1024                               # ★ 顶满了,一个都开不出来了

根因(后来想清楚的):
1. ★ systemd 启动的服务,【不读】/etc/security/limits.conf,
   也【不继承】我在某个 shell 里敲的 ulimit。
2. systemd 服务的资源限制,由【unit 文件里的 LimitNOFILE】
   单独决定。我没配它,就用默认值 1024。
3. 我在交互式 shell 里 ulimit -n 65535,只对【那个 shell
   及它的子进程】有效;systemd 是另一条进程树,够不着。
4. 所以服务重启后,systemd 还是按 unit 文件给它 1024。
5. ★ 而服务本身可能还有 fd 泄漏 —— 连接用完不关,
   fd 只涨不跌,迟早撞上限。限制要调,泄漏更要查。
我改的限制,和服务进程根本不在同一条进程树上。

修复 1:文件描述符是什么——一种有限的资源

# === ★ Linux 里"一切皆文件",文件描述符(fd)是它们的句柄 ===
# 进程每打开一个东西,内核就给它一个【整数编号】——
# 这个编号就是文件描述符 fd。之后读写都用这个号。
# ★ 关键:fd 不只对应"磁盘文件",它还对应:
#   - 普通文件
#   - 网络 socket(每个 TCP 连接,都占一个 fd!)
#   - 管道 pipe
#   - 设备
# ★ 所以"Too many open files"经常不是文件多,而是【连接】多。

# === 每个进程都有 3 个标准 fd ===
# fd 0 = 标准输入 stdin
# fd 1 = 标准输出 stdout
# fd 2 = 标准错误 stderr
# 之后打开的东西,fd 从 3 开始递增。

# === ★ 直接看一个进程当前开着哪些 fd ===
$ ls -l /proc/8123/fd
lrwx------ 1 ... 0 -> /dev/null
lrwx------ 1 ... 1 -> 'socket:[123456]'        # 一个 socket
lrwx------ 1 ... 2 -> /var/log/myapp/app.log   # 一个日志文件
lrwx------ 1 ... 3 -> 'socket:[123457]'        # 又一个 socket
# ★ /proc/PID/fd 这个目录,把进程开的每个 fd 都列得清清楚楚。

# === 数一个进程开了多少 fd ===
$ ls /proc/8123/fd | wc -l
1024
# ★ 这个数字逼近上限,就是"Too many open files"的前兆。

# === 为什么 fd 要有上限 ===
# 每个打开的 fd,内核都要为它维护数据结构、占用内存。
# fd 无上限 = 一个进程能把内核内存吃光 ——
# 所以内核给每个进程都设了"最多能开多少 fd"的限制。

修复 2:ulimit——soft limit 与 hard limit

# === ulimit:看 / 改当前 shell 的资源限制 ===
$ ulimit -n                     # 看"最多开多少文件"(open files)
1024
$ ulimit -a                     # ★ 看所有资源限制
core file size          (blocks, -c) 0
open files                      (-n) 1024     # ★ 就是它
max user processes              (-u) 4096
...

# === ★ soft limit 与 hard limit:两个不同的上限 ===
$ ulimit -Sn                    # soft limit(软限制)—— 当前实际生效的值
1024
$ ulimit -Hn                    # hard limit(硬限制)—— soft 能调到的天花板
4096
# 规则:
#   - 进程实际受 soft limit 约束。
#   - 普通用户可以把 soft 在【0 ~ hard】之间随意调。
#   - ★ 普通用户【只能调低】hard,不能调高;调高 hard 要 root。
$ ulimit -n 4096                # 把 soft 调到 hard 那么高(允许)
$ ulimit -n 999999              # 超过 hard,报错(普通用户做不到)

# === ★ ulimit 的作用域:只影响当前 shell 及其子进程 ===
# 你在一个 shell 里 ulimit -n 65535,
# 只对【这个 shell,以及从它里面启动的进程】有效。
# 关掉这个 shell、换一个终端,一切复原。
# ★ 这正是这次的坑:我在交互 shell 里改的,
#   和 systemd 那条进程树,八竿子打不着。

# === 永久改 ulimit:/etc/security/limits.conf ===
$ vim /etc/security/limits.conf
# 用户名  限制类型  限制项   值
myapp    soft    nofile   65535
myapp    hard    nofile   65535
*        soft    nofile   65535      # * 对所有用户
*        hard    nofile   65535
# ★ 重要:limits.conf 由 PAM 模块在【用户登录时】加载。
#   所以它对【登录会话】有效 —— ssh 登录、su 切换。
#   改完要【重新登录】才生效。
$ ulimit -n                     # 重新登录后再看

# === ★ 但 limits.conf 有个致命盲区(下一节就是它)===
# limits.conf 走的是登录会话这条路。
# 而 systemd 启动服务,【根本不经过登录会话】——
# 所以 limits.conf 对 systemd 服务【完全无效】。

修复 3:systemd 服务的限制——这次的根因

# === ★ 核心认知:systemd 服务不读 limits.conf,不继承你的 ulimit ===
# systemd 服务的资源限制,只由【它自己的 unit 文件】决定。
# 你在 shell 里 ulimit、在 limits.conf 里写的,它统统不看。

# === 看一个 systemd 服务【真实】的 fd 限制 ===
$ systemctl show myapp -p LimitNOFILE
LimitNOFILE=1024                # ★ systemd 给它的值
# 或者直接看进程实际生效的(最权威):
$ cat /proc/$(pgrep myapp)/limits | grep 'open files'
Max open files    1024    1024

# === ★ 正确的修法:改 unit 文件的 LimitNOFILE ===
$ systemctl edit myapp          # ★ 推荐:创建一个 override 片段
# 在打开的编辑器里写:
[Service]
LimitNOFILE=65535
# 这会生成 /etc/systemd/system/myapp.service.d/override.conf
# —— 不动原 unit 文件,升级也不会被覆盖。

# === 或者直接改 unit 文件本体 ===
$ vim /etc/systemd/system/myapp.service
# 在 [Service] 段里加一行:
# LimitNOFILE=65535

# === ★ 改完 unit 文件,必须这两步 ===
$ systemctl daemon-reload       # ① 让 systemd 重新读取 unit 配置
$ systemctl restart myapp       # ② 重启服务,新限制才作用到新进程
# ★ 只 daemon-reload 不 restart,老进程还是老限制。

# === 验证生效(必做)===
$ systemctl show myapp -p LimitNOFILE
LimitNOFILE=65535
$ cat /proc/$(pgrep myapp)/limits | grep 'open files'
Max open files    65535   65535   files     # ★ 这才算真的生效

# === systemd 还有一个【全局默认】值 ===
$ grep DefaultLimitNOFILE /etc/systemd/system.conf
# DefaultLimitNOFILE=1024
# unit 文件没单独写 LimitNOFILE 时,就用这个全局默认。

# === ★ 一句话纪律:服务进程的限制,永远以 /proc/PID/limits 为准 ===
# 别信你在 shell 里 ulimit 看到的 —— 那是 shell 的,不是服务的。

修复 4:数 fd、找 fd 泄漏——别只顾调大限制

# === ★ 调大限制只是"止血",fd 为什么涨上来才是病根 ===
# 如果服务有 fd 泄漏,你把限制从 1024 调到 65535,
# 只是把"撞墙"推迟几小时 —— 它照样会涨满。

# === lsof:列出打开的文件(fd 排查神器)===
$ lsof -p 8123                  # 看某进程打开的所有 fd
$ lsof -p 8123 | wc -l          # 数一数总数
$ lsof | wc -l                  # 全系统打开的文件总数

# === ★ 按进程统计 fd 数,揪出 fd 大户 ===
$ ls /proc/8123/fd | wc -l      # 单个进程的 fd 数(最快)
# 一条命令找出 fd 开得最多的几个进程:
$ for p in /proc/[0-9]*; do echo "$(ls $p/fd 2>/dev/null|wc -l) $p"; done \
    | sort -rn | head
65530 /proc/8123                # ★ 一眼看出谁在狂吃 fd

# === ★ 判断是不是 fd 泄漏:看 fd 数随时间的趋势 ===
$ watch -n 5 'ls /proc/8123/fd | wc -l'
# fd 数【只涨不跌、单调上升】 -> 几乎肯定是 fd 泄漏。
# 正常服务的 fd 数应该是【上下波动、稳定在一个区间】。

# === ★ 看泄漏的是哪种 fd —— 定位泄漏点 ===
$ lsof -p 8123 | awk '{print $5}' | sort | uniq -c | sort -rn
  64000 IPv4              # ★ 大量 socket 没关 -> 网络连接泄漏
     20 REG               # 普通文件
      5 DIR
# socket 泄漏:多半是 HTTP 客户端 / 数据库连接用完没关、
#             或连接池配置有问题。
# REG 泄漏:打开文件没 close。

$ lsof -p 8123 | grep TCP | awk '{print $8,$9}' | sort | uniq -c | sort -rn
# 进一步看泄漏的连接都连向哪 —— 往往直指某个下游。

# === fd 泄漏的常见代码病根 ===
# - 打开文件 / 建连接后,异常路径上漏了 close()
# - HTTP 客户端拿到 response 不读完、不关闭
# - 连接池只借不还,或池子配置无上限
# ★ 根治:在代码里用 try-with-resources / defer / with 这类
#   "保证关闭"的写法,别手动 close。

修复 5:进程 ulimit 之上,还有内核全局上限

# === ★ fd 的限制是【两层】的,别只盯着 ulimit ===
# 第一层:ulimit -n / LimitNOFILE —— 【单个进程】最多开多少
# 第二层:内核参数 —— 【整台机器所有进程加起来】最多开多少
# 两层都要够,才真的够。

# === fs.file-max:整个系统能打开的文件总数上限 ===
$ cat /proc/sys/fs/file-max
2097152
$ sysctl fs.file-max
# 调大它(临时):
$ sysctl -w fs.file-max=3000000
# 永久:写进 /etc/sysctl.conf 再 sysctl -p
$ echo 'fs.file-max = 3000000' >> /etc/sysctl.conf
$ sysctl -p

# === 看系统当前一共开了多少文件 ===
$ cat /proc/sys/fs/file-nr
9600    0    2097152
# 三个数:已分配的fd数  已分配但未使用  上限(file-max)
# ★ 第一个数逼近第三个数 -> 系统级 fd 要耗尽了。

# === fs.nr_open:单个进程 fd 数的【硬天花板】 ===
$ cat /proc/sys/fs/nr_open
1048576
# ★ 你的 ulimit -n / LimitNOFILE,【不能超过】 nr_open。
#   想把 LimitNOFILE 设到 200 万,得先把 nr_open 调上去。

# === ★ 两层限制的关系,一句话理清 ===
# 一次"打开文件"要同时满足:
#   1. 该进程已开 fd 数 < 它的 ulimit(LimitNOFILE)
#   2. 全系统已开 fd 数 < fs.file-max
# 任何一层满了,都会 Too many open files。
# ★ 单个服务报错,九成是第一层(进程 ulimit);
#   多个不相关进程同时报错,才怀疑第二层(file-max)。

# === 排查时,两层都确认一遍 ===
$ cat /proc/$(pgrep myapp)/limits | grep 'open files'   # 第一层
$ cat /proc/sys/fs/file-nr                              # 第二层

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

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

# === 1. ★ 服务的真实限制,看 /proc/PID/limits,别信 shell 的 ulimit ===
$ cat /proc/$(pgrep 服务名)/limits | grep 'open files'
# 你 shell 里 ulimit -n 看到的,是 shell 的,不是服务的。

# === 2. ★ systemd 服务改限制,改 LimitNOFILE,不是 limits.conf ===
$ systemctl edit 服务名         # 加 [Service] LimitNOFILE=65535
$ systemctl daemon-reload && systemctl restart 服务名
# limits.conf 对 systemd 服务无效,这是头号坑。

# === 3. 改完一定要验证,别想当然 ===
$ systemctl show 服务名 -p LimitNOFILE
$ cat /proc/$(pgrep 服务名)/limits | grep 'open files'
# 两处都对上了,才算真生效。

# === 4. ★ 调大限制只是止血,fd 趋势才是病根 ===
$ watch -n 5 'ls /proc/$(pgrep 服务名)/fd | wc -l'
# fd 数只涨不跌 = fd 泄漏,光调限制治标不治本。

# === 5. 泄漏定位:看泄漏的是 socket 还是普通文件 ===
$ lsof -p PID | awk '{print $5}' | sort | uniq -c | sort -rn
# socket 占绝大多数 -> 连接没关 / 连接池问题。

# === 6. 别忘了内核全局上限 fs.file-max ===
$ cat /proc/sys/fs/file-nr      # 多进程同时报错时查这层
# 单进程报错多半是 ulimit,多进程一起报才怀疑 file-max。

# === 7. 排查 Too many open files 的命令链 ===
$ cat /proc/PID/limits | grep 'open files'   # ① 进程真实限制
$ ls /proc/PID/fd | wc -l                    # ② 当前开了多少
$ watch 'ls /proc/PID/fd|wc -l'              # ③ 是不是只涨不跌
$ lsof -p PID | awk '{print $5}'|sort|uniq -c # ④ 泄漏的是哪种 fd
$ cat /proc/sys/fs/file-nr                   # ⑤ 系统全局够不够
# 按这个顺序,fd 问题基本能定位。

命令速查

需求                        命令
=============================================================
看当前 shell 的 fd 限制      ulimit -n
看 soft / hard 限制          ulimit -Sn / ulimit -Hn
看进程真实生效的限制         cat /proc/PID/limits
看 systemd 服务的限制        systemctl show 服务名 -p LimitNOFILE
改 systemd 服务的限制        systemctl edit 服务名
数进程开了多少 fd            ls /proc/PID/fd | wc -l
列出进程打开的所有文件       lsof -p PID
看泄漏的 fd 类型             lsof -p PID | awk '{print $5}' | sort | uniq -c
看系统全局 fd 上限           cat /proc/sys/fs/file-max
看系统当前已开 fd 数         cat /proc/sys/fs/file-nr

口诀:服务限制看 /proc/PID/limits 不看 shell ulimit
      systemd 服务改 LimitNOFILE -> fd 只涨不跌就是泄漏

避坑清单

  1. Too many open files 常不是文件多,是 socket 多——每个 TCP 连接占一个 fd
  2. 服务进程真实生效的限制看 /proc/PID/limits,别信 shell 里的 ulimit
  3. ulimit 只影响当前 shell 及其子进程,换个终端就复原
  4. systemd 服务不读 limits.conf、不继承你的 ulimit,限制由 LimitNOFILE 决定
  5. 改 LimitNOFILE 后要 daemon-reload 加 restart,只 reload 老进程不变
  6. soft limit 是实际生效值,hard 是天花板,普通用户调不高 hard
  7. 调大限制只是止血,fd 数只涨不跌说明有 fd 泄漏,要查代码
  8. 泄漏定位看 lsof 里 socket 还是 REG 占多数,socket 多就是连接没关
  9. fd 限制有两层:进程 ulimit 和内核 fs.file-max,两层都要够
  10. 单进程报错多半是 ulimit,多个不相关进程同时报才怀疑 file-max

总结

这次"ulimit 改了还是 Too many open files"的事故,纠正了我一个关于"我下的命令,影响范围到底有多大"的模糊认知。在这次之前,我心里有一个含糊的、从未被审视过的画面:我以为我在一个终端里敲下的 ulimit -n 65535,是在"给这台机器"调整设置——既然是这台机器上的服务在报错,那我在这台机器上把限制调大,服务自然就该好了。正是这个"我在给机器调设置"的错觉,让我在 ulimit -n 明明显示着 65535、服务却依然撞在 1024 的墙上时,陷入了彻底的困惑。复盘到根上,我才真正想清楚 ulimit 这个命令的作用域:它根本不是"给机器"设置什么,它设置的,是【当前这个 shell 进程】的资源限制,而这个限制,只会被这个 shell 以及由它【亲手 fork 出来的子进程】所继承。我在交互式终端里敲的 ulimit,影响的是那个终端那一条进程树。而我的那个服务,是由 systemd 启动的——systemd 是系统的 1 号进程,是另一条完全独立的、与我的登录终端毫无血缘关系的进程树。我的 ulimit 命令,和那个服务进程之间,隔着两条永不相交的进程树,我对着自己这棵树喊话,那棵树上的服务,自然一个字都听不见。更进一步,我还搞清了一件让我恍然大悟的事:不仅我的 ulimit 够不着 systemd 服务,连那个被无数教程奉为"永久修改"标准答案的 /etc/security/limits.conf,对 systemd 服务一样【完全无效】。原因在于,limits.conf 是由 PAM 模块在"用户登录"这个动作发生时加载的——它服务的是 ssh 登录、是 su 切换这类"登录会话";而 systemd 启动一个后台服务,根本不经过任何"登录"的过程。于是,systemd 服务的资源限制,走的是一条完全独立的路:它只认自己 unit 文件里那一行 LimitNOFILE;我没有写它,它就安安静静地用着 systemd 的全局默认值 1024。找到这个根因,正确的修法就清晰了——用 systemctl edit 给服务追加一段 [Service] 配置,写上 LimitNOFILE=65535,再 daemon-reloadrestart,最后用 cat /proc/PID/limits 亲眼确认它真的变了。这个 /proc/PID/limits,是我这次带走的最锋利的一把刀:无论你在 shell 里、在配置文件里做了什么,一个进程【真实生效】的限制,永远、唯一地写在 /proc/PID/limits 这个文件里——排查时不看它而去看 shell 的 ulimit,就是在看一个根本不相干的东西。但这次事故还逼着我想明白了另一半、也是更重要的一半道理:我一开始的思路,本质上是"限制不够,那就调大限制",这是一种治标的、甚至危险的思路。因为如果这个服务本身存在 fd 泄漏——它建立的连接用完之后没有被关闭,fd 只增不减——那么我把限制从 1024 调到 65535,做的全部事情,不过是把它撞墙的时刻,从"一小时后"推迟到了"两天后"而已,墙还在那里,它迟早还会撞上去。真正判断该不该调限制的依据,是观察这个进程的 fd 数量随时间的变化趋势:如果它是上下波动、稳定在某个区间的,那说明限制确实是容量不足,该调大;但如果它是单调上升、只涨不跌的,那就是一个确凿无疑的 fd 泄漏信号,这时候唯一正确的事,是拿着 lsof 去看泄漏的究竟是 socket 还是普通文件,然后回到代码里,找到那个忘记 close 的地方。这次从一个"命令改了却不生效"的困惑出发,我最大的收获有两点:一是终于看清了进程树是有边界的,一条命令的影响力,被它所在的那条进程树死死地框住,跨不过去;二是想明白了"调大限制"和"修复泄漏"是两件性质完全不同的事——前者是止血,后者才是治病,而一个只涨不跌的 fd 曲线,就是那张让你别再满足于止血的诊断书。

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

两台机器日志时间差了 8 秒:一次 Linux 时间同步与 chrony 排查复盘

2026-5-20 18:56:59

Linux教程

load 飙到 40 但 CPU 几乎空闲:一次 Linux 负载与 CPU 排查复盘

2026-5-20 19:03:18

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