avatar🌌
TangentTangent Blog

技术博客 | Go & Kubernetes & Linux

技术博客 | Go & Kubernetes & Linux

runc 源码解读(四):从 SPEC.md 看 Intel RDT 微架构资源隔离

前言 ​

在容器技术中,Cgroups 经常被用来限制 CPU 和内存资源。但是,即使分配了固定的 CPU 核心,不同容器之间仍然可能互相干扰——比如由于共享 L3 缓存和内存总线,一个 CPU 密集型或内存带宽消耗型的“邻居”(Noisy Neighbor)会导致其他容器的性能严重下降。

为了解决更底层的微架构资源隔离问题,runc 的 SPEC.md 中引入了对 Intel RDT 的支持。本文将按照文档顺序,为你解析这一硬核特性。

什么是 Intel RDT? ​

SPEC.md 在该章节开篇明确指出:

Intel platforms with new Xeon CPU support Resource Director Technology (RDT). Cache Allocation Technology (CAT) and Memory Bandwidth Allocation (MBA) are two sub-features of RDT.

在较新的 Intel Xeon 平台上,硬件提供了资源调配技术(RDT, Resource Director Technology)。在 runc 的语境下,主要用到了它的两个子特性:

  1. CAT(Cache Allocation Technology):L3 缓存分配技术。
  2. MBA(Memory Bandwidth Allocation):内存带宽分配技术。

CAT:划分 L3 缓存 ​

Cache Allocation Technology (CAT) provides a way for the software to restrict cache allocation to a defined 'subset' of L3 cache which may be overlapping with other 'subsets'. The different subsets are identified by class of service (CLOS) and each CLOS has a capacity bitmask (CBM).

硬件背景:传统的 CPU 隔离只做到了核心级别(Core),但 L3 缓存通常是多个核心甚至整个 CPU 插槽共享的。CAT 允许软件通过服务类别(CLOS, Class of Service)来将 L3 缓存划分为多个子集。每个 CLOS 都对应一个容量位掩码(CBM, Capacity Bitmask),例如 0x7FF。通过设置不同的位掩码,可以控制特定容器只能使用某几路 L3 缓存,从而防止缓存被其他容器“洗掉”。

MBA:限制内存带宽 ​

Memory Bandwidth Allocation (MBA) provides indirect and approximate throttle over memory bandwidth for the software. A user controls the resource by indicating the percentage of maximum memory bandwidth or memory bandwidth limit in MBps unit…

如果说 CAT 是管理缓存“容量”,那么 MBA 就是管理内存“流量”。它为软件提供了一种间接且近似的内存带宽节流机制。你可以通过指定最大内存带宽的百分比(如 20%),或者直接指定 MB/s 的上限,来防止某个容器占用所有的内存总线带宽。

resctrl 文件系统接口 ​

那么在 Linux 系统中,如何操作硬件上的 Intel RDT 呢?SPEC.md 指出了内核的交互方式:

In Linux 4.10 kernel or newer, the interface is defined and exposed via "resource control" filesystem, which is a "cgroup-like" interface. Comparing with cgroups, it has similar process management lifecycle and interfaces in a container. But unlike cgroups’ hierarchy, it has single level filesystem layout.

Linux 内核在 4.10 和 4.12 中分别引入了 CAT 和 MBA,并暴露了一个名为 resctrl 的虚拟文件系统。它的操作逻辑和 Cgroups 非常相似(也是通过向特定文件写入 PID 来管理进程),但有一个显著区别:resctrl 是扁平的单层结构,不支持像 Cgroups 那样嵌套的树状层级。

SPEC.md 展示了其经典的目录树结构:

text
mount -t resctrl resctrl /sys/fs/resctrl
tree /sys/fs/resctrl
/sys/fs/resctrl/
|-- info
|   |-- L3
|   |   |-- cbm_mask
|   |   |-- min_cbm_bits
|   |   |-- num_closids
|   |-- MB
|       |-- bandwidth_gran
|       |-- delay_linear
...
|-- schemata
|-- tasks
|-- <container_id>
    |-- ...
    |-- schemata
    |-- tasks
  • info/:包含当前 CPU 支持的 RDT 特性信息,例如 L3 掩码最大长度、最大支持的 CLOS 数量等。
  • schemata:用于定义当前组的资源分配规则(比如 L3 掩码是多少,内存带宽百分比是多少)。
  • tasks:包含受此规则约束的进程 ID(PID)。创建 <container_id> 目录就相当于创建了一个新的 CLOS 隔离组。

OCI 配置映射 ​

为了让 runc 能够利用上述特性,OCI 的 config.json 提供了 intelRdt 配置字段,SPEC.md 给出了如下示例:

