ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux资源限制详解:ulimit、cgroups、systemd资源控制

Linux资源限制详解:ulimit、cgroups、systemd资源控制

「No space left on device」但磁盘没满,「Too many open files」但进程才刚起,「Out of memory」但机器内存还有富余——这些诡异的报错,十有八九撞上了资源限制。Linux 允许多个进程共享同一台机器,为了防止它们互相拖垮,内核提供了层层资源隔离机制。这一篇把ulimit、cgroups、systemd三套资源控制手段讲清楚,从进程级到容器级,一次搞懂这些「限额」从哪来、怎么调。

一、三层资源控制的坐标系

先建立一张全景图,知道每种手段管什么:

层次手段管什么典型场景
进程级ulimit/ rlimit单进程的文件描述符、栈、进程数等调大打开文件数
系统级/etc/security/limits.conf登录用户的默认限额全局调整
组/容器级cgroups一组进程的 CPU、内存、IO、PID 总量容器、systemd
服务级systemd资源指令单个 service 的资源上限生产服务隔离

从下往上,控制粒度越来越粗、能力越来越强。日常最常打交道的是ulimit和systemd,容器运维则绕不开cgroups。

二、ulimit:进程级的软性枷锁

ulimit设置的是进程能使用的资源上限,源自内核的 rlimit 机制。它最常用的两个场景是「最大打开文件数」和「最大进程数」。

语法格式

ulimit [选项] [值]

常用参数

参数资源常见问题
-n最大打开文件描述符数Too many open files
-u最大用户进程数fork: retry: Resource temporarily unavailable
-s栈大小(KB)栈溢出
-ccore dump 大小生成 core 失败
-v虚拟内存大小内存申请失败
-l可锁定内存—
-a显示所有限制排查第一步

查看当前限制

ulimit -a

预期输出:

core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 62692 max locked memory (kbytes, -l) 65536 max memory size (kbytes, -m) unlimited open files (-n) 1024 <-- 关键 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 62692 virtual memory (kbytes, -v) unlimited

open files默认 1024,这对高并发服务(Nginx、MySQL、Java)远远不够。

临时修改(当前 shell 有效)

ulimit -n 65535 ulimit -n
65535

⚠️ 软限制与硬限制

rlimit 分两级:

级别含义谁能改
软限制(soft)实际生效的当前上限进程自己可以调,但不能超过硬限制
硬限制(hard)软限制的天花板只有 root 能提高

查看:

ulimit -Sn # 软限制 ulimit -Hn # 硬限制

普通用户可以把软限制往硬限制方向调(如ulimit -n 4096),但不能超过硬限制,也不能提高硬限制。这是为什么有时明明 root 调大了,普通用户登录后还是 1024——他的硬限制没变。

三、永久修改 ulimit

临时改只对当前会话有效,要永久生效得改配置。

方式一:/etc/security/limits.conf

sudo vim /etc/security/limits.conf

添加:

# 对 * 生效(所有用户) * soft nofile 65535 * hard nofile 65535 # 对特定用户 nginx soft nproc 65535 nginx hard nproc 65535

四个字段:用户名/组、软/硬、资源类型、值。

方式二:limits.d 目录(推荐)

sudo tee /etc/security/limits.d/99-nofile.conf <<'EOF' * soft nofile 65535 * hard nofile 65535 * soft nproc 65535 * hard nproc 65535 EOF

limits.d/下的文件按字典序加载,适合分发配置。

方式三:systemd 服务(现代系统重点)

注意!在 systemd 系统(Ubuntu 16.04+、CentOS 7+)上,通过 SSH 登录时的limits.conf对由 systemd 启动的服务不生效!因为服务不是通过登录会话启动的。要改服务的限额,得在 unit 文件里设置:

[Service] LimitNOFILE=65535 LimitNPROC=65535

然后systemctl daemon-reload && systemctl restart myservice。

⚠️ 排查「明明改了却没生效」的经典坑

  1. 改的是 limits.conf,服务由 systemd 管理→ 要用 unit 的LimitNOFILE。
  2. 改了没重启服务 / 没重新登录→ limits 在会话/进程启动时读取。
  3. nofile受/proc/sys/fs/nr_open上限限制→ 单个进程的 fd 上限不能超过nr_open(默认 1048576),设太大要同步调它。
  4. 容器里 limits 被 docker/k8s 覆盖→ 容器进程的 rlimit 由容器运行时(如--ulimit)决定。
  5. user@.service影响系统级默认→ 有时要改/etc/systemd/system/user@.service.d/里的LimitNOFILE。

