1. 从一次线上告警说起这个报错到底在说什么凌晨两点监控面板突然飘红一台跑了三年都没出过岔子的服务节点开始疯狂刷错误日志核心业务接口的失败率从 0.1% 直接飙到 30%。登上机器一看日志里密密麻麻全是同一句话Too many open files。这个报错在 Linux 运维圈里算是老熟人了但每次遇到它排查过程都不太一样因为触发它的原因实在太多——可能是连接泄漏可能是配置没调对也可能是某个不起眼的模块偷偷打开了大量文件句柄没释放。先把话说清楚Too many open files本质上是一个资源配额触顶的问题。Linux 设计哲学里有一条“一切皆文件”你打开的每一个普通文件、每一个网络连接socket、每一个管道pipe、每一个设备句柄在系统层面都占用一个“文件描述符”file descriptor简称 fd。系统对单个进程能持有的 fd 数量是有上限的一旦某个进程打开的 fd 数量撞到了这个上限再想打开新的文件或建立新的连接内核就会直接拒绝并抛出这个错误。这个问题的杀伤力在于它的隐蔽性和连锁性。它不像内存溢出那样会立刻把进程干掉而是先让部分功能悄悄失效——比如新用户连不进来但老连接还跑得好好的监控上可能只是成功率轻微下滑很容易被忽略。等到你发现的时候往往已经积压了大量失败请求。更麻烦的是如果这个进程还负责写日志而写日志本身也需要打开文件那它可能连错误都记不下来直接变成“哑巴故障”。这篇文章适合谁看如果你是中高级运维、后端开发或者任何需要跟 Linux 服务器打交道的人这篇内容应该能帮到你。我会从 fd 的基础概念讲起把排查思路、工具用法、参数计算、临时止血和根治方案全部串一遍最后再分享几个我踩过的坑和独家排查技巧。新手也能看懂因为我会尽量用生活化的类比来解释但该有的深度一点都不会少。2. 文件描述符与系统限制先把底层逻辑捋清楚2.1 什么是文件描述符为什么它这么重要你可以把文件描述符想象成一家餐厅里的“桌号牌”。顾客进程进店吃饭操作文件或连接前台内核会给他分配一个桌号fd之后所有服务读写操作都凭这个桌号进行。桌号牌的数量是有限的餐厅里一共就那么多桌子坐满了再来人就得在门口等着等不到就只能拒绝。在 Linux 里每个进程启动时默认会打开三个 fd0 是标准输入1 是标准输出2 是标准错误。之后每打开一个文件、创建一个 socket、建立一个管道都会分配一个新的 fd数字依次递增。这个数字本身不重要重要的是同时打开的数量。为什么 fd 这么关键因为几乎所有网络服务本质上都是在跟 fd 打交道。一个 Web 服务器每来一个 HTTP 请求就要 accept 一个 socket这个 socket 就是一个 fd。如果并发是 1000那理论上至少需要 1000 个 fd 同时存在。如果代码里有 fd 泄漏——比如打开了文件忘了关或者连接异常时没有正确释放——那 fd 数量就会像漏水的水龙头一样慢慢涨上去最终触顶。2.2 三层限制系统级、用户级、进程级很多人以为Too many open files就是一个简单的“调大上限”就能解决的问题但实际上 Linux 对 fd 的限制是分层的你得一层一层看漏掉任何一层都可能白忙活。第一层是系统级限制由内核参数fs.file-max控制。这个值决定了整个操作系统所有进程加起来最多能打开多少个 fd。你可以用cat /proc/sys/fs/file-max查看当前值用cat /proc/sys/fs/file-nr查看当前已分配、已使用和最大可用的数量。现代服务器上这个值通常很大比如几十万甚至上百万一般不会成为瓶颈但在一些老旧的或者容器化的环境里它可能被设得比较小。第二层是用户级限制由/etc/security/limits.conf或者/etc/security/limits.d/下的配置文件控制。这里分软限制soft limit和硬限制hard limit。软限制是当前实际生效的上限普通用户可以自己调高但不能超过硬限制硬限制是软限制的天花板只有 root 才能调高。你可以用ulimit -n查看当前 shell 的软限制用ulimit -Hn查看硬限制。第三层是进程级限制每个进程在启动时都会继承父进程的限制但也可以在运行过程中通过setrlimit系统调用调整自己的软限制不能超过硬限制。很多服务程序在启动脚本里会显式设置ulimit -n比如 Nginx、MySQL 这些它们的配置文件里通常都有相关参数。这三层的关系是系统级限制是总闸用户级限制是分闸进程级限制是每个用电器的功率上限。任何一个闸跳了都会报Too many open files。我见过最典型的情况是运维把limits.conf里的值调到了 65535重启了服务但服务是通过 systemd 管理的而 systemd 有自己的限制配置结果服务实际生效的还是默认的 1024问题依旧。2.3 软限制与硬限制的区别以及 systemd 带来的新坑软硬限制的区别其实很好理解软限制是“当前允许你用的额度”硬限制是“你最多能申请到的额度”。普通用户可以把自己的软限制往上调但不能超过硬限制。比如硬限制是 4096软限制是 1024那你可以ulimit -n 4096把软限制提上去但不能再高了。只有 root 才能把硬限制也提上去。这里要特别提一下 systemd。现在大多数 Linux 发行版都用 systemd 来管理服务而 systemd 管理的服务不会读取/etc/security/limits.conf。这是一个非常容易踩的坑。systemd 服务有自己独立的限制配置你需要在 unit 文件里通过LimitNOFILE参数来设置或者用systemctl edit来覆盖。如果你只改了limits.conf就重启服务大概率会发现ulimit -n还是老值因为 systemd 根本没理那个文件。还有一个细节LimitNOFILE可以同时设置软限制和硬限制格式是LimitNOFILE65535:65535冒号前面是软限制后面是硬限制。如果你只写一个数字那软硬限制都会被设成那个值。我建议两个都写清楚避免以后想调软限制的时候发现硬限制卡住了。3. 排查实战从现象到根因的完整路径3.1 第一步确认当前限制和实际使用量遇到报错先别急着改配置先把现状摸清楚。你需要回答三个问题当前进程的限制是多少当前用了多少是哪个进程在报错查看当前 shell 的限制很简单ulimit -n # 软限制 ulimit -Hn # 硬限制但要注意这个命令查的是当前 shell 的限制不一定等于出问题那个进程的限制。要查特定进程的限制得用/proc文件系统cat /proc/pid/limits这个文件会列出该进程所有资源的限制包括Max open files你会看到软限制和硬限制两个值。这是最准确的方式因为它是从内核里直接读的。查看某个进程当前打开了多少 fdls /proc/pid/fd | wc -l这个命令会列出该进程所有 fd 的符号链接数一下就知道用了多少。如果这个数字接近限制值那基本可以确认是 fd 耗尽。还有一个更全局的视角cat /proc/sys/fs/file-nr输出三个数字已分配、未使用、最大可用。第一个数字是当前系统总共分配出去的 fd 数量第三个是系统级上限。如果第一个数字接近第三个说明系统级也快满了。3.2 第二步定位是谁在疯狂占用 fd确认了某个进程 fd 用量异常之后下一步就是找出这些 fd 到底是什么。是普通文件是 socket还是管道ls -l /proc/pid/fd | awk {print $NF} | sort | uniq -c | sort -rn | head -20这个命令会把 fd 指向的目标做统计看看哪类文件最多。如果大量指向/var/log/下的日志文件那可能是日志轮转出了问题如果大量是socket:开头的那可能是连接泄漏如果是pipe:那可能是进程间通信没关干净。再进一步如果你想看具体是哪些文件ls -l /proc/pid/fd | grep -v socket: | grep -v pipe: | head -50对于 socket你可以用ss或者netstat来看连接状态ss -s # 汇总统计 ss -tnp | grep pid # 查看该进程的 TCP 连接如果发现大量CLOSE_WAIT状态的连接那基本可以断定是代码里没有正确关闭连接。CLOSE_WAIT的意思是对方已经关闭了连接但本端还没有调用close()这个 fd 就一直占着不放。3.3 第三步用 lsof 做精细化分析lsof是排查 fd 问题的瑞士军刀虽然有时候输出很慢但信息最全。lsof -p pid | wc -l # 统计该进程打开的 fd 总数 lsof -p pid | awk {print $5} | sort | uniq -c | sort -rn # 按类型统计 lsof -p pid | grep -i log # 查看日志文件相关 lsof -p pid | grep -i sock # 查看 socket 相关如果lsof输出太慢可以用-n参数禁止 DNS 解析用-P参数禁止端口号转服务名能快不少lsof -nP -p pid还有一个技巧如果你怀疑是某个目录下的文件被大量打开可以直接用lsof D /path/to/dir来查不过这个命令在文件多的目录下会非常慢慎用。3.4 第四步区分“真泄漏”和“正常高并发”不是所有 fd 用量高都是问题。一个高并发的网关服务同时打开几万个 socket 是正常的。关键要看fd 数量是否随时间持续增长以及是否与业务量成正比。我的经验是如果 fd 数量在业务低峰期也不下降或者下降得很慢那大概率是泄漏。如果 fd 数量随请求量波动高峰期高、低峰期低那可能是正常的只是限制设得太低。你可以用sar或者自己写个脚本每隔几秒采集一次 fd 数量画个趋势图。如果曲线是单调上升的那就是泄漏无疑。如果曲线是锯齿状的那说明有正常的打开和关闭只是峰值太高。还有一个判断方法看 fd 的“年龄”。如果大量 fd 是很久以前打开的一直没关那很可能是泄漏。你可以通过/proc/pid/fdinfo/下的文件查看每个 fd 的详细信息不过这个比较繁琐一般用lsof的输出时间列也能大致判断。4. 解决方案从临时止血到根治4.1 临时止血调大限制先让服务跑起来线上出问题第一优先级是恢复服务。调大限制是最快的止血手段但要注意方法。如果是当前 shell 里启动的进程可以直接ulimit -n 65535但这只对当前 shell 及其子进程有效而且不能超过硬限制。如果硬限制不够得先用 root 调硬限制ulimit -Hn 65535 ulimit -n 65535如果是 systemd 管理的服务改 unit 文件systemctl edit service-name在打开的编辑器里加上[Service] LimitNOFILE65535:65535然后systemctl daemon-reload再重启服务。注意daemon-reload是必须的否则 systemd 不会重新读取配置。如果是通过/etc/security/limits.conf管理的加上* soft nofile 65535 * hard nofile 65535然后重新登录或者重启服务。但记住systemd 服务不看这个文件。系统级限制如果也需要调sysctl -w fs.file-max1000000要永久生效写到/etc/sysctl.conf里。注意调大限制只是争取时间不是解决问题。如果存在 fd 泄漏调大之后只是让触顶时间推迟该泄漏还是会泄漏最终还是会满。所以止血之后必须继续排查根因。4.2 根治泄漏代码层面的常见问题fd 泄漏的根因几乎都在代码里常见的有这么几类第一类文件打开后没有关闭。这是最典型的。比如读配置文件、写日志、处理上传文件时打开了File对象但没有在finally块里关闭。在 Java 里就是InputStream、OutputStream没关在 Python 里就是open()之后没有close()在 Go 里就是os.Open之后没有defer f.Close()。第二类连接没有正确释放。HTTP 客户端、数据库连接池、Redis 连接等如果异常处理不当连接可能没有归还到池子里也没有关闭。比如在 Java 里用HttpURLConnection如果没调disconnect()连接可能一直挂着。第三类异常路径下的资源泄漏。这是最隐蔽的。正常路径下资源都关了但某个异常分支里忘了关。比如打开文件 A然后打开文件 B如果打开 B 失败抛异常A 就没关。这种问题在代码审查时很难发现往往要等到线上出问题才暴露。第四类日志框架配置不当。有些日志框架在特定配置下会频繁打开关闭文件或者文件轮转时旧文件句柄没释放。特别是按小时或按分钟轮转的日志如果轮转逻辑有问题fd 会越积越多。第五类第三方库的 bug。这个比较少见但确实存在。某些库在特定条件下会泄漏 fd比如老版本的某些 HTTP 客户端库、数据库驱动等。遇到这种情况升级库版本或者换库是唯一办法。4.3 连接池与超时配置预防胜于治疗与其等泄漏了再排查不如在设计和配置阶段就做好预防。连接池要设上限。数据库连接池、HTTP 连接池、Redis 连接池都必须设置最大连接数。不设上限的连接池等于没有池高峰期会把 fd 耗尽。同时要设置合理的空闲连接回收策略避免连接长期闲置占用 fd。超时要设。连接超时、读超时、写超时一个都不能少。没有超时的连接一旦遇到网络问题就会永远挂在那里fd 永远不释放。我见过一个服务因为没设读超时对端不响应时连接一直挂着几天下来积累了几万个 fd。健康检查要定期做。对于长连接定期做健康检查发现死连接及时清理。很多连接池框架都支持这个功能配置一下就行。日志轮转要配好。用logrotate管理日志文件确保轮转时旧文件被正确关闭。如果服务自己管理日志文件要确保轮转逻辑正确旧文件的 fd 要关闭。4.4 监控与告警让问题在爆发前被发现最好的解决方案是让问题根本不发生或者至少在爆发前就被发现。监控 fd 使用率。对关键进程采集 fd 使用数量和限制值计算使用率。当使用率超过 70% 时告警给自己留出处理时间。监控 CLOSE_WAIT 连接数。如果 CLOSE_WAIT 数量持续增长说明有连接没关闭这是 fd 泄漏的早期信号。监控新建连接失败率。如果服务开始报Too many open files新建连接会失败这个指标能直接反映问题。定期做 fd 趋势分析。每周或每月看一下 fd 使用的长期趋势如果发现缓慢上升即使还没触顶也要提前排查。5. 常见问题速查与避坑指南5.1 为什么改了 limits.conf 不生效这是最高频的问题。原因通常有三个原因一服务由 systemd 管理。前面说过systemd 不读limits.conf要在 unit 文件里设LimitNOFILE。原因二没有重新登录。limits.conf是在用户登录时由 PAM 模块读取的改完之后需要重新登录或者重启服务已经运行的进程不会自动生效。原因三改错了文件。/etc/security/limits.d/下的文件会覆盖limits.conf里的设置如果那里有个文件设了更小的值你的修改就被覆盖了。用grep -r nofile /etc/security/检查一下。5.2 为什么 ulimit -n 显示的值和 /proc/ /limits 不一样ulimit -n查的是当前 shell 的限制/proc/pid/limits查的是特定进程的限制。如果进程是通过 systemd 启动的或者启动脚本里改了限制那两者可能不一样。排查时以/proc/pid/limits为准。5.3 系统级限制 fs.file-max 需要调吗大多数情况下不需要。现代 Linux 的默认值已经很大了比如 CentOS 7 默认是 761675足够几万个并发连接用。但如果你的服务特别多或者每个服务并发特别高可以算一下所有进程的 fd 峰值加起来不能超过fs.file-max。如果接近了就调大。调大方法sysctl -w fs.file-max2000000 echo fs.file-max2000000 /etc/sysctl.conf5.4 容器环境下的特殊注意事项在容器里跑服务fd 限制又多了一层。容器的 fd 限制受宿主机和容器运行时双重影响。Docker 可以通过--ulimit nofile65535:65535来设置Kubernetes 可以在 Pod 的securityContext里设置或者通过LimitNOFILE在 systemd 里设置如果容器由 systemd 管理。容器里还有一个坑/proc/sys/fs/file-max在容器里看到的是宿主机的值但实际能用的可能受 cgroup 限制。如果容器里报Too many open files但宿主机看起来还很空要检查 cgroup 的pids.max或者相关限制。5.5 常见问题速查表现象可能原因排查命令解决方向服务报 Too many open files进程 fd 触顶cat /proc/pid/limits调大限制或修泄漏改了 limits.conf 不生效systemd 不读该文件systemctl showgrep Limitfd 数量持续增长代码泄漏ls /proc/ /fdwc -l 多次采样大量 CLOSE_WAIT连接未关闭ss -tnpgrep CLOSE_WAIT日志文件 fd 多日志轮转问题lsof -pgrep log容器内报错但宿主机正常cgroup 限制cat /sys/fs/cgroup/...调容器限制5.6 几个我踩过的坑坑一只调了软限制没调硬限制。有一次我把ulimit -n调到 65535当时生效了但服务重启后又变回 1024。查了半天才发现硬限制还是 4096软限制不能超过硬限制重启后软限制被重置了。坑二忘了 systemd 的 daemon-reload。改了 unit 文件后直接systemctl restart发现没生效。后来才知道必须先daemon-reload否则 systemd 用的还是缓存的旧配置。坑三lsof 太慢导致排查效率低。在一个 fd 数量几万的进程上跑lsof等了快一分钟才出结果。后来改用ls /proc/pid/fd加awk统计秒出结果。虽然信息没lsof全但排查方向足够用了。坑四以为是泄漏其实是正常高并发。有一次看到 fd 数量到 3 万多以为泄漏了排查半天代码没发现问题。后来看趋势图才发现业务高峰期就是 3 万多低峰期会降到几千只是限制设的 65535 快到了。最后调大限制就解决了根本不用改代码。坑五忽略了管道和 eventfd。有一次排查发现大量 fd 指向pipe:和eventfd一开始没在意后来发现是某个模块创建了大量管道做进程间通信但没正确关闭。这类 fd 不像文件和 socket 那么直观容易被忽略。6. 一套可复用的排查脚本与操作清单6.1 快速诊断脚本把下面这个脚本保存成check_fd.sh出问题时直接跑能省不少时间#!/bin/bash PID$1 if [ -z $PID ]; then echo Usage: $0 pid exit 1 fi echo Process limits cat /proc/$PID/limits | grep open files echo Current fd count ls /proc/$PID/fd | wc -l echo FD type distribution ls -l /proc/$PID/fd 2/dev/null | awk {print $NF} | sed s/[0-9]*$// | sort | uniq -c | sort -rn | head -10 echo Top file targets ls -l /proc/$PID/fd 2/dev/null | awk {print $NF} | grep -v socket: | grep -v pipe: | sort | uniq -c | sort -rn | head -10 echo Socket states ss -tnp 2/dev/null | grep pid$PID | awk {print $1} | sort | uniq -c echo System-wide fd cat /proc/sys/fs/file-nr这个脚本会输出进程限制、当前 fd 数量、fd 类型分布、占用最多的文件、socket 状态分布和系统级 fd 使用情况。基本上跑一遍就能定位到方向。6.2 操作清单遇到Too many open files按这个顺序来确认是哪个进程报错拿到 PID。跑诊断脚本看限制和实际用量。如果用量接近限制先调大限制止血。观察 fd 趋势判断是泄漏还是正常高并发。如果是泄漏用lsof或/proc/pid/fd定位泄漏的 fd 类型。根据 fd 类型排查代码文件没关、连接没释放、日志轮转问题等。修复后持续监控确认 fd 数量稳定。6.3 预防性配置模板对于新服务我通常会做这些配置systemd unit 文件[Service] LimitNOFILE65535:65535日志轮转配置/path/to/log/*.log { daily rotate 7 missingok notifempty compress delaycompress copytruncate }注意copytruncate这个选项它会在轮转时复制文件内容然后清空原文件而不是重命名。这样服务持有的 fd 不会失效避免轮转后服务还在往旧文件写。但copytruncate有个小问题复制和清空之间如果有新日志写入可能会丢失少量日志。如果日志不能丢就得用create模式配合服务重新打开日志文件。监控告警规则以 Prometheus 为例- alert: HighFdUsage expr: process_open_fds / process_max_fds 0.7 for: 5m labels: severity: warning annotations: summary: Process {{ $labels.instance }} fd usage above 70%这个规则会在 fd 使用率超过 70% 持续 5 分钟时告警给自己留出足够的处理时间。7. 最后分享几个实战心得排查Too many open files这件事技术本身不复杂难的是在压力下快速定位。我自己的经验是先止血再排查最后根治。不要一上来就想着找到根因线上服务等着恢复先把限制调大让服务跑起来然后再慢慢分析。另外趋势比绝对值更重要。fd 数量高不一定有问题但如果它一直在涨那就一定有问题。平时就要做好监控别等到报警了才开始查。还有一点不要迷信默认值。很多服务的默认 fd 限制是 1024这在现代高并发场景下根本不够用。新服务上线前先把限制调到合理值比如 65535 或者更高。这个操作成本很低但能避免很多麻烦。最后如果你排查了半天发现代码没问题限制也调了但还是报错那就要看看是不是系统级限制或者容器限制卡住了。这种“隐藏的闸门”最容易让人抓狂但一旦找到问题就迎刃而解了。
阅读完成 · 觉得有帮助?