Building Burstables: CPU slicing with cgroups
构建可突发实例:使用 cgroups 进行 CPU 分片
2025年4月29日 · 阅读时长 6 分钟
Maciek Sarnowicz
贡献者
Ubicloud 是 AWS 的开源替代方案。您可以自行托管我们的软件或使用我们的托管服务,将您的云成本降低 3 倍。 一些客户告诉我们,专用 VM 的价格过于高昂,并要求降低入门门槛。 我们对此进行了研究,并提出了可突发 VM。 这些 VM 在共享 CPU 的一部分上运行,并突发到更高的 CPU 使用率水平,以支持偶尔的使用高峰。 为了实现可突发 VM,我们利用了 Linux Control Groups v2 (cgroups v2),这是一种有助于管理资源使用情况的 Linux 内核功能。 我们认为我们开源的可突发 VM 实现可能很有趣,值得撰写一番。 我们还在此过程中学到了很多关于 Linux cgroups 的知识!
构建块
Linux cgroups 以分层方式组织。 每个组充当一个“容器”,可以托管子组、进程和线程。 这些组控制特定的系统使用情况,例如 cpu、io、memory、hugetlb、pids 和 rdma。 您还可以配置 cpusets 来控制 Linux 进程的 cpu 和内存位置。 在每个组级别,您可以启用特定的_控制器_来设置资源限制。 这些资源限制适用于组的子项(其他组和进程),并且还使该控制器可用于子项的组。
例如,如果我们为名为 "webservices" 的组启用 memory 控制器,我们可以为此组设置 4GB 的内存限制。 然后,此组中的任何进程都将共享此 4GB 分配。 我们可以进一步创建子组,如 "webservices/frontend",使用 1.5GB,而 "webservices/backend" 使用 2.5GB,从而在子项之间分配父级的资源。
您可以通过虚拟文件系统或通过 Linux 的系统管理器 (systemd) 使用 cgroups。 但是,并非 cgroup 的所有功能都通过 systemd 公开。 特别是,CPU 突发设置没有。
通过虚拟文件系统的 Cgroups
您可以通过挂载在 /sys/fs/cgroup 的虚拟文件系统检查控制组层次结构和配置。 关键文件包括:
- cgroup.controllers:列出所有可用的控制器
- cgroup.subtree_control:定义为此 cgroup 及其后代选择的控制器
- cpuset.*:cpuset 控制器的配置文件
- cpu.*:CPU 控制器的配置文件
这些文件中的大多数都是可写的,允许您根据需要修改配置设置。
通过 systemd 的 Cgroups
在 systemd 世界中,控制组表现为 slices - 用于管理系统资源的逻辑单元。 默认情况下,systemd 已经将进程组织成预定义的 slices,如 system.slice 和 user.slice,从而创建了一个自然层次结构。
我们可以通过创建我们自己的自定义 slice 来扩展此组织以进行资源管理。 例如,假设我们要隔离和控制 CPU 密集型工作负载的资源:我们的 slice 定义非常简单:
# /etc/systemd/system/example.slice
[Slice]
将在我们的 slice 中运行的服务:
# /etc/systemd/system/stress.service
[Service]Slice=example.slice
ExecStart=stress-ng --cpu 1 --cpu-load 50 --cpu-load-slice 100
启动此服务后,systemd 会自动将我们的新 slice 合并到层次结构中:
CGroup: /
├─example.slice
│ └─stress.service
│ ├─1886 stress-ng --cpu 1 --cpu-load 50 --cpu-load-slice 100 │ └─1887"stress-ng-cpu [run]" ├─system.slice
│ ├─cron.service
│ │ └─637 /usr/sbin/cron -f -P ...
└─user.slice
└─user-1000.slice
├─session-1.scope
│ ├─724"sshd: ubi [priv]" ...
这种方法之所以强大,是因为它提供了对资源分配的细粒度控制。 每个 slice 都成为管理多个相关进程作为一个有凝聚力的单元的容器。 这种组织不仅仅是视觉上的 - 它提供了一个应用资源控制的框架。 我们的自定义 example.slice 现在已成为一个管理点,我们可以在其中使用各种 cgroup 控制器应用特定的资源约束。
控制器的来龙去脉
我们利用两个控制器来管理 CPU 限制并使 VM 实例能够突发到超出其分配的限制:cpuset 和 cpu。 cpuset 控制器允许设置组可以使用的 CPU 范围。 此外,将分区类型设置为 root 会创建一个专用于该组和其他子组和进程的隔离 cpu 集。 这使我们能够创建一个资源“框”来托管单个专用 VM 或一组共享 CPU 的可突发 VM,并防止这些 VM 在属于其他组的 CPU 上运行工作负载。 cpu 控制器允许设置最大 CPU 限制 (cpu.max),该限制控制分配给各个 VM 的主机 CPU 量。 当我们将多个 VM 放入一个组中时,我们可以限制每个 VM 级别的 CPU 分配,以控制组中的 CPU 共享量。 此外,我们可以为每个 VM 设置 cpu.max.burst 限制,以允许它偶尔超过常规 CPU 限制。 突发能力控制最初是由阿里巴巴工程师 Huaixin Chang 在 Linux 内核中实现的 ((source),并随着 cgroups v2 的实现而进一步发展。 内核控制突发量,并根据进程在特定限制内随时间累积的 CPU“积分”来奖励进程。 例如,如果进程的最大 CPU 限制设置为 100%,突发限制设置为额外的 100%,并且该进程平均以 70% 的速度运行,则它会累积 30% 的 CPU 积分,使其能够突发到高达 130%。 当然,这一切也受到时间的限制。 积分以 CPU 调度间隔累积和分配,这是亚秒级的。 通过我们现有的 example.slice,我们现在可以利用 cgroup 文件系统接口来应用资源约束。 让我们检查一下当前的 CPU 分配:
$ cat /sys/fs/cgroup/example.slice/cpu.max
max 100000
输出表明我们的 slice 当前在 100,000 微秒的标准配额期限内具有无限的 CPU 访问权限(以“max”表示)。 我们可以通过修改此文件轻松地限制 CPU 使用率:
$ echo "25000 100000"| sudo tee /sys/fs/cgroup/example.slice/cpu.max
通过此更改,我们仅将 25% 的 CPU 资源分配给我们的 example.slice 中的所有内容 - 包括我们的压力测试服务。 正在运行的 stress-ng 进程会立即受到影响,现在被限制为最多使用可用 CPU 资源的四分之一。 要查看我们更改的效果,我们可以检查 CPU 统计信息:
$ cat /sys/fs/cgroup/example.slice/cpu.stat
这揭示了 CPU 使用率、限制事件和突发利用率的实时指标 - 准确显示了如何在工作负载上强制执行我们的约束。
整合所有内容
结合以上所有内容,以下是我们现在如何控制主机上的虚拟机:
我们创建 slice 单元,并将一组专用的主机 CPU 分配给每个单元。 标准 VM 各自在其自己的 cgroup (slice) 中运行,并分配了一组隔离的主机 CPU。 我们在每个 VM 上按需创建这些 slice,并且它们的寿命联系在一起。 可突发 VM 放置在实例共享一组主机 CPU 的 cgroup 中。 我们将新实例放置在现有的 slice 单元中,或者在需要时创建一个新的 slice。 每个 VM 都提供最低 CPU 分配,该分配为 vCPU 限制的 50%,并且可以在共享空间中突发到高达 vCPU 限制的 100%。 因为 CPU 是共享的,所以无法保证突发能力。 同时,可突发实例被限制在它们自己的组中,并且不能干扰在同一主机上运行的标准 VM。 我们可以通过检查存储在每个 cgroup 单元级别下的 /system/fs/cgroup/ 下的虚拟文件的内容来检查每个 cgroup (slice) 的配置。 以下是我们实现中的一个示例:
cgroup.controllers: cpuset cpu memory pids
cgroup.subtree_control: cpu memory pids
cpuset.cpus: 4-5cpuset.cpus.effective: 4-5cpuset.cpus.partition: root
cpu.max: max 100000cpu.max.burst: 0
所有这些功能都由我们的控制平面协调。 它跟踪每个 cgroup 和每个 VM 的 CPU 分配,计算限制,并根据 VM 大小及其映射到主机架构的规范来分配这些限制。 某些 cgroup 设置在主机重新启动后不会保留,因此,我们确保控制平面在需要时以及在需要时重新应用它们。 此外,我们还注意了 VM 的预置时间,并确保所有这些新功能都不会增加预置时间。 我们使 cgroup 设置逻辑与 VM 预置期间执行的其他任务并行运行,并保持总的经过时间不变。
性能测试 – CPU 密集型工作负载
我们运行了一个简单的压力测试,模拟了 CPU 密集型和突发的工作负载,以查看如何使用突发能力。 为此,我们使用了 stress-ng 实用程序。
stress-ng --cpu 2 --cpu-load 60 --cpu-load-slice 100 --timeout 60s
我们在以下服务器集上运行 stress-ng:
- 一个 standard-2 实例,用于观察它在相对“不受约束”的实例上的行为。 请注意,工作负载并未完全使用 standard-2 实例
- 一个 burstable-2 实例,代表低密度场景,其中没有其他工作负载在相邻实例上运行
- 多个 burstable-2 实例,代表高密度场景,其中共享一组 CPU 的所有实例都以相同的负载运行
- burstable-1 VM 的类似的单实例和多实例运行。
工作负载的大小经过调整,使其充分利用可突发实例的全部容量,但因为它不是恒定负载,所以它允许突发能力的一些影响。
我们可以观察到以下情况:
- 当 burstable-2 实例在没有太多相邻工作负载的情况下运行时,它们可以利用突发能力,因为它们有扩展空间。 我们观察到此工作负载大约提高了 30%。
- 相反,当共享 CPU 集中的所有实例都完全加载时,没有突发空间,并且 VM 无法获得相同的结果。
- 当工作负载对于实例的大小来说太大时,例如当我们在 burstable-1 实例上运行 2-cpu 工作负载时,没有累积突发积分的空间。 因此,我们不使用突发能力(图表上的最后两列)。
然后,我们仅在 burstable-1 实例上运行 1-cpu 工作负载:
stress-ng --cpu 1 --cpu-load 60 --cpu-load-slice 100 --timeout 60s
在这里我们可以看到,当工作负载的大小正确调整到服务器大小时,并且没有相邻实例占用共享 CPU,则工作负载有突发的空间,并且提高了大约 30%。
下一步
结论
我们从构建可突发实例中获得了四个结论。 首先,对于可以在 vCPU 的一部分上运行的工作负载,可突发 VM 提供了一种经济高效的解决方案。 示例包括小型网站、SaaS 服务、开发和测试环境。 例如,一个简单的容错 Web 应用程序至少需要负载均衡器后面的两个 VM,并由托管的 Postgres 数据库提供支持。 在引入可突发 VM 之前,在 Ubicloud 上使用专用 8GB 实例的该设置的总成本为每月 100 美元(0.14 美元/小时)。 使用可突发实例,成本降至每月不到 25 美元(0.035 美元/小时)。 其次,我们的实验表明,一旦将可突发实例正确调整到工作负载,您将在资源高峰期间获得大约 30% 的突发容量。 第三,可悲的是,cgroups v2 将此突发容量限制为微间隔 - 并且突发积分不会在几分钟或几小时内累积。 在更长的时间段内聚合和应用突发积分需要我们收集统计信息并在 cgroups v2 实现之外应用突发积分。 我们尚未将这些更改纳入我们的实现中。 最后,即使存在这个小小的警告,我们发现 cgroups v2 也可以在 VM 之间提供强大的资源管理。 VM 在需要时超出其基线分配,同时在工作负载组之间保持强大的隔离。 自云的早期以来,Linux 和开源虚拟化解决方案已经走了很长一段路。 随着我们改进我们的开源云,我们致力于透明地分享我们的实施细节。 如果您有任何问题或意见,请随时发送电子邮件至 support@ubicloud.com。