四、cgroups:容器时代的资源隔离

ulimit只管单个进程,且只覆盖少数几类资源。当你要限制一组进程(比如一个容器的所有进程)的 CPU、内存、IO 时,需要cgroups(control groups)——把进程分组,对组统一限流。

cgroups 能控制什么

子系统(v1)控制
cpuCPU 时间份额
cpuacctCPU 使用统计
memory内存上限、OOM 行为
blkio块设备 IO 带宽
pids进程数上限
cpuset绑定 CPU 核与内存节点
devices设备访问控制
net_cls网络分类标记

Docker、Kubernetes 的底层资源隔离全靠 cgroups。你写docker run --cpus=1 --memory=512m,翻译成内核操作就是创建 cgroup 并写上限值。

手动体验 cgroups v1

# 挂载点(多数发行版已自动挂载) ls /sys/fs/cgroup/ # 创建一个组 sudo mkdir /sys/fs/cgroup/memory/mygroup # 限制内存 100MB echo 104857600 | sudo tee /sys/fs/cgroup/memory/mygroup/memory.limit_in_bytes # 把某进程加入该组 echo 12345 | sudo tee /sys/fs/cgroup/memory/mygroup/cgroup.procs # 查看该组内存使用 cat /sys/fs/cgroup/memory/mygroup/memory.usage_in_bytes

当组内内存超过上限,组内进程会被 OOM Killer 干掉(在 cgroup 范围内选择)。

cgroups v1 vs v2

特性v1v2
层级每个子系统独立挂载统一单层级
局限多层级难协调结构清晰
主流发行版老系统Ubuntu 22.04+、新内核默认

v2 的挂载点是统一的/sys/fs/cgroup/,文件如memory.max、cpu.max:

# v2 限制内存 echo "100000000" | sudo tee /sys/fs/cgroup/mygroup/memory.max # v2 限制 CPU(100ms 周期内最多用 20ms = 0.2 核) echo "20000 100000" | sudo tee /sys/fs/cgroup/mygroup/cpu.max

五、systemd 资源控制:生产服务首选

生产环境里,服务基本都托管给systemd。直接在 unit 文件里配资源限制,比手写 cgroups 命令干净得多,systemd 会自动帮你创建 cgroup。

常用资源指令

[Service] # CPU:最多用 1 个核(100%);也可写 200% 用两核 CPUQuota=100% # CPU 权重(相对份额,默认 100,越大越优先) CPUWeight=200 # 内存上限 MemoryMax=512M # 内存软限制(超了会尽量回收,不立刻杀) MemoryHigh=400M # 进程数上限 TasksMax=100 # 打开文件数 LimitNOFILE=65535 # IO 权重(0-10000,默认 100) IOWeight=200

完整示例:限制一个 Java 服务

[Unit] Description=My API Service After=network.target [Service] Type=simple User=app ExecStart=/usr/bin/java -jar /opt/app/api.jar Restart=on-failure RestartSec=5 # 资源限制 CPUQuota=200% # 最多用 2 核 MemoryMax=2G # 内存上限 2G MemorySwapMax=0 # 禁用 swap TasksMax=500 LimitNOFILE=65535 [Install] WantedBy=multi-user.target

应用:

sudo systemctl daemon-reload sudo systemctl restart my-api

验证限制生效

# 看服务所属 cgroup systemctl status my-api | grep CGroup # 直接读 cgroup 文件 cat /sys/fs/cgroup/system.slice/my-api.service/memory.max cat /sys/fs/cgroup/system.slice/my-api.service/cpu.max # 看实时内存用量 systemd-cgtop

systemd-cgtop像top,但按 cgroup(服务)聚合资源,是排查「哪个服务在吃资源」的利器。

六、⚠️ 常见错误

  1. 只改了ulimit忘了 systemd。这是最常见的坑,服务由 systemd 拉起时,limits.conf不生效,必须配LimitNOFILE。
  2. 改完没重启服务/重登会话。资源限制在进程启动时读取,不重启不生效。
  3. nofile超过nr_open报错。ulimit -n 2000000报Operation not permitted,先把fs.nr_open调大。
  4. 容器内调 ulimit 无效。容器进程的限额由运行时决定,容器里改不了宿主设置,要用docker run --ulimit或 k8s 的securityContext。
  5. cgroup v1/v2 混用导致命令失效。新系统是 v2,路径和文件名都变了(memory.limit_in_bytes→memory.max),照抄老教程会失败。
  6. MemoryMax设太小导致进程被杀。设了内存上限,超了就是 OOM Kill,不是慢慢变慢。要留足余量。
  7. TasksMax太小导致 fork 失败。TasksMax=100的 Java 服务如果线程数超了,会Can't fork。线程也算任务数。
  8. 忽略MemorySwapMax。默认服务可能能用 swap,想要「绝不换页」得显式设MemorySwapMax=0。

