news 2026/10/23 21:32:00

用systemd和cgroups v2锁死agent资源:CPU、内存、IO限制实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用systemd和cgroups v2锁死agent资源:CPU、内存、IO限制实战

你有没有遇到过这种情况:明明只是部署了一个 agent 服务,结果它在高峰期把整台机器的 CPU 吃满,内存一路涨到 swap 都救不回来,最后把同机上的数据库、网关、日志采集全部拖垮。我在生产环境里踩过这个坑,而且不止一次。后来我把所有 agent 的启动方式统一收编到 systemd 下,用 cgroups v2 对 CPU、内存、IO 逐项加锁,才算真正把资源失控这件事摁死。这篇文章就把整套思路、配置细节、验证方法和排查心得完整写出来,给同样被 agent 资源问题折磨过的人一个可以直接抄作业的方案。

1. 先想清楚:agent 到底该怎么约束

1.1 你跑的 agent 属于哪一类

"agent"这个词现在确实太宽泛了。我在实际接触中至少见过三类:

  • AI agent 服务:比如跑在服务器上的多智能体协作框架、定时触发的推理任务,特点是并发高、计算密集、上下文缓存吃内存,高峰期 CPU 和内存曲线像过山车。
  • 监控采集 agent:比如负责抓指标、采集日志、做健康检查的常驻进程,特点是平时负载低,但遇到大量文件扫描或网络轮询时,会把 CPU 和 IO 突然拉高。
  • 自动化任务 agent:比如定时脚本、消息处理 worker、爬虫任务,特点是线程池容易膨胀,任务堆积时可能创建出几千个线程。

不同 agent 的资源模型不一样,但约束手段是同一套:在操作系统层面给进程组划一条清晰的资源边界。我的思路很简单——不指望 agent 自己写得足够收敛,而是假设它有失控的可能,从外部给它套上笼子。

1.2 为什么是 systemd 加 cgroups v2,而不是改 agent 代码

很多人第一反应是优化 agent 代码,比如加限流、控制并发数、调整缓存大小。这当然能做,但有几个现实问题:agent 可能是第三方开源的,你改不到核心代码;即使能改,你不可能预测到所有极端场景;而且代码里的自控逻辑一旦写错,反而引入新的 bug。

相比之下,systemd 加 cgroups v2 这套组合有几个别人比不了的优势:

  • systemd 是几乎所有现代发行版的 init 系统,天然管理服务生命周期。把 agent 写成 systemd unit,开机自启、崩溃重启、日志管理全都顺带解决。
  • 资源限制直接在 unit 配置里声明,不需要额外装工具,不需要改业务代码。
  • cgroups v2 是内核级约束,agent 内部的垃圾回收、线程池、缓存模块完全感知不到,也无法绕过。
  • 配置即代码,把 unit 文件放进 Git 仓库,环境之间迁移非常干净。

1.3 cgroups v2 相比 v1 到底改了什么

cgroups v2 不是简单换个挂载点,它有几处设计变化直接影响使用方式:

  • 统一层级树:v1 时代每个子系统一个挂载点,cpu、memory、blkio 各自为政;v2 统一挂载在 /sys/fs/cgroup,所有控制器都在同一棵树上,管理逻辑清爽很多。
  • 内部进程约束:一个 cgroup 目录要么直接包含进程,要么包含子目录,不能既跑进程又建子目录。这个约束让资源统计更干净,不会出现父子混在一起算不清账的情况。
  • 控制器合并:原来独立的 cpu 和 cpuacct 合并成 cpu 控制器,内存控制器也做了大量重构,统计口径更统一。
  • systemd 深度集成:每个 unit 服务自动落在 /sys/fs/cgroup/system.slice/xxx.service 目录下,你不需要手动创建 cgroup,改 unit 配置加参数就行。

这里面最需要注意的是"内部进程约束"。在 v2 里,你的 agent 进程如果直接跑在一个 cgroup 目录里,你就不能往它下面再挂子目录;反过来,如果有子目录,你就不能直接往里放进程。实际操作中 systemd 会帮你处理好,但如果你手动建 cgroup 调试,很容易被这个规则绊一下。

