直接答案:Too many open files 通常表示某个进程达到 RLIMIT_NOFILE 软限制并收到 EMFILE,也可能是系统级文件句柄接近上限。先找到报错进程,读取 /proc/PID/limits,按文件、socket、pipe、匿名 inode 分类其打开描述符,并观察数量是否持续单调增长。若业务工作集合理,再为该 systemd 服务设置经过容量评估的 LimitNOFILE;若存在泄漏,必须修应用、连接池或文件关闭逻辑,不能只把限制从 1024 改成几十万。
本文适合 Web、数据库、代理或守护进程间歇报 EMFILE、accept 失败、连接异常的场景。Shell 中的 ulimit -n 只代表当前 shell,不能直接证明 systemd 启动的服务限额;容器还可能叠加运行时限制。
本文按 2026 年 9 月可查的官方文档核对。示例命令以只读取证为主;凡涉及删除、覆盖、重启、权限、网络或数据库参数的动作,都应先确认操作对象、备份、维护窗口和带外恢复入口。不同发行版与软件版本的路径可能不同,必须先查询实际配置再改。
先用证据判断故障在哪一层
| 现象或检查点 | 它能说明什么 | 直接证据 | 不要误判为 |
|---|---|---|---|
| 单进程 fd 接近软限制 | 进程级 RLIMIT_NOFILE 是直接边界 | /proc/PID/limits 与 /proc/PID/fd 数量 | 系统 fs.file-max 已耗尽 |
| fd 数持续上升不回落 | 可能有文件或连接泄漏 | 按类型和时间采样 | 正常并发峰值 |
| 大量 ESTABLISHED/等待 socket | 连接池、客户端或下游未释放 | ss、lsof与应用指标 | 普通文件泄漏 |
| 系统 file-nr 接近上限 | 全机句柄资源紧张 | /proc/sys/fs/file-nr 与 file-max | 某服务软限制 |
| 手工启动正常、systemd启动失败 | 服务管理器限额不同 | systemctl show LimitNOFILE | 二进制版本不同 |
这张表的用法不是“看到一个关键词就改一个参数”,而是把客户端、DNS或边缘、Web 代理、应用、数据层依次分开。至少保留故障时间、完整 URL、状态码、关键响应头、同一时刻的服务日志和一份正常样本。只有时间能够对齐,才能判断两个现象是否属于同一次故障。
六步排查与修复顺序
1. 锁定进程与错误时间
从应用、Nginx 或系统日志找到具体 PID、服务名和首次报错时间。记录并发、连接数、发布版本和重启时间;重启会把 fd 计数清零,因此排障前先保存 /proc/PID/fd 数量与类型。
2. 比较软硬限制与实际使用
读取 /proc/PID/limits 和 systemd 的 LimitNOFILE,再统计当前 fd。限制值必须与正在运行的 PID 对应;旧进程不会因为编辑 unit 文件自动获得新限制,需在维护窗口重启服务后复验。
3. 按描述符类型找增长源
查看 fd 符号链接,区分普通文件、TCP/Unix socket、pipe、eventfd、inotify 和已删除文件。按分钟采样总数和主要类型;持续单调增长比故障时的一次高值更像泄漏。
4. 追到应用和连接池
将 socket 端点与应用线程、连接池、数据库和外部服务对齐,检查响应体/文件是否关闭、keepalive 是否合理、失败路径是否释放资源。CLOSE_WAIT 多常提示本地应用未正确关闭已由对端结束的连接。
5. 容量评估后再调限额
估算峰值连接、每请求文件/管道、后台任务和安全余量,同时核对内存、端口和下游容量。通过 systemd drop-in 调整服务专属 LimitNOFILE,不要只改 /etc/security/limits.conf 期待所有守护进程生效。
6. 重启与高峰双重验收
执行 daemon-reload 后在维护窗口重启目标服务,读取新 PID 的 limits,并用真实高峰或等价负载观察 fd 曲线。验收标准应包括错误消失、fd 达到平台后回落、内存和连接正常,而不是只看上限变大。
可复制的只读取证命令
以下命令中的域名、服务名、路径和时间范围要替换为自己的真实值。先执行查询命令并保存原始输出;不要把“命令能运行”误当成“业务已恢复”。
text
pid=$(systemctl show -p MainPID --value your-service); echo $pid
cat /proc/$pid/limits | grep -i 'open files'
find /proc/$pid/fd -maxdepth 1 -type l 2>/dev/null | wc -l
ls -l /proc/$pid/fd 2>/dev/null | sed -n '1,80p'
systemctl show your-service -p LimitNOFILE -p MainPID
pid=$(systemctl show -p MainPID --value your-service); echo $pid:获取 systemd 主进程 PID;把服务名替换为真实值cat /proc/$pid/limits | grep -i 'open files':读取运行进程的软硬文件描述符限制find /proc/$pid/fd -maxdepth 1 -type l 2>/dev/null | wc -l:统计当前打开描述符数量ls -l /proc/$pid/fd 2>/dev/null | sed -n '1,80p':抽样描述符指向,输出可能含敏感路径需脱敏systemctl show your-service -p LimitNOFILE -p MainPID:查看 systemd 对该服务的限制与主进程
如命令输出包含公网 IP、用户名、Cookie、令牌、数据库连接串或客户数据,分享工单前先脱敏。对比时保留时间戳、时区和命令本身,否则后来无法复现同一观察条件。
五个最容易扩大故障的误区
- 只运行 ulimit -n:它代表当前 shell,不一定代表 systemd 服务或容器进程。
- 把限制直接改成百万:每个 fd 都有内核和应用成本,还可能把压力推给数据库或网络。
- 先重启再取证:计数和泄漏类型会被清空,只剩无法验证的猜测。
- 只看 lsof 行数:线程、内存映射和输出格式会影响统计,应以
/proc/PID/fd和具体类型交叉验证。 - 调整 limits.conf 后不查新 PID:systemd 服务可能不读取 PAM limits,修改也要重启服务才作用于新进程。
变更、回滚与数据边界
修改前保存 unit、drop-in、当前进程 limits 与 fd 基线。若提高限额后内存、连接池、下游数据库或系统 file-nr 越过阈值,恢复旧 drop-in 并重启目标服务,不影响其他服务。若已确认泄漏,应先限流或滚动重启缓解,再发布修复;单实例重启要考虑会话、队列和未完成写入。
如果只能先恢复服务、还不能确认根因,应把措施标记为“临时缓解”,例如限流、摘除单节点、切回上一配置或临时绕过一层代理。恢复后仍要在受控环境复现原因,并验证同类故障不会在下一次重启、发布或高峰重新出现。
统一验收清单
- [ ] 报错进程与服务名已确认
- [ ] 软硬限制来自运行PID而非当前shell
- [ ] fd 总量和主要类型有时间序列
- [ ] 泄漏或合理峰值已有证据结论
- [ ] systemd drop-in范围仅限目标服务
- [ ] 新PID读取到预期限额
- [ ] 高峰后fd能回落
- [ ] 内存、端口与下游容量未被转移压垮
验收必须同时覆盖机器内部和真实外部路径。首页返回 200 不能证明上传、长连接、登录、数据库写入或移动端都正常;同样,控制台显示“运行中”也不能证明应用已能处理请求。至少选一个正常请求、一个边界请求和一个失败请求,记录预期与实际结果。
相关内容与隔离测试入口
建议先阅读服务器负载高但CPU不高、服务器CPU 100%、监控告警怎么做,把本问题放回完整请求链路。若生产环境不适合直接试验,可查看美国弹性云服务器的当前规格与库存,搭建与生产隔离的复现环境;测试机不能替代生产备份,价格、库存和可选配置以实时产品页为准。
常见问题
ulimit -n 显示很大,为什么服务仍报错?
当前终端和 systemd 服务可能有不同 RLIMIT。读取 /proc/服务PID/limits 才是该进程实际值。
file-max 和 nofile 有什么区别?
nofile 是进程级描述符上限,file-max 是系统级文件句柄相关上限。排障要分别看,不应混为一个数字。
大量 socket 一定是泄漏吗?
不一定。真实高并发和 keepalive 会产生大量 socket;关键是与并发、状态和时间趋势是否匹配,以及高峰后是否释放。
为什么重启后几天又复发?
泄漏会从零重新累积。重启只是缓解,需通过曲线和类型定位未释放资源。
提高 LimitNOFILE 会影响性能吗?
上限本身未必显著耗费资源,但允许更多连接会增加内存、调度和下游压力,必须做整体容量预算。