Systemd Cgroups 资源限制与隔离完全指南
使用 systemd cgroups、切片和单元属性来限制 CPU、内存和 I/O,无需编辑原始 cgroup 文件。
Systemd Cgroups 资源限制与隔离完全指南
Systemd 已经将服务放入 Linux 控制组中。您无需手动创建原始 cgroup 目录,即可防止批处理工作进程耗尽整个机器。在许多情况下,您只需向服务或切片添加几个属性,重新加载 systemd,即可获得 CPU、内存、任务和 I/O 控制,这些控制会在重启后保留,并显示在常规的 systemctl 工具中。
关键在于选择合适的限制类型。硬性内存上限可以保护主机,但如果设置过低,可能会杀死服务。CPU 权重在系统繁忙之前是温和的。CPU 配额是严格的,但可能会增加延迟。I/O 限制取决于存储堆栈和 cgroup 版本。资源控制不是一个复选框;它是一种操作权衡。
理解控制组 (cgroups)
在深入 systemd 的实现之前,掌握 cgroups 的基本概念至关重要。Cgroups 是 Linux 内核中的一种层次化机制,允许您将进程分组,然后为这些组分配资源管理策略。这些策略可以包括:
- CPU:限制 CPU 时间,优先分配 CPU 访问。
- 内存:设置内存使用限制,防止内存不足 (OOM) 情况。
- I/O:限制磁盘读写操作。
- 网络:可以通过 Linux 流量控制和相关工具进行网络控制,但 systemd 内置的单元属性主要关注 CPU、内存、进程数、设备访问和块 I/O。
- 设备访问:控制对特定设备的访问。
内核通过虚拟文件系统暴露 cgroup 配置,通常挂载在 /sys/fs/cgroup。每个控制器(例如 cpu、memory)都有自己的目录,在这些目录中,目录层次结构代表组及其关联的资源限制。
Systemd 的 Cgroup 管理架构
Systemd 通过提供一个结构化的单元管理系统,抽象了直接 cgroup 操作的复杂性。它将进程组织成单元的层次结构,然后映射到 cgroup 层次结构。与资源管理相关的主要单元类型有:
- 切片 (Slices):这些是服务单元的抽象容器。切片形成层次结构,允许委派资源。例如,用户会话的切片可能包含单个应用程序的切片。Systemd 会自动为系统服务、用户会话和虚拟机/容器创建切片。
- 范围 (Scopes):这些通常用于临时或动态创建的进程组,通常与用户会话或未作为完整服务单元管理的系统服务相关联。它们是瞬态的,只要其中的进程在运行,它们就存在。
- 服务 (Services):这些是管理守护进程和应用程序的基本单元。当服务单元启动时,systemd 会将其进程放入 cgroup 层次结构中,通常在一个切片内。资源限制可以直接应用于服务单元。
Systemd 的默认层次结构通常如下所示:
-.slice (根切片)
|- system.slice
| |- <service_name>.service
| |- another-service.service
| ...
|- user.slice
| |- user-1000.slice
| | |- session-c1.scope
| | | |- <application>.service (如果由用户启动)
| | | ...
| | ...
| ...
|- machine.slice (用于虚拟机/容器)
...
使用 Systemd 单元文件应用资源限制
Systemd 允许您直接在 .service、.slice 或 .scope 单元文件中指定 cgroup 资源限制。这些指令分别放在 [Service]、[Slice] 或 [Scope] 部分下。
CPU 限制
CPU 资源控制的主要指令有:
CPUQuota=:限制单元可以使用的总 CPU 时间。指定为百分比(例如,50%表示半个 CPU 核心)或 CPU 核心的分数(例如,0.5)。也可以指定每个周期的微秒数。默认周期为 100 毫秒。CPUWeight=:在 cgroup v2 系统上设置 CPU 时间的相对权重。当存在争用时,权重较高的单元会获得更大的份额,但在机器空闲时不会保留 CPU。CPUShares=:较旧的 cgroup v1 时代权重。在现代发行版上,除非您确定需要 v1 兼容性,否则首选CPUWeight=。CPUQuotaPeriodSec=:设置CPUQuota的周期。默认为100ms。
示例:将 Web 服务器限制为一个 CPU 核心的 75%:
创建或编辑一个服务文件,例如 /etc/systemd/system/mywebapp.service:
[Unit]
Description=我的 Web 应用程序
[Service]
ExecStart=/usr/bin/mywebapp
User=webappuser
Group=webappgroup
# 限制为一个 CPU 核心的 75%
CPUQuota=75%
[Install]
WantedBy=multi-user.target
创建或修改服务文件后,重新加载 systemd 守护进程并重启服务:
sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service
内存限制
内存限制由以下指令控制:
MemoryMax=:设置单元进程可以消耗的内存的硬性限制。可以以字节为单位指定,或使用后缀如K、M、G、T(例如512M)。MemoryLimit=:某些系统上为了兼容性而保留的旧拼写。在现代 systemd 版本上,首选MemoryMax=。MemoryHigh=:设置软限制。当接近此限制时,内存回收(交换)会更积极地触发,但硬限制尚未强制执行。MemorySwapMax=:限制单元可以使用的交换空间量。
示例:将数据库限制为 2GB 内存:
创建或编辑一个服务文件,例如 /etc/systemd/system/mydb.service:
[Unit]
Description=我的数据库服务
[Service]
ExecStart=/usr/bin/mydb
User=dbuser
Group=dbgroup
# 将内存限制为 2 GB
MemoryMax=2G
[Install]
WantedBy=multi-user.target
重新加载并重启:
sudo systemctl daemon-reload
sudo systemctl restart mydb.service
I/O 限制
I/O 限制可以使用以下指令控制:
IOWeight=:设置 I/O 操作的相对权重。值越高,I/O 优先级越高。范围为 1 到 1000(默认 500)。IOReadBandwidthMax=:限制读取 I/O 带宽。指定为[<device>] <bytes_per_second>。例如,IOReadBandwidthMax=/dev/sda 100M将/dev/sda上的读取操作限制为 100MB/s。IOWriteBandwidthMax=:限制写入 I/O 带宽。格式与IOReadBandwidthMax类似。
示例:将后台处理服务限制为特定磁盘上的 50MB/s:
创建或编辑一个服务文件,例如 /etc/systemd/system/batchproc.service:
[Unit]
Description=批处理服务
[Service]
ExecStart=/usr/bin/batchproc
User=batchuser
Group=batchgroup
# 将 /dev/sdb 上的写入操作限制为 50MB/s
IOWriteBandwidthMax=/dev/sdb 50M
# 给予中等读取优先级
IOWeight=200
[Install]
WantedBy=multi-user.target
重新加载并重启:
sudo systemctl daemon-reload
sudo systemctl restart batchproc.service
管理和监控 Cgroups
Systemd 提供了检查和管理与单元关联的 cgroups 的工具。
检查 Cgroup 状态
systemctl status 命令提供有关单元的 cgroup 成员身份和资源使用情况的信息。
systemctl status mywebapp.service
查找指示 cgroup 路径的行。例如:
● mywebapp.service - 我的 Web 应用程序
Loaded: loaded (/etc/systemd/system/mywebapp.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2023-10-27 10:00:00 UTC; 1 day ago
Docs: man:mywebapp(8)
Main PID: 12345 (mywebapp)
Tasks: 5 (limit: 4915)
Memory: 15.5M
CPU: 2h 30m 15s
CGroup: /system.slice/mywebapp.service
└─12345 /usr/bin/mywebapp
您也可以直接检查 cgroup 文件系统:
systemd-cgls # 显示由 systemd 管理的 cgroup 层次结构
systemd-cgtop # 类似于 top,但用于 cgroups
要查看应用于服务 cgroup 的特定限制:
# 在典型的 cgroup v2 主机上查看内存限制
cat /sys/fs/cgroup/system.slice/mywebapp.service/memory.max
# 查看 CPU 限制
cat /sys/fs/cgroup/system.slice/mywebapp.service/cpu.max
确切的路径和文件名因 cgroup 版本和发行版而异。在 cgroup v1 系统上,特定于控制器的路径(例如 /sys/fs/cgroup/memory/...)可能仍然存在。在 cgroup v2 系统上,/sys/fs/cgroup/... 下的统一层次结构是正常视图。
动态修改 Cgroup 限制
虽然最佳实践是在单元文件中设置限制,但您可以使用 systemctl set-property 临时调整它们:
sudo systemctl set-property mywebapp.service CPUQuota=50%
根据 systemd 版本和标志,set-property 可能会在 /etc/systemd/system.control/ 下写入一个 drop-in 文件以保留属性。使用 systemctl cat mywebapp.service 和 systemctl show mywebapp.service -p CPUQuota -p MemoryMax 来确认发生了什么。对于基础设施即代码和同行评审,显式的单元 drop-in 通常更清晰。
用于资源委派的切片
切片对于管理服务或应用程序组非常有用。您可以在切片上定义资源限制,并且该切片内的所有服务或范围都将继承或受这些限制约束。
示例:为资源密集型批处理作业创建专用切片:
创建一个切片文件,例如 /etc/systemd/system/batch.slice:
[Unit]
Description=批处理切片
[Slice]
# 将此切片中所有作业的总 CPU 限制为 1 个核心
CPUQuota=100%
# 将总内存限制为 4GB
MemoryMax=4G
现在,您可以使用 .service 文件中的 Slice= 指令将服务配置为在此切片内运行:
[Unit]
Description=特定批处理作业
[Service]
ExecStart=/usr/bin/mybatchjob
# 将此服务放入 batch.slice
Slice=batch.slice
[Install]
WantedBy=multi-user.target
重新加载 systemd,如有必要启用/启动切片(尽管它通常是隐式激活的),然后启动服务。
sudo systemctl daemon-reload
sudo systemctl start mybatchjob.service
这种方法允许您对相关进程进行分组并管理它们的集体资源消耗。
最佳实践和注意事项
- 从增量限制开始:设置限制时,从保守值开始,然后根据需要逐渐增加。激进的限制可能会破坏应用程序的稳定性。
- 监控:定期监控系统的资源使用情况以及 cgroup 设置的影响。
systemd-cgtop、htop、top和iotop等工具非常有用。 - 理解 Cgroup v1 与 v2:Systemd 同时支持 cgroup v1 和 v2。虽然许多指令相似,但 v2 提供了统一的层次结构和一些行为差异。如果遇到复杂问题,请确保您知道您的系统正在使用哪个版本。
- 优先级与硬限制:在资源稀缺时使用
CPUWeight进行优先级排序,使用CPUQuota进行严格限制。类似地,MemoryHigh用于在达到硬限制之前施加压力,而MemoryMax是硬限制。 - 服务与切片:对单个应用程序使用服务单元,对管理相关应用程序组或资源池使用切片。
- 文档:清晰记录应用于关键服务的资源限制,尤其是在生产环境中。
- OOM 杀手:请注意,如果进程超过其
MemoryMax限制,内核的 OOM 杀手可能会终止它,即使它在 cgroup 内。Systemd 可以使用OOMPolicy=等指令管理 OOM 杀手对特定 cgroup 的行为。
更安全的推出限制方式
从观察开始。在添加限制之前,查看服务在正常负载和预期最差负载下的行为:
systemctl status mywebapp.service
systemd-cgtop
systemctl show mywebapp.service -p MemoryCurrent -p CPUUsageNSec -p TasksCurrent
对于内存,一个好的第一步通常是 MemoryHigh= 而不是 MemoryMax=:
[Service]
MemoryHigh=1G
MemoryMax=1536M
MemoryHigh= 告诉内核在服务达到硬上限之前施加压力。MemoryMax= 是墙壁。如果进程越过它并且无法回收内存,内核可能会杀死 cgroup 中的一个进程。对于失控的工作进程来说,这可能是您想要的,但对于数据库来说,除非您计划好了,否则这是一个糟糕的意外。
对于 CPU,决定您想要公平性还是硬性限制:
[Service]
CPUWeight=50
这会降低争用下的优先级,但仍然允许服务使用空闲 CPU。对于后台作业,这通常比配额更好。
[Service]
CPUQuota=200%
这将服务限制在大约两个 CPU 核心的时间。这对于嘈杂的批处理处理器很有用,但如果工作线程在流量高峰期间受到限制,它可能会损害延迟敏感的应用程序。
对于进程爆炸,添加任务限制:
[Service]
TasksMax=200
这可以保护主机免受意外的 fork 风暴。将其设置为足够高以容纳正常的线程数。Java、数据库和类似浏览器的工作负载可能使用比您预期更多的任务。
使用 Drop-In 文件而不是编辑供应商单元
避免编辑 /usr/lib/systemd/system/ 或 /lib/systemd/system/ 下由软件包提供的单元文件。使用 drop-in 文件:
sudo systemctl edit mywebapp.service
然后添加:
[Service]
MemoryHigh=1G
MemoryMax=1536M
CPUWeight=80
保存后:
sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service
systemctl cat mywebapp.service
systemctl cat 会同时显示供应商单元和您的覆盖。这使得未来的调试更加容易,因为活动配置在一个命令中可见。
用于团队、租户和工作负载类别的切片
当您不再一次只考虑一个服务时,切片就变得有用。假设一台主机运行 API、一个报告生成器和几个导入工作进程。您可能不关心哪个导入工作进程使用 CPU,但您确实关心所有导入工作一起不能饿死 API。
创建一个切片:
# /etc/systemd/system/import.slice
[Unit]
Description=导入和回填工作负载
[Slice]
CPUWeight=30
MemoryHigh=4G
MemoryMax=5G
将导入服务放入其中:
[Service]
Slice=import.slice
ExecStart=/usr/local/bin/import-worker
现在该组具有共享压力。这比为每个工作进程设置单独的硬性上限并希望有人在添加新工作进程后数学仍然有效更清晰。
有一个命名细节会让人困惑:切片名称编码层次结构。customer-a.slice 是一个顶级切片。customer-a-batch.slice 不是 customer-a.slice 的子级;它只是另一个顶级名称。层次化切片以特定方式使用破折号作为分隔符,因此在设计大型切片树之前,请阅读 systemd.slice(5)。
资源限制无法解决的问题
Cgroups 可以阻止一个工作负载压垮主机,但它们无法使一个配置不足的机器变快。如果数据库需要的内存超过您允许的工作集,它可能会花费更多时间回收内存或在负载下失败。如果 API 需要短响应时间,严格的 CPU 配额可能会创建看起来像随机延迟的限制延迟。如果存储设备已经饱和,I/O 权重可能会提高公平性,但不会创造吞吐量。
将限制视为护栏。将它们与应用程序级别的设置配对:数据库缓冲区大小、工作进程数、队列并发性、JVM 堆限制、Go GOMEMLIMIT、Node 内存标志或您的运行时提供的任何设置。最好的设置通常是两者兼而有之:应用程序知道自己的内存和并发模型,而 systemd 在该模型崩溃时保护机器的其余部分。
要记住的心智模型
对单个守护进程使用服务级别限制。对一组相关的工作负载使用切片级别限制。当您希望在争用下获得优先级时使用权重。当您需要严格的边界并准备好承担后果时,使用配额和硬性内存上限。使用 systemctl show 验证有效属性,使用 systemd-cgtop 观察行为,并将配置保存在您的团队可以审查的 drop-in 文件或单元文件中。