尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
用systemd与cgroups v2给AI Agent套上资源限制的笼子
1. 为什么选择 systemd cgroups v2 来给 agent 上锁1.1 agent 资源失控先别再裸跑了最近 AI agent、自动化脚本这类常驻进程越来越常见我自己就在香橙派 zero2 这类小内存板子上跑过离线语音 agent也在服务器上挂过定时任务和网络采集类的 agent。这类程序的共性是长时间运行、会派生子进程、依赖各种运行时环境而且一旦逻辑里出现死循环或者内存泄漏CPU 和内存就会像脱缰一样涨上去。更头疼的是很多 agent 还会启动一堆子进程你 kill 掉主进程子进程照样在后台偷偷吃资源。在没做任何限制的情况下一个失控的 agent 能直接把整台机器拖到 OOM连 SSH 都连不进去。我遇到过最夸张的一次某个 agent 的内存涨到 8 个 G把同机跑着的数据库直接挤崩了。从那以后我养成了一个习惯凡是常驻型 agent一律用 systemd 托管并且强制套上资源限制的“笼子”。1.2 cgroups v2 相比 v1 好在哪要限制资源光靠 systemd 还不够底层真正干活的是 cgroups。cgroups v2 是内核 4.5 开始引入、4.15 之后逐步稳定的新一代资源控制机制和老的 v1 相比最大的变化是统一了层级结构一个进程组只能属于一个 cgroup控制文件的组织方式从“每种资源一个子系统目录”变成了“统一目录下挂一堆控制文件”。这套设计彻底避免了 v1 时代多个子系统层级不一致导致的“双份记账”问题。对我们做资源限制的人来说v2 直接带来的好处有三个内存和 CPU 的统计更精确不再有 v1 那种 accounting 误差支持 cgroup.kill 这样的原子化清理操作还支持 no internal process 规则父进程不能一边在自己的 cgroup 里跑任务、一边把子进程丢到子 cgroup 里去绕开限制。所以说v2 不只是“新版本”它从底层堵住了很多 v1 时代的管理漏洞。从实践角度看现在主流发行版默认都已经在使用 cgroups v2。Ubuntu 从 21.04 开始、Debian 从 11 开始、RHEL 从 8 开始x86_64 架构上默认就是 v2。嵌入式设备如果用的是较新的主线内核一般也已经切到了 v2。如果你想确认自己的系统是不是 v2后面第 2 节会给出具体命令。1.3 systemd 和 cgroups v2 的配合逻辑systemd 从 232 版本开始支持 cgroups v2从 240 版本开始把 v2 作为推荐模式。它做的事可以理解成“翻译官”你在 unit 文件里写几行人类能读懂的配置比如 CPUQuota、MemoryMax、IOWeightsystemd 会把它们翻译成 cgroup fs 里对应的控制文件写入操作。这里有个关键点你要明白systemd 不是把 agent 直接丢进某个 cgroup 就完事了它会为每个 service unit 创建独立的 slice 路径形如/sys/fs/cgroup/system.slice/你的服务名.service。你可以在系统启动后用systemd-cgtop或者直接 cd 到/sys/fs/cgroup/system.slice/下面看到每个服务的 cgroup 目录手动查看控制文件的内容。这种“声明式配置 内核强制约束”的组合比你自己写脚本去改 cgroup 文件要可靠得多。脚本可能因为路径写错、权限不足、服务重启后丢失规则而失效但 systemd unit 文件是声明式的服务每次启动都会重新应用规则。而且它能覆盖进程的所有后代包括 agent fork 出来的子进程这是单纯在 shell 里ulimit做不到的。2. 环境确认与前置准备2.1 先确认内核和系统是否支持 cgroups v2在动笔写 unit 文件之前先花两分钟确认环境。不同的发行版、不同的内核参数直接决定了你后面写的配置能不能生效。比如一台老破小的设备内核对 cgroups v2 支持不完整你写了 MemoryMax 也没用甚至服务起都起不来。检查命令很简单# 查看 cgroup 版本v2 会输出 cgroup2 stat -fc %T /sys/fs/cgroup/ # 内核是否支持 v2 grep cgroup /proc/filesystems | grep cgroup2 # systemd 版本至少 240 systemd --version | head -1第一行的输出如果是cgroup2fs说明系统已经挂在 cgroups v2。如果是tmpfs说明还在 v1。需要说明的是现在很多系统是混合模式同时挂了 v1 的控制器和 v2 的控制器但 systemd 在 unit 文件里写的那些指令默认只作用于 v2 层级里的对应文件。如果你的系统输出是cgroup2fs但又同时存在/sys/fs/cgroup/unified/和/sys/fs/cgroup/这种双目录结构说明启动了混合模式。混合模式下 systemd 依然能用 v2 的大部分能力但个别指令比如 CPUWeight可能不生效。要想完全切纯 v2需要在内核启动参数里加systemd.unified_cgroup_hierarchy1或者cgroup_no_v1all。这个操作改完要重启前期确认越早做后面踩坑越少。2.2 systemd 版本与 unit 文件的几个关键指令确认完内核再看 systemd 版本。这个非常关键因为资源限制相关指令并不是所有版本都支持也不是所有版本的行为都一样。这里有张我在反复踩坑后整理的速查表指令作用最低 systemd 版本备注CPUQuota限制 CPU 使用率百分比232作用于整个 cgroup包含所有子进程CPUWeight控制 CPU 权重相对值232只在 cgroup 之间有竞争时生效MemoryMax内存硬上限超出触发 OOM232v2 下对应 memory.maxMemoryHigh内存软上限超出后回收压力增大232触发内存回收而非直接 OOMMemorySwapMax限制 swap 使用上限233避免 agent 疯狂写 swapIOWeight磁盘 IO 权重232相对值有竞争才生效IOReadBandwidthMax磁盘读带宽上限232在 v2 下同样有效IOWriteBandwidthMax磁盘写带宽上限232在 v2 下同样有效TasksMax限制进程/线程总数235防止 fork 炸弹式失控AllowedCPUs限制进程只能使用指定 CPU 核232用 CPU 编号列表这里我想强调一下CPUQuota和CPUWeight是有本质区别的。CPUQuota 是绝对上限比如CPUQuota50%就表示最多占半个核而 CPUWeight 是相对权重比如 A 服务设 100、B 服务设 200在 CPU 繁忙时 B 拿到的份额是 A 的两倍但 CPU 空闲时两者都能跑满。对 agent 这类容易被逻辑 bug 带飞偏的程序建议用绝对的CPUQuota而不是权重。2.3 给 agent 准备独立的运行账号和目录这一步很多人容易忽略直接在 root 下跑 agent。这是个不太好的习惯因为 resource 限制管的是“进程树的资源总量”但如果你用 root 跑万一 agent 失控排查问题时不好区分是 agent 的问题还是系统其他服务的问题而且从安全角度讲agent 一旦被攻破root 权限直接曝光。我推荐的做法单独建一个低权限账号给 agent 用。# 创建无登录权限的系统账号 sudo useradd --system --shell /usr/sbin/nologin --home /opt/agent-home agent-runner sudo mkdir -p /opt/agent-home sudo chown agent-runner:agent-runner /opt/agent-home然后在 unit 文件里指定Useragent-runner、Groupagent-runner。这样做的好处不止是安全还有一个隐藏收益cgroup 内的进程都属于同一个非 root 用户当你发现资源异常时可以用systemd-cgtop按用户维度过滤定位更利索。独立目录的意义在于日志和临时文件的管理。agent 跑起来之后如果它自己往 home 目录写缓存而这些缓存又不受 cgroup 内存限制注意磁盘占用和内存占用是两码事时间长了容易把磁盘塞满。给个独立目录至少你能定期清理不至于污染系统分区。3. 实战编写 agent 的 service unit 并限制资源3.1 最小可用 unit 文件长什么样先给一个我最常用的最小配置模板这个文件要放到/etc/systemd/system/agent-guard.service[Unit] DescriptionAI Agent Guard Service Afternetwork-online.target Wantsnetwork-online.target [Service] Typesimple Useragent-runner Groupagent-runner WorkingDirectory/opt/agent-home EnvironmentRUST_LOGinfo ExecStart/opt/agent-home/agent --config /etc/agent/config.toml Restarton-failure RestartSec5 # 资源限制 CPUQuota50% MemoryHigh300M MemoryMax400M MemorySwapMax0 TasksMax64 IOWeight20 LimitNOFILE1024 NoNewPrivilegestrue [Install] WantedBymulti-user.target先说最基本的部分Typesimple表示 ExecStart 直接启动主进程不 fork。这是绝大多数 agent 程序的标准形式如果你用的 agent 框架要求先 fork 再 daemon 化比如通过 shell 脚本启动的老程序可能需要改成Typeforking并指定PIDFile。但我强烈建议你去关掉它自带的 daemon 化特性因为 systemd 自己就擅长把进程放后台你只要前台运行就行否则 systemd 无法准确追踪主进程的 PID资源限制会跟着失灵。Restarton-failure是另一个容易被低估的配置。agent 这种长时间运行的程序崩溃是常态不管是被 cgroup 杀了、被信号杀了还是内部 panic都要让 systemd 立刻拉起来。RestartSec5 表示崩溃后等 5 秒再重启避免疯狂重启循环打满 CPU。3.2 资源限制指令配置CPU、内存、IO、任务数接下来是核心部分。我逐条讲为什么这样设关键在你实际配置时能根据自己的 agent 特性调整。CPU 限制CPUQuota50%代表最多用半个逻辑 CPU 核。如果你的机器是 8 核50% 额度轮换起来agent 实际感觉到的计算能力大概相当于 0.5 个核的持续算力。如果 agent 是 AI 推理类任务需要保证最低算力那就大胆放宽到CPUQuota300%也就是最多 3 个核但要确保宿主机有足够的富余核。这里我的建议是先让它自由跑观察极限需求再按“峰值×1.5”给出上限留一点余量又不会失控。内存限制我用了双阈值方案。MemoryHigh300M是软限制超过这个值内核会对 cgroup 内所有页面进行回收强制压缩页缓存、回收可回收内存这种过程中 agent 会变慢但不会被杀掉。MemoryMax400M是硬限制超过后内核会直接在 cgroup 内选一个进程 OOM kill。为什么要设两层因为很多 agent 是常驻会话进程直接 OOM 杀掉会导致状态丢失而高水位回收是渐进式的它只是逼着 agent “瘦身”不会打断会话。设计上就要尽量让 agent 死在“羞羞答答被回收”阶段而不是“一刀斩”阶段。MemorySwapMax0是我个人强烈建议的一条。agent 程序一旦开始疯狂 swap性能会急剧恶化而且 swap 写入会消耗大量磁盘 IO反过来拖慢磁盘、影响其他进程。很多 agent 框架为了规避 OOM会先写一堆 swap 再挂这种策略对常驻型 agent 没有任何意义。直接禁掉 swap让它要么在内存里活下去要么被 OOM kill 然后由 systemd 重启对稳定性反而是正向帮助。IO 限制IOWeight20是相对权重默认是 100。这意味着当你的机器还有其他服务在扛大量磁盘读写时agent 的 IO 会被显著压低优先保证核心业务。但如果你希望 agent 在做批量任务时不要拖垮数据库这个 20 是合理的。如果 agent 需要大量读文件、写模型缓存可以把权重提到 100。注意IOWeight只在有 IO 竞争时生效如果磁盘长期空闲agent 还是可以跑满带宽。如果你需要无条件的带宽上限用IOReadBandwidthMax和IOWriteBandwidthMax。任务数限制TasksMax64是防“进程爆炸”。agent 如果实现里频繁起协程、多线程或者通过 shell 调了一堆耗时的外部命令进程或线程数会指数级增长。64 这个数值不高不低对大多数 agent 足够了但对那些用线程池并发的 agent 可能要调大到 128 或 256。你需要先看 agent 框架的并发设计如果是「单进程 async/await」64 足够如果是「每个任务一个线程」要算一下最大并发数再加个余量。文件描述符限制LimitNOFILE1024限制打开的文件数量。agent 如果爬虫类容易打开大量 socket 和文件句柄设一个上限能避免系统文件描述符耗尽。这个值不建议设太高因为 agent 大多用协程几百个 fd 就够日常跑了。3.3 配置参数的计算与取舍思路很多人在这一步犹豫这些配额该怎么算我的经验是“从需求倒推不拍脑袋”。先明确几个问题这台机器上除了 agent 还有没有别人跑任务agent 的核心功能是计算密集还是 IO 密集它允许的响应延迟是多少它单日处理的峰值是多少举个我自己配过的案例我有一台云主机是 4 核 8G上面跑了数据库和 Web 服务还挂了一个网络采集 agent。我先观察数据库的峰值占用CPU 平均 50%内存 2.5G。那么 agent 可用的 CPU 资源就是剩下“4 - 0.5×4 ≈ 2 核”我砸了CPUQuota200%给它。内存方面系统本身就吃 1G 左右数据库预留 2.5G我留 1.5G 给系统缓存缓冲那么 agent 最多能拿到 3G 内存于是MemoryMax2500M再往下压一个软水位MemoryHigh2000M。这里有个关键原则MemoryHigh和MemoryMax的差不要太小。如果差太小触发软限制后稍微波动一下就直接触硬限制导致 OOM 杀进程差太大又起不到“提前回收”的作用。我个人习惯把差值控制在 15% 到 30% 之间。你要根据 agent 是否感知 OOM 决定如果是无状态 agent死了重启就行那直接MemoryMax小一点简单粗暴如果是有状态 agent必须跑完当前会话那MemoryHigh一定要比MemoryMax低很多提前逼迫 GC/回收/压缩让存活时间变长。还有一类典型的取舍是 CPU 配额和内核数目的关系。如果你设了AllowedCPUs0-1limits 只对 CPU 0、1 生效那么即使别的核很空闲agent 也用不上。这通常是某些嵌入式场景下的硬性需求比如要在某个核上跑实时音频处理agent 被锁在别的高性能核上。但如果只是为了防止 agent 吃满所有核用 CPUQuota 就够了不要轻易去锁 CPU 亲和性不然你在 NUMA 架构机器上反而会因为跨核迁移带来额外延迟。3.4 让配置生效daemon-reload、enable、start写完 unit 文件按顺序执行这几条命令# 1. 重载 systemd 配置让它发现新写的 unit 文件 sudo systemctl daemon-reload # 2. 设置开机自启 sudo systemctl enable agent-guard.service # 3. 启动服务 sudo systemctl start agent-guard.service # 4. 查看状态 systemctl status agent-guard.service这里有个新手很容易踩的坑改了 unit 文件之后只是systemctl restart并不会重新加载配置其实会。但再强调一次你先要daemon-reload再 restart顺序不能反。如果你的服务已经在运行而你改了资源限制参数只执行 restart 也是有效的因为 systemd 会用新配置重新创建 cgroup。不过有一种情况例外如果你的 unit 里用了${VAR}这类环境变量替换这些变量是在加载 unit 时就解析的必须 daemon-reload。启动之后不要立刻以为万事大吉一定要进状态页看两样东西Active: active (running)是第一位然后是Memory和CPU两栏的数值。比如● agent-guard.service - AI Agent Guard Service Loaded: loaded (/etc/systemd/system/agent-guard.service; enabled; vendor preset: enabled) Active: active (running) since Sat 2025-01-01 12:00:00 CST; 2h 30min ago Main PID: 12345 (agent) Memory: 120.5M (limit: 400.0M) CPU: 2h 12min看到limit: 400.0M就说明 MemoryMax 已经被写进 cgroup 了。如果这里显示(no limit)说明你的 systemd 版本不支持该指令或者 cgroup 控制器没有被正确挂载需要回到第 2.1 节排查。4. 验证限制是否真的生效4.1 用 systemctl status 和 systemd-cgtop 看实时数据unit 文件写对了不代表万事大吉你还需要确认“设了上限”和“上限真的被内核强制执行”是两码事。首先看systemctl status里的实时数据它能告诉你 systemd 自己统计的资源使用量但它用的是 CPU 时间累积和当前内存占用没法让你直观看到瞬时压力。我习惯再开一个窗口用systemd-cgtopsystemd-cgtop -d 2这个命令有点像 top但它是专门看 cgroup 维度的资源使用量。每两秒刷新一次能看到每个 service 的 CPU 使用率、内存、IO、任务数。如果 agent 的 CPU 使用率长期贴着配额上限跑说明配额设得太紧了它一直在排队等 CPU 时间片如果内存一直顶在 MemoryHigh 附近说明 agent 存在内存增长冲动软限制逼着它做回收动作。一个值得注意的点systemd-cgtop显示的 CPU 使用率是「当前 cgroup 内所有进程的综合使用率」上限就是你的配额百分比。如果你看到某个 service 显示CPU 0.0%但整体系统负载很高那是它在被 CPU 节流单位时间只跑了配额允许的时间片。这不是 bug是 qouta 在起作用。4.2 直接读 cgroup 控制文件做验证光看状态偶尔会被表象骗了最直接的办法是进 cgroup 文件系统看控制文件里的实际数值。systemd 会给每个 service 生成一个目录路径是/sys/fs/cgroup/system.slice/agent-guard.service/在这个目录下有几个关键文件# 查看内存硬限制 cat /sys/fs/cgroup/system.slice/agent-guard.service/memory.max # 查看当前内存使用量 cat /sys/fs/cgroup/system.slice/agent-guard.service/memory.current # 查看 CPU 配额单位是 microseconds per second即每秒最多多少微秒 cat /sys/fs/cgroup/system.slice/agent-guard.service/cpu.max # 查看进程/线程数量 cat /sys/fs/cgroup/system.slice/agent-guard.service/pids.max cat /sys/fs/cgroup/system.slice/agent-guard.service/pids.currentcpu.max的内容可能是50000 100000意思是在每一个 100000 微秒也就是 100ms的窗口里这个 cgroup 最多最多只能用 50000 微秒的 CPU 时间也就是半核。这跟CPUQuota50%是完全对应的。这里要提醒一下v2 的cpu.max是周期配额制不是瞬时限制所以瞬时 CPU 峰值超过 50% 是有可能的但长时间平均值不会超过。如果你的 agent 有大量突发任务瞬时跑到 70% 可能也不奇怪长期稳定就可以。pids.current如果长期贴近pids.max说明 agent 的线程/进程数在满负荷运转你要回头检查是不是有线程泄漏。另外还要重点强调一个文件cgroup.events。它里面有一个字段populated如果为 0表示 cgroup 里没有任何存活进程。有时候 agent 挂了但子进程还活着你就看到populated1。这时说明你 kill 主进程没杀干净systemd 可能会误以为服务还在跑。这也是为什么我一直强调用systemctl stop停服务而不是直接kill -9 pid因为 systemd stop 会清掉整个 cgroup 的进程树。4.3 压测 agent 观察限流表现纸上谈兵不算完建议你做一轮“暴力压测”。在测试环境里故意让 agent 干重活看看限制规则到底有没有兜住。压测方法取决于 agent 做什么如果它是打大规模请求的网络采集 agent你可以给它一个超大任务队列如果它是本地计算 agent你可以往它目录里丢大量待处理文件。压测时要重点盯着这几个现象第一memory.current达到memory.max后agent 是否被 OOM kill。如果 kill 了systemctl status会显示Status: FAILURE之类的信息同时 journal 里会有oom-kill字样。如果你不想让它被 kill就把 MemoryHigh 设低一些、MemoryMax 设高一些给内核足够的缓冲空间做回收。第二CPU 配额节流后agent 的任务吞吐量是否下降。这是最直观的指标比如平时每秒处理 100 个请求压测时最多只能处理 50 个说明它真的是被卡在 50% 配额上。第三会不会出现“进程数超限”导致的 fork 失败。当你把 TasksMax 设成 64而 agent 尝试启动第 65 个线程时程序内部会报Resource temporarily unavailable这类错误不一定导致崩溃但会导致某些任务直接失败。我压测时试过一个模拟内存泄漏的 agent每隔一段时间分配一块内存如果没设 MemoryMax它能在十几分钟内吃光所有内存设了 MemoryMax400M 后它稳定崩溃在被杀、重启、再被杀这个循环里systemd 每 5 秒拉一次虽然服务看似“永远活着”但任务实际上没推进多少。这种情况下我会调大RestartSec到 30 秒减少无意义的反复重启。5. 常见问题与排查技巧实录5.1 agent 启动失败Failed to start transient scope unit这个报错我碰到过不少次多发生在你用了 systemd-run 或者某些监控工具临时创建 scope 的时候。报错内容一般是Failed to start transient scope unit: Unit agent-guard.socket(50000) has too few PIDs.或者Operation not permitted (mount(2) failed: Operation not permitted)原因通常是权限不足或 cgroup 控制器没有被正确授权。对你的 service unit 来说首先要确认 systemd 有权限写 cgroup 树如果你是User指定了非 root 用户而运行用户没有 cgroup 写权限systemd 内部会先用 root 权限创建 cgroup再切换用户执行任务一般不会出问题。但如果 agent 程序内部自己尝试调用 cgroup 相关 api比如某些容器 runtime 或 sandbox 工具这时它已经降权到 agent-runner就会遇到 Operation not permitted。我的解决办法是要么去掉不必要的嵌套 cgroup 操作要么在 unit 里面加Delegateyes允许 agent 自己管理它的 cgroup 子树。Delegate 是双刃剑给出去之后主配额仍然生效但 agent 可以在自己的子树里自由创建子 cgroup。如果 agent 是不可信代码不建议开。5.2 内存限制设了但进程还是被 OOM 杀掉这种情况优先级最高、最需要注意。现象是unit 里写了MemoryMax4G但 agent 内存在 2G 就被 kill 了。很多人的第一反应是“systemd 设置没生效”但实际上这是 cgroups v2 的一个重要特性当 cgroup 的内存使用量逼近memory.max时OOM killer 会挑选 cgroup 内“得分最高”的进程来杀而这个得分不仅看该进程本身的内存占用还会考虑它占整个 cgroup 的比例。还有一个隐藏场景如果系统没有开启 swap或者MemorySwapMax0内核没有地方可换页那么即使memory.current还没到memory.max但系统的整体内存压力已经很大内核也会触发 OOM。所以你要先分清是被“你的内存限制”触发 OOM还是被“系统内存压力”触发 OOM。看 journaljournalctl -k -f journalctl -u agent-guard.service如果日志里有Memory cgroup out of memory: Killed process 12345 (agent)字样那就是 cgroup OOM你的 MemoryMax 设得太低了或者 agent 的实际内存膨胀速度远超你预期MemoryHigh 没有及时回收。如果日志里有Out of memory: Killed process但没提 cgroup那就是整机内存不足了。后一种情况你需要检查是不是其他 service 把内存吃光了比如日志服务、数据库等。排查完还要警惕一种常见套路swap 满了。如果你MemorySwapMax0进程宁可 OOM 也不写 swap这时你只要给 agent service 设一个小的 swap 配额它可能就不会被杀当然我前面也说了常驻型 agent 一般不建议开 swap。5.3 限制对子进程不生效怎么办agent 跑着跑着会派生很多子进程这是最常见的失控来源。systemd 的 cgroup 是进程树级别的理论上无论它 fork 出多少子进程只要是用clone派生的都在同一个 cgroup 里配额都会累积。但有一种情况例外agent 通过sudo、setuid或者某些提权手段启动了另一个用户权限的进程这个进程脱离了 systemd 的User约束吗不会。进程的归属不是根据 uid 决定的而是以 fork 时的 parent 关系为准所以哪怕子进程换了 uid它还在同一个 cgroup 里。真正导致“子进程逃逸”的是 agent 重新通过systemd-run --user或者daemonize方式启动了一个独立服务。比如 agent 内部调用了systemd-run --user --unitsidecar /path/to/script这个脚本会作为独立的 user service 启动进入另一个 cgroup绕开你为 agent 设置的所有配额。这种情况常出现在贪图方便拿 systemd-run 当进程守护工具的场景里。解决思路有两个方向一是给 agent 权限收缩禁止它调 systemd-run 这类命令可以用RestrictSUIDSGIDtrue、ProtectSystemstrict、CapabilityBoundingSet把能力限制到最低二是如果 agent 确实需要派生多个无关联的 worker你就别用一个 service unit 包所有东西了改成 template unitagent-worker.service给每个 worker 单独建 cgroup每个 worker 独立配额避免“一个 worker 失控拖垮全部”。5.4 Delegation 与嵌套容器场景的坑如果你在 Docker 容器里再跑 systemd然后又在容器里用 systemd unit 来限制 agent那问题就来了。容器内的 systemd 需要Delegateyes才能拥有完整的 cgroup 子树管理权。默认情况下Docker run 的参数里如果不加--cgroupnsprivate和相应的权限容器内 systemd 操作 cgroup 会失败。我踩过的坑还有在 host 上用 systemd 限制 docker 容器内的 agent你会发现MemoryMax写进去但容器内 agent 能看到的 memory 上限还是 host 的全部内存。因为容器有独立的内存 cgroup 命名空间host 上的 systemd 限制的是“docker 容器整个 cgroup”的上限容器内部还要再做一层配额。最省心的做法在容器外面直接写一个针对 docker.service 的 unit 配置用systemctl set-property docker.service MemoryMax2G这样从外面把所有容器总内存卡死。容器内部再各自限制两层配合。注意如果你在 host 上运行时用的是podman这类 rootless 容器cgroup v2 的处理方式又不同rootless 容器一般需要systemd --user的 cgroup manager 配合限制指令经常不生效尤其在旧版本内核上。这块比较复杂建议能少嵌套就少嵌套把 agent 直接跑在宿主机的 systemd 下是最理想的状态。5.5 其他小技巧动态调整、按时间段限流、日志处理mq 最后一个板块分享一些偏门但实用的技巧。动态调整配额。agent 跑起来之后你可能发现配额不合适但又不想重启服务打断它的任务。systemd 支持热更新systemctl set-property agent-guard.service CPUQuota80% systemctl set-property agent-guard.service MemoryMax600M这个操作不需要 daemon-reload也不中断服务它会直接写 cgroup 控制文件立即生效。用完之后我一般会顺手systemctl show agent-guard.service | grep -E CPUQuota|MemoryMax确认一下当前值。这个特性在 agent 执行长任务时非常有用比如任务高峰期你临时放宽限制任务结束后再收紧。按时间段限流。如果你的 agent 是采集类任务希望夜间少跑降低噪音或者白天高峰期让出资源给业务可以用OnCalendar配合systemctl start/stop但实际限制的是 CPU 配额把配额做成两个 unit一个是正常模式、一个是夜间节能模式然后用systemd-run --on-calendar定时切换属性。示例如下# 每天 22:00 切换成全天的节能配额 systemd-run --on-calendar*-*-* 22:00:00 \ systemctl set-property agent-guard.service CPUQuota10% MemoryHigh200M这种动态调度做下来的效果是agent 白天有充足的资源干活夜里也不会因为跑得太多影响其他定时任务。日志处理。agent 失控的时候日志往往是最难排查的资源占用源。建议在 unit 里加上StandardOutputjournal StandardErrorjournal SyslogIdentifieragent-guard然后用journalctl -u agent-guard.service看日志。千万别让 agent 把日志写到和系统分区同一个磁盘上我见过 agent 日志占满磁盘导致 SSH 登录都产生问题的场景。如果你想精细控制日志大小在/etc/systemd/journald.conf里设SystemMaxUse500M也行。还有一种做法StandardOutputfile:/var/log/agent-guard.log但要确保/var/log有独立分区或者配额否则同样存在写满磁盘风险。看门狗配合。这是最近在嵌入式设备上还挺实用的组合。如果你的 agent 程序支持 watchdog 协议向 systemd 发心跳可以在 unit 里加WatchdogSec30 Restarton-failure超过 30 秒没心跳systemd 直接把 agent 判定为卡死并杀掉重启。配合 Resource control相当于一方面控制“最多能吃多少”另一方面控制“必须保持活着多久心跳一次”这两者的结合才真正让 agent 在无人值守环境下也能长期稳定工作。不过这个功能要求 agent 程序本身实现 sd_notify 或者 systemd watchdog 协议。如果你的 agent 是现成的框架往往没这个支持那就做不了。这时退而求其次的办法用systemd-time-wait-sync或者定时器定期检查 agent 的心跳文件本质上是在 service 外再套一层监督逻辑会复杂一些。结尾这套 systemd cgroups v2 的限制方案我用在论坛里很多朋友分享过的场景上也给自己的 agent 项目做过一轮加固。实际跑下来最大的感受不是“限制有效”而是“被限制的 agent 更可预测”。agent 这类常驻程序失控不是会不会的问题而是什么时候的问题与其等它把整机干挂再上去救火不如一开始就在 unit 文件里把笼子扎好。CPU 配额、内存上限、进程数上限、IO 权重这四样下来大多数失控场景都能在萌芽阶段被按住。最后再分享一个小技巧每次改完配置后把它固化进 git 仓库比如/etc/systemd/system/agent-guard.service纳入版本管理下次新机器部署时直接拉下来就能用。毕竟这类配置写一次容易维护起来才是最费心力的版本化管理能减少很多重复劳动。
RELATED

