ulimit 的值从来不是越大越好,也不是越小越安全。它的核心在于精确匹配你的业务场景。对于高并发 Web 服务器,首要矛盾是打开文件数;对于数据库或 Java 应用,内存限制是生死线;而对于运行不受信代码的评测系统,CPU 时间限制是唯一防线。设定错了,轻则服务降级,重则系统因 OOM 或资源耗尽而彻底卡死。下面直接切入具体的设定逻辑和数值参考。

理解 ulimit 的两个维度:硬限制与软限制

在动手修改之前,必须分清软限制(Soft Limit)和硬限制(Hard Limit)。软限制是当前生效的值,普通用户可以在 0 到硬限制之间自由上调;硬限制是软限制的天花板,只有 root 用户才能调高。这种设计允许应用程序在启动脚本中临时提升资源上限,而不需要永久赋予过高权限。生产环境中,你应当在 /etc/security/limits.conf 中同时指定 soft 和 hard,且通常设为相同值,避免混淆。如果只设硬限制而不调软限制,应用默认仍受低值约束。

nofile:打开文件描述符数的生死线

现代 Linux 服务器上,几乎所有东西都是文件——网络 socket、管道、事件轮询句柄,甚至 inotify 监控。默认的 1024 在并发达到数百时就会耗尽,表现是日志里反复出现 “Too many open files”,然后服务拒绝新连接。对于 Nginx、Apache、HAProxy 这类反向代理,建议直接设为 65535 或更高。计算方式是:每个 worker 进程的连接数乘以 worker 数量,再乘以 2 到 4 的安全系数。例如 4 个 worker,每个处理 8192 个连接,至少需要 65536。对于 Redis、MySQL 等数据库,除了客户端连接,还要算上内部临时表和日志文件,建议不低于 100000。注意这个值不能超过 /proc/sys/fs/nr_open 和 /proc/sys/fs/file-max 的约束,如果发现无法生效,需要先调整内核参数。

nproc:最大进程数与线程池陷阱

nproc 限制的是单个用户能创建的最大进程数,但很多运维忽略了一个事实:Linux 线程也是进程,clone 调用产生的轻量级进程同样计入这个配额。Java 应用的线程池、Python 的多线程 Worker、甚至 Go 的 goroutine 在系统层面都表现为任务。一旦 nproc 设得太低,JVM 启动时就会报 “java.lang.OutOfMemoryError: unable to create new native thread”,但此时物理内存其实还有大量剩余。对于运行 Java 或容器化微服务的用户,建议设为 65535 甚至 unlimited。对于多用户共享的开发机或跳板机,可以设为 4096 到 16384,防止某个用户误操作 fork 炸弹把机器拖死。

memlock:锁定物理内存,避免 swap 颠簸

高性能数据系统如 Elasticsearch、Redis、Apache Flink 严重依赖 mlockall 系统调用,将热数据锁定在物理内存中,防止内核将其换出到磁盘。如果 memlock 设为默认的 64KB,这些应用启动时会直接抛出警告并拒绝锁定内存,导致延迟抖动剧烈。建议为这类应用单独设置 unlimited 或一个足够大的字节数,例如 134217728(128MB)起步,数据量大时直接 unlimited。但务必同时预留足够物理内存给操作系统页面缓存,否则系统会因无内存可用而触发 OOM Killer。

stack:栈空间与深度递归的风险

默认栈大小通常是 8MB,对于普通 Web 服务足够,但对于使用深度递归或分配大数组的科学计算程序,可能瞬间耗尽虚拟地址空间。如果 ulimit -s 设得太小,程序会收到 SIGSEGV 而崩溃。反过来,在运行大量线程的场景下,每个线程都会分配独立栈,8MB 默认值乘以数千线程会吃掉大量虚拟内存。对于高并发微服务,可以主动将栈缩小到 1024KB 或 512KB,前提是确认应用没有深递归。这个值一般保持默认即可,除非你明确知道自己在做什么。

core:控制 core dump 文件,兼顾调试与磁盘空间