json
"intelRdt": {
    "closID": "guaranteed_group",
    "l3CacheSchema": "L3:0=7f0;1=1f",
    "memBwSchema": "MB:0=20;1=70"
}
  • closID:在 /sys/fs/resctrl 下创建的目录名称。
  • l3CacheSchema:指定每个 Socket 上的 L3 缓存掩码(例如 Node 0 分配 7f0,Node 1 分配 1f)。
  • memBwSchema:指定每个 Socket 上的内存带宽比例(例如 Node 0 限制为 20%,Node 1 限制为 70%)。

当容器启动时,runc 会负责在 resctrl 文件系统中创建相应的 closID 目录,将 schema 规则写入,并把容器的主进程 PID 填入 tasks 文件中。

实验与动手 Demo ​

为了加深理解,我们可以脱离 runc,直接在宿主机上模拟容器的 RDT 资源限制过程。

注意:此实验需要物理机(或支持 RDT 虚拟化的虚拟机)搭载支持 RDT 的 Intel Xeon CPU,且 Linux 内核 >= 4.12。

1. 挂载 resctrl 文件系统 ​

首先,检查系统是否支持并挂载 resctrl:

bash
$ sudo mount -t resctrl resctrl /sys/fs/resctrl

$ cat /sys/fs/resctrl/info/L3/cbm_mask
fffff

(注:输出 fffff 表示 L3 缓存掩码为 20 位,即硬件将 L3 缓存划分为 20 个可分配的子块。)

2. 创建隔离组并分配资源 ​

我们要模拟 config.json 中的隔离需求,创建一个名为 demo_container 的组:

bash
$ sudo mkdir /sys/fs/resctrl/demo_container

$ echo "L3:0=f" | sudo tee /sys/fs/resctrl/demo_container/schemata
L3:0=f

$ echo "MB:0=30" | sudo tee -a /sys/fs/resctrl/demo_container/schemata
MB:0=30

3. 将进程放入隔离组 ​

现在我们启动一个内存/缓存密集型的测试程序(例如 stress-ng),并将其 PID 加入隔离组:

bash
$ stress-ng --cpu 2 --cache 2 &
[1] 12345

$ echo 12345 | sudo tee /sys/fs/resctrl/demo_container/tasks
12345

4. 监测 RDT 限制的实际效果 ​

资源限制生效后,我们如何验证 stress-ng 确实受到了 L3 缓存和内存带宽的压制呢?主要有两种监测手段。

方法一:使用 perf 观察缓存未命中率 (LLC-misses)

由于分配给该进程的 L3 缓存(LLC,Last Level Cache)变小了,它的缓存未命中率必然会上升。我们可以使用内核自带的 perf 工具对该 PID 进行监测:

bash
$ sudo perf stat -e LLC-loads,LLC-load-misses -p 12345

 Performance counter stats for process id '12345':

        24,512,331      LLC-loads
        18,435,120      LLC-load-misses          #  75.21% of all LL-cache accesses

       10.001234560 seconds time elapsed

通过对比将 PID 放入 demo_container 前后的输出数据,你会发现放入限制组后,LLC-load-misses 的比例(本例中高达 75%)会有显著的增加,说明缓存空间被成功限制,进程不得不频繁访问主存。

方法二:使用 intel-cmt-cat 提供的监控工具 (pqos)

Intel 官方提供了一个名为 intel-cmt-cat 的工具包(其中包含 pqos),专门用于管理和监控 RDT 指标。

bash
$ sudo pqos -p all:12345

[PID]      CORE   IPC      LLC MISSES   LLC[KB]    MBL[MB/s]  MBR[MB/s]
12345      2      0.45     854023       2048       345.5      0.0

pqos 会实时显示该进程消耗的 Local/Remote 内存带宽(MBL[MB/s])。当你在 schemata 中调整 MB:0=30 的数值时,你能直观地看到监控面板上的带宽速率随之被物理节流,同时 IPC(执行效率)也会因为等待内存而降低。

通过以上监测,我们不仅“配置”了硬件限制,还“看到”了物理层面的节流效果。

结语 ​

相较于传统的内核态 Cgroups 隔离,Intel RDT 提供了一种直接基于 CPU 硬件微架构的隔离方案,是解决多租户容器场景下“吵闹邻居”问题的终极武器。而 runc 对 RDT 的原生支持,让用户在创建 OCI 容器时,可以像配置 Cgroups 一样简单地配置微架构资源。

下一篇,我们将继续深入 SPEC.md,探索容器中关键的安全防线——Linux Capabilities(安全能力)。

Reading runc Source Code (Part 4): Intel RDT Micro-Architecture Isolation in SPEC.md
Reading runc Source Code (Part 3): Container Resource Control in SPEC.md
由 Valaxy v1.2.2 驱动|主题-Yunv1.2.2