Linux运维12 min read次阅读

Too many open files:ulimit、LimitNOFILE 与 fd 泄漏的完整排查链路

凌晨收到告警,某台接口机 Nginx 开始批量 502,应用日志里一行接一行 accept4() failed (24: Too many open files)。SSH 上去第一反应自然是 ulimit -n,一看默认 1024,心想找到了,改大重启。结果服务是 systemd 管的,ulimit 在登录 shell 改了半天,进程还是 1024,重启之后依旧 502。这时候才意识到:fd 限制这东西,远不是 ulimit -n 一个数那么简单。

先把"文件句柄"这个东西从感觉上掰清楚。Linux 里几乎一切皆文件——普通文件、socket、管道、设备,进程每打开一个就得占一个 fd(file descriptor)。一个进程能同时打开多少个,受三层限制卡着:单进程上限(ulimit -n / systemd 的 LimitNOFILE)、系统总上限(fs.file-max)、以及内核里几个相关参数。任何一层被顶满,都会冒出 "Too many open files"。所以排查不能只盯一处,得一层层往上翻。

第一层:先看清系统到底撑不撑得住

先确认不是内核层面的总限额先爆了。/proc/sys/fs/file-nr 这一行三个数,第二个是"当前已分配但尚未释放"的 fd 数,第三个是系统上限:

cat /proc/sys/fs/file-nr
# 例如: 98304   82011   6553500
#        已分配   使用中    上限(file-max)

cat /proc/sys/fs/file-max        # 系统级 fd 总量上限
sysctl fs.file-max               # 同上,等价写法

如果"使用中"已经逼近"上限",那是真全局不够,得先调 fs.file-max

# 临时生效
sysctl -w fs.file-max=10000000
# 永久生效
echo "fs.file-max = 10000000" >> /etc/sysctl.conf
sysctl -p

但说实话,现代机器 file-max 默认几百万,真被顶满的少。绝大多数 "Too many open files" 是某个进程自己撞到了单进程上限,也就是 ulimit -n 那一层——而这恰恰是最容易误诊的地方。

第二层:进程视角看真实上限

别信登录 shell 里的 ulimit -n,那是你这个 shell 会话的上限,未必等于出问题的那个进程。要看法拉第线的真值,看 /proc/<pid>/limits

# 假设 nginx worker 的 PID 是 2245
cat /proc/2245/limits | grep "open files"
# Max open files            1024                 1024                 files

# 或者一行拿到某个服务主进程的当前使用量和上限
pid=$(pgrep -o nginx)        # 取一个 worker
echo "PID=$pid"
ls /proc/$pid/fd 2>/dev/null | wc -l          # 当前已用 fd 数
awk '/open files/{print "soft="$4" hard="$5}' /proc/$pid/limits

这里有个关键判断:如果 ls /proc/$pid/fd | wc -l 已经接近上限(比如 1024 用了 1021),那就是这个进程 fd 用满了,而且大概率在漏——正常服务不会把 1024 个 fd 都吃满还稳得住。如果远没到上限却报 "Too many open files",要怀疑是别层(比如 fs.file-max 或 cgroup 的 pids/max 限制),但那种罕见,先按"进程漏 fd"处理。

第三层:到底谁在占着这些 fd

定位 fd 泄漏,最实用的两个命令:lsof 和直接数 /proc/<pid>/fd

# 看某个进程当前打开的所有 fd,按类型归类
ls -l /proc/2245/fd | awk '{print $NF}' | sed 's/.* -> //' | awk -F'[' '{print $1}' | sort | uniq -c | sort -rn

# 用 lsof 看该进程 fd 总数(比数目录慢但信息全)
lsof -p 2245 | wc -l

# 看某个用户下所有进程的 fd 分布,揪出大户
lsof -u www-data 2>/dev/null | awk '{print $2}' | sort | uniq -c | sort -rn | head