相关推荐

工控PCBA代工厂生产有哪些工艺?SMT、DIP、AOI与X-Ray解析

工控PCBA代工厂生产有哪些工艺?SMT、DIP、AOI与X-Ray解析

工业控制器、自动化控制板、电机控制板等产品,对PCBA制造工艺通常有较高要求。一块工控PCBA可能同时包含MCU、存储器、通信芯片、QFN、BGA、连接器、继电器以及功率器件,因此生产过程并不是简单的SMT贴片。从实际制造来看,工控PCBA通常涉及SM…

📅 2026/10/9 2:52:15
基于Vibe Coding的OJ平台(三)

基于Vibe Coding的OJ平台(三)

基于Vibe Coding的OJ平台(三) 目录 基于Vibe Coding的OJ平台(三) 一、阶段五 1.1.ui-ux-pro-max-skil 1.2.登录注册页面开发 1.3.题目列表页面开发 1.4.题目详情页面开发 1.5.管理后台页面开发 1.6.提交历史页面开发 1.…

📅 2026/10/9 2:47:15
JSP+Servlet+JavaBean进销存系统实战:三层架构与库存并发控制

JSP+Servlet+JavaBean进销存系统实战:三层架构与库存并发控制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/9 2:47:15
MORE NEWS

更多资讯