七、原理探究:从 rlimit 到 cgroup 的演进

rlimit:进程级的原始限制

rlimit是内核最古老的资源限制机制,属于进程属性。fork创建子进程时,rlimit 会继承,所以调整 shell 的 ulimit 能影响它启动的所有子进程。

它的局限很明显:

  • 只管单进程,管不了一组。
  • 覆盖的资源类型有限(fd、栈、进程数、CPU 时间等)。
  • 是「上限」不是「配额」:ulimit -n 65535只表示「最多能开到 65535」,不是「分配了 65535」。

cgroup:面向进程组的配额

cgroups 把进程分组(group),对整组做配额和计量:

cgroup = 一组进程 + 一组资源控制器 + 一套层级树

它解决了 rlimit 管不了的场景:

  • CPU 配额:一组进程总共只能用 X% 的 CPU。
  • 内存配额:一组进程内存总和不超过 Y,超了组内 OOM。
  • IO 限速:一组进程的磁盘带宽上限。

cgroup 的层级(hierarchy)是一棵树,子组继承父组限制(父组的额度由子组共享)。容器的本质 = namespace(隔离视图)+ cgroups(限制资源)(第 110 篇会展开)。

systemd:cgroup 的友好封装

systemd 直接以 cgroup 树组织所有服务:每个 service、每个 slice 都是一个 cgroup 节点。所以你配MemoryMax、CPUQuota,systemd 自动写对应 cgroup 文件。

这也解释了为什么 systemd 服务的限额必须在 unit 里配:因为服务的 cgroup 由 systemd 创建,登录会话的limits.conf根本管不到它。

三者的关系

┌──────────────────────────────────────────┐ │ 进程 P │ │ ├─ rlimit:自身能开多少 fd、多少进程 │ ← ulimit │ └─ 属于某个 cgroup │ │ ├─ memory.max / cpu.max │ ← cgroup │ └─ 由 systemd 创建和管理 │ ← systemd 指令 └──────────────────────────────────────────┘

rlimit 管「进程自己」,cgroup 管「进程组配额」,systemd 是「配额的自动化管家」。三者不冲突,是不同粒度的叠加。

八、资源限制排查速查表

# 1. 看当前 shell 的限制 ulimit -a # 2. 看某进程的实际限制 cat /proc/1234/limits # 3. 看系统的全局 fd 上限 cat /proc/sys/fs/file-max cat /proc/sys/fs/nr_open # 4. 看当前 fd 使用 cat /proc/sys/fs/file-nr # 5. 看某进程打开的 fd 数 ls /proc/1234/fd | wc -l # 6. 看服务的 cgroup 资源 systemctl status my-api | grep CGroup cat /sys/fs/cgroup/system.slice/my-api.service/memory.max # 7. 按服务聚合看资源 systemd-cgtop

找报错根因的对应关系:

报错大概率原因去哪儿调
Too many open filesnofile太小ulimit / systemd LimitNOFILE
fork: Resource temporarily unavailablenproc太小ulimit -u / TasksMax
Out of memorycgroup 内存上限MemoryMax / docker --memory
No space left on device(df 却有空间)inode 满 / fd 满df -i/ ulimit
容器进程被杀超 cgroup 限制docker --memory / k8s limits

九、收尾:资源限制的工程哲学

这一篇放在第七篇的最后,是因为它是「性能监控」的逆向工程——前面讲的是观察资源用了多少,这里讲的是规划资源能用多少。

工程上的核心原则是「隔离与配额」:

  • 多个服务共享机器,必须限制各自上限,否则一个泄漏的程序能吃光所有内存,拖垮全机。
  • 容器化时代的最大进步,就是把 cgroup 这套资源隔离能力标准化了,让「一个容器一个配额」成为默认。
  • 生产环境的最佳实践:关键服务用 systemd +MemoryMax/CPUQuota明确划界,容器用limits/requests声明资源,把「无限量」变成「有预算」。

一句话收尾:ulimit管进程、cgroup管容器、systemd管服务。理解了这三层,再遇到那些「明明有资源却报错」的怪现象,就能顺着/proc/*/limits和/sys/fs/cgroup/一路找到那个卡住你的限制值。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进