2. 动手前的地基检查:cgroup 版本与 systemd 环境

2.1 确认当前系统挂载的是 v2 还是 v1

很多教程上来就写配置,结果读者配置完发现参数没生效,十有八九是系统还跑在 cgroups v1 上。所以第一步永远是确认版本。

最简单的命令:

mount | grep cgroup

如果你看到的是:

cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

说明已经启用 cgroups v2,可以直接继续。如果你看到一堆 cgroup 挂在 /sys/fs/cgroup/cpu、/sys/fs/cgroup/memory 这种子目录,那就是 v1。

还有一种方式,看 /sys/fs/cgroup 目录里有没有 cgroup.controllers 这个文件:

cat /sys/fs/cgroup/cgroup.controllers

有输出类似 cpu memory io 这样的列表,就是 v2。这个文件在 v1 里不存在。

2.2 还在 v1 的老系统如何切换

如果你的系统还在 v1,并且内核支持 v2,可以通过内核参数切换。编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX 里加上:

systemd.unified_cgroup_hierarchy=1 cgroup_no_v1=all

然后重新生成引导配置:

# Debian/Ubuntu update-grub # CentOS/RHEL grub2-mkconfig -o /boot/grub2/grub.cfg

重启后确认挂载情况。切换前一定要评估一下:机器上是否有老程序硬编码依赖 v1 的路径,比如 /sys/fs/cgroup/memory/memory.limit_in_bytes。这类老工具切到 v2 后可能不工作,需要一并升级。

2.3 systemd 版本和 agent 服务的 unit 归属

systemd 对 cgroups v2 的完整支持在 246 版本之后才比较稳,建议至少 250 以上。查看版本:

systemd --version

另外,你要确认 agent 进程到底落在哪个 cgroup 下面:

cat /proc/<pid>/cgroup

如果输出是:

0::/system.slice/agent.service

说明它由 systemd 的 system 用户管理,资源限制可以直接通过 unit 参数生效。

如果输出是 user.slice 之类,说明 agent 是用手动 nohup 或者用户会话方式启动的。这种情况下你想用 systemd 限制,最好先把启动方式改成 systemd service,否则后面配置都是空谈。这也是我坚持"先收编再限制"的原因——运行方式不规范,限制就无从谈起。

3. 核心配置实操:从 CPU 到内存再到 IO,逐层加锁

3.1 CPU 限制:CPUQuota 与 CPUWeight 怎么选

CPU 限制有两个核心参数,很多人搞混。

  • CPUQuota:硬性配额,格式是百分数。CPUQuota=100% 代表最多使用 1 个核,CPUQuota=300% 代表最多使用 3 个核,CPUQuota=50% 代表最多使用半个核。它是真正意义上的上限,不管系统多空闲,agent 都不能超过这条线。
  • CPUWeight:相对权重,范围 1 到 10000,默认 100。它管的是"系统繁忙时大家怎么分 CPU",权重越高,竞争时分配到的份额越大。系统空闲时,权重高的进程照样能跑满。

对 agent 来说,我推荐优先用 CPUQuota。原因是防的就是它高峰时吃满 CPU,不是想让它在低负载时让位。给 AI agent 类服务我一般这样配:

CPUQuota=300% CPUAffinity=0-3

CPUQuota 是上限,CPUAffinity 是把进程绑定到指定 CPU 核心。绑定核心的好处是减少缓存抖动,尤其适合计算密集的 agent;坏处是核心不足时可能浪费算力,所以要不要绑得看机器情况。

你可能会问:那 CPUWeight 完全没用吗?也不是。如果你的机器上同时跑着多个同等重要的 agent,你希望它们大致公平地分享 CPU,而不想给某个硬上限,那 CPUWeight 更合适。实际项目里我是两者都写:CPUQuota 定死天花板,CPUWeight 控制竞争优先级。

3.2 内存限制:MemoryHigh 和 MemoryMax 的配合

内存参数里最常用的是 MemoryMax 和 MemoryHigh,含义差别很大。

MemoryMax 是硬上限,比如 MemoryMax=2G。agent 使用的内存一旦超过这个值,内核就会触发 OOM,在对应 cgroup 里挑进程杀掉。这是保命线,不能没有。

