ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux系统资源限制:文件描述符与进程数上限详解

Linux系统资源限制:文件描述符与进程数上限详解 1. 项目概述一次线上抖动牵出的系统级限制在 Linux 服务器上跑应用跑着跑着突然日志里开始刷Too many open files或者进程直接起不来、报max user processes超限这种场景对运维和开发来说都不陌生。我第一次被这类问题缠上是多年前接手一个 Java 网关服务白天流量一上来连接数猛增紧接着大量请求报错进程还偶尔挂掉。刚接触的人可能会下意识重启服务但重启完过一会儿又复现这时才意识到问题根本不是业务代码而是 Linux 系统的资源限制在作祟。我写这篇内容是想把这类问题的根源彻底讲透。文件描述符File Descriptor和进程数限制nproc / max user processes这两个概念它们的配置文件、查看命令、临时修改方式、永久生效方式以及排查手段很多人零零散散知道一点但一旦问题变得复杂比如 systemd 管理下改了 limits.conf 却不生效或者 Nginx 明明调大了 worker_connections 依然报错就开始抓瞎。这篇文章适合正在学习 Linux 基础的人也适合已经有一定经验、想系统补齐这块短板的读者。我不打算只贴命令我会把每个参数背后的原理和适用场景都拆开讲清楚让你看完之后遇到类似问题能自己定位不用满网搜答案。提示下面的内容会频繁涉及三个维度——内核全局限制、用户级限制、进程级限制。先把这三层的关系放在脑子里内核全局参数如 fs.file-max是顶层天花板用户级 ulimit如 nofile、nproc是中间闸门进程自身能打开多少 fd 则是受前两者共同约束后的结果。任何一层到了上限表现都会不一样。2. 先搞清楚文件描述符是什么为什么它会被打满2.1 文件描述符的本质内核给进程的一张索引表文件描述符在 Linux 里其实就是一个非负整数进程每次 open、socket、accept、pipe 这些操作内核都会返回一个 int 作为句柄。你可以把进程想象成一个人文件描述符就是这个人手里的号牌你要读文件、写 socket、操作设备不需要记住文件在内核里的具体位置只要把号牌递给内核就行。这个机制起初是为了 Unix 哲学里“一切皆文件”而设计的网络连接、管道、设备节点、普通文件全都可以用 fd 来指代。所以当你的服务承载大量 TCP 长连接时每一条连接至少占一个 fd打开了几个日志文件又占几个甚至你加载的动态库、读取的配置文件在进程运行期间也都会占用对应的 fd。等你用lsof -p pid一统计才发现一个进程轻松就能到几百上千个 fd。这里有个容易忽略的点fd 的分配是整型递增的内核会在进程内部维护一张 fd 表。虽然看数字好像能无限增长但每个进程、每个用户、甚至整个系统都有明确上限。这个上限由三个层面的参数共同决定分别是fs.file-max系统全局、fs.file-nr观察当前占用、以及用户级 ulimit 的-n设置。理解这一点后面所有排查逻辑都顺了。2.2 软限制与硬限制一对经常被搞混的兄弟ulimit -n显示的是当前 shell 的文件描述符限制但你仔细看会发现有 soft 和 hard 两档。用ulimit -Sn看软限制ulimit -Hn看硬限制。软限制是内核建议进程不要超过的值进程可以自行调高但不能超过硬限制硬限制则是真正不可跨越的墙只有 root 或者拥有 CAP_SYS_RESOURCE 权限的进程才能修改。这个设计的初衷是给普通用户一个“临时放宽”的余地但实际工作中很多人只改了软限制不改硬限制或者反过来结果服务一启动就依然受限。更常见的情况是你在/etc/security/limits.conf里写了* soft nofile 65535忘记写* hard nofile 65535然后 Java 进程启动时 JVM 里说要去扩展 fd 数结果发现硬限制才 1024直接放弃。这种问题不会报明显的权限错误只会让你怎么调都调不上去。我的习惯是所有线上配置都同时写死软硬限制并且数值保持一致。原因很简单你永远不知道进程内部哪个阶段会调用 setrlimit 来调整自己的限制如果软硬不一致进程想临时抬高时可能直接被内核拒绝引发难排查的运行时异常。保持一致后行为就变得可预期了。2.3 打开文件太多系统里到底哪里满了当报错Too many open files时别急着只调 ulimit。需要先分清是系统级满了还是进程级满了。第一优先看/proc/sys/fs/file-nr这个文件里依次显示已分配文件句柄数、已使用但未分配数、以及最大文件句柄数。如果你发现第一个数值已经逼近第三个数值说明全局限量此时调 ulimit 也白搭要调fs.file-max。第二优先级才是检查进程级用lsof -p pid | wc -l或者直接看/proc/pid/fd/目录下的条目数量。这里有个细节lsof统计的条数可能会和 /proc 下看到的略有出入因为 lsof 还统计了 cwd、txt、mem 等映射项而 /proc/PID/fd 只统计真正的 fd 编号。定位时建议两个都看一眼以/proc/PID/fd的数值为准更准确。注意很多应用的连接池、线程池在初始化时会一次性预留大量 fd但不会真的建立连接。所以数字很接近上限不一定代表有泄漏也可能是正常扩容。判断泄漏的可靠方法是观察一段时间内的 fd 曲线如果随着时间持续增长、且业务流量没有显著变化那就是泄漏如果只是随着流量高峰同步涨落那就是正常现象。3. 进程数限制max user processes 到底限制了什么3.1 线程也是进程这个坑最容易被踩ulimit -u显示的是当前用户可以创建的最大进程数但这里说的进程在 Linux 下其实包含了线程。因为 Linux 的线程是用轻量级进程LWP实现的clone系统调用创建出来的每个线程内部都对应一个 task_struct也计入用户进程数限制。这就是为什么很多 Java 应用、容器应用会频繁踩中max user processes报错你明明就启动了一个 Java 进程可它内部的线程池一扩几百上千个线程就算几百上千个“进程数”。再加上 JVM 的 GC 线程、编译线程、JMX 线程数量很快就上来了。如果你给某个账号设置了nproc 1024本意是防别人乱起进程结果把一个高并发 Java 服务给误伤了。我在一个坑里就栽过给部署账号设置了* soft nproc 4096结果线上一个微服务启动后光线程就有 3000 多紧接着 JVM crash。当时查了半天看 GC、看内存都没有异常最后是dmesg里出现Resource temporarily unavailable和fork failed才意识到问题。从此之后我的经验是在没搞清楚应用线程模型之前不要轻易给核心业务账号设置过低的 nproc。3.2 用户和 cgroup 双重限制改了 ulimit 还可能被拦现在很多 Linux 发行版尤其是使用 systemd 的系统用户进程数的限制除了看 ulimit还要看 systemd 的 cgroup 设置。/etc/systemd/system.conf里的DefaultTasksMax、以及每个 service 单元文件里的TasksMax都会影响一个 service 能创建的进程/线程总数。就算你 limits.conf 里给用户设了 65535systemd 的 cgroup 层可能只允许 512 个 task那照样到 512 就报错。这是现代 Linux 环境下最容易被忽视的矛盾点传统的 ulimit/limits.conf 代表的是资源限制systemd 的 cgroup 再叠加了一层控制。很多人只会改前者不会改后者自然不生效。检查这个限制可以用systemctl show service名 | grep TasksMax如果发现值特别小就用systemctl set-property 服务名 TasksMax65535调整或者直接在 service 单元文件加TasksMaxinfinity。另外还有 PAM 层面的因素。/etc/security/limits.conf属于 pam_limits.so 模块这个模块必须在 PAM 配置里被调用才会生效。好在大多数发行版默认在sshd、login等会话类型中都会加载它但如果你用的不是标准登录流程比如某些自定义监控脚本、容器编排系统直接拉起的进程可能压根不会走 PAM那 limits.conf 就形同虚设。遇到这类场景要么改服务脚本里主动ulimit要么在 systemd 单元里声明LimitNOFILE、LimitNPROC。3.3 排查进程数瓶颈的三板斧当系统出现无法创建新进程的报错时我一般按这个顺序排查一是看当前用户已经创建了多少进程/线程用ps -u 用户名 -L | wc -l这个统计比较准确它按线程数LWP统计如果想要轻量一点可以用ps -u 用户名 --no-headers | wc -l但这个是按进程缕计不包含线程只能作为辅助参考。二是看系统整体有没有到 pid_max 上限。/proc/sys/kernel/pid_max默认是 32768但依然可能被打满。如果系统里跑着大量短生命周期线程典型场景是频繁创建线程处理请求的应用pid 号会快速上涨一旦用完所有进程和线程都无法创建。这时候dmesg里会有pid allocation failed之类的提示解决办法是临时调高kernel.pid_max或降低应用层线程创建频率。三是最容易被忽略的——没有写ulimit -u的 shell启动脚本里如果用了nohup或者拉起后台进程这些进程的 nproc 继承自父进程。如果你用 root 用户跑了个脚本脚本里又用su切到普通用户PAM 会话没初始化完全的话继承的 nproc 可能还是 root 的这时候的表现千奇百怪建议直接cat /proc/pid/limits去看实际生效值别猜。4. 实操配置从临时修改到永久生效的完整路径4.1 临时生效改参数之前先想清楚影响范围临时修改是最容易做的但也是影响范围最容易被低估的。先说文件描述符。ulimit -n 65535这样改只对当前 shell 和它的子进程生效而且前提是当前 shell 的硬限制允许提升。如果硬限制本身就低普通用户会直接报Operation not permitted这时候只能用 root 先提高硬限制或者改 limits.conf 后重新登录。对于已经运行的进程ulimit没法直接影响因为资源限制在进程 fork/exec 时已经继承好了。你需要修改/proc/pid/limits吗不能这个文件是只读的。正确的临时调整方式是使用prlimit命令prlimit --pid pid --nofile65535:65535这个命令可以实时修改一个运行中进程的资源限制非常有用尤其适合在不重启服务的情况下紧急扩容连接数。进程数限制的临时修改同理ulimit -u 65535只影响当前 shellprlimit --pid pid --nproc65535:65535则可以调整已经在跑的进程。我再提醒一句这些临时修改一旦进程重启或者机器重启就全部失效所以只能用来应急不能当作正式配置。4.2 永久生效limits.conf 和 systemd 的正确写法规避永久修改文件描述符和进程数限制的经典方式是编辑/etc/security/limits.conf格式是“域 类型 项目 值”。域可以是用户名、组名用 开头、或者通配符*类型就是 soft 和 hard项目包括 nofile、nproc、fsize、core 等。例如* soft nofile 65535 * hard nofile 65535 * soft nproc 65535 * hard nproc 65535一个最常见的坑是通配符*只对非 root 用户生效root 用户的 limits 默认不读取这里的设置。所以很多人在 limits.conf 里写完后用 root 登录去ulimit -n验证发现还是 1024就以为配置没生效。实际上 root 确实不受这个通配符限制你需要验证哪个业务账号就用哪个账号重新登录再看。systemd 管理的服务limits.conf不一定能完全覆盖到。因为 systemd 在启动服务进程时默认不会读取 PAM 的 limits.conf它有自己的资源控制参数。你在 service 单元文件里对应写[Service] LimitNOFILE65535 LimitNPROC65535然后在 systemd 的 service 文件所在目录执行systemctl daemon-reload再systemctl restart 服务名才生效。很多人在改完单元文件后忘了 reload重启服务时用的还是旧参数排查半天才找到原因。4.3 内核全局参数的调整fs.file-max 和 pid_max如果确认是全局文件句柄不足或者全局 pid 号不够就需要调内核参数。文件描述符全局上限对应/proc/sys/fs/file-max查看当前使用情况用cat /proc/sys/fs/file-nr。我在生产环境遇到上千个容器节点时一般会把这个值调到几百万甚至上千万因为每台宿主机上所有容器加起来文件句柄数量非常惊人。修改方式有两种临时用sysctl -w fs.file-max2097152永久写入/etc/sysctl.conffs.file-max 2097152注意fs.file-max只是上限内核实际的分配策略受vm.max_map_count、内存等影响。如果一个进程需要打开海量 fd而系统内存不足可能会出现“明明没到 fs.file-max但就是 open 失败”的情况这时候就要检查内存和vm.max_map_count了。kernel.pid_max同理默认 32768 在一些大并发的 Linux 机器上确实不够。调高到 4194304 也常见每次分配 pid 会消耗一点点内核内存但现代服务器不在乎这点开销。改完后用sysctl -p刷新或者干脆重启机器因为有些内核参数在运行中改虽然能生效但一些旧内核特性不会自动回收旧的 pid 位图重启最稳妥。4.4 参数计算小案例一个高并发 Nginx 实例需要多少 fd为了让你真正理解怎么算数值我以一台跑着 Nginx 的服务器为例简单推演一下。假设这台机器要承载 5 万个并发长连接Nginx 采用 event-driven 模型把每个连接对应一个 fd。每个连接进来Nginx 自身至少使用一个 fd可能再加上后端 upstream 的连接。如果这台 Nginx proxy_pass 到后端那每个客户端连接会对应一个与后端建立的连接fd 数量差不多要翻倍。如果真的只算 Nginx 单个 worker 进程的 nofile那么 5 万连接加后端连接保守估计需要 10 万到 12 万个 fd。除此之外Nginx master 进程要读日志、读配置、可能还要做 cache 管理再预留一部分余量。所以我会把 worker 的 nofile 设到 200000 左右。系统级fs.file-max则要考虑这台机器上所有进程的总量如果除了 Nginx 还跑了监控 agent、日志采集器、SSH 会话等至少要给到整个机器的上限为所有进程需求之和 20% 冗余。例如总需求 30 万那 fs.file-max 设 400000 比较安心。进程数限制方面Nginx worker 通常不会被 nproc 限制影响因为它是 event-driven 而非 thread-per-connection 模型线程数基本固定。但如果你在同一个账号下跑了大量容器或者 sidecar 代理那 nproc 就要按“静态进程数 线程池上限”来估。比如账号下运行 20 个 Java 微服务每个服务线程池最大 200再加上 JVM 内部线程每个服务约 400 线程那就需要20 * 400 8000。加上 shell、监控命令、计划任务这些零散进程nproc 给 12000 以上比较合理。实际部署时宁可多给也不要卡着临界值否则流量一抖就崩。5. 排查与定位当问题暴发时用这些命令快速收口5.1 三张表/proc 动态信息、lsof 与系统日志配合使用遇到资源受限问题第一件事不是改参数而是搞清楚限制发生在哪个环节。我惯用的手段是同时开三个窗口分别观察/proc动态信息、lsof输出、以及系统日志。第一个窗口重点看/proc/sys/fs/file-nr和/proc/pid/fd/。前者判断全局是否紧张后者判断单进程是否吃紧。/proc/pid/limits也要看一下确认进程实际生效的 nofile、nproc 是多少这一步能快速判断是不是配置没生效还是真的超了。第二个窗口用lsof -p pid按文件类型筛选用途。比如lsof -p 1234 | grep TCP可以看到这个进程打开的 TCP 连接lsof -p 1234 | grep \.log可以看到日志文件句柄。如果发现大量 TIME_WAIT 状态的连接占着 fd 不放那问题很可能是连接回收策略不合理而不是 fd 上限太低。ss -s可以快速确认系统级 socket 统计ss -tan state time-wait可以列出所有 TIME_WAIT 连接。第三个窗口看一下dmesg -T的尾部输出。资源类问题很多时候会在内核日志里留下线索例如fork failed: Resource temporarily unavailable、socket: too many open files。这些日志虽然简略但能帮你快速锁定是文件句柄还是进程数的问题。5.2 慢查询与长期观察从监控图里发现规律如果资源问题已经发生过多次只看当下是没用的必须从监控里找规律。我会用两个核心指标建监控图一是每个节点的fd 使用率二是用户的线程/进程总数。fd 使用率可以用(fs.file-nr 第一个值 / fs.file-max) * 100%得到进程总数可以通过ps -eLf | wc -l定时采集。这两个指标一旦超过 70%我就会开始预警超过 90% 就视为高风险。另外pid 使用率也值得监控/proc/sys/kernel/pid_max固定值而当前最大 pid 可以用cat /proc/sys/kernel/ns_last_pid或者ps -eo pid | sort -n | tail -1来观察。如果最大 pid 总在高位徘徊说明系统一直处于高频创建/销毁进程的状态得防止 pid_wrap 问题。这类问题一旦发生短时间内容器、进程起不来非常危险。5.3 典型故障定位全过程演示我举一个典型的 Java 服务 fd 泄漏定位流程。假设服务叫 order-servicePid 是 23333。首先确认进程当前 fd 数和限制ls /proc/23333/fd | wc -l cat /proc/23333/limits | grep -E nofile|nproc接着用 lsof 看这些 fd 属于什么类型的文件lsof -p 23333 | awk {print $5} | sort | uniq -c | sort -rn | head -20比如结果里 TCP 占了大多数而且ss -tanp | grep 23333发现大量CLOSE_WAIT状态的连接那么基本可以判断是应用层没有正确关闭连接导致的泄漏。这时候调大 ulimit 只是掩盖问题真正要做的是修代码里的连接释放逻辑或者调整连接池的空闲回收策略。如果看到的是cwd、txt这类文件很多那可能是某个动态库被反复加载但没有释放或者某个目录被频繁 open 后未 close。这些场景都意味着你要回去检查代码而不是先加参数。6. 边界场景与联动文件描述符和进程数限制如何相互影响6.1 连接数上限不只是 fd还受 ephemeral port 与内存影响很多人以为只要把 nofile 调得足够大就能支撑百万连接。实际上高并发连接还受两个隐藏因素制约。第一是本地可用端口范围通过/proc/sys/net/ipv4/ip_local_port_range控制默认通常是 32768-60999也就是最多约 2.8 万个客户端端口可用。当一个进程面对同一个远端 IP 时TCP 四元组能区分连接的数量受限于本地端口数量超出后新连接就 bind 不上端口了。这个可以通过增加本地端口范围或者绑定多 IP 来缓解。第二是每个 TCP 连接要占用内核内存包含 socket 缓冲区等。虽然tcp_rmem、tcp_wmem可以被调小但每条连接的内存开销依然存在。如果机器内存只有 8G想支撑百万连接还期望低延迟大概率是不现实的。所以在设定 fd 上限时不要只盯着 fd 数字还要算一下连接的内存预算例如每条连接预留 16KB 内核内存10 万连接就是 1.6GB这个预算得能覆盖你机器的可用内存。6.2 fork 与并发模型的连带效应fd 继承导致的数量暴涨进程数限制还有一个容易被忽视的联动点fork出来的子进程会继承父进程的 fd 表。如果你用一个父进程管理大量 worker 进程比如 Apache prefork 模式、某些 master-worker 架构的网关父进程里打开的 fd例如监听 socket、日志文件会被复制到每个子进程中。当子进程数量特别多时这些继承的 fd 叠加起来就是一个惊人的数字。这也解释了为什么某些服务的“进程数限制”提高后反而把“fd 限制”或者系统 fs.file-max 打满。比如你把一个账号的 nproc 从 512 提高到 4096假设每个进程平均继承 50 个 fd光这一块就增加了 20 万个 fd 消耗。所以我在做大调整时会先模拟计算进程数上限 × 每进程平均 fd 数是否超过了 fs.file-max 的预算。如果超了就要同步调大 fs.file-max否则问题只是从 nproc 转移到了 nofile。6.3 容器环境下的特殊考量namespace 与 cgroup 协同限制容器化部署普及后资源限制又多了一层。在 Kubernetes 里Pod 内的进程会看到 cgroup 的 pids.max 控制可能和宿主机用户的 nproc 不一致。比如 Pod 的 limits 没设那 cgroup 的 pids.max 是 max看起来不受限但实际上容器内进程属于某个宿主机用户如果这个用户有 nproc 限制那依然会被卡住。建议在容器环境里统一采用 cgroup 来控制避免依赖宿主机用户级 nproc。因为 nproc 的粒度太粗一个宿主机上多个 Pod 共享同一个用户彼此之间会因为 nproc 互相干扰一个 Pod 的线程打满会连累另一个 Pod 起不了线程。而 cgroup 是每个 Pod 独立控制的隔离效果更好。同理文件描述符层面容器也要关注 Pod-level 的 limits比如 Kubernetes 的ephemeral-storage限制会影响日志写文件的空间但不直接限制 fdfd 仍由宿主机 fs.file-max 和进程 nofile 控制。我还见过容器里 PID 耗尽的情况容器内的 pid_max 是受 namespace 限制的默认值继承自宿主机的 kernel.pid_max但你可以用--pids-limit给 Docker 容器单独设置或者在 Kubernetes 里配置pidsLimit。如果没设置容器内进程疯狂创建线程可能导致 PID namespace 耗尽但宿主机 pid_max 还没打满表面上看起来难以理解。定位这种问题要靠cat /sys/fs/cgroup/pids/pids.max和cat /sys/fs/cgroup/pids/pids.current来对比。7. 问题排查速查表与实用经验7.1 高频故障与应对方向一览我在实际运维中整理了一张速查表遇到资源类报错直接对照能少走很多弯路。报错现象最可能的根因快速排查命令解决方向Too many open files单进程 fd 超限或全局 file-max 超限cat /proc/系统pid/limitscat /proc/sys/fs/file-nr调大 nofile 或 fs.file-max修复 fd 泄漏max user processes或Resource temporarily unavailable用户 nproc 或 cgroup TasksMax 超限ulimit -usystemctl show 服务名 | grep TasksMax调大 nproc/TasksMax优化线程模型创建线程时报Cannot allocate memorycgroup pids 限制、内存不足、pid_max 耗尽cat /sys/fs/cgroup/pids/pids.currentdmesg扩容内存、调大 pid_max、设置 pidsLimit连接数上不去且无报错本地端口范围耗尽或文件描述符预留不足sysctl net.ipv4.ip_local_port_rangess -s扩大端口范围、绑定多 IP、调大 nofilesystemd 服务改了 limits.conf 无效systemd 未读 limits.conf需配置单元文件systemctl show 服务名 | grep LimitNOFILE在 service 里写 LimitNOFILE/LimitNPROC这张表不能覆盖所有情况但它代表了我排查这类问题时的核心判别逻辑先明确问题发生在哪一层系统、用户、cgroup、进程再针对那一层调整不要一上来就调大所有参数。7.2 关于调整顺序的三点经验第一先定位后修改。我见过太多人把 fs.file-max、nofile、nproc 全部调大结果问题还在原因可能是 cgroup 没动。这种“一刀切”的做法不仅掩盖了真正的问题还会让系统在异常场景下打开更多句柄、消耗更多内存反而加重故障。第二修改之前一定要有回滚方案。limits.conf改错了可能导致服务起不来甚至登录用户无法正常开启 shell。有一位同事曾把* hard nproc 1写进 limits.conf结果 root 都差点登不进去最后还是通过单用户模式修的。所以改之前先备份原文件尽量用visudo这类带语法检查的工具或者先写个临时文件测试一下再覆盖正式配置。第三操作级别上要区分“当前 shell”“重启后”“运行中进程”三个场景。每次修改完配置最好在多个地方验证新开的 shell 用ulimit -n查看systemd 服务用systemctl show查看运行中的进程用prlimit --pid查看。只有三处数值都符合预期才说明配置真正生效了。7.3 一个容易复现的测试场景帮你练手验证配置有人总觉得自己理解到位了但一到实际问题就发怵。我建议你在一台测试机上故意复现一下这个场景练练手先限制 nofile 为 64然后写一个简单的 Python 脚本不断 open 新文件观察什么时候报OSError: [Errno 24] Too many open files。这时用ls /proc/pid/fd | wc -l数一下实际 open 的数量你会发现非常接近限制值 64而不是 1024。这是因为每个进程启动时还会默认打开 stdin/stdout/stderr以及加载动态库时产生的临时 fd。同样的思路测试 nproc把 ulimit -u 设成 20然后用 Python 或 Shell 不断后台启动子进程观察什么时候报fork: retry: Resource temporarily unavailable。这个实验能很直观地让你体会到“线程也是进程”的占用方式因为你一旦用 Python 的 threading 模块创建上百个线程很快也会碰到同样的问题。这类实验最大的价值不是命令本身而是让你把之前零散的知识串起来。等到线上出问题时你不再是病急乱投医地搜命令而是脑子里已经有了完整的层次模型和排查顺序。最后再分享一个小技巧对关键业务服务我习惯在启动脚本里主动输出当前生效的资源限制比如ulimit -n; ulimit -u打到日志里。这样每次发布或者重启后只要看一眼日志就能确认服务是在预期的限制下运行。如果某次变更意外把限制改低了日志对比一眼就能发现。这个小动作几乎零成本但能帮你省下一大堆排查时间。
返回列表