常见的"漏"长这样:

  • 大量 socket 处于 ESTABLISHED 或 TIME_WAIT:连接没关。可能是应用没复用连接、每次请求新建 TCP,或者服务间调用没用连接池。TIME_WAIT 堆积尤其常见于短连接高并发场景,这种其实不是 fd 泄漏,而是连接模型问题——治本是上连接池、开 SO_REUSEADDR、调 net.ipv4.tcp_tw_reuse=1,而不是一味加 fd 上限。
  • 大量 pipe / eventpoll 不释放:典型的 epoll 注册了事件但没清理,或者子进程继承了父进程 fd 没关(fork 之后忘了 FD_CLOEXEC)。
  • 普通文件 fd 一直涨:日志文件、临时文件打开后没 close,这种最直白,也最容易被发现。

我踩过最阴的一次是某 Java 应用,用 lsof -p 看 fd 全是 pipe:[xxxxx],普通文件没几个,但 fd 数一直涨。最后定位是它用了 Runtime.exec() 调外部命令,每次都没消费子进程的输入/错误流,导致 pipe 不被回收,进程起来的 pipe 越来越多。这种用 ls /proc/$pid/fd | grep pipe | wc -l 一数就露馅。

临时救急:不停机把上限抬上去

如果线上还在 502,先别急着改配置重启(重启有风险,而且 systemd 服务改了 limits.conf 还不一定生效)。可以用 prlimit 直接给正在跑的进程动态改上限,立竿见影、不影响业务:

# 把 PID 2245 的 nofile soft/hard 都临时设成 65536
prlimit --pid 2245 --nofile=65536:65536

# 验证
awk '/open files/{print $4, $5}' /proc/2245/limits

prlimit 改的是内核里该进程的实际限制,立刻生效,Nginx 这种多 worker 的要对每个 worker 都执行(或对整个 master 及其子进程)。这是半夜止血的首选,比重启稳得多。但它是临时措施——进程一重启就回到旧上限,所以救急之后必须做永久修复。

永久修复:三处到底改哪里

这是最容易被坑的一段。很多人改了 /etc/security/limits.conf 然后发现服务重启还是 1024,气得想砸键盘。原因是:systemd 起的服务根本不经过 PAM 的 limits 机制,limits.conf 只对"通过 PAM 登录的会话"有效(比如你 SSH 进去敲命令),对 systemd unit 拉起来的进程无效。

所以要分三种情况:

情况 A:你自己的命令行程序 / 通过 SSH 跑的脚本

/etc/security/limits.conf,末尾加:

# <domain>   <type>  <item>    <value>
*            soft    nofile    65536
*            hard    nofile    1048576
www-data     soft    nofile    65536
www-data     hard    nofile    1048576

注意 type 有 soft 和 hard,soft 是进程实际能用到的,hard 是软上限能调到的天花板。普通用户只能把 soft 往 hard 里调,调不超 hard。改完重新登录才生效(limits 是登录时 PAM 注入的)。

情况 B:systemd 管理的服务(Nginx、MySQL、你自己的 daemon 等)——这才是大头

必须写进 unit 文件。两种写法:

写法一,在 service 单元里直接加:

# /etc/systemd/system/nginx.service.d/limits.conf
[Service]
LimitNOFILE=65536

或者改原 unit(不推荐直接改 /usr/lib/systemd/system/ 下的,升级会被覆盖),用 drop-in 覆盖:

mkdir -p /etc/systemd/system/nginx.service.d
cat > /etc/systemd/system/nginx.service.d/limits.conf <<'EOF'
[Service]
LimitNOFILE=65536
EOF
systemctl daemon-reload
systemctl restart nginx

写法二,如果不想一个个服务配,可以改 systemd 全局默认——/etc/systemd/system.conf 里的 DefaultLimitNOFILE

# 影响所有由 systemd 拉起、且自身没显式设 LimitNOFILE 的服务
sed -i 's/^#DefaultLimitNOFILE=.*/DefaultLimitNOFILE=65536/' /etc/systemd/system.conf
systemctl daemon-reexec    # 注意是 reexec 不是 reload,全局默认改完要这个

