2023 年,一次"我把 ulimit 明明改成了 65535,服务还是死活报 Too many open files"的事故,把我对"设置一个限制"这件事的理解,从头到尾翻新了一遍。那台服务器上跑着一个 Java 服务,平时好好的,可一到流量高峰,日志里就开始刷 java.io.IOException: Too many open files,连接处理一片混乱。我一看就知道:文件描述符不够了,把上限调大就行。我登上服务器,敲 ulimit -n,显示 1024——果然太小。我立刻 ulimit -n 65535,再 ulimit -n 确认,显示 65535,改好了。我重启服务,满心以为这事结了。可流量一上来,日志里 Too many open files——【一个字没少】,照刷。我懵了,又去查,网上说要改 /etc/security/limits.conf 才持久。我照改,加了 * - nofile 65535,重启服务——还是报。我盯着屏幕,脑子里全是问号:我 ulimit -n 查出来明明白白是 65535,我配置文件也改了,我能改的地方【全都改了】,可那个服务,就像活在另一个世界里,死守着它那个 1024 不放。我改的那个"限制",和那个服务身上真正生效的"限制",到底是不是【同一个东西】?如果是,它为什么不听我的?如果不是——那我这半天,改的究竟是【谁】的限制?这件事逼着我把软硬限制、limits.conf 的作用边界、systemd 服务的资源限制,还有"我设的限制"和"进程实际受的限制"的天壤之别,彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,systemd 管理的 Java 服务,流量高峰报错
事故现象:
- 高峰期日志刷:java.io.IOException: Too many open files
- ulimit -n 查到是 1024,改成 65535
- ★ 重启服务,还是报 Too many open files
- 改了 /etc/security/limits.conf,重启服务,★ 依然报
现场排查:
# 1. ★ 我在 shell 里查、改 ulimit
$ ulimit -n
1024
$ ulimit -n 65535
$ ulimit -n
65535 # ★ shell 里确实改成了 65535
# 2. ★ 但服务还在报。看服务进程【实际】的限制
$ ps -ef | grep java # 拿到 java 进程 PID,假设 8888
$ cat /proc/8888/limits | grep 'open files'
Max open files 1024 1024 files # ★★ 进程实际上限,还是 1024!
# 3. ★ 我改了 limits.conf
$ grep nofile /etc/security/limits.conf
* - nofile 65535 # ★ 我加的这行
# 4. ★ 关键:这个 java 服务,是谁启动的
$ systemctl status myapp
● myapp.service # ★★ 是 systemd 管的服务!
$ cat /proc/8888/limits | grep 'open files'
Max open files 1024 1024 files # ★ limits.conf 改了也没用
# 5. ★ 看 systemd 给服务设的限制
$ systemctl show myapp -p LimitNOFILE
LimitNOFILE=1024 # ★★ 真凶在这!systemd 设的
根因(后来想清楚的):
1. ★ "文件描述符上限"不是一个全局开关。它是【每个
进程各自】的一份属性,进程启动时从父进程【继承】。
2. ★ 我 ulimit -n 65535 —— 只改了【我这个 shell
进程】的那一份。它只对"我这个 shell,以及我从
这个 shell 里直接启动的子进程"生效。
3. ★ 那个 java 服务,不是我从 shell 启动的。它是
systemd 启动的 —— 它的父进程是 systemd,继承的
是【systemd 给它的限制】,跟我的 shell 毫无关系。
4. ★ /etc/security/limits.conf 是 PAM 的配置。它只
在【用户登录(走 PAM)】时生效。systemd 启动
守护进程,【根本不走 PAM】-> limits.conf 对它无效。
5. ★ systemd 管的服务,它的资源上限,由 service
单元文件里的 LimitNOFILE= 决定(没写就用全局
默认 DefaultLimitNOFILE)。我没改这个 -> 服务
一直是 systemd 给的那个 1024。
6. 真相:我改了三个地方,但【没有一个】是那个
服务真正继承限制的来源。我对着空气调了半天。
不是限制改不动,是限制根本没有"全局"这回事,
我改的限制,和那个进程继承的限制,不是一份。
修复 1:ulimit 是什么——每个进程各自的资源上限
# === ★ 先搞清楚 ulimit 到底在限制什么 ===
# === ★ ulimit 是"每个进程"的资源上限 ===
# 操作系统怕单个进程失控耗尽资源,给【每个进程】都
# 套了一组上限:能开多少文件、能用多大内存栈、
# 能开多少进程……这组上限,就是 ulimit(resource
# limit)。
$ ulimit -a # 看当前 shell 的所有限制
# open files (-n) 1024 # ★ 最多能开的文件描述符数
# max user processes (-u) 4096 # 最多能开的进程数
# stack size (-s) 8192 # 栈大小 ...
# === ★ 本文的主角:open files(-n)===
# ★ Linux 里,"打开的文件"不只是磁盘文件 —— 每一个
# 网络连接(socket)、每一个管道,都占一个【文件
# 描述符(fd)】。
# ★ -n 限的就是"一个进程最多能同时持有多少个 fd"。
# 高并发服务,连接多 -> fd 多 -> 容易撞上这个上限
# -> 报 Too many open files。
$ ulimit -n # 只看 open files 这一项
# === ★ 关键:每个限制都有"软"和"硬"两个值 ===
$ ulimit -Sn # 软限制(soft):实际生效的值
1024
$ ulimit -Hn # 硬限制(hard):软限制的天花板
4096
# ★ 软限制(soft):★ 真正起作用、进程实际受的那个值。
# ★ 硬限制(hard):软限制能调到的【最大值】。
# ★ 规则:普通用户,可以把软限制【在 0 到硬限制
# 之间】随便调;但【不能把硬限制调高】(只能调低)。
# 想突破硬限制,得 root。
# === ★ 一个常见的"改不动"陷阱 ===
$ ulimit -n 65535 # 普通用户这么改
bash: ulimit: open files: cannot modify limit: ...
# ★ 报这个错,是因为 65535 【超过了硬限制】。普通
# 用户够不到那么高。要么先(以 root)抬高硬限制,
# 要么这个值本就由 root 在配置里设好。
# === 认知 ===
# ★ ulimit 是操作系统给【每个进程】套的资源上限,
# open files(-n)限的是一个进程能同时持有多少
# 文件描述符,而每个网络连接也占一个 fd,所以高
# 并发服务容易撞上。每个限制有软、硬两个值:软
# 限制是实际生效的,硬限制是软限制的天花板,普通
# 用户只能在硬限制以内调软限制,抬硬限制要 root。
修复 2:ulimit 命令为什么"改了不生效"
# === ★ 把"ulimit -n 改了却没用"讲透 ===
# === ★ ulimit 命令,只改"当前这个 shell" ===
# ★ ulimit -n 65535 这条命令,改的是【你当前这个
# shell 进程】的限制。它的作用范围,只有两个:
# ① 你当前这个 shell 自己;
# ② ★ 你【从这个 shell 里、往后】启动的子进程
# —— 因为子进程会【继承】父进程的限制。
# === ★ 所以它有两个致命的局限 ===
# ★ 局限一:【关掉 shell 就没了】。ulimit 改的是
# 内存里这个 shell 进程的属性,不写任何文件。你
# 一退出登录,下次再进来,又是默认值。它【不持久】。
# ★ 局限二:★ 它【管不到】"不是这个 shell 启动的"
# 进程。一个早就在跑的服务、一个 systemd 启动的
# 服务 —— 它们的限制,在它们【出生时】就从【各自
# 的父进程】继承定了,和你这个 shell 八竿子打不着。
# === ★ 本文事故的第一个错,就在这 ===
# ★ 我 ulimit -n 65535,改的是【我登录的这个 shell】。
# ★ 那个 java 服务,是 systemd 早就启动好的,它的
# 父进程是 systemd。我那个 shell,和那个服务,是
# 两条【毫不相干的进程谱系】。
# ★ 我在我的 shell 里把限制喊到 65535,那个服务
# 听都听不见 —— 我俩根本不在一条继承链上。
# === ★ 顺带:限制只能在启动时继承,不能"事后改" ===
# ★ 一个进程【已经在跑】了,你【没有办法】从外面
# 把它的 fd 软限制改大(prlimit 能改,但需谨慎,
# 且很多程序启动时已按旧值分配好了内部结构)。
# ★ 所以正确姿势永远是:【先把限制配对,再启动 /
# 重启那个进程】,让它出生时就继承到大的限制。
# === ★ 验证"继承":从改过的 shell 启子进程 ===
$ ulimit -n
1024
$ ulimit -n 4096 # 改当前 shell(不超硬限制)
$ bash -c 'ulimit -n' # ★ 启个子 shell 看它继承到什么
4096 # ★ 子进程继承了 4096
# ★ 这印证了:限制是【父传子】继承的。改了爹,只有
# 爹和"改完之后生的儿子"受影响。
# === 认知 ===
# ★ ulimit 命令只改【当前这个 shell 进程】的限制,
# 作用范围仅限这个 shell 和它之后启动的子进程
# (子进程继承父进程的限制)。它两个致命局限:
# 不写文件、退出登录就失效;管不到不是这个 shell
# 启动的进程 —— 服务的限制在它出生时就从自己的
# 父进程继承定了。所以要先配好限制再启动进程。
修复 3:limits.conf——持久化,但它只管"PAM 登录会话"
# === ★ limits.conf 能持久化,但有个关键的作用边界 ===
# === ★ /etc/security/limits.conf 是什么 ===
# ★ ulimit 命令不持久,那持久的配置在哪?——
# /etc/security/limits.conf(及 limits.d/ 目录)。
$ vi /etc/security/limits.conf
# 格式:<对谁> <soft/hard> <限制项> <值>
* soft nofile 65535 # 所有用户,软限制
* hard nofile 65535 # 所有用户,硬限制
deploy soft nofile 100000 # 只对 deploy 用户
root hard nofile 100000 # 对 root
# ★ nofile 就是 open files。soft/hard 对应软硬限制。
# ★ 两行都要写:hard 是天花板,soft 才是实际值。
# === ★ 致命的关键:limits.conf 由【谁】来执行 ===
# ★ limits.conf 这个文件,【不是内核读的】,也不是
# "系统全局"自动生效的。它是 ★ PAM 的一个模块
# (pam_limits.so)在读、在应用的。
$ grep pam_limits /etc/pam.d/* # 看哪些场景挂了这个模块
/etc/pam.d/login: ... pam_limits.so
/etc/pam.d/sshd: ... pam_limits.so
# ★ PAM 是"用户认证 / 会话"框架。pam_limits.so 只
# 在【一次 PAM 会话建立时】被触发 —— 也就是
# 【一个用户登录的那一刻】。
# === ★ 所以 limits.conf 的作用边界是 ===
# ★ 它【只对"经过 PAM 登录"的会话】生效:
# - 你 SSH 登录 -> 走 pam_limits.so -> 生效 ✓
# - 你 su / 切换用户登录 -> 生效 ✓
# - 你在登录后的 shell 里启动的程序 -> 继承,生效 ✓
# ★ 它【对这些,完全无效】:
# - ★ systemd 启动的守护进程 —— 【不走 PAM 登录】!
# - 开机时随系统拉起的服务
# ★ 这就是本文第二个错:我改了 limits.conf,可那个
# java 服务是 systemd 拉起的,根本没经过 PAM 登录
# 这道门 -> limits.conf 那行字,它一个标点都没读到。
# === ★ 怎么确认 limits.conf 对登录会话生效了 ===
# ★ 改完 limits.conf,必须【重新登录】(退出 SSH
# 再连)才生效 —— 因为它在"登录那一刻"才被应用。
$ exit # 退出,重新 SSH 登录
$ ulimit -n # 新会话里看,应是新值
65535
# === 认知 ===
# ★ /etc/security/limits.conf 能持久化资源限制,但它
# 不是内核读的、不是系统全局自动生效的 —— 它由
# PAM 模块 pam_limits.so 在【用户登录建立 PAM 会话
# 那一刻】读取应用。所以它只对 SSH/su 等经过 PAM
# 登录的会话生效,对 systemd 启动的守护进程【完全
# 无效】(它们不走 PAM 登录)。改完要重新登录才生效。
修复 4:真凶——systemd 服务的限制,在 unit 文件里
# === ★ systemd 管的服务,限制到底由谁定 ===
# === ★ systemd 服务,资源限制是 systemd 自己给的 ===
# ★ 一个由 systemd 启动的服务,它【既不读你的
# ulimit,也不读 limits.conf】。它的资源上限,
# 完全由 ★ systemd 的配置决定。
# ★ 看 systemd 实际给某服务设的值:
$ systemctl show myapp -p LimitNOFILE
LimitNOFILE=1024 # ★ 这才是那个 java 进程的真上限
# === ★ 两个层级:服务单独设 / 全局默认 ===
# ★ 层级一(优先):service 单元文件里的 LimitNOFILE=
# —— 只管这一个服务。
# ★ 层级二(兜底):/etc/systemd/system.conf 里的
# DefaultLimitNOFILE= —— 所有没单独设的服务,
# 都用这个全局默认。
$ grep DefaultLimitNOFILE /etc/systemd/system.conf
# === ★ 正确做法:给服务单独设 LimitNOFILE ===
# ★ 不要直接手改 /usr/lib/systemd 里的原始单元文件
# (升级会被覆盖)。用 systemctl edit 加覆盖配置:
$ systemctl edit myapp
# 在打开的编辑器里写:
[Service]
LimitNOFILE=65535 # ★ 给 myapp 单独设 fd 上限
# ★ 保存后,它会生成 /etc/systemd/system/myapp.service.d/
# override.conf,不污染原文件,升级也不丢。
# === ★ 改完必须 daemon-reload + 重启服务 ===
$ systemctl daemon-reload # ★ 让 systemd 重新读配置
$ systemctl restart myapp # ★ 重启服务,新限制才被继承
# ★ 再次强调:限制是【启动时继承】的。不重启服务,
# 改 LimitNOFILE 对【正在跑的】那个进程毫无作用。
# === ★ 验证:回到唯一的真相源 ===
$ ps -ef | grep myapp # 拿新 PID
$ cat /proc/<新PID>/limits | grep 'open files'
Max open files 65535 65535 # ★★ 这下真的是 65535 了
# === ★ 想一次性抬高所有服务的默认值 ===
$ vi /etc/systemd/system.conf
DefaultLimitNOFILE=65535 # ★ 改全局默认
$ systemctl daemon-reexec # 让 systemd 重新执行自身
# ★ 之后【新启动】的服务都用这个默认。但已在跑的
# 要重启才生效。
# === 认知 ===
# ★ systemd 管的服务既不读 ulimit 也不读 limits.conf,
# 它的资源上限由 systemd 决定:优先看 service 单元
# 文件里的 LimitNOFILE=,没写则用 /etc/systemd/
# system.conf 里的 DefaultLimitNOFILE=。正确做法是
# systemctl edit 服务名 加 [Service] LimitNOFILE=,
# 再 daemon-reload + restart 服务,新限制才被继承。
修复 5:唯一的真相——/proc/PID/limits
# === ★ 别再"猜"限制,去看那个进程真正的限制 ===
# === ★ 排查 ulimit 问题,最大的弯路是"看错地方" ===
# ★ 本文我栽的根,是我一直在看【我以为的限制】:
# 我在 shell 里 ulimit -n,看到 65535,我就以为
# "限制是 65535"。可那是【我 shell 的限制】,不是
# 【那个出问题的服务进程的限制】。
# ★ 我对着一个不相干的数字,自我感觉良好了半天。
# === ★ 唯一可信的真相:/proc/PID/limits ===
# ★ 每个正在运行的进程,内核都在 /proc/<它的PID>/limits
# 里,如实记录着【它自己实际受的】每一项限制。
# ★ 这个文件,不骗人。它显示什么,这个进程就真的受
# 什么限制 —— 不管你在别处怎么设、怎么以为。
$ ps -ef | grep myapp # ① 先拿到出问题进程的 PID
$ cat /proc/8888/limits # ② 看它【真正】的全部限制
Limit Soft Limit Hard Limit Units
Max open files 65535 65535 files # ★ 它真受的 fd 上限
Max processes 4096 4096 processes
...
# === ★ 排查纪律:先看 /proc/PID/limits,再谈改哪 ===
# ★ 一报 Too many open files,第一件事【不是】去
# ulimit -n,而是:
$ cat /proc/$(pgrep -n -f myapp)/limits | grep 'open files'
# ★ 看到的如果是 1024 —— 那不管你 ulimit 显示啥、
# limits.conf 写了啥,这个进程【就是】只有 1024。
# 接下来才去判断:它是 systemd 起的(改
# LimitNOFILE)?还是登录会话起的(改 limits.conf
# 并重新登录)?
# === ★ 配套:看进程现在用了多少 fd ===
$ ls /proc/8888/fd | wc -l # 这个进程当前打开了多少 fd
$ lsof -p 8888 | wc -l # 另一种数法
# ★ 把"当前用量"和"/proc/limits 的上限"一比,就
# 知道是真撞上限了,还是 fd 泄漏(只涨不降)。
# === ★ 还有一层:应用自己可能也有限制 ===
# ★ 有些服务,在 OS 的 ulimit 之内,自己【又设了
# 一道更小的限制】。比如 Nginx:
$ grep worker_rlimit_nofile /etc/nginx/nginx.conf
worker_rlimit_nofile 65535; # ★ Nginx 自己的 fd 上限
# ★ 如果 OS 给了 65535,但 Nginx 配置里只写了 1024,
# 那 Nginx 进程实际还是受 1024 限制。OS 层、应用层,
# 两层都要对上。
# === 认知 ===
# ★ 排查 ulimit 问题别看"你以为的限制"(shell 里
# ulimit -n 是 shell 自己的),唯一可信的真相是
# /proc/<出问题进程PID>/limits —— 内核在这里如实
# 记录该进程实际受的每项限制。一报 Too many open
# files 先看这个文件,再判断该改哪。注意应用层
# (如 Nginx worker_rlimit_nofile)可能还有自己的限制。
修复 6:文件描述符上限排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ ulimit 是每个进程各自的限制,启动时从父进程继承,没有"全局"这回事 ===
# === 2. ★ 排查第一步永远是 cat /proc/出问题进程PID/limits,看它真正的限制 ===
$ cat /proc/$(pgrep -n -f 服务名)/limits | grep 'open files'
# === 3. ★ ulimit 命令只改当前 shell,不持久,管不到别的进程谱系 ===
# === 4. ★ limits.conf 只对经过 PAM 登录的会话生效,对 systemd 服务无效 ===
# === 5. ★ systemd 服务的限制看 LimitNOFILE,用 systemctl edit 改 ===
$ systemctl edit 服务名 # 加 [Service] / LimitNOFILE=65535
# === 6. 改完 systemd 配置必须 daemon-reload,改完限制必须重启服务才继承 ===
# === 7. 全局默认在 /etc/systemd/system.conf 的 DefaultLimitNOFILE ===
# === 8. ★ 限制只能在进程启动时继承,正在跑的进程改不了,要先配好再重启 ===
# === 9. 应用自己可能还有限制,如 Nginx worker_rlimit_nofile,两层都要对 ===
# === 10. 排查 Too many open files 的步骤链 ===
$ cat /proc/PID/limits | grep 'open files' # ① 进程真实上限
$ ls /proc/PID/fd | wc -l # ② 当前用了多少 fd
$ systemctl status PID所属服务 # ③ 它是不是 systemd 起的
# 是 systemd 起的 -> 改 LimitNOFILE;是登录会话起的
# -> 改 limits.conf 并重新登录。改完都要重启进程。
命令速查
需求 命令
=============================================================
看当前 shell 所有限制 ulimit -a
看/改 open files 软限制 ulimit -Sn / ulimit -n 65535
看 open files 硬限制 ulimit -Hn
★ 看某进程真实的限制 cat /proc/PID/limits
看某进程当前打开多少 fd ls /proc/PID/fd | wc -l
看 systemd 给服务的限制 systemctl show 服务 -p LimitNOFILE
给 systemd 服务改限制 systemctl edit 服务名
改完 systemd 配置 systemctl daemon-reload
看 systemd 全局默认 grep DefaultLimitNOFILE /etc/systemd/system.conf
持久化登录会话的限制 编辑 /etc/security/limits.conf
口诀:ulimit 是每个进程各自的限制 启动时从父进程继承 没有全局开关
systemd 服务不读 limits.conf 要改 LimitNOFILE,真相只看 /proc/PID/limits
避坑清单
- ulimit 是每个进程各自的资源上限,进程启动时从父进程继承,根本没有"系统全局"这个东西
- 排查 Too many open files 第一步永远是 cat /proc/出问题进程PID/limits,看它真正受的限制
- ulimit -n 命令只改当前这个 shell 进程,不写文件不持久,也管不到别的进程谱系
- 软限制是实际生效的值,硬限制是软限制的天花板,普通用户只能在硬限制内调软限制
- limits.conf 由 PAM 模块在用户登录那一刻读取,只对经过 PAM 登录的会话生效
- systemd 启动的守护进程不走 PAM 登录,改了 limits.conf 对它一个标点都不生效
- systemd 服务的限制由 LimitNOFILE 决定,用 systemctl edit 加覆盖配置而不是改原单元文件
- 改完 systemd 配置要 daemon-reload,改完限制要重启服务,正在跑的进程改不了限制
- 全局默认在 /etc/systemd/system.conf 的 DefaultLimitNOFILE,改完新启动的服务才用
- 应用自己可能还设了更小的限制如 Nginx 的 worker_rlimit_nofile,操作系统层和应用层都要对上
总结
这次"ulimit 改了又改、服务岿然不动"的事故,纠正了我一个关于"设置"的、最根深蒂固的错觉。在我过去的脑子里,设置一个系统的限制,就像拧一个【墙上的总开关】。我以为机房某处,有一个叫"文件描述符上限"的旋钮,我把它拧到 65535,那么这台机器上的【所有进程】,就都跟着享受 65535。所以我登上服务器,ulimit -n 65535 一敲,ulimit -n 一查,显示 65535——在那一刻,我确信我已经"拧动了那个总开关"。我甚至没想过去问:我刚才到底拧的是【什么】?现场用一个冷冰冰的 /proc/8888/limits 文件,把我那个"总开关"的幻觉,击得粉碎。它告诉我:那个出问题的 java 进程,它身上的限制,清清楚楚还是 1024。我那个"已经拧到 65535"的操作,对它【完全没有发生过】。我这才被逼着去理解一件我从没正视过的事:这个系统里,根本【就没有】一个叫"文件描述符上限"的总开关。这个限制,它不是挂在"系统"这面墙上的,它是【长在每一个进程身上】的——每个进程,从它出生的那一刻起,就从它的【父亲】手里,继承了一份【属于它自己的】限制副本,然后揣着这份副本过完一生。我 ulimit -n 65535,我没有拧动任何总开关,我只是改写了【我登录的这个 shell 进程】身上揣的那一份。而那个 java 服务,它的父亲是 systemd,不是我的 shell;它继承的那份限制,来自 systemd,和我那个 shell 上的副本,是两份毫不相干、永不相通的纸。我朝着我手里那份纸大喊大叫,而那个服务,在另一条家谱上,安静地守着它从 systemd 那里继承来的、另一份纸。复盘到根上我才看清,我错的不是某条命令,而是我脑子里那个"系统是一个有中央控制台的整体"的模型。这个系统,它不是一栋有总闸的大楼,它更像一个【家族】——每一项属性,资源限制、环境变量、当前目录、打开的文件,都不是挂在"家族"名下的公共财产,而是每个成员在【出生时,从父母手里继承】的私产。你想改变一个成员的私产,你必须找到【他】,或者找到他【还没出生的父母】——你冲着家族的某个【远房亲戚】(我的 shell)调整,对他(那个服务)毫无意义,因为属性根本不在家族层面流动,它只沿着【父子继承】这一条线传递。这次最大的收获,是我学会了在动手"设置"任何东西之前,先停下来,问一个我以前从不问的问题:我现在要改的这个东西,它的作用域,究竟是【谁】?是我眼前这个进程,还是它将来要生的孩子,还是某个登录会话,还是 systemd 的某个角落?我面对的那个出问题的对象,它真正继承属性的【那条血脉】,源头又在哪里?我不能再凭"我改了一个看起来对的地方"就心安——因为一个设置,只有落在那条正确的继承链的【上游】,才会流到下游那个我真正想影响的进程身上;落错了链,它改得再大、我查得再确凿,对那个进程而言,都【等于什么都没发生】。验证一个限制是否真的生效,也永远不能问"我设了吗",只能去问那个进程自己——去读它身上那份 /proc/PID/limits,因为那张纸上写的,才是它这一生真正背负的东西,而我的"我以为",一个字都不算数。
—— 别看了 · 2026