用 cgroup v2 / systemd slice 限制服务的 CPU / 内存

起因

一台服务器同时跑应用 + cron 任务 + 数据库。某个 cron 任务(数据
处理脚本)偶尔吃 90% CPU + 8GB 内存,把主应用挤到 OOM kill。
"给 cron 任务限定最大资源"是基础隔离。

cgroup(control group)是 Linux 内核功能,systemd 在 v2 之后用得很直接。

解决方案

1. 系统当前 cgroup 状态

# 看 cgroup 树
systemd-cgls

# 看每个 unit 的资源占用
systemd-cgtop

# 当前 cgroup 版本(v2 推荐,现代 distro 默认)
cat /sys/fs/cgroup/cgroup.controllers
# memory cpu io pids ...

2. 给单个 service 限资源

# /etc/systemd/system/data-pipeline.service
[Service]
ExecStart=/usr/local/bin/process-data.sh

# CPU:最多用 2 个核(200% = 2 * 100%)
CPUQuota=200%
CPUWeight=50         # 相对优先级(默认 100,调低让出 CPU)

# 内存:硬上限 4GB,超过 OOM kill 这个 service(不影响别的)
MemoryMax=4G
MemoryHigh=3.5G      # 软上限,超过后系统 throttle 这个进程的分配

# IO:限速 50MB/s 读 + 50MB/s 写(针对某块磁盘)
IOReadBandwidthMax=/dev/sda 50M
IOWriteBandwidthMax=/dev/sda 50M

# tasks(线程 / 进程数)
TasksMax=64

sudo systemctl daemon-reload && sudo systemctl restart data-pipeline

之后这个 service 超过 4GB 内存就被 OOM killed(service 重启),
不会影响系统其它进程。

3. 实时调整(不重启 service)

sudo systemctl set-property data-pipeline.service \
  MemoryMax=2G CPUQuota=150%

# 校验
systemctl show data-pipeline.service -p MemoryMax,CPUQuota

set-property 立刻生效 + 写到 /etc/systemd/system.control/,重启
保留。

4. 把多个 service 归一组:slice

# /etc/systemd/system/background.slice
[Unit]
Description=Background batch jobs

[Slice]
CPUWeight=10              # 后台任务低优先级
CPUQuota=400%             # 整组合计最多 4 核
MemoryMax=8G              # 整组合计最多 8GB
IOWeight=10
# data-pipeline.service
[Service]
Slice=background.slice
# 不需要再单独设 CPUQuota / MemoryMax,由 slice 限制

# log-cleaner.service
[Service]
Slice=background.slice

# image-resizer.service
[Service]
Slice=background.slice

background.slice 整体限到 4 核 + 8GB,里面 3 个 service 自动竞争分配。
主应用(不在这个 slice 里)不受影响。

5. 给用户 / 会话限资源

/etc/systemd/system/user-1001.slice(用户 ID 1001 的所有进程):

[Slice]
CPUQuota=200%
MemoryMax=4G

或编辑用户的:

sudo systemctl edit user-1001.slice

防"某个用户跑了一个内存炸弹把整机器 OOM"。

6. 临时跑一个命令带限制

systemd-run --scope --slice=background.slice -p MemoryMax=1G -p CPUQuota=50% \
  ./bigjob.sh

不需要建 service unit,临时挂在 background.slice 下跑。

7. 看 cgroup 实际占用

# 某个 service 当前 RAM
cat /sys/fs/cgroup/system.slice/data-pipeline.service/memory.current
# 上限
cat /sys/fs/cgroup/system.slice/data-pipeline.service/memory.max

# CPU 时间累计
cat /sys/fs/cgroup/system.slice/data-pipeline.service/cpu.stat

systemctl status data-pipeline 也会显示 Tasks / Memory / CPU。

或更直观:

systemd-cgtop -d 1
# 实时刷新每 cgroup 资源占用

8. 监控 OOM kill

# 查 dmesg 里的 oom_kill
sudo dmesg | grep -i 'killed process'

# journal 里看是哪个 cgroup 触发 OOM
sudo journalctl -k --since '1 day ago' | grep oom

MemoryMax 触发的 OOM 只 kill 那个 cgroup 里的进程,不影响其它。
进程死了 systemd 按 Restart=on-failure 重启它(或留死状态等告警)。

几个常用 hardening 选项

# 文件系统
ProtectSystem=strict          # / 全只读
ProtectHome=true              # /home 不可见
ReadWritePaths=/var/log/myapp # 例外可写

# 内核能力
NoNewPrivileges=true          # exec 后不能升 cap
PrivateTmp=true               # /tmp 私有
PrivateDevices=true           # /dev 极简
PrivateNetwork=true           # 网络命名空间隔离(注意:不能访问外网)

# user 隔离
User=trio
Group=trio
DynamicUser=true              # 动态分配 UID(最安全,但要求 service 真不需要持久 user)

# 资源
LimitNOFILE=65536             # 文件描述符
TasksMax=1024

systemd-analyze security <unit> 给每个 service 评一个 0-10 分的
安全分,按提示加 hardening。

效果

  • 后台 batch job 不再挤死主应用
  • 一个 cron 任务跑飞了只 OOM 自己 cgroup,systemd 重启它
  • 多个低优先级服务自动让 CPU 给业务
  • 资源审计:每个 service / slice 用了多少一目了然

与 Docker / K8s 对比

容器(docker / k8s)其实就是 cgroup + namespace + 镜像分发。
单机服务用 systemd cgroup 已经够,不需要为了"资源隔离"就上 Docker。

# 等价物
docker run --cpus 2 --memory 4g ...
# vs
systemctl set-property myservice.service CPUQuota=200% MemoryMax=4G

踩过的坑

  1. cgroup v1 vs v2:老 distro / docker 默认 v1,systemd unified
    hierarchy 用 v2。混用导致某些 unit option 不生效。
    systemd.unified_cgroup_hierarchy=1 内核参数强制 v2。

  2. MemoryMax 太严格:服务正常工作时偶尔 spike → 频繁 OOM 重启
    雪崩。给 50% buffer。

  3. PrivateNetwork=true 但服务要访问 DB:隔离过头,service 起不来
    连不上 DB。这个选项只给完全离线的工具用。

  4. CPUQuota=100%(一个核)但服务是多线程:会被严格限速到 1 核
    throughput,多线程优势没了。看实际负载决定。

  5. DynamicUser + 文件持久化:DynamicUser 每次启动 UID 可能不同,
    服务写到 /var/lib/myservice/ 的文件下次 UID 变了读不了。
    StateDirectory=myservice 让 systemd 管这个目录的权限。

精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。