Nginx 还有自己一层:worker 进程能用的 fd 还受 worker_rlimit_nofile 限制,这个值你要设得比 LimitNOFILE 小或等于,否则 Nginx 自己会报"超出 rlimit"。典型配置:

worker_processes  auto;
worker_rlimit_nofile 65536;   # 必须 <= systemd LimitNOFILE
events {
    worker_connections 10240;  # 单 worker 连接数,乘以 worker 数别超 rlimit
}

算一下:4 个 worker × 10240 = 40960 个连接,加上监听 socket、日志文件、upstream 连接,留点余量,65536 是稳的。

情况 C:MySQL / 数据库类

MySQL 的 open_files_limit 是它自己读的,但同样受 systemd LimitNOFILE 和 fs.file-max 卡着。实践中见过 MySQL 配了 open_files_limit=65535,结果 systemd 默认 1024,直接被压到 1024,表一多就 Too many open files,连接抖动。解法同样是给 mysqld 的 unit 加 LimitNOFILE=65535,再 systemctl restart mysqld

验证修复真的生效了

改完别凭感觉,要用进程真实值确认:

# 重启后看目标进程的硬上限
pid=$(pgrep -o nginx)
awk '/open files/{print "soft="$4" hard="$5}' /proc/$pid/limits

# 压一压,确认不再报 24
# 用 ab 或 wrk 打一会,同时 tail -f 错误日志看还有没有 Too many open files
wrk -t4 -c2000 -d30s http://127.0.0.1/

如果压测下 fd 数涨上去但不再报错、连接稳定,说明三层(file-max / LimitNOFILE / 应用自身上限)都打通了。

一个容易被忽略的源头:应用自己该修

最后说句实在的——把 fd 上限从 1024 抬到 65536,很多时候只是把爆炸推迟了。如果根因是应用 fd 泄漏,那 65536 早晚也会被吃满,只是从"半夜两点炸"变成"跑三天炸"。所以抬上限是止血,定位泄漏才是治病。

排查泄漏的节奏:先用 ls /proc/$pid/fd | wc -l 看是不是在单调上涨(隔几分钟数两次,涨就是漏);再用 lsof -p $pid 看涨出来的都是什么类型;如果是 socket,查连接池配置和 tcp_tw_reuse;如果是 pipe,查有没有 exec 外部命令或 fork 后没关 fd;如果是普通文件,查日志/临时文件有没有 close。这活儿没有银弹命令,得顺着 fd 类型往代码或配置里翻。

顺带提一个:容器环境下(Docker / Kubernetes)fd 限制还可能被 cgroup 或容器 runtime 再卡一道,比如 Docker 默认 ulimit -n 是 1048576 但某些编排会覆盖,K8s 里则可以写 securityContext.limits 里的 nofile。那种情况上面的 systemd 套路不适用,得去容器层面调。

回到开头那台 502 的机器——那次就是 Nginx 的 systemd unit 没设 LimitNOFILEworker_rlimit_nofile 写的是 65536 但被 1024 的 rlimit 压着,等于空设。加了一行 LimitNOFILE=65536 重启,502 立刻消失,再用 prlimit 把当时还活着的旧 worker 也抬上去,半夜的血止住了。第二天顺着 fd 类型一查,是 upstream 到某后端没配 keepalive,每请求建一条新连接,调了连接池才算是治了本。

分享:

相关文章

磁盘显示没满却写不进去:df 和 du 对不上的三种真实场景
Linux运维12 min read

磁盘显示没满却写不进去:df 和 du 对不上的三种真实场景

报警说磁盘 100%,可 du 加起来差了一大截;或者 df -h 明明还有空间,应用却报 No space left on device。这两种情况我都遇过不止一次,根因通常是已删除但被进程占用的文件、inode 耗尽,或者文件被挂载点盖住。逐个拆开讲怎么定位、怎么在不重启服务的前提下把空间腾出来。

评论区