ulimit 与 systemd LimitNOFILE 实战指南

Linux ulimit 与 systemd LimitNOFILE 调优示意

本文教你在 systemd 时代把 ulimit、limits.conf 与 LimitNOFILE 三套机制理清楚,覆盖文件句柄数、进程数与栈大小这三类高频限制。读完你会拿到三套机制的优先级关系图、Nginx 与 MySQL 调优模板、以及句柄不足与 fork 失败的快速排查清单。

一、三套机制的关系与生效路径

Linux 的资源限制分两层:硬性限制是上限,普通用户只能降不能升;软性限制是当前生效值,可在硬限制之内随时调。配置入口至少三处:交互 shell 的 ulimit 命令、PAM 模块 pam_limits 读取的 /etc/security/limits.conf、以及 systemd 单元文件里的 LimitNOFILE 与 LimitNPROC 等指令。

为什么很多人改了 limits.conf 仍然报句柄不足?关键是限制在哪个进程层级被设置:systemd 启动的服务不走 PAM 也不读 limits.conf,必须在 unit 文件里写 Limit 项;sshd 登录的交互 shell 走 PAM 读 limits.conf,两条路径互不影响,是新手最常踩的坑。云服务器官方镜像对系统范围限制有不同默认值,相关选型可参考 云服务器选购指南

二、ulimit 与 limits.conf 实战

交互式登录的常用配置写在 /etc/security/limits.conf,按格式 域 类型 项 值 给所有用户或 root 用户分别设 soft 与 hard 的 nofile 与 nproc,比如普通用户 nofile soft 65535 hard 1048576、nproc 都设 65535,root 用户 nofile 两者都设 1048576。

注意 nproc 还有 /etc/security/limits.d/20-nproc.conf 这种切片文件,按文件名顺序加载、后者覆盖前者。改完用新登录的 ssh 会话验证 ulimit -nulimit -u,如果数字没变检查 /etc/pam.d/sshd 是否包含 pam_limits 行,部分发行版默认没启用。

容器内的限制不受宿主机 limits.conf 影响,而是由容器运行时设置,需要在 docker run 里加 --ulimit 参数,或在 Kubernetes pod spec 通过 securityContext 间接控制。Web 服务器配置相关可参考 美国 VPS 部署教程

三、systemd 单元文件的 Limit*

systemd 启动的服务在 unit 文件 [Service] 段里用 Limit* 指令,比 limits.conf 优先级高且专门生效:写 LimitNOFILE 设句柄上限、LimitNPROC 设进程数、LimitSTACK 与 LimitMEMLOCK 设栈与锁内存。

不要直接编辑发行版自带的 unit 文件,用 systemctl edit 创建 drop-in,写完执行 systemctl daemon-reload && systemctl restart。验证用 cat /proc/<pid>/limits 看 Max open files 与 Max processes 两行。

systemd 较新版本默认 LimitNOFILE 软限制只有 1024,不少老镜像没显式改过,应用进程一启动就只有 1024 个句柄可用,遇到中等并发就会报错。所以无论 limits.conf 怎么改,systemd 服务都要在 unit 文件里再设一遍,不能依赖系统默认值。

四、应用层的最后一道关卡

把进程的硬限制撑到位后,部分应用还会主动调低软限制。例如 Nginx 在多 worker 时每个 worker 会按 worker_rlimit_nofile 主动设置自己的句柄上限;MySQL 通过 my.cnf 的 open_files_limit 设置,会主动调用 setrlimit 把句柄上限改成期望值。

Nginx 在主配置里加 worker_rlimit_nofile 65535;、events 块里 worker_connections 16384;;MySQL 在 my.cnf 的 [mysqld] 段加 open_files_limit = 65535

Java 应用一般不主动调,但 JVM 启动时如果 hard limit 太低,部分 NIO 库会在创建大量 channel 时报错。容易忽视的是 Apache 与 Tomcat 也都有自己的限制项要逐一过一遍。回源代理与边缘节点的句柄消耗特别大,相关性能调优可看 W3 Total Cache 调优实战

五、Too many open files 的快速排查

报错出现时按顺序排查:先看进程当前限制(cat /proc/<pid>/limits),再看实际打开数(ls /proc/<pid>/fd | wc -l),用 lsof 看具体打开了哪些,最后看系统总句柄使用与上限(sysctl fs.file-nr 与 fs.file-max)。

如果进程上限就是 1024 而实际用了 1020,肯定是 systemd 没改;如果上限六万实际也用了六万,要么真高并发要么有句柄泄漏,用 lsof 看是不是同一类资源反复打开。Java 应用尤其要注意,没关闭的流或 socket 会一直占用句柄,借助 jcmd 或 jconsole 看 OpenFileDescriptorCount 趋势。更多服务器维护见 WordPress 分类

六、内核层全局限制

进程级限制之上还有一层系统全局限制:fs.file-max 是整个系统能打开的文件总数,现代内核按内存自动算一般无需调;fs.nr_open 是单进程 hard limit 上限,要把 LimitNOFILE 设更大必须先调它;kernel.pid_max 是进程号上限新内核可调到四百万级;vm.max_map_count 是单进程能 mmap 的内存映射数,Elasticsearch 与 ClickHouse 这类应用要调到 262144。

写到 sysctl.d 把上述参数批量提到合适数量级,这套全局限制基本是一次性配置,配好后多年不动。

七、常见误区与排查实战

整理一组生产里真实遇到的误区。第一,limits.conf 给 root 用户 nofile 改到一百万但实测仍是 1024,原因是 sshd 的 systemd unit 文件 LimitNOFILE 限到 1024,会话继承上层服务的限制,修复方法是改 sshd.service 的 drop-in。

第二,Docker 容器内 ulimit 看起来很高但实际句柄打开几千就报错,原因是宿主机 fs.nr_open 没调够,要先调宿主机 sysctl 才能让容器 nofile 真正生效。第三,Java 进程报”Too many open files”但 lsof 看只有几千个,原因是 JVM 用 epoll 创建大量 selector 每个占一个 fd,用 jcmd 查 OpenFileDescriptorCount 才能看到全貌。第四,cron 任务报句柄不足但同样命令在 shell 里跑没问题,原因是 cron 不走 PAM 也不读 limits.conf,解决方法是脚本开头显式 ulimit 或改用 systemd timer。

八、压力测试与日常巡检

调完限制后必须做一次压力测试验证。推荐工具组合:用 ab 或 wrk 压 HTTP 服务到目标 QPS,同时用 watch 监控 /proc/<pid>/limits 实际使用;用 stress-ng 的 fork 模式制造大量 fork 看 nproc 限制是否到位;用 prlimit 实时查看与修改运行中进程的 limit 不必重启。

日常巡检脚本建议每天跑一次:扫描所有 systemd 服务的 LimitNOFILE 与 LimitNPROC 对比阈值;记录关键进程的 fd 使用率超过 80% 提前告警;按周看 EMFILE 错误的累计趋势。这套巡检对线上系统帮助很大,避免上线半年后突然出问题的尴尬。

总结与建议

ulimit 三套机制的关键是先认清生效路径再下手:交互登录走 PAM,systemd 服务走 unit 文件,容器走运行时参数。建议按本文顺序落地:先调 sysctl 全局上限,再配 limits.conf,最后给每个 systemd 服务的 drop-in 文件写 Limit 项。多机部署可把这套配置纳入 Ansible 标准角色,新机器初始化一次性下发。事前一次性把基线配好能避免后续无数次熬夜排查。

发表评论