📰

Agent-Reach 实战:CLI 驱动的 AI Agent 执行框架与工具调用

1. 从零认识 Agent-Reach:它到底解决什么问题第一次看到 Agent-Reach 这个名字,很多人会以为又是一个套壳的聊天机器人。实际用下来你会发现,它更像是一套给 AI Agent 装上“手脚”的中间层工具。简单说,Agent-Reach 是一个基于 C…

📰

Agent-Reach:AI Agent生产可用的关键触达能力,你了解吗?

这两年只要聊到 AI Agent,大家习惯性先比模型参数和推理能力,仿佛 prompt 调得越花,Agent 就越接近“智能”。但真正把 Agent 推上线、跑业务的人心里都清楚:模型只是大脑,Agent 能不能干活,还得看它能不能…

📰

万字长论文批量降AI:从全篇扫描到分章精修的完整流程

长文档的降AI处理,听起来像是应该放在论文写完以后再做的事,但我的实操经验正好相反:如果你写的是几万字、十几章的长论文,等到全文拼起来才发现“AI味”过重,那工作量几乎是灾难级的。我之前处理一篇五万多字的硕士论…

📰

单词拆分LeetCode 139:从动态规划到面试追问的完整拆解

LeetCode热题100刷到第82题,单词拆分(Word Break),这道题我太有印象了——去年面一家独角兽的时候被原题面过,当时只要求判断能否拆分,答完后面试官轻描淡写补了一句"那如果要求输出所有拆分方案呢&qu…

📰

栈算法核心:单调栈、表达式求值与回溯递归的实战指南

1. 先把栈的本质聊透:不只是“先进后出”栈这个数据结构,几乎所有写代码的人第一天就见过,但真正到算法题里能把它用明白的,其实不多。很多朋友问我“栈怎么刷题”,我的回答永远是:先把三个场景啃透&#x…

📰

JCache接口键不存在时get与put行为详解及避坑指南

后台总有读者在准备Java面试,问得比较多的一道"基础篇"题目就是今天要聊的:JCache(JSR-107)中 Cache 接口的 put 和 get 方法,在键不存在时到底是什么行为。题目确实只有一句话,但这句话背后牵出…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