起因
一台服务器同时跑应用 + 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
踩过的坑
-
cgroup v1 vs v2:老 distro / docker 默认 v1,systemd unified
hierarchy 用 v2。混用导致某些 unit option 不生效。
systemd.unified_cgroup_hierarchy=1内核参数强制 v2。 -
MemoryMax 太严格:服务正常工作时偶尔 spike → 频繁 OOM 重启
雪崩。给 50% buffer。 -
PrivateNetwork=true 但服务要访问 DB:隔离过头,service 起不来
连不上 DB。这个选项只给完全离线的工具用。 -
CPUQuota=100%(一个核)但服务是多线程:会被严格限速到 1 核
throughput,多线程优势没了。看实际负载决定。 -
DynamicUser + 文件持久化:DynamicUser 每次启动 UID 可能不同,
服务写到/var/lib/myservice/的文件下次 UID 变了读不了。
StateDirectory=myservice让 systemd 管这个目录的权限。
登录后参与评论。