ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Cgroup v2实战:同时限制CPU与内存的完整测试与排查指南

Cgroup v2实战:同时限制CPU与内存的完整测试与排查指南 1. 测试背景与总体目标最近在调一组跑在物理机上的大数据任务遇到一个非常现实的问题某个进程既吃CPU又吃内存单靠JVM参数或者ulimit根本管不住。比如Spark Executor你在spark-submit里设置了executor-memory它只约束堆内内存堆外、元空间、线程栈照样把宿主机内存吃穿CPU更不用说了默认情况下一个多线程进程能把机器上所有核心全部打满其他任务直接被饿死。后来我把注意力放到了Cgroup上——这套Linux内核自带的资源隔离机制它的设计初衷就是做这类“进程组级”的配额控制。这次测试的核心目的很明确在一个Cgroup里同时限制进程的CPU和内存占用验证以下几件事Cgroup v2下CPU配额和内存上限能不能同时生效二者叠加时是否互相干扰当进程尝试突破CPU配额时是直接报错还是被静默节流当进程内存占用超过memory.max时OOM Killer触发后的行为是否可控通过systemd-run创建的临时scope和手写cgroup文件哪种操作方式更适合日常运维接入。这篇文章会把整个测试从环境准备、参数计算到问题排查完整记录下来适合数据库运维、大数据平台工程师、后端开发以及所有需要给“不听话的进程”套限制的同学参考。测试不涉及任何复杂代码全部基于Linux系统自带命令你可以在自己的测试机上完整复现。2. Cgroup资源限制的整体设计与原理拆解2.1 为什么同时限制CPU和内存而不是只限制一个先说一个我踩过的坑。早先我只给一个批量导入进程限了内存没有限CPU。结果进程虽然不OOM了但GC线程和CPU密集的计算线程把整机16核全部占满业务接口响应时间从50ms飙升到3秒。反过来只限CPU不限内存的情况更危险——进程被限制到50%的CPU后原本能快速处理完的内存数据滞留时间变长内存中的中间结果越积越多即便你给JVM设置了Xmx也无法限制堆外DirectBuffer最终触发整机层面的OOM。同时限制的本质是给进程画一个“二维边界”CPU决定进程能跑多快内存决定进程能跑多远。二者缺一不可。Cgroup v2对这两类资源的控制路径是独立的CPU配额走cpu.max控制器内存上限走memory.max控制器互不依赖但叠加使用后效果是“木桶效应”——任何一个先触底进程都会被迫降速或终止。这次测试的核心诉求就是验证这种叠加效果并理清处理顺序。2.2 Cgroup v2的控制器层级结构与选择理由当前Linux发行版普遍默认使用Cgroup v2内核5.4以后基本都切过来了。Cgroup v2相比v1最大的变化是控制器不再按子系统独立挂载而是统一挂载在/sys/fs/cgroup下且CPU、内存、IO控制器通过一个层级结构统一管理。也就是说你不需要再去挂载/sys/fs/cgroup/cpu和/sys/fs/cgroup/memory两个目录所有限制都写在同一个控制组目录下天然支持“一个组内同时控制多类资源”的场景。在开始正式测试之前建议先确认当前系统挂载的是v2还是v1方法很简单mount | grep cgroup如果看到类似cgroup2 on /sys/fs/cgroup type cgroup2的输出就是v2。如果是v1部分控制器路径会有差异比如CPU配额文件的路径是cpu.cfs_quota_us而不是cpu.max。我建议直接用主流内核的v2环境测试也省得踩一些老版本兼容性坑。整个测试对系统版本要求不高Ubuntu 22.04、Debian 12、CentOS Stream 9都能跑通唯一硬性要求是内核版本大于等于5.4。使用Cgroup v2还有一个额外的好处系统d和systemd-run已经原生支持Cgroup v2资源控制参数的动态下发。你可以通过systemd-run创建一个临时scope直接在命令行指定CPUQuota和MemoryMax完全不用手写sysfs路径。这个特性在后面的实操环节会起到关键作用。2.3 核心参数cpu.max与memory.max的含义解读Cgroup v2里CPU和内存限制的参数各有各的格式不搞明白这两个文件后面的测试就是瞎跑。cpu.max文件里存放的是两段式数值$MAX $PERIOD。MAX代表在一个周期内该Cgroup内所有进程累计可以使用CPU的时间单位微秒PERIOD是固定周期也是微秒。比如我测试机上写的是100000 200000意思是每200ms为一个调度周期这个Cgroup最多跑100ms的CPU时间换算下来就是50%的CPU配额。如果你有4个核心并且想让进程用满2个核就设置200000 100000——200ms周期里最多用200ms时间相当于2核满载。如果MAX直接写max代表不限制。memory.max文件就直观得多直接写字节数比如536870912代表512MB。这个限制比JVM的堆上限要硬核得多它覆盖的是整个Cgroup内所有进程的全部内存使用包括匿名页、文件页缓存、内核对象、socket缓冲区等。也就是说即使你的进程完全由C语言直接malloc分配内存绕过JVM也逃不出memory.max的控制范围。可以插入表格参数控制目标限制粒度触顶行为v2路径cpu.maxCPU时间微秒配额/周期被节流不杀死进程cpu.maxmemory.max全部内存占用字节数OOM Killer介入默认杀掉进程memory.max3. 核心细节解析与实操要点3.1 测试环境准备内核版本检查与工具安装这次测试我用了一台4核8GB内存的虚拟机操作系统为Ubuntu 22.04 LTS内核版本5.15。这个组合能完整支持Cgroup v2的所有特性同时系统自带的systemd版本249也完全支持资源控制参数下发。在开始之前先做三轮检查# 内核版本 uname -r # 确认cgroup v2挂载 mount | grep cgroup # 确认CPU控制器已启用v2默认启用但有些云厂商镜像会关掉 cat /sys/fs/cgroup/cgroup.controllers正常输出里应该包含cpu和memory。如果memory不在controllers列表里说明当前Cgroup下memory控制器没启用需要检查系统启动参数中是否带了cgroup_no_v1all或者内核配置问题。这类问题在物理机上很少见但云服务器上偶尔会有。工具方面只需要一个压测工具stress-ng。这个工具的优势是能同时生成CPU密集型和内存密集型负载而且支持精确控制每个工作线程的行为。安装方法apt install -y stress-ng另外建议安装htop和systemd-cgtop前者用来观察进程实时CPU/内存占用后者专门监控Cgroup维度的资源使用量能直接看到每个scope和service的资源消耗。3.2 测试负载设计如何模拟一个“既吃CPU又吃内存”的进程实操中我们很难要求别人的进程配合测试所以最好自己生成一个标准的混合负载。stress-ng可以一条命令搞定stress-ng --cpu 2 --vm 1 --vm-bytes 400M --timeout 30s这个命令会生成2个CPU工作线程不断做数学运算和1个内存分配线程分配400MB并持续写入。在没有任何限制的情况下4核8G的测试机上这个进程会把2个核心的CPU完全打满同时额外占用400MB内存。如果我把内存限制设为300MB那么它必然触顶从而触发Cgroup的OOM Killer。如果我把CPU限制设为50%这个进程虽然生成了2个CPU线程但合计可用CPU时间只有半核它会明显变慢但不会退出。为了方便观察我给stress-ng进程加了一个-manifest参数让它打印每个线程的PID和状态。测试完可以把PID和Cgroup内的进程列表做对照确认限制是否精确作用到了所有子线程上。3.3 参数计算公式与初始配置的选型在写限制之前先要确定峰值参数。我的测试机是4核CPU模拟业务场景是“限制到50%”因此默认period200000微秒200ms允许使用的CPU时间200000 * 50% 100000微秒100ms所以cpu.max 100000 200000如果希望限制为2个完整核心那么cpu.max 200000 100000。这个地方要注意PERIOD不能太小低于1000微秒会不稳定也不建议太大超过1秒会让调度反应变得迟钝。200ms是一个保守且通用的值。内存上限的设定要纠结一些。300MB是一个偏小但完全可行的限制值原因是测试机上总内存8GB而stress-ng的单个vm线程只分配400MB。设300MB可以明确触发OOM事件。如果限制设置太大比如6GB整个测试要等很久才会看到效果。这里有一个关键细节设置memory.max之前最好了解一下默认的memory.high值。memory.high是软件层面的软限制超过后内核会回收内存但不会杀进程。很多发行版默认memory.high很保守它可能导致进程在还没达到memory.max之前就表现得非常卡顿误导我们的判断。测试时建议先把memory.high调成max只保留memory.max作为单一限制条件echo max /sys/fs/cgroup/test/memory.high echo 314572800 /sys/fs/cgroup/test/memory.max4. 实操过程与核心环节实现4.1 通过systemd-run创建并限制进程第一种实操方式是用systemd-run动态创建一个临时scope并把压测进程放进去。这样的好处是无需手动拼接cgroup路径systemd会帮你创建好目录并写入参数退出后自动清理。创建命令如下systemd-run --scope \ -p CPUQuota50% \ -p MemoryMax300M \ --unitcgtest \ stress-ng --cpu 2 --vm 1 --vm-bytes 400M --timeout 30s执行这条命令后systemd会在/sys/fs/cgroup/system.slice/下生成cgtest.scope目录。此时可以用systemd-cgtop实时观察资源使用情况systemd-cgtop正常情况下你会看到cgtest.scope的CPU占用率被压在50%附近内存占用在300MB左右开始挣扎。stress-ng的输出也会显示内存分配失败的日志但进程不会立即退出而是持续重试直到触发OOM退出。如果你更习惯命令行确认细节可以同时开一个终端查看cat /sys/fs/cgroup/system.slice/cgtest.scope/cpu.max cat /sys/fs/cgroup/system.slice/cgtest.scope/memory.max cat /sys/fs/cgroup/system.slice/cgtest.scope/memory.eventsmemory.events文件是排查过程中最核心的观察点里面包含low、high、max、oom、oom_kill等事件的累计计数。如果max计数在持续增长说明内存限制正在有效地给进程施压如果oom_kill计数从0变成1说明内核已经执行了杀进程动作。4.2 通过手写sysfs文件进行精确控制systemd-run方式虽然方便但有些场景下比如你要挂到已经存在的某个Cgroup下或者测试Cgroup的嵌套层级手写文件更灵活。操作步骤如下# 创建控制组目录 mkdir -p /sys/fs/cgroup/cgtest # 设置CPU配额每200ms周期内最多100ms echo 100000 200000 /sys/fs/cgroup/cgtest/cpu.max # 设置内存硬上限 echo 314572800 /sys/fs/cgroup/cgtest/memory.max # 关闭swap避免swap干扰测试结果 echo 0 /sys/fs/cgroup/cgtest/memory.swap.max # 启动一个压测进程 stress-ng --cpu 2 --vm 1 --vm-bytes 400M --timeout 30s # 获取进程PID并写入Cgroup echo $! /sys/fs/cgroup/cgtest/cgroup.procs这里最关键的是cgroup.procs文件。往里面写入PID后这个进程及其所有子线程都会被纳入cgtest这个控制组的管辖范围。注意stress-ng会派生子线程但Cgroup的继承机制会自动让子线程归属于同一个控制组所以不需要对每个线程单独写PID只需要写主进程的PID就够了。这是Cgroup v2与v1的一个明显差异v2以进程为粒度进程内的所有线程必然处于同一个Cgroup中。可以插入代码块# 观察限制效果 cat /sys/fs/cgroup/cgtest/cpu.stat cat /sys/fs/cgroup/cgtest/memory.current cat /sys/fs/cgroup/cgtest/memory.eventscpu.stat里面有两个关键字段usage_usec是总CPU使用时间nr_periods是已过去的周期数nr_throttled是被节流的周期数。如果nr_throttled/nr_periods接近0.5说明进程有接近一半的时间被限制在周期外执行50%配额生效无疑。4.3 对比测试不设置限制时的基准数据既然要做测试就应该有对照组。同样跑30秒stress-ng但这次不进任何Cgroup直接在默认Cgroup下运行stress-ng --cpu 2 --vm 1 --vm-bytes 400M --timeout 30s观察htop显示两个CPU线程分别占满2个核心内存占用400MB无任何波动。然后运行cat /sys/fs/cgroup/cpu.statnr_throttled为0因为默认的cpu.max是max。这组数据证明限制确实是由Cgroup施加的而不是stress-ng进程本身保守。对比之后我还做了一组“混合限制”的详细记录指标无限制CPU 50% 内存300MCPU总占用200%2核满载50%稳定被节流内存峰值402MB300MB左右触顶进程状态持续运行直至timeoutOOM事件后进程被杀memory.events oom_kill01stress-ng退出码01这个表格能直观看懂“同时限制”的收益所在。CPU限制让进程不再抢别人的算力内存限制让进程在到达危险水位之前被硬性推翻整个宿主机的稳定性容器性显著增强。4.4 触发OOM后的回收观察当stress-ng尝试写入超出300MB上限的内存时内核触发OOM Killer。这里有一个容易忽略的观察点OOM后Cgroup不会自动删除它的memory.current会回落到一个较低水平memory.events中的oom_kill计数增加1。如果此时重新往同一个Cgroup里写入一个正常进程这个Cgroup依旧可以正常使用。这解决了生产环境里一个常见的疑问Cgroup触顶后是否要重建目录不需要它只针对超额进程执行了SIGKILL控制组本身的配置完全保留。如果你希望OOM后进程重启并自动回到同一个Cgroup可以通过systemd的Restarton-failure机制配合scope配置实现这是另一个话题但测试中我用systemd-run创建的服务单元会默认记录退出状态方便自动化拉起。4.5 长时运行进程的可恢复性测试除了压测进程直接被OOM杀掉之外我还测了一种更贴近生产的场景进程本身内存占用在限制边缘波动偶尔触顶但不被杀死。这种场景通常发生在GC类语言中。我写了下面这段shell逻辑for i in $(seq 1 5); do echo 209715200 /sys/fs/cgroup/cgtest/memory.max # 先设200MB stress-ng --vm 1 --vm-bytes 180M --timeout 5s wait echo 314572800 /sys/fs/cgroup/cgtest/memory.max # 放宽到300MB sleep 2 done观察结果是当memory.max突然收紧到200MB但当前内存峰值在180MB时系统不会立即回收而是等到下一次触顶才产生OOM事件。这个“滞后性”对生产调度很有参考价值——不要指望限制下发后立刻回收内存给进程和你自己都留一点缓冲时间。5. 常见问题与排查技巧实录5.1 CPU限制生效但不明显先查nr_throttled和load average在实际压测中有同学反馈我在Cgroup里设了50%的CPU限制但ps显示进程CPU%还是会跳到100%多这不是没生效吗其实不是。ps查的是瞬时CPU占用率采样间隔内进程如果集中执行瞬时占用率超过配额很正常的——配额是周期内平滑限制不是瞬时冰封。正确的观察方式是看cpu.stat里的nr_throttled增量。还有一种情况CPU限制确实生效了但load average依然很高。这是因为load average统计的是R状态进程数一个被节流的进程仍然可以处于R状态只是它的执行时间被切断了。所以生产环境里不要用load average来判断Cgroup CPU限制是否生效要用cpu.stat。排查思路速查先确认cat /sys/fs/cgroup/名字/cpu.max配置正确再跑cat /sys/fs/cgroup/名字/cpu.stat连续取两次值看nr_throttled是否增长如果nr_throttled无增长但sg-tip观察CPU占用超限检查是否把进程写进了错误的cgroup.procs比如写的是父进程而非子进程。5.2 内存限制设了但进程仍然把系统内存吃光了这是我见过最多人踩的坑因为很多人只设置memory.max忽略了swap的影响。Cgroup v2的memory控制器默认允许使用swap如果memory.max等于300MB但memory.swap.max没设置进程可以在内存不足时把部分匿名页换出到swap最终表现是物理内存被控制在300MB以内但swap持续增长对整机的影响依然很大。解决路径测试和生产都显式设置echo 0 /sys/fs/cgroup/名字/memory.swap.max或者通过systemd-run的-p MemorySwapMax0参数来禁止交换。这样进程一旦触顶就会被OOM Killer处理而不是靠swap兜底。还有另一层更隐蔽的因素内存膨胀往往来自页缓存page cache。Cgroup v2默认会把文件页缓存计入memory.current如果你限制一个频繁做文件读写的进程它的内存消耗看起来会非常大甚至在进程自身堆内存很少的情况下就触顶。排查方法是查memory.stat中的file字段如果file占比超过50%说明page cache是主要消耗者这时可以考虑加上memory.swap.max限制后观察进程是否正常。5.3 手写cgroup.procs时Permission denied直接echo PID到cgroup.procs有时会报Permission denied。这个问题很典型原因是往Cgroup里写进程需要具备CAP_SYS_ADMIN能力或者目标Cgroup的cgroup.procs文件不可写没有正确设置目录属主。解决方法是把整个测试Cgroup目录的所有者改成当前用户chown -R youuser:yougroup /sys/fs/cgroup/cgtest或者直接用root操作。另外需要注意一个进程PID同时只能存在于一个Cgroup的cgroup.procs中如果你把它从A组移到B组旧Cgroup中的条目会自动消失无需手动删除。操作系统安全策略比如SELinux也可能拦下写操作测试环境下可以用setenforce 0临时关闭但生产环境不建议这样操作更稳妥的是给运行Cgroup操作的进程加对应的SELinux布尔值。5.4 动态调整限制如何影响运行中的进程这次测试中我还验证了一个生产运维必备的能力进程运行过程中动态修改Cgroup参数。操作很简单# 进程运行中把CPU配额从50%调到10% echo 20000 200000 /sys/fs/cgroup/cgtest/cpu.max # 立即生效 cat /sys/fs/cgroup/cgtest/cpu.stat结果是CPU节流水平实时改变na_throttled的增幅随之变化无需重启进程。内存限制也可以动态调整但要注意把memory.max调低到低于当前memory.current时内核不会马上回收而是在下一次内存分配时触发OOM或强制回收。如果调高memory.max进程可以继续分配内存。这个动态调整能力对云原生场景非常关键比如Kubernetes中调整Pod的内存limit底层就是通过Cgroup动态下发。理解了这个机制你排查容器内存问题时就有了底层视角。6. 经验总结与后续扩展方向6.1 同时限制的协同收益与配置顺序建议这次测试验证下来同时限制CPU和内存并不是简单地把两个参数凑在一个Cgroup里而是有着明显的协同收益。只限制CPU会让进程变慢但它咬着内存不放只限制内存会杀掉进程但它在被杀之前仍然依托全部CPU把自己推入极端状态。而当两个限制同时存在时进程会在“算力不够”和“空间不够”之间更早稳定到一个平衡点对整机的影响范围也严格限定在控制组边界内。配置顺序上我建议先配内存再配CPU原因很现实内存超限的后果是杀进程属于硬性报复CPU超限只是节流影响相对温和。先把内存边界画死再放宽CPU配额会让新接入限制的业务平滑过渡不至于一上来就被OOM Kill。6.2 从测试到生产与systemd服务的融合测试证明可行之后生产环境的最佳落地路径不是手写sysfs而是通过systemd服务单元声明资源限制。比如写一个cgtest.service[Service] ExecStart/opt/myservice/myserver CPUQuota50% MemoryMax300M MemorySwapMax0 Restarton-failure这样由systemd统一管理开机自启、日志采集、资源限制一条龙。对容器平台而言Docker底层的--cpus和--memory参数实际上就是在帮你封装Cgroup配置。理解底层原理后你再做Pod级别的limit时就能准确判断limit设多少才合理设置了为什么没生效。6.3 性能损耗评估与监控建议测试数据表明Cgroup本身带来的性能损耗可以忽略不计因为CPU限制只是调度器层面的带宽控制内存限制只是在页分配路径上多了一次计数判断。但要注意过度限制会让进程反复触发节流和回收导致运行效率急剧下降。比如CPU配额只有10%却要跑CPU密集型任务进程会长期处于饥饿状态不仅慢还可能导致大量线程上下文切换开销增加。监控层面不要只看进程自身的CPU/内存指标还要看Cgroup级别的memory.events和cpu.stat。这两个文件里累积的节流和OOM事件数量比容器状态本身更早反映隐患。我自己习惯用node_exporter自带的cgroup采集器做趋势告警超出阈值提前介入调整而不是等进程被杀或宿主机告警。6.4 我个人的实操建议与兜底方案测试做了几轮之后我总结出一条实用经验首次给一个陌生进程加Cgroup限制前先用宽松限制运行两天通过memory.peak记录它在真实业务下的峰值内存再逐步收紧。别指望一次性把限制调到合理值真实业务的内存曲线和压力测试差异非常大直接压死会酿成生产事故。如果担心Cgroup限制误杀关键进程可以在系统层做一道保险给Cgroup内的进程设置父级守护。比如通过systemd的Restarton-failure进程因OOM被杀后可以自动拉起并在启动参数中带上相同的资源限制。这样即使限制配置不合理也只是进程重启一次不会造成永久不可用。最后再提一个和本次测试无关但非常重要的经验做这类底层资源限制实验强烈建议在虚拟机或非生产容器内验证不要直接在共享的开发机上乱跑。一个OOM Kill可能不小心带走别人正在运行的进程这个风险是任何参数技巧都无法挽回的。
返回列表