MemoryHigh 是软上限,比如 MemoryHigh=1.5G。超过之后内核不会立刻杀进程,而是加大内存回收力度,把这个 cgroup 的页缓存、可回收内存先压出来。如果 agent 自身有缓存清理或 GC 机制,它就会在接近 MemoryHigh 的地方开始自我调节。我把这一层比喻成"财务预算"——超过预算后开始节省开支,但不会直接冻结账户。

生产环境里我建议两个配合使用:

MemoryHigh=1.5G MemoryMax=2G

只设 MemoryHigh 不设 MemoryMax 的问题在于:如果 agent 存在真正的内存泄漏,回收也兜不住,最终会把宿主机内存吃光。只设 MemoryMax 不设 MemoryHigh 的问题在于:agent 会毫无预警地撞墙被 OOM,频繁重启反而更不稳定。

另外还要关注 Swap。默认情况下 cgroup 的交换空间没有单独限制,agent 可以把 swap 也吃满。可以用 MemorySwapMax 单独限制:

MemorySwapMax=512M

这里有一个比较容易踩的认知误区:cgroup 的 memory 统计是包含页缓存(page cache)的。agent 如果大量读文件,它的 memory.current 会很高,但那些页缓存是可以在内存压力下回收的。所以别一看到内存占用超过 MemoryMax 就以为泄漏了,先看 memory.events 文件里的 oom 字段是否在涨。

3.3 IO 限制:防止 agent 打满磁盘带宽

CPU 和内存是大家最先想到的,IO 往往被忽略,但 agent 打满磁盘的问题一点都不少见。典型场景是:多个 agent 实例同时启动,各自读取模型文件、写日志、写缓存目录,瞬间把磁盘 IO 拉满,整个宿主机上的服务全部卡顿。

cgroups v2 的 IO 控制通过 systemd 配置时,核心是两个方向:

IOWeight=20 IOReadBandwidthMax=/dev/sda 100M IOWriteBandwidthMax=/dev/sda 50M

IOWeight 是权重,默认 100,范围 1 到 10000。性能敏感型服务可以给到 200 以上,不重要的 agent 给 10 到 50。IOReadBandwidthMax 和 IOWriteBandwidthMax 是按设备名限制读写带宽,属于硬上限。

注意一点:设备名要写对。用 lsblk 查看实际设备名,有些云主机上是 /dev/vda,有些是 /dev/nvme0n1,别照抄我这边的 /dev/sda。

还有一点需要提醒:cgroups v2 的 IO 控制器不是在所有文件系统上都生效,而且某些内核配置下 io 控制器可能没有启用。排查方式就是看 /sys/fs/cgroup/cgroup.controllers 里有没有 io 这个关键字。没有的话,你的 IOWeight 和带宽限制参数会被 systemd 接受,但实际不会起作用。

3.4 额外防线:TasksMax 与进程数量控制

agent 用线程池时特别容易疯狂创建线程。我见过一个 Python agent 在任务积压时线程数涨到 5000 多,直接把整个机器拖死。这个用 TasksMax 就能兜底:

TasksMax=512

TasksMax 限制的是 cgroup 里的任务数,也就是线程和进程的总数。考虑到 agent 本身的线程池、异步任务、子进程,512 是个比较稳的起步值;如果实际业务并发需求高,可以放宽到 1024。

除了 TasksMax,systemd 还有一批安全加固参数,虽然不是资源限制,但对 agent 这类不可信服务非常有用。我在这类 unit 里一般会带上去:

PrivateTmp=true ProtectSystem=strict ProtectHome=true NoNewPrivileges=true

这些参数主要解决 agent 的安全问题,同时也有副带好处:防止 agent 把自己或者临时文件写到系统的不可控位置,间接避免 IO 污染。

3.5 一份可以直接抄的 agent.service 完整配置

把前面的参数合成一个完整 unit 文件。我目前在生产环境用的模板是这样的:

[Unit] Description=My AI Agent Service After=network-online.target Wants=network-online.target [Service] Type=simple User=agent-runner Group=agent-runner ExecStart=/opt/agent/run-agent Restart=on-failure RestartSec=5 TimeoutStopSec=30 # CPU CPUQuota=300% CPUWeight=20 # Memory MemoryHigh=1.5G MemoryMax=2G MemorySwapMax=512M # IO IOWeight=20 IOReadBandwidthMax=/dev/sda 100M IOWriteBandwidthMax=/dev/sda 50M # Tasks TasksMax=512 # Security PrivateTmp=true ProtectSystem=strict ProtectHome=true NoNewPrivileges=true [Install] WantedBy=multi-user.target

把这个文件放到 /etc/systemd/system/agent.service,然后执行:

systemctl daemon-reload systemctl enable --now agent

注意两个细节。一是 User=agent-runner 必须提前创建好,而且建议用独立账号跑 agent,不要直接用 root。这样即使 agent 内部出了问题,也没有权限去修改 cgroup 限制,等于把资源锁和权限锁一起上。二是 CPUAffinity 我没有写进模板,因为不是所有机器都适合绑核,如果你确定要绑定,自己加一行即可。

4. 验证、压测与动态调优

4.1 实时观测:systemd-cgtop 和 systemctl show

配置改完了,第一件事是确认它真的生效。看状态最常用的两个工具:

systemd-cgtop

它会实时显示每个 slice/service 的 CPU、内存、IO 占用,效果类似 top,但是站在 cgroup 视角汇总数据。你一眼就能看到 agent.service 当前吃了多少资源,以及它离你设置的上限还有多远。

另一个命令是查询 unit 实际生效的参数:

systemctl show agent.service -p CPUQuota -p MemoryMax -p MemoryHigh -p TasksMax -p IOWeight

输出里的值会告诉你 systemd 到底给这个服务下了什么配置。注意,如果某些参数显示为空,说明 systemd 认为这个参数不适用,或者值没有配置成功。

4.2 用压力工具验证限制是否真的生效

配置生效与否,不要光看参数,要实际压一把。我常用的压测方法是:

# 在 agent 服务里跑一段压力的临时命令 systemctl stop agent systemd-run --unit=agent-pressure-test --service-type=simple \ --property=CPUQuota=200% --property=MemoryMax=512M \ --property=IOWriteBandwidthMax="/dev/sda 10M" \ bash -c "stress --cpu 4 --timeout 60 & dd if=/dev/zero of=/tmp/testfile bs=1M count=200 oflag=direct"

当然更简单的方式是直接重启 agent,让它跑真实业务,然后盯着 systemd-cgtop 看曲线。如果 CPU 曲线精确卡在配额上,说明限制生效;如果明显超过配额,那就要排查是不是运行方式和预期不一致,或者配置没加载。

对于内存限制,可以这样验证:写一个小脚本读取大文件并把内容放进内存,观察到达上限后是否被 OOM。

4.3 读懂 cgroup 状态文件:memory.events 与 cpu.stat

这是排查资源问题最硬核的手段,也是常规教程基本不会讲的部分。systemd 创建的每个 unit 在 /sys/fs/cgroup/system.slice/agent.service/ 下都有状态文件。重点看两个:

cat /sys/fs/cgroup/system.slice/agent.service/memory.events

输出类似:

low 0 high 137 max 5 oom 2 oom_kill 1

这里的 high 表示 MemoryHigh 被触发的次数,max 表示 MemoryMax 被触发的次数,oom 和 oom_kill 表示发生过 OOM 及实际杀进程的次数。如果你的 high 增长很快,说明 MemoryHigh 设置得太紧;如果 max 有增长,说明内存已经撞硬墙了;如果 oom_kill 有增长,说明已经有进程被杀,这在生产环境是重大事故信号。

CPU 要看 cpu.stat:

cat /sys/fs/cgroup/system.slice/agent.service/cpu.stat

重点关注 usage_usec,它表示该 cgroup 累计使用 CPU 的微秒数。你可以在一个压测周期前后各读一次,差值除以时间,就能算出实际 CPU 占用百分比,用来验证 CPUQuota 是否真的拦住。

4.4 线上动态调整:systemctl set-property

资源限制不一定要停服才能改。systemd 支持热修改资源参数:

