报错排查 04|Too many open files:ulimit 改了没用?文件描述符的四层限制链

发布时间:2026/10/2 16:26:50
报错排查 04|Too many open files:ulimit 改了没用?文件描述符的四层限制链
这个坑我踩得很典型nginx: [emerg] open() /var/log/nginx/error.log failed (24: Too many open files)搜了一圈答案全一样改/etc/security/limits.conf加两行重启。照做了。重启完还是这个报错。后来才知道这个文件根本管不到我的 Nginx。〇、先花两分钟文件描述符和两层限制文件描述符fd是进程打开文件时领的「号码牌」。进程每打开一个文件、一个网络连接、一个管道内核就发它一张号码牌编号从 0 开始。0、1、2 是固定的0 是标准输入、1 是标准输出、2 是标准错误。之后每开一个资源就往下发一张。为什么要限制如果不限制一个进程可以开几百万张牌把内核内存吃光。所以内核给每个进程设了上限这个上限叫RLIMIT_NOFILE。它分软限制和硬限制两个数含义谁能改软限制soft当前实际生效的上限普通用户可以自己往上调但不能超过硬限制硬限制hard天花板只有 root 能往上调所以你看到1024 524288这种两个数字的写法就是「软限制 1024硬限制 524288」——当前只能用 1024但你有权自己提到 52 万。在终端里一敲就能看到这对数字下面是我那台 Ubuntu 26.04.1 的真实输出$ ulimit -Sn # S soft软限制 1024 $ ulimit -Hn # H hard硬限制 524288n指的是 nofile。不加-S/-H时ulimit -n默认显示软限制——这也是它经常误导人的原因。这里就是所有坑的根子「当前生效的限制」和「你改的那个文件」可能压根没关系。一、第一步先看这个进程现在到底受多少限别先急着改先看现状。一条命令看透$ cat /proc/self/limits | grep -i open files Max open files 1024 524288 files怎么读这一行只有两个数字——软限制 1024硬限制 524288。上面是我在自己机器终端里真跑出来的。self指「当前这个进程」看自己不用 sudo换成某个服务就这么写sudo cat /proc/$(pgrep -x nginx | head -1)/limits | grep -i open files左边那个1024就是正在生效的天花板。它一旦被顶到进程就开始报Too many open files右边那个524288是天花板的上限软限制可以往上提但超不过它。看到软限制是 1024答案基本就出来了这不是没配而是配的那一层不管这个进程。再顺手看看它现在用掉多少$ sudo ls /proc/1042/fd | wc -l 1021怎么读1021 除以 1024……已经 99% 了。这个数字就是「它手上现在攥着多少张牌」。⚠️ 别用lsof -p PID | wc -l来数lsof会把工作目录、可执行文件、内存映射的库都算进去数字偏大会让你误判。比较限制只信/proc/PID/fd。二、四层限制链为什么你改的那一层不管用这是全文的核心。限制是从上到下四层叠下来的你改了 A 层服务却受 B 层管这就是「改了没生效」的全部原因。第 4 层最上层内核全局总量内核自己有两个天花板所有进程加起来不能超过$ sysctl fs.file-max fs.nr_open fs.file-max 9223372036854775807 fs.nr_open 2147483584怎么读这是我机器上的真实值fs.file-max是全系统能同时打开的文件总数。这个9223372036854775807是 64 位有符号整数的最大值2⁶³−1——等于「不限制」现代内核早就不拿它当瓶颈了fs.nr_open是单个进程的硬上限。它决定了你能把硬限制调多高——你在 systemd 里写一个比它更大的值内核会直接拒绝。注意fs.nr_open在不同发行版和内核版本上差异不小别背数字跑一遍。第 3 层登录会话limits.confpam_limits 管这就是大多数人改的那一层。规则写在/etc/security/limits.conf里格式是四列#domain type item value #* soft core 0#开头就是注释。我机器上滤掉注释和空行之后$ grep -vE ^#|^$ /etc/security/limits.conf 无输出一条配置都没生效这就是 Ubuntu 的出厂状态。那真正在起作用的是哪些看这个目录——散装配置放在这儿而且很多系统限制压根不用改limits.conf$ ls /etc/security/limits.d/ 10-coredump-debian.conf 10-gamemode.conf 25-pw-rlimits.conf $ for f in /etc/security/limits.d/*.conf; do echo --- $f; grep -vE ^\s*#|^\s*$ $f; done --- /etc/security/limits.d/10-coredump-debian.conf * soft core 0 root soft core 0 * hard core infinity root hard core infinity --- /etc/security/limits.d/10-gamemode.conf gamemode - nice -10 --- /etc/security/limits.d/25-pw-rlimits.conf pipewire - rtprio 95 pipewire - nice -19 pipewire - memlock 4194304怎么读——这里有个关键发现limits.d/里的文件会按文件名顺序读取后面的覆盖前面的文件名必须以.conf结尾别的后缀不读这三个文件里一条nofile都没有。它们管的是 core dump、nice 优先级、rtprio、memlock——没有一条跟「能开多少个文件」有关所以结论很硬Ubuntu 桌面版上你登录会话的nofile限制根本不是limits.conf给的它来自 systemd 的默认值下一小节。别照着网上教程去limits.conf里翻答案——那里面本来就是空的另外注意gamemode、pipewire这种以开头的写法 用户组不是用户名。这一层由 PAM 模块pam_limits读取而 PAM 只在「登录会话」里起作用——SSH 登录、su、login都算登录会话。关键结论systemd 开机启动的服务不是登录会话它从来不读这个文件。这就是我的 Nginx 为什么不理它。这个文件对「你 SSH 进去之后手动敲命令启动的进程」有效对「systemd 拉起来的服务」无效——而生产环境里几乎所有服务都是后者。第 2 层systemd 的默认值DefaultLimitNOFILE1024:524288既然服务不读limits.conf那它读什么读 systemd 给的默认值。systemd 这套默认值的设定很讲究软限制给 1024硬限制给 524288。为什么故意把软限制压得这么低两个历史原因select()这个老系统调用处理不了编号 ≥1024 的 fd软限制低一点能早暴露问题有些老程序启动时会「从 0 到软限制挨个关一遍 fd」如果软限制是一百万那启动就得卡好几秒。设计意图是需要更多 fd 的程序自己往上提。而这个设计带来了一个特别有意思的现象——同一台机器上Go 写的服务可能一点事没有Nginx 却在报错。因为 Go 运行时在启动时会把软限制直接提到硬限制524288。所以它天生不受 1024 的约束。这条能解释很多「为什么隔壁那个服务好好的」的困惑。先看全局默认到底是多少这条我在自己机器上跑过$ systemctl show -p DefaultLimitNOFILE -p DefaultLimitNOFILESoft DefaultLimitNOFILE524288 DefaultLimitNOFILESoft1024怎么读DefaultLimitNOFILESoft1024是软限制、DefaultLimitNOFILE524288是硬限制——前面说的1024:524288被实机证实了。再看具体某个服务实际拿到多少$ systemctl show -p LimitNOFILE -p LimitNOFILESoft smbd.service LimitNOFILE16384 LimitNOFILESoft16384 $ systemctl show -p LimitNOFILE -p LimitNOFILESoft cups.service LimitNOFILE524288 LimitNOFILESoft1024怎么读这两条放在一起信息量很大——smbdSamba 文件共享软硬都是16384明显不是 systemd 的默认值说明它被单独设过cups打印服务是524288 / 1024原样继承默认值。同样是服务限制可以完全不同。所以「某个服务报 Too many open files」时第一步永远是看它自己的值而不是看系统默认值。想追「是谁把 smbd 设成 16384 的」看它读过哪些配置文件$ sudo systemctl cat smbd.service | grep -n -E LimitNOFILE|^# / 1:# /usr/lib/systemd/system/smbd.service 11:LimitNOFILE16384怎么读systemctl cat会把这条服务用到的所有配置片段按顺序打出来每个片段的来源用# 路径标出grep的第二个条件就是把来源行也一起捞出来。输出里只有一个来源文件/usr/lib/systemd/system/smbd.service——说明没有override.conf覆盖它而且写在第 11 行、位置很靠前——这是发行版打包时就定死的不是谁后来改的所以想改它就用systemctl edit加覆盖片下一节别去动/usr/lib下的原文件——那个文件会被包管理器覆盖掉。第 1 层最下层单元文件 / 覆盖配置真正该改的地方在这里。sudo systemctl edit nginx.service它会打开一个覆盖文件/etc/systemd/system/nginx.service.d/override.conf写进去[Service] LimitNOFILE65536怎么读LimitNOFILE65536写一个数表示软硬都是 65536想分开写就LimitNOFILE65536:524288软:硬。为什么推荐用systemctl edit而不是直接改/usr/lib/systemd/system/nginx.service因为那个文件是软件包的地盘升级时会被覆盖掉——你的修改会无声消失。systemctl edit写的覆盖文件在/etc下属于你的地盘永远优先这条规则在Linux避坑 03那篇里讲过。改完两个动作缺一不可sudo systemctl daemon-reload sudo systemctl restart nginx⚠️必须是restart不能是reload。资源限制是在进程创建的那一刻设置的reload只让进程重读配置、不重建进程fd 限制原地不动。三、优先级四层谁压谁单元文件里的 LimitNOFILE ← 最高你该写的地方 ↓ 没写才看 /etc/systemd/system.conf 里的 DefaultLimitNOFILE ← 全局默认改这里影响所有服务 ↓ 没写才看 systemd 内置默认值 1024:524288 ← 兜底内核的fs.nr_open是所有之上的硬天花板它压不住谁但谁也不能超过它。四、三种典型场景对号入座① 服务systemd 起的→ 用systemctl edit写LimitNOFILEdaemon-reloadrestart。② 你 SSH 上去手动跑的程序→ 改/etc/security/limits.conf或/etc/security/limits.d/下的文件重新登录一次生效。③ 容器Docker / Podman→ 限制由容器运行时给要看容器的配置$ docker inspect --format {{.HostConfig.Ulimits}} 容器名 [{nofile 65536 65536}]没有docker很多环境没装就用cat /proc/$(pgrep 容器主进程)/limits同一套读法。五、三个坑坑 1改完只看ulimit -n。ulimit -n看到的只是你当前这个 shell的限制跟服务没有任何关系。服务看/proc/PID/limits或systemctl show。坑 2LimitNOFILE写得比fs.nr_open还大。# 假如你的 fs.nr_open 是 1048576写这么大就无效 LimitNOFILE2000000 # 超过 fs.nr_open会被拒绝 # 先看自己的上限再写不超过它的值 sysctl fs.nr_open坑 3只改硬限制忘了软限制。写成LimitNOFILE65536:524288这种「软:硬」形式时软限制左边那个才是生效值。写反了就等于没改。六、还报错那是真的在泄漏如果限制已经调到 65536/proc/PID/fd的数字还在稳步上涨、直到又报错——问题不是限制太小是程序在泄漏 fd打开的文件没关。判断方法很简单隔几分钟看两次$ sudo ls /proc/1042/fd | wc -l # 第一次3120 $ sleep 60 sudo ls /proc/1042/fd | wc -l # 一分钟后再看3245只涨不落 泄漏。这时候往上调限制只是把爆炸时间往后推真正的解法是修程序——顺便用第一条命令看看它到底在开什么sudo ls -l /proc/1042/fd | awk {print $NF} | sort | uniq -c | sort -rn | head怎么读输出是「某种文件被打开了多少次」的排行榜排第一的那个就是它没收手的资源常见的是没关闭的 socket、没释放的临时文件。速查表文末收藏版看进程实际软/硬限制 → sudo cat /proc/PID/limits | grep -i open files 看进程用掉多少 fd → sudo ls /proc/PID/fd | wc -l 别用 lsof 计数 看某个服务的限制 → systemctl show -p LimitNOFILE -p LimitNOFILESoft 服务名 改服务限制正确姿势→ sudo systemctl edit 服务名 → [Service] LimitNOFILE65536 让修改生效 → sudo systemctl daemon-reload sudo systemctl restart 服务名 改全局默认 → /etc/systemd/system.conf 里 DefaultLimitNOFILE 登录会话的限制 → /etc/security/limits.conf仅对登录会话有效 内核单进程上限 → sysctl fs.nr_open 内核全局总量 → sysctl fs.file-max systemd 全局默认 → systemctl show -p DefaultLimitNOFILE -p DefaultLimitNOFILESoft 看服务的配置来自哪一层 → systemctl cat 服务名 | grep -n LimitNOFILE 登录会话的散装配置 → /etc/security/limits.d/*.conf按文件名顺序覆盖 看是不是在泄漏 → ls /proc/PID/fd 隔一分钟各数一次只涨不落就是泄漏 看泄漏的是什么 → sudo ls -l /proc/PID/fd | awk {print $NF} | sort | uniq -c | sort -rn | head 容器里的限制 → docker inspect --format {{.HostConfig.Ulimits}} 容器名