core 限制决定了程序崩溃时能生成多大的 core 文件。设为 0 会禁止生成,这在注重磁盘空间的生产环境很常见,但代价是失去排查罕见崩溃的机会。更合理的做法是设为 unlimited,同时配合 /proc/sys/kernel/core_pattern 将 core 文件集中存放到专用分区,并配合 systemd-tmpfiles 或 logrotate 做定期清理。如果磁盘空间紧张,可以设一个具体值如 1073741824(1GB),足以保留大部分堆栈信息而不撑满根分区。

生产环境推荐值速查

以下数值经过大规模集群验证,可直接套用,但请根据实际内存和业务做微调。

* soft nofile 65535
* hard nofile 65535
* soft nproc 65535
* hard nproc 65535
* soft memlock unlimited
* hard memlock unlimited
* soft core unlimited
* hard core unlimited

对于数据库专用服务器,nofile 建议提升至 100000 以上,并同步调整内核参数:

echo "fs.file-max = 200000" >> /etc/sysctl.conf
echo "fs.nr_open = 200000" >> /etc/sysctl.conf
sysctl -p

对于运行容器的主机,注意 ulimit 的继承关系。Docker 容器默认继承 Docker daemon 的限制,但可以在 docker run 时通过 --ulimit 单独指定,Kubernetes 则需要通过 kubelet 的 --max-pods 和 pod 级别的 securityContext 配合控制。不要在容器内盲目设 unlimited,而是根据 Pod 的资源限制反向推算。

如何验证 ulimit 是否真正生效

很多运维修改 /etc/security/limits.conf 后重启服务发现没变化,原因通常是忽略了 PAM 模块的加载。对于 SSH 登录,确保 /etc/pam.d/sshd 包含 session required pam_limits.so;对于通过 systemd 管理的服务,limits.conf 的设置完全无效,必须在 unit 文件中用 LimitNOFILE=、LimitNPROC= 等指令显式声明。验证方法不是看 ulimit -a 的输出,而是查看运行中进程的实际限制:cat /proc/<PID>/limits,这里显示的才是内核真正施加给该进程的值。

动态调整与监控

线上服务出现资源紧张时,可以用 prlimit 命令对指定 PID 动态调整,无需重启进程。例如给一个正在运行的 Nginx worker 紧急提升文件描述符:prlimit --pid 1234 --nofile=65535:65535。但这只是临时止血,持久化必须修改启动脚本或 systemd 配置。监控层面,在 Prometheus 中采集 node_exporter 的 node_filefd_allocated 和 node_filefd_maximum 指标,当已分配接近上限时提前告警,比半夜接到 “too many open files” 的报警电话要从容得多。

常见误区与纠正

第一个误区是认为 ulimit 可以替代 cgroup。ulimit 是进程级别的资源边界,cgroup 是层级化的资源控制与统计,二者是互补关系,不能互相替代。在容器化场景,应当用 cgroup 做内存和 CPU 的硬隔离,用 ulimit 控制 fd 数量和 core 文件。第二个误区是把所有限制都设为 unlimited。memlock 设为 unlimited 后,一个内存泄漏的进程可以锁死所有物理内存,导致内核无法释放页面,系统直接僵死。第三个误区是忽略 systemd 的 Limit 指令,导致配置看似正确实则未生效。第四个误区是在 64 位系统上过度担心虚拟地址空间,栈和 memlock 的虚拟地址消耗几乎不影响物理内存,真正需要警惕的是 RSS 和实际分配。

面向未来的自适应方案

静态配置已经无法适应弹性扩缩容的云原生环境。更先进的实践是将 ulimit 的设定嵌入到 CI/CD 流水线和初始化脚本中,根据实例规格动态计算。例如在 AWS EC2 的 UserData 中,用 nproc=$(nproc) 和 mem=$(free -b | awk '/^Mem/ {print $2}') 获取实际资源,然后按比例设定 nofile 和 nproc。对于 Kubernetes,可以开发 Mutating Admission Webhook,根据 Pod 的 annotation 自动注入合适的 limits 配置。这样既能避免小规格实例资源浪费,也能防止大规格实例因默认值过低而成为瓶颈。