systemctl set-property agent.service MemoryMax=3G systemctl set-property agent.service CPUQuota=400%

这会即时生效,并且会把配置持久化写到 /etc/systemd/system.agent.service.d/ 下。我踩过一个小坑:用 set-property 调整之后,如果后来直接编辑主 unit 文件,两者可能产生覆盖关系,行为不好预测。所以我的建议是:主 unit 文件保存稳定基线,临时调优统一用 set-property,或者统一用 drop-in override 文件管理,避免两套配置混在一起打架。

5. 踩坑记录与排查速查表

5.1 限制不生效的五个常见原因

我排查过不少"配置了没用"的案例,最常见的原因排序如下:

  • 系统还跑在 cgroups v1 上。这是最大概率的原因,很多人直接跳过环境检查。
  • agent 不是由 systemd 启动的。如果你用 crontab 或者 nohup 起进程,systemd unit 里的所有资源参数都和它无关。
  • 修改配置后没有 daemon-reload,或者没有重启服务。systemd 资源参数在启动服务时固化到 cgroup 里,修改后只 reload 不 restart 是不行的。
  • 参数写错了位置。比如把 MemoryMax 写到了 [Unit] 段而不是 [Service] 段,systemd 解析时会忽略。
  • 被 drop-in 配置覆盖。用 systemctl cat agent.service 查看最终生效的配置,如果和预期不一致,多半是 /etc/systemd/system/agent.service.d/ 下有覆盖文件。

5.2 no internal process 约束导致的奇怪现象

cgroups v2 的层级规则比较死板:一个 cgroup 目录如果已经有进程在里面,就不能再创建子目录;如果已经有子目录,就不能直接往里面放进程。我第一次手动调试时,想给 agent 的 cgroup 下创建一个子 cgroup 来隔离子进程,结果 mkdir 直接返回 -ENOMEM。

后来才明白这是 v2 的设计约束,目的是保证资源统计没有歧义。实际使用 systemd 时,这条规则一般不会影响你;但如果你有"在一个服务里再分 cgroup"的需求,需要换思路,用 systemd-run 在现有服务的 pids 层级下再创建 scope,而不是手动去改 cgroup 目录。

5.3 agent 频繁 OOM 怎么办

如果 memory.events 里 oom_kill 一直在涨,先别急着加大 MemoryMax。正确排查顺序是:

  • 看日志:journalctl -u agent.service 会输出内核 OOM 的具体记录,确认是不是 agent 主进程被杀。
  • 看内存构成:进入 cgroup 目录执行 cat memory.stat,区分 anon(匿名页,真正的进程数据)和 file(页缓存,可回收)。如果 file 占比很高,说明不是泄漏,而是缓存占用太多,可以适当降 MemoryHigh,或者调低读取缓存行为。
  • 确认 agent 是否存在内存泄漏:连续观察 memory.current 和 memory.events 的 high 次数,如果每轮任务后内存不下降,那就是泄漏,这种情况加不加 MemoryMax 都治标不治本,该修的还得修。

5.4 agent 线程/进程数失控

TasksMax 能拦住线程数,但如果你没设这个参数,systemd 默认值是很大的。我的习惯是任何 agent unit 必须显式设置 TasksMax,不能依赖默认。

不过要提醒一点:进程数限制对你的排查工作也有影响。如果 agent 控制的子进程很多,而且你怀疑某个子进程异常,可以这样查:

systemd-cgls -u agent.service for pid in $(cat /sys/fs/cgroup/system.slice/agent.service/cgroup.procs); do echo "PID $pid: $(cat /proc/$pid/comm)" done

cgroup.procs 文件会列出这个 cgroup 里所有进程,排查线程和子进程特别方便。

5.5 排查速查表

症状排查命令常见原因
所有限制不生效mount | grep cgroup系统仍在 cgroups v1
CPU 上限未拦住cat cpu.stat 前后差值进程不属于该 cgroup
内存频繁被杀cat memory.events页面缓存占比过高或内存泄漏
IO 限制无效cat cgroup.controllersio 控制器未启用
线程数失控wc -l cgroup.procs未设置 TasksMax
配置显示生效但行为不对systemctl cat agent.service存在 drop-in 覆盖文件
手动建子目录报错mkdir 返回 ENOMEMv2 的 no internal process 约束

5.6 关于"agent 自己改配置"的防御思路

cgroups v2 的 cgroup.procs 和 cgroup.subtree_control 等文件,写入权限受严格限制。非 root 用户的 agent 无法修改自己所在 cgroup 的资源配置,所以让 agent 以独立非 root 用户运行,本身就是一道有效的资源安全锁。

如果你管理的是不可信 agent,我建议再把 cgroup 相关目录的写权限再收一收,同时配合 systemd 的 ProtectSystem=strict 和 NoNewPrivileges=true。这套组合打下来,agent 既不能改自己的资源上限,也不能通过 sudo 之类的方式提权,属于比较稳妥的防护姿势。

写在最后的经验

我从第一次被 agent 搞挂生产环境到现在,已经形成一套固定习惯:任何 agent 类服务,落地第一步就是写 systemd unit,CPU、内存、IO、任务数四项限制全部显式声明,不允许裸奔。你可能会觉得加限制会影响 agent 性能,实际情况是,一个被资源边界约束的 agent 只是不能无限膨胀,正常业务流量下它该跑多快还是多快。

另外,限制参数不是写完就完事的。我每次升级 agent 版本或调整业务规模后,都会重新看一眼 systemd-cgtop,如果 memory.events 的 high 次数持续上升,说明资源水位贴近了,提前调参数比线上 OOM 再救火舒服得多。最后分享一个小技巧:把 systemd-cgtop 的输出做成定时采集,丢到现有监控系统里画曲线,资源趋势有了数据支撑,调参时就不用靠猜,这套流程跑顺之后,agent 的资源问题基本可以不用再操心了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 15:27:42

【C++入门】编译链接模型 - 03 翻译单元才是编译器真正看到的文件

博主介绍&#xff1a;程序喵大人 35 - 资深C/C/Rust/Android/iOS客户端开发10年大厂工作经验嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手《C20高级编程》《C23高级编程》等多本书籍著译者更多原创精品文章&#xff0c;首发gzh&#xff0c;见文末&#x1f447;&#x…

作者头像 李华
网站建设 2026/10/23 6:13:59

节后高效开工:基于 Canvas 2D 与增量 AST 的流式动态图表即时渲染实践

假期归来&#xff0c;面对堆积如山的数据看板需求&#xff0c;技术团队往往需要尽快交付能够提升业务洞察效率的新特性。在生成式商业智能&#xff08;Generative BI&#xff09;与对话式分析日益普及的今天&#xff0c;大模型不仅要用文字阐述业务指标的升降&#xff0c;更被期…

作者头像 李华
网站建设 2026/10/22 13:34:37

Codex Skill 改完到底更稳了吗?用 CSV 清洗任务做一次可重复评测

Codex Skill 改完到底更稳了吗?用 CSV 清洗任务做一次可重复评测 [!NOTE] Skill 写得更长,不代表 Codex 执行更稳。本文设计一个 CSV 清洗任务,从输入夹具、输出合同、正负触发样例到双版本对照,演示如何记录成功、失败与误触发,而不是只凭一次满意回答判断改进。 事实核验…

作者头像 李华
网站建设 2026/10/14 2:11:24

面向对象基础与类的定义

知识点&#xff1a;面向对象思想、类与对象、类的定义、对象的创建与使用一、面向对象思想Java 是一门面向对象的编程语言。1.1 什么是面向对象面向对象是一种思考和编程方式&#xff1a; 把程序中的事物抽象成对象&#xff0c;让对象来完成具体功能&#xff1b; 程序由多个对象…

作者头像 李华
网站建设 2026/10/15 3:44:42

Hoppscotch自托管部署指南:从Docker到源码与云平台

Hoppscotch这个API调试工具&#xff0c;我用了快三年&#xff0c;从它还叫Postwoman那会儿就开始关注。当时吸引我的就一点&#xff1a;轻。浏览器里打开就能用&#xff0c;发请求、看响应、测WebSocket&#xff0c;完全不用像Postman那样装一个笨重的桌面客户端&#xff0c;也…

作者头像 李华