资讯动态

Linux ulimit 资源限制不生效排查:limits.conf 与 systemd 两大坑

发布时间:2026/10/26 2:42:55 来源:尧图企业网站定制
线上服务在凌晨两点报警日志里刷屏Too many open files。我打开终端敲ulimit -n回显 1024跟我几个月前在limits.conf里认认真真写下的 65535 对不上。这件事我前前后后遇到过三次每次卡在不同的环节最后一次才把整条链路彻底捋明白。ulimit这个命令几乎所有碰过 Linux 的人都用过它管的是一个进程能用多少资源——最多能开多少个文件描述符、最多能起多少个进程、栈能有多大、core dump 能写多大。表面上看就是改几个数字的事真正踩过坑的人知道这东西的生效路径长得离谱从 PAM 到 SSH 会话从 systemd 到容器运行时中间任何一环断了你写的配置就是一张废纸而且它不会报错只会安静地不生效。这篇内容适合两类人。一类是正被Too many open files折腾、改配置改到怀疑人生的开发和运维另一类是想搞清楚 Linux 资源限制到底怎么一路传递下来的同学。我把最常踩的两个坑掰开讲——一个是limits.conf改了没反应另一个是 systemd 拉起的服务压根不认limits.conf——再补几个边界上的隐性问题最后给一套能直接抄的排查命令。1. ulimit 到底在限制什么先把概念对齐1.1 soft 与 hard两道闸门而不是一个数字很多人对ulimit的误解从这里就开始了以为它是一个数值实际上是成对出现的两个数值——soft limit 和 hard limit。soft 是当前实际生效的天花板hard 是 soft 能调到多高的上限。用一个生活化的说法hard 是你银行卡里的余额soft 是你今天给自己定的消费预算。预算可以随时往上调但调不过余额想提高余额得去银行柜台也就是 root 或者带CAP_SYS_RESOURCE能力的进程。这条规则带来的第一个实操细节是普通用户执行ulimit -n 65535如果当前 hard 是 1048576那它确实能成功但会把 soft 和 hard 一起设成 65535——bash 在既没给-S也没给-H的时候默认两个都设。这就等于你顺手把 hard 从 1048576 降到了 65535而且在这个 shell 生命周期内不可逆后面再想往上调就得重新登录。我见过有人在启动脚本里写了这么一句结果整个应用跑起来之后 hard 被锁死排查了半天以为是内核限制。所以正确的姿势永远带显式参数ulimit -Sn 65535 # 只动 soft ulimit -Hn 65535 # 只动 hard需要 root 权限进程的限制是怎么传递的fork 的时候子进程完整继承父进程的 rlimitexecve 之后保留不变。也就是说你的 shell 的限制会一路传给 shell 里起的每一个程序中间没有任何重新加载配置文件的机会。这一点决定了后面所有的坑想改限制要么改父进程要么改启动路径上的某个配置点。资源项参数名作用域常见踩坑点打开文件数nofile每进程改了不生效实际是 soft 没变用户进程数nproc每 UID按线程统计容易被 limits.d 覆盖栈大小stack每进程设 unlimited 会让线程创建失败core dumpcore每进程受 core_pattern 影响锁定内存memlock每进程数据库类服务经常需要调大1.2 每进程的限制与每用户的限制分属不同维度第二个容易搞混的地方nofile和nproc根本不是一类东西。nofile是每进程的。你把它设成 65535不代表整台机器只能开 65535 个文件而是每个进程各自都能开到 65535。跑十个 worker理论上就能开到六十五万个 fd。真正管全系统总量的是fs.file-max管单个进程 fd 天花板的是fs.nr_open。这三个数字是三层不同的闸门很多人只盯着ulimit结果撞上了另外两层。nproc是每用户严格说是每个 UID的而且内核统计的是 task 数量线程也算。也就是说你在一个进程里起了一千个线程就消耗了这个 UID 的一千个额度。它有别于 cgroup 的pids.max——后者限制的是一个 cgroup 里所有进程加起来的 task 总数跟 UID 无关。在老版本内核上没有 cgroup pids 控制器的时候nproc是唯一的防线现在两个同时存在谁先撞上谁报错报错信息还都是Resource temporarily unavailable特别容易误判。还有一个常被忽略的联动关系nofile的 hard 值不能超过fs.nr_open。默认nr_open是 1048576如果你在limits.conf里写了 2000000pam_limits 设置失败后果可能是配置被静默截断也可能是登录会话直接起不来——具体表现取决于发行版和 PAM 版本后面第 4 节会详细说这个。2. 第一个坑limits.conf 改完重启了ulimit -n 还是 10242.1 现象拆解配置写了不等于生效最典型的复现路径是这样的登录机器vim /etc/security/limits.conf末尾加两行* soft nofile 65535 * hard nofile 65535保存exit退出重新 ssh 登录敲ulimit -n还是 1024。然后开始怀疑人生是不是没重启是不是要source一下是不是文件写错位置了问题在于limits.conf不是一个被读取的配置文件而是一组 PAM 模块pam_limits.so的数据源。它只在特定的时机、被特定的程序、以特定的 PAM 类型调用时才会生效。任何一个环节不满足文件写得再漂亮也没用。会读它的典型场景是 PAM session 建立的那一刻——也就是你登录的时候。所以修改之后必须重新建立一个会话source没用在同一个 shell 里执行bash起子 shell 也没用因为子 shell 只是继承了父 shell 已经被 PAM 设置过的限制。有人图省事用su - root切一下再切回来这也不一定有效su走自己的 PAM 配置而su不带减号时环境继承关系又很微妙。还有一类更隐蔽的情况你用sudo执行命令然后sudo ulimit -n发现还是 1024。这不是 bug是sudo的 PAM 配置里通常只有auth和account类型没有session类型的pam_limits.so所以sudo根本不会重新应用限制它只是继承调用者 shell 的 rlimit。想验证这点直接看/etc/pam.d/sudo里有没有 session 行就知道了。2.2 沿着 PAM 链路一层层查我现在的排查顺序固定成四步基本能在五分钟内定位。第一步确认当前 shell 拿到的到底是什么ulimit -Sn # soft nofile ulimit -Hn # hard nofile cat /proc/$$/limits | grep -i open files注意/proc/$$/limits会同时列出 soft 和 hard单位也标出来了。很多人只看ulimit -n那是 soft如果 soft 是 1024 而 hard 是 65535说明 hard 生效了但 soft 没设上这种情况往往是limits.conf里只写了hard那一行。第二步确认 PAM 有没有加载pam_limitsgrep -r pam_limits /etc/pam.d/正常的输出里应该有类似session required pam_limits.so的行。如果没有那就是模块根本没被调用。另外要看它挂在哪条链路上——登录用的sshd、本地登录用的login、切用户用的su、定时任务用的crond各有各的配置文件。你改了sshd但问题出在 cron 起的任务上那当然没用。第三步确认limits.d目录里有没有东西把配置覆盖了。这个目录下的文件按文件名字典序依次加载后加载的覆盖前面的而且它们的优先级高于limits.conf本身。有些发行版默认在这里放一个20-nproc.conf里面写着* soft nproc 4096文件名字典序排在你自定义文件之前结果就是你写在limits.conf里的nproc设置被它顶掉了。查的时候别只看主文件ls -l /etc/security/limits.d/ grep -r . /etc/security/limits.d/第四步如果前三步都没问题打开 debug 看 PAM 到底干了什么。在对应服务的 PAM 配置里给pam_limits.so加上debug参数然后重新登录一次去系统日志里翻详细输出。这个输出会告诉你它读了哪些文件、匹配了哪条规则、设置成功还是失败。说实话这个 debug 输出比反复猜有用一百倍。2.3 落地改法与验证闭环我的常用配置是这样注意几个细节# /etc/security/limits.conf * soft nofile 65535 * hard nofile 65535 * soft nproc 65535 * hard nproc 65535 root soft nofile 65535 root hard nofile 65535为什么root要单独写两行*这个通配符在不同 PAM 版本上的实际匹配范围是有分歧的有的版本里*不覆盖 root 特定条目。与其去赌版本行为不如显式写一行反正也不费事。这个细节我在两套环境上验证过差异确实存在写上就完事了。另外我把自定义的规则放在/etc/security/limits.d/99-custom.conf数字取大一点保证字典序排在最后避免被系统自带的文件覆盖。这条经验是用一次线上事故换来的当时limits.conf里明明写了nproc unlimited实际生效的却是 4096追了两个小时才发现是目录里另一个文件的锅。验证必须走完整闭环缺一步都不算数# 1. 完全退出会话重新登录关键 exit ssh ... # 2. 确认 shell 本身的限制 ulimit -Sn ulimit -Hn # 3. 确认起出来的子进程继承正确 bash -c cat /proc/$$/limits | grep open files # 4. 确认目标服务进程的实际值 cat /proc/$(pgrep -f your-service | head -1)/limits对于已经在跑、不方便重启的进程可以用prlimit动态改prlimit --pid 12345 --nofile65535:65535要注意prlimit只能在 hard 限额以内调整而且改了之后如果进程自己再 fork 子进程子进程继承的是新值——这点比改动配置文件生效快得多但重启后就丢了只能当临时手段用。注意不要在limits.conf里写nofile unlimited。RLIMIT_NOFILE的 hard 上限就是fs.nr_open它不可能真的无限写unlimited通常会被拒绝或者被截断成某个值行为不可预期。想要大数值就直接写明确的数字。3. 第二个坑systemd 拉起的服务压根不认 limits.conf3.1 服务进程的限制从哪儿继承这是我认为最坑的一个因为它有一个欺骗性极强的表象你在命令行手工跑程序ulimit -n是 65535一切正常一旦写成 service 用 systemd 拉起来立刻报Too many open files。同一台机器、同一个用户、同一个二进制结果完全不同。原因在于 systemd 是 PID 1它拉起的服务进程在 fork/exec 时直接被注入了 rlimit走的是 systemd 自己的配置体系完全不经过 PAM。limits.conf对它是无效的你改一百遍也没用。服务进程的限制来源于两个地方systemd 自身的默认值DefaultLimitNOFILE之类在/etc/systemd/system.conf里以及 unit 文件里的LimitXXX指令。排查方法很直接。先看 systemd 认为这个服务该是什么限制systemctl show -p LimitNOFILE your-service systemctl show -p LimitNPROC your-service再看进程实际拿到的cat /proc/$(systemctl show -p MainPID --value your-service)/limits如果这两个一致但都不对说明 unit 配置需要改如果 unit 里明明写了却对不上那就要怀疑改完没执行daemon-reload或者被 drop-in 目录里的配置覆盖了。顺带一提/etc/systemd/system.conf里还有一组DefaultLimitXXX它决定了所有没有显式指定限制的服务的默认值。有些人为了图省事把这里改成很大的数然后发现所有服务都开了很大的 fd 额度——这其实是个隐含风险fd 额度大意味着进程可以悄悄泄漏更多描述符而不被发现我一般只给确实需要的服务单独配。3.2 LimitNOFILE 的正确写法与生效验证在 unit 的[Service]段里加[Service] LimitNOFILE65535 LimitNPROC65535LimitNOFILE65535这种写法会同时把 soft 和 hard 设成 65535。如果你的 systemd 版本足够新还可以分开写LimitNOFILESoft65535 LimitNOFILEHard524288分开写的好处是保留一个较高的 hard 上限进程内部如果需要自己调高 soft 还有空间。这个思路跟前面讲的 soft/hard 关系是一致的hard 留高一点soft 给一个够用的值。改完必须走这两步顺序不能反systemctl daemon-reload systemctl restart your-servicedaemon-reload是让 systemd 重新读取 unit 文件restart是让新配置真正注入到进程。只 reload 不 restart老进程还是老限制只 restart 不 reloadsystemd 还在用内存里的旧配置。验证的时候我习惯用一条命令串起来省得来回敲PID$(systemctl show -p MainPID --value your-service) echo PID$PID cat /proc/$PID/limits | grep -Ei open files|processes还有一个进阶玩法在 unit 里加PAMNameyour-service让服务在启动时走一遍 PAM session这样limits.conf就对它生效了。这个方案的代价是会把整条 PAM 栈都带进来包括各种认证、日志、会话模块可能引入你意料之外的副作用。我一般只在极端情况下用常规做法还是老老实实写LimitNOFILE。3.3 容器与时序多一层隔离带来的偏差容器环境里这个问题会叠两层。以 Docker 为例容器的默认nofile是从 dockerd 继承的而 dockerd 又是 systemd 拉起来的所以真正的源头是docker.service这个 unit 的LimitNOFILE。你在容器里ulimit -n看到的数本质上是这条继承链最末端的结果。要单独给某个容器设置启动时加参数docker run --ulimit nofile65535:65535 your-imagedocker-compose 里对应写services: app: ulimits: nofile: soft: 65535 hard: 65535在编排平台上pod 级别的限制往往需要在运行时配置或者安全上下文里声明不同平台的字段名不一样这里不展开思路是一致的找到真正决定 rlimit 的那一层而不是在容器里反复ulimit。这里有个特别容易踩的点容器内的/proc/sys/fs/nr_open显示的可能是宿主机的值但你在容器里能设置的上限受宿主机该进程的 hard limit 约束。也就是说容器里ulimit -Hn 2000000可能在 nr_open 上看着没问题实际设置时却失败因为宿主的 hard 只有 1048576。排查的时候一定要跳出容器往上看一层。4. 几个容易被当成灵异事件的边界问题4.1 nofile 设太大反而登不进机器这个坑我亲身经历一次印象深刻。当时为了压测把某台机器的nofile直接写成了 2000000改完 exit 重登ssh 卡在密码输入之后不动了等半天超时。换其他用户能登就这个用户不行。原因就是前面提过的fs.nr_open。pam_limits在设置 hard limit 时如果值超过了nr_opensetrlimit会返回失败。不同 PAM 版本对这个失败的处理不一样有的会打印一条日志然后跳过有的会让整个 session 建立失败。碰上后者就是改配置把自己锁在门外。正确的顺序是先把内核参数抬上去再改用户限制sysctl -w fs.nr_open2000000 sysctl -w fs.file-max2000000而且要写进/etc/sysctl.d/里持久化否则重启之后nr_open回到默认值而limits.conf里的 2000000 还在下次重启直接登不进去。这种配置在重启后才引爆的问题最阴险因为改的时候一切正常。还有一个全局视角的账要算nofile是每进程的file-max是全系统的。如果一台机器上跑了一百个进程每个都开 65535理论上需要六百多万个全局额度。虽然实际不会全部打满但把file-max保持在nr_open之上、并且留足余量是个稳妥的习惯。4.2 nproc 被 limits.d 里的文件悄悄覆盖nproc这个参数的坑比nofile更隐蔽因为它的生效层级更多。第一层是文件加载顺序。前面说过limits.d里的文件按字典序加载而且覆盖limits.conf。部分发行版默认在这个目录放一个20-nproc.conf内容大致是* soft nproc 4096。你在主文件里写* soft nproc unlimited被它一覆盖实际还是 4096。第二层是统计口径。nproc限制的是这个 UID 下所有 task 的总数线程也算。一个 Java 服务开了 500 个线程就吃掉 500 个额度。有时候看到nproc设了 65535 还是报fork: Resource temporarily unavailable第一反应是额度不够实际上是内存不够导致 fork 失败报错码一样但根因完全不同。排查时先看free -m和dmesg别一上来就调限制。第三层是 root 的影响。nproc对 root 同样生效这一点很多人不知道。如果你把* soft nproc设成了很小的值比如 100root 登录之后可能连 shell 都起不来——因为登录过程本身就要 fork 一堆进程。这是把自己锁在门外的另一种形态。还有一层来自 cgroup。现代内核用pids.max限制 cgroup 内的 task 总数容器环境里这个限制往往比ulimit -u更早触发。两套机制同时存在的时候报错信息是一样的判断方法是看/sys/fs/cgroup/.../pids.max和pids.current如果 current 贴着 max那问题就在 cgroup 不在 ulimit。4.3 stack、FD_SETSIZE 与那些看不见的关联ulimit -s栈大小有个经典陷阱设成unlimited之后某些程序反而起不来。原因是 glibc 在创建线程时如果没有显式指定栈大小会参考RLIMIT_STACK的值当这个值是unlimited时pthread 的行为在不同版本上不一致可能导致pthread_create返回EAGAIN。虚拟机类运行时对这点尤其敏感经常表现为进程起来了但创建线程失败。我的做法是永远不给stack设unlimited要调就调一个明确的大值比如ulimit -s 65536单位是 KB即 64MB。这样既够用又不会触发边界行为。另一个跟 fd 数量直接相关但常被忽略的东西是FD_SETSIZE。select()能监控的 fd 数量在编译期就被写死成 1024你在系统层面把nofile调到十万也没用——程序用select就是只能处理 1024 个。表现出的症状和 fd 不够一模一样但调ulimit完全无效。碰到这种唯一的解法是把代码里的select换成epoll。服务类软件的配置文件里通常也有对应的参数。反向代理类软件的worker_connections、缓存类服务的maxclients这些值加上监听端口、日志文件、内部通信的描述符总数要落在nofile以内。我一般按配置值 × 2 100来估算nofile留出日志重开、配置热加载时候的瞬时峰值。5. 排查清单与可复用的检查脚本5.1 症状到根因的对照表踩坑踩多了之后我整理了一张对照表遇到问题先查表比漫无目的地翻配置快得多。症状最可能的根因处置方向改完limits.conf重登无效PAM 未加载 modules 或 limits.d 覆盖查/etc/pam.d/与limits.d顺序手工跑正常服务跑报错systemd 不走 PAM改 unit 的LimitNOFILEsudo下限制不对sudo 无 session 类型 PAM直接改调用者会话限制改完能登录重启后登不进nr_open小于配置值先调sysctl并持久化nproc设置被忽略limits.d 后加载文件覆盖用 99 前缀的文件重写fork失败但额度充足内存不足或 cgroup pids 触顶查free与pids.current调大 nofile 仍报错程序用了select1024 上限换epoll改代码容器内设置失败宿主 hard limit 更低从 dockerd/service 层往上查这张表里每一行背后都是一次真实的排查过程其中程序用了 select那一条我印象最深因为当时把nofile从 1024 一路调到 65535重启了七八次最后发现方向完全错了。5.2 一套能直接抄的命令序列最后给一套我平时用的检查序列新接手一台机器或者排查一个陌生服务时按顺序跑一遍基本能覆盖八成情况。#!/bin/bash # 检查当前会话与指定服务的资源限制 echo 当前 shell 的限制 ulimit -a echo soft / hard 分项 echo nofile soft: $(ulimit -Sn) echo nofile hard: $(ulimit -Hn) echo nproc soft: $(ulimit -Su) echo nproc hard: $(ulimit -Hu) echo 内核级上限 sysctl fs.nr_open fs.file-max echo limits 配置文件清单注意加载顺序 ls -1 /etc/security/limits.d/ 2/dev/null grep -vE ^\s*#|^\s*$ /etc/security/limits.conf echo PAM 是否加载 limits 模块 grep -rn pam_limits /etc/pam.d/ 2/dev/null echo 指定进程的实际限制 if [ -n $1 ]; then PID$(pgrep -f $1 | head -1) if [ -n $PID ]; then echo 进程 $1 的 PID 是 $PID grep -Ei open files|processes|stack /proc/$PID/limits else echo 没找到匹配的进程 fi fi用法就是./check_limits.sh 你的服务名输出里重点看三处ulimit -Hn是不是你以为的那个数、limits.d里有没有意料之外的文件、目标进程的/proc/PID/limits跟 shell 的是否一致。这三处只要有一处对不上问题就在那一层。我个人在实际操作中的体会是ulimit相关的问题九成不是值设得不够大而是设的值没走到该走的地方。所以每次动手改配置之前先花两分钟确认生效路径比改完之后反复重启要省时间得多。另外养成一个习惯凡是改完要重启才生效的配置改的时候顺手把重启后的风险也过一遍像nr_open那种重启才引爆的坑就是这么躲过去的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