ARTICLE DETAIL

资讯详情

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

Linux文件描述符与进程数限制:从ulimit到cgroup的排查调优指南

Linux文件描述符与进程数限制:从ulimit到cgroup的排查调优指南 接手一台新服务器我最先看的三样东西永远是内存、磁盘、以及ulimit -n的输出。前面两个好理解第三个很多人不重视直到线上服务半夜报Too many open files或者一个脚本怎么都 fork 不出新进程时才会想起来“哦当时要是把限制看清楚就好了”。这篇文章就围绕 Linux 下的文件描述符File DescriptorFD和进程数限制展开把这两块由浅到深讲透包括底层机制、三层限制模型、排查思路和调优模板适合后端开发、运维、SRE 以及对 Linux 参数感兴趣的朋友。1. 先用大白话搞懂文件描述符一个整数背后的三张表1.1 fd 的本质内核替你保管的“文件档案编号”文件描述符在 Linux 里的本质其实就是一个非负整数比如 0、1、2、3……看起来轻飘飘的但它背后挂着内核里三张互相引用的表。第一张是进程级的文件描述符表每个进程一份里面记录了这个进程打开了哪些文件、每个 fd 对应的文件指针位置、以及打开时的标志位比如是否 close-on-exec。第二张是系统级的打开文件表open file description table记录了文件当前的读写偏移量、访问模式只读/只写/读写等。第三张是inode 表对应磁盘上真实的文件或设备。你每次调用open()、socket()、accept()返回的整数就是第一张表里的下标。真正操作文件时内核通过这个下标找到第二张表里的记录再通过第二张表找到 inode一层层索引下去。你可以把它类比成图书馆的借书单fd 是借书单上的编号打开文件表是书库的登记册inode 是书架上那本实体书。用户空间拿到的永远只是编号摸不到实体但又不是纯粹的数字游戏。1.2 为什么 0、1、2 是约定俗成的每个进程启动时内核会默认分配三个 fd0 代表标准输入stdin1 代表标准输出stdout2 代表标准错误stderr。这不是“硬性规定”而是 POSIX 约定几乎所有 shell、C 库都遵循这个约定所以你的程序才能理所当然地用printf往 1 号 fd 上写用perror往 2 号 fd 上写。理解了这一点很多坑就能想明白了。比如你在 shell 里执行command log.txt 21本质是做两次重定向先把 1 号 fd 指向 log.txt再把 2 号 fd 复制为 1 号 fd 的副本于是 stdout 和 stderr 都进了同一个文件。如果写成command 21 log.txt顺序反了stderr 会先复制到旧的 stdout也就是终端然后 stdout 才重定向到文件最终 stderr 仍然打到屏幕上。1.3 报错信息里的门道到底是谁不够用了当你看到Too many open files时最常见的情况是进程已经达到了单进程文件描述符上限RLIMIT_NOFILE但也有可能系统级的fs.file-max已经用完或者 inode 数量耗尽。同样是这个报错根因可能完全不同。还有一类容易被忽略的accept()返回EMFILE时很多新手第一反应是“连接太多了”其实不一定。连接数只是 fd 消耗的一部分高并发场景下每进来一个 TCP 连接就多占用一个 fd但 fd 还可能被日志文件、socketpair、定时器 fd、eventfd 占着。如果一个服务有 10000 个正常连接但因为日志轮转配置有问题产生了大量未关闭的文件句柄fd 照样会涨到上限。1.4 怎么观察一个进程到底开了多少 fd最简单的入口是/proc/pid/fd/目录。这个目录下列出的是进程所有已打开的 fd 的符号链接比如ls -l /proc/1234/fd会看到类似0 - /dev/null、1 - /var/log/app.log这样的输出。如果看到大量指向已删除文件的链接文件名后面带(deleted)那基本可以判定是文件没关干净正在泄露 fd。另一个常用命令是lsof -p pid输出更友好能看到 fd 对应的是 socket、普通文件还是字符设备。但注意lsof在部分精简容器里没有安装想纯手工检测用/proc/pid/fd配合ls -l就行。提示/proc是虚拟文件系统读这些文件不产生实际磁盘 I/O也没有权限时用sudo补一下即可。这个目录本身就是排查 fd 问题的第一现场。2. 限制从哪来ulimit、systemd、内核三层模型2.1 ulimit -n 的软限制与硬限制Linux 的 fd 限制写在进程的 rlimit 里分软限制soft和硬限制hard。软限制是“当前生效的值”内核会在超过它时报错硬限制是“软限制能涨到的天花板”。普通用户可以把自己的软限制往高调到不超过硬限制但往上调硬限制需要CAP_SYS_RESOURCE权限也就是通常说的 root 或特权容器。你执行ulimit -n看到的是软限制执行ulimit -Hn看到的是硬限制。如果软硬不一样而你的程序需要更多 fd就要先确认硬限制够不够。很多生产环境里的 Java 服务因为启动时被 systemd 限制了硬限制即使启动脚本里写了ulimit -n 65535也会失败因为普通用户不能越过硬限制。修改方式有几种ulimit -n 65535仅对当前 shell 及其子进程生效重新登录就失效。/etc/security/limits.conf针对登录会话生效配置* soft nofile 65535这类规则。systemd 服务单元通过LimitNOFILE字段指定这只对服务生效跟登录会话是两套体系。从 RHEL/CentOS 7、Ubuntu 16.04 之后主流系统的默认会话限制基本是 1024 的软限制、4096 的硬限制或者直接 1024/1024。这个数值对日常 shell 操作够用但如果跑数据库、消息队列这类连接密集服务远远不够。2.2 为什么改了 limits.conf 不生效这是排障时最常踩的坑。/etc/security/limits.conf只对通过 PAM 登录的会话生效也就是你 ssh 进去之后的 shell。但如果你通过 systemd 启动服务systemd 会在启动进程前直接设置 rlimit根本不读limits.conf而是读服务单元文件里的LimitNOFILE。所以你会看到一种诡异现象手动在终端启动应用ulimit -n显示 65535一切正常但当/etc/systemd/system/myapp.service里的LimitNOFILE没设置时服务继承的是 systemd 默认值通常是 1024应用一启动就报 fd 不够。排查时不要只看limits.conf一定要看服务单元文件。修改 systemd 服务的方式很简单[Service] LimitNOFILE65535 LimitNPROC65535改完执行systemctl daemon-reload systemctl restart myapp。如果想确认生效没有可以cat /proc/pid/limits其中Max open files那一行会显示实际的软硬限制。这是最权威的验证方式比在各种配置文件里猜来猜去靠谱得多。2.3 内核层fs.file-max 与 file-nr除了单进程限制系统还有一个全局限制就是内核参数fs.file-max它代表整个系统能打开的 FD 总数。查看当前全局已打开的 fd 数量看/proc/sys/fs/file-nr三个数字分别代表已分配 fd 数量未使用但已分配的 fd 数量历史上我见过这个数字为 0 的注释实际上它是“已分配但未使用”的存量系统上限也就是fs.file-max如果你的file-nr的第一个数字持续逼近第三个数字说明系统层面 fd 已经快耗尽了。修改方式是sysctl -w fs.file-max1000000 echo fs.file-max1000000 /etc/sysctl.conf注意还有一个参数叫fs.nr_open它是单个进程可分配 fd 的硬上限默认 1048576约 100 万。RLIMIT_NOFILE的硬限制不能超过nr_open否则内核会直接报EFAULT或“数值超出允许范围”。这个参数一般不用动但如果你想把某个进程的 fd 调到几百万得先确认nr_open够大。三层模型可以用一张表概括层级关键位置作用范围常见修改方式单进程 rlimitulimit -n/ 进程 rlimit单个进程limits.conf、systemdLimitNOFILE全局 fd 上限/proc/sys/fs/file-max整个系统sysctl -w fs.file-max单进程上限天花板/proc/sys/fs/nr_open单进程硬限制上限sysctl -w fs.nr_open3. 进程数限制nproc、线程与 cgroup 的演进3.1 ulimit -u 与 TasksMax进程数限制比 fd 限制更隐蔽但同样致命。当你敲fork()却收到Resource temporarily unavailable或者Unable to fork大概率是进程数限制到了。查看当前用户的进程数软硬限制用ulimit -u ulimit -Hu这个限制只对普通用户生效root 默认不受ulimit -u限制但仍然受 cgroup 的 pids 限制。这也是为什么容器里动不动就报Cannot fork因为容器平台往往通过 cgroup 限制了 pids.max跟传统 ulimit 体系不直接相关。系统级参数对应的还有内核参数kernel.threads-max限制整个系统所有线程的数量。不过实际场景中cgroup 的pids控制器才是最常见、最直观的限制来源。3.2 线程也算进程理解 Linux 的 PID/TID很多刚从 Windows 转过来的开发者会困惑为什么我一个 JVM 应用有几百个线程ps -eLf | wc -l能数出几百行Linux 里线程和进程不是同一层概念。线程在用户态表现为同一个 PID 下的多个 TIDThread ID但在内核里每个线程都是一个可调度的任务task都有自己的 task_struct也都占用一个 pid 编号。所以在传统的kernel.threads-max和 cgroup pids 计数器里线程就是“进程”一个线程算一个 pids 单位。这就导致一个很现实的问题一个 8GB 堆的 Java 服务JVM 线程池 GC 线程 异步日志线程加起来可能有几百个如果再让 JVM 跑一些 sidecar 进程或多次 fork进程数配额很容易被打满。看起来每个进程都没做错什么整体配额却先到顶了。3.3 cgroup 的 pids.max容器环境下的隐藏枷锁如果你在容器平台Kubernetes、Docker 等都支持里跑服务cgroup的 pids 控制才是真正生效的那一层。它可以在cgroup路径下看到cat /sys/fs/cgroup/pids/pids.max cat /sys/fs/cgroup/pids/pids.current在 Kubernetes 场景下kubelet 默认会根据spec.containers[].resources.pidsLimit给容器设置 pids 限制如果没显式配置很多平台会默认给个配额。我见过最离谱的一次某个 Java 服务在单 Pod 里 pids.max 被设成了 1024JVM 一启动就占去几百稍微有点线程池就立刻Cannot fork。那一次排查了一个下午最后发现不是代码问题而是一个没写进 YAML 的默认策略。修改容器的 pids 限制在 Kubernetes 里是resources: pidsLimit: 1000Docker 命令行对应--pids-limit。如果是在裸机上直接改 cgroup可以用echo 1000 /sys/fs/cgroup/pids/pids.max注意cgroup v2 的路径通常是/sys/fs/cgroup/pids.max不同发行版路径略有差异不要照抄。先用find /sys/fs/cgroup -name pids.max 2/dev/null找一下最直接。4. 一次线上“Too many open files”的完整排查复盘4.1 现象请求失败、日志刷错误、监控曲线异常今年早些时候我处理过一个真实案例。某后端服务在压测时突然大量报错日志里反复出现java.net.SocketException: Too many open files同时进程的 CPU 飙升但请求成功率掉到 50% 以下。第一反应当然是加 fd 上限但加完之后发现只是把报警延后了几个小时问题依然存在。这就意味着不是“上限不够”而是“用量失控”。如果只是上限不够调高之后应该能安稳一段时间但很快又逼近新上限说明有 fd 在持续增长没被释放。4.2 排查链路从 lsof、ss 到 /proc/pid/fd我的排查顺序是这样的# 1. 找到目标进程 ps aux | grep app.jar # 2. 查看当前 fd 统计 ls /proc/pid/fd | wc -l # 3. 按类型分组看 fd 分布 ls -l /proc/pid/fd | awk {print $NF} | sed s/.*\.// | sort | uniq -c | sort -rn # 4. 看 TCP 连接状态 ss -tanp | grep pid第 3 步最关键。打开/proc/pid/fd后看到的都是符号链接filename (deleted)自然代表文件句柄没释放socket:开头则代表网络连接。我那次统计出的结果是普通 TCP 连接大概 2000 多个但socket:总数却有 8000 多说明存在大量已经不在正常连接列表里的 socket fd。为了找到这些 fd 对应的对端我用的是awk /^socket:/ {print $1} /proc/pid/fdinfo/*但fdinfo里只有ino信息需要再配合ss -tm或lsof -i才能反查到远端地址。实际操作上最便捷的还是直接抽样看几个 socket fd 对应的具体 TCP 连接for fd in $(ls /proc/pid/fd | sort -n); do if [ -L /proc/pid/fd/$fd ]; then target$(readlink /proc/pid/fd/$fd) echo $fd - $target fi done | grep socket | head -50最后发现大量连接处于TIME_WAIT状态的残留 socket 没有真正从进程中移除加上应用层没有正确关闭连接fd 被反复占用。4.3 根因连接泄漏与 fd 泄露同时发生继续深挖后发现问题出在客户端的连接池配置。服务端应用关闭连接时只是做了io.cleanUp()一类的清理没有调用底层 socket 的 close而客户端那边的新连接不断建立旧的却不释放导致 socket fd 被一次又一次地复制、遗留。再加上压测期间 QPS 很高泄漏速率远大于释放速率fd 数量像滚雪球一样涨。这类问题的核心规律是fd 持续上涨 不回落无论上限调多高都只是延迟爆炸时间。如果不查根因、只调上限唯一的结果是让系统晚一点宕机宕机时规模更大。4.4 修复改代码、调参数、加监控三管齐下修复方案分三层代码层确保所有 socket 和流都走 try-with-resources 或 finally close杜绝半关不关的情况。参数层把net.ipv4.tcp_tw_reuse和net.ipv4.tcp_fin_timeout调到合适值让 TIME_WAIT 里的连接尽快回收。但注意tcp_tw_reuse只对主动连接方生效有一定副作用配置前先理解你的服务是连接发起方还是接受方。监控层把/proc/pid/fd数量、ss -s的连接状态统计接入监控一旦 fd 在正常业务之外持续增长超过 N% 就报警。这一步是防止“复发”的核心。那次调完之后fd 稳定在 3000 左右压测 QPS 再涨也不会有问题。5. 调优不是拍脑袋量化指标、配置模板与长期治理5.1 给服务定 FD 数量级很多人在limits.conf里写 65535、131072纯粹是“看到别人这么写”。真实的 fd 需求量应该由你的业务性质决定连接密集型服务如网关、长连接服务FD 数 ≈ 最大并发连接数 日志/内部内存映射文件数 线程池内部 fd。一个真实连接因为内核 socket 缓冲区、TCP 控制块等还要额外占内存每个连接按 2~3 个 fd 估算比较安全。常规 Web 服务如 Nginx PHP-FPMNginx 单 worker 的 fd 数 ≈ 每个 worker 的连接数PHP-FPM 则主要吃进程数限制而不是 fd。Java 服务Java NIO 会占用 fd 作为 selector 的管道Netty 的 EventLoop 也会消耗 fd估算时在并发连接数基础上再留 10%~20% 余量。进程数nproc的设定稍微特殊一点。对 JVM 来说进程数需求主要为JVM 内部线程GC 线程、编译器线程、JMX 线程等 应用线程池 少量外部进程。保守起见单容器内给 512 或 1024对多数不是重度线程密集型的 Java 应用够用但如果你跑的是 Tomcat 的 maxThreads800 Netty 各种 scheduler1024 可能立刻吃紧。5.2 一套可复制的配置模板以下是我在 Linux 服务器上常用的基线配置适合大多数生产环境/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 soft nproc unlimited root hard nproc unlimited/etc/sysctl.conf增加fs.file-max 2097152 fs.nr_open 2097152 net.ipv4.ip_local_port_range 1024 65000 net.ipv4.tcp_fin_timeout 30 net.ipv4.tcp_tw_reuse 1如果是 systemd 服务在 service 单元里写[Service] LimitNOFILE1048575 LimitNPROC65535LimitNOFILE我一般不会直接给infinity。虽然 systemd 支持infinity这种写法但它等同于直接把硬限制拉到内核允许的最大值一旦业务代码有 fd 泄漏服务会拖到整个系统资源的墙角才被 OOM 或自动重启拦截。给一个明确的上限反而能让监控更早发现问题。5.3 治理建议把 FD 当一等公民监控长期实践下来我最大的体会是文件描述符和进程数限制不能当成“出了问题再调”的参数而应该是上线前的验收项。每个新服务部署前至少检查四件事cat /proc/pid/limits确认软硬限制符合预期。压测时记录 fd 峰值按峰值 1.5 倍预留上限。观察 24 小时内的 fd 走势正常业务曲线应该有波峰波谷如果是一根持续向上的直线直接视为 fd 泄漏报警。对容器环境确认 cgroup 的pids.max大于进程数峰值。另外面试考点里也爱出“fd 耗尽会导致什么问题”“软限制和硬限制的区别”“ulimit -n 和 fs.file-max 有什么关系”之类的题。如果你在准备运维或后端面试把上面几节内容串起来基本能答得比讲 PPT 的人更有实操感。提示有不少服务端框架自带连接池和线程池监控比如 Java 的Tomcat Connector metrics、Netty 的EventExecutorGroup指标可以先把这些指标接到监控平台比直接读/proc更省心。根据我自己的习惯每台机器装机后第一件事就是写一个启动巡检脚本自动检查fs.file-max、ulimit -n、limits.conf、sysctl.conf的一致性输出一张表格。这不是洁癖而是这类问题通常在业务跑起来之前是最容易修的一旦上了生产再去动 rlimit既要滚动重启进程又要小心翼翼不打断流量成本完全是两个量级。把这套检查做成自动化后面能省下无数个熬夜排查的夜晚。
返回列表