Volcano Agent Cgroup V2 适配设计深度解析:双版本检测、统一管理与事件处理器改造
【免费下载链接】volcanoA Cloud Native Batch System (Project under CNCF)项目地址: https://gitcode.com/GitHub_Trending/vol/volcano
导读
本文围绕 Volcano Agent 的 cgroup v2 适配设计(见 docs/design/agent-cgroup-v2-adaptation.md),系统讲解 Agent 如何在 cgroup v1 与 v2 两代内核接口之间实现无缝共存:包括基于statfs魔数的版本探测、多来源 cgroup 驱动识别、版本无关的统一管理接口,以及 Resources / CPUQoS / MemoryQoS / CPUBurst 等事件处理器的逐项改造。读完本文,你将掌握 Volcano Agent 中 cgroup 路径的构造规律、v1/v2 控制文件的映射关系(如cpu.shares→cpu.weight、memory.limit_in_bytes→memory.max),以及仓库源码中对应的实现位置与测试验证方式。
背景与动机:容器运行时向 cgroup v2 迁移的必然性
主流的 Kubernetes 发行版与容器运行时正在将 cgroup v2 作为默认配置。相比 cgroup v1 将各子系统分散挂载的方式,cgroup v2 提供了**统一层级(unified hierarchy)**与更强的资源管理能力。作为运行在节点侧、负责按扩展资源与 QoS 级别配置 cgroup 的组件,Volcano Agent 必须同时兼容两代接口。该适配工作的核心挑战可归纳为四点:
- 层级结构不同:cgroup v1 使用分离的子系统挂载(
/sys/fs/cgroup/cpu、/sys/fs/cgroup/memory),而 cgroup v2 使用统一的挂载点(/sys/fs/cgroup),不再按子系统拆分目录; - 控制文件改名:两代版本的控制文件名称与格式不同,例如
cpu.shares对应 v2 的cpu.weight,memory.limit_in_bytes对应 v2 的memory.max; - 资源表示方式不同:CPU 与内存的限额在 v2 中采用新的表达形式(
cpu.max的 "quota period" 格式、"max" 表示不设限); - 驱动兼容性:systemd 与 cgroupfs 两种 cgroup 驱动都要能同时工作在两代 cgroup 版本之上。
设计目标包括:自动探测 cgroup 版本与驱动配置、提供 v1/v2 一致的统一 API、保持对既有 cgroup v1 部署的完全向后兼容、更新全部相关事件处理器以适配 v2 文件格式,并为两代版本提供完整的单元测试覆盖。而迁移工具、混合 v1/v2 环境、性能优化明确不在本次设计范围内。
分层架构:事件处理器与底层细节解耦
适配方案采用分层架构,将版本差异隔离在底层,使上层事件处理器无需关心 v1/v2 的路径与文件差异:
┌─────────────────────────────────────────────────────────┐ │ Event Handlers Layer │ │ (Resources, CPUQoS, MemoryQoS, CPUBurst) │ └─────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────┐ │ CgroupManager Interface │ │ (Version-agnostic operations) │ │ - GetRootCgroupPath() │ │ - GetQoSCgroupPath() │ │ - GetPodCgroupPath() │ │ - GetCgroupVersion() │ └─────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────┐ │ CgroupManagerImpl │ │ Unified implementation supporting both v1 & v2 │ │ • cgroupDriver: systemd/cgroupfs │ │ • cgroupVersion: v1/v2 (auto-detected) │ │ • buildCgroupPath(): version-aware path builder │ │ ┌──────────────┴──────────────┐ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │ v1 Behavior │ │ v2 Behavior │ │ │ │ Separate paths │ │ Unified path │ │ │ │ per subsystem │ │ hierarchy │ │ │ └─────────────────┘ └─────────────────┘ │ └─────────────────────────────────────────────────────────┘ │ ┌─────────────────────────────────────────────────────────┐ │ Detection & Driver Layer │ │ • IsCgroupsV2(): filesystem magic number check │ │ • GetCgroupDriver(): multi-source driver detection │ │ • DetectCgroupVersion(): version detection with env │ │ override for testing │ └─────────────────────────────────────────────────────────┘从源码结构看,这一分层对应仓库中的 pkg/agent/utils/cgroup/cgroup.go:顶层是各事件处理器(pkg/agent/events/handlers 下的resources、cpuqos、memoryqos、memoryqosv2、cpuburst等目录),中间是CgroupManager接口与CgroupManagerImpl实现,底层是IsCgroupsV2、GetCgroupDriver、DetectCgroupVersion等探测函数。
Cgroup 版本探测:魔数校验与测试覆盖
1. 文件系统魔数探测(生产环境)
版本探测的核心是判断挂载点文件系统类型是否为 cgroup v2:
func IsCgroupsV2(unifiedMountpoint string) bool { var st unix.Statfs_t err := unix.Statfs(unifiedMountpoint, &st) if err != nil { // Error handling } return st.Type == unix.CGROUP2_SUPER_MAGIC }- 通过
statfs系统调用读取挂载点文件系统类型; CGROUP2_SUPER_MAGIC(0x63677270)用于标识 cgroup v2 文件系统;- 该方法参考了 runc 等成熟容器运行时项目的实现思路(见 cgroup.go)。
值得注意的细节是,源码在statfs失败时做了场景区分:如果挂载点不存在且进程运行在 user namespace 中(userns.RunningInUserNS()),则忽略 "not found" 错误并假定为 v1;其余错误则记录日志后同样回退为 v1。此外isUnified通过sync.Once保证只探测一次,避免重复系统调用。
2. 测试环境覆盖(单元测试注入)
func DetectCgroupVersion(cgroupRoot string) (string, error) { // Support test environment override if testVersion := os.Getenv("VOLCANO_TEST_CGROUP_VERSION"); testVersion != "" { // Handle test cases } // Production detection logic }- 环境变量
VOLCANO_TEST_CGROUP_VERSION专用于单元测试; - 支持取值:"v1"、"v2"、"1"、"2";非法值会记录警告并回退到文件系统探测;
- 生产环境则走文件系统探测逻辑。
在源码实现 DetectCgroupVersion 中,探测结果会写入包级变量CgroupVersion,供后续构造管理器使用。
Cgroup 驱动探测:五级优先级回退
cgroup 驱动(systemd 与 cgroupfs)决定了 cgroup 路径的命名方式——systemd 驱动下目录使用xxx.slice切片命名,cgroupfs 驱动下则使用传统层级目录。GetCgroupDriver()(见 cgroup.go)按照以下优先级依次探测:
- 环境变量
CGROUP_DRIVER:若取值为systemd或cgroupfs则直接返回; - Kubelet 配置文件:依次解析
/var/lib/kubelet/config.yaml、/etc/kubernetes/kubelet.conf、/var/lib/kubelet/kubeadm-flags.env,从中提取 YAML 形式的cgroupDriver:字段或--cgroup-driver命令行参数; - Kubelet 进程参数:若配置文件不存在或未包含驱动信息,则扫描
/proc目录找到名为kubelet的进程,读取/proc/<pid>/cmdline,解析--cgroup-driver参数(同时兼容--cgroup-driver=value与--cgroup-driver value两种写法),若进程带--config参数还会回读对应配置文件(见 cgroup.go); - 系统探测:通过文件系统特征判断——存在
/sys/fs/cgroup/system.slice或/sys/fs/cgroup/systemd判定为 systemd;存在/sys/fs/cgroup/cpu判定为 cgroupfs;cgroup v2 挂载下若cgroup.controllers存在且无system.slice则判定为 cgroupfs;还处理了 hybrid 模式下/sys/fs/cgroup/unified的探测(见 DetectCgroupDriver); - 默认回退:以上均失败时返回
cgroupfs。
这种多来源 + 回退的设计保证了在不同 Kubernetes 部署形态(kubeadm、二进制、云厂商托管等)下都能尽可能准确地识别驱动。
统一管理接口:CgroupManager 与路径构造
接口与实现结构
CgroupManager接口向事件处理器提供版本无关的操作(见 cgroup.go):
type CgroupManager interface { GetRootCgroupPath(cgroupSubsystem CgroupSubsystem) (string, error) GetQoSCgroupPath(qos corev1.PodQOSClass, cgroupSubsystem CgroupSubsystem) (string, error) GetPodCgroupPath(qos corev1.PodQOSClass, cgroupSubsystem CgroupSubsystem, podUID types.UID) (string, error) GetCgroupVersion() string BuildContainerCgroupName(containerID string) string Memory() MemorySubsystem }CgroupManagerImpl的字段结构(见 cgroup.go):
type CgroupManagerImpl struct { cgroupDriver string // systemd or cgroupfs cgroupRoot string // mount point kubeCgroupRoot string // kubelet cgroup root cgroupVersion string // v1 or v2 memory MemorySubsystem }其中kubeCgroupRoot与 kubelet 配置中的cgroup-root保持一致,非空时会作为路径前缀拼接到kubepods之前。管理器通过NewCgroupManager(cgroupDriver, cgroupRoot, kubeCgroupRoot)构造:内部先调用DetectCgroupVersion探测版本,若传入的驱动为空则自动调用GetCgroupDriver()(见 cgroup.go)。
版本感知的路径构造
buildCgroupPath()是路径差异的核心收敛点(见 cgroup.go):
func (c *CgroupManagerImpl) buildCgroupPath(cgroupSubsystem CgroupSubsystem, cgroupPath string) string { switch c.cgroupVersion { case CgroupV1: // v1: each controller has its own hierarchy return filepath.Join(c.cgroupRoot, string(cgroupSubsystem), cgroupPath) case CgroupV2: // v2: unified hierarchy, no subsystem in path return filepath.Join(c.cgroupRoot, cgroupPath) default: // Fallback to v1 behavior return filepath.Join(c.cgroupRoot, string(cgroupSubsystem), cgroupPath) } }两代版本的实际路径差异如下:
- Cgroup v1 示例(按子系统分离):CPU 路径为
/sys/fs/cgroup/cpu/kubepods/burstable/pod<uid>,内存路径为/sys/fs/cgroup/memory/kubepods/burstable/pod<uid>; - Cgroup v2 示例(统一层级):所有子系统共用
/sys/fs/cgroup/kubepods/burstable/pod<uid>。
同时,Pod 级与 QoS 级路径的构造逻辑GetRootCgroupPath/GetQoSCgroupPath/GetPodCgroupPath会在kubepods之后追加burstable/besteffort等 QoS 目录(Guaranteed 类 Pod 不追加 QoS 子目录),Pod 级路径末尾追加pod<uid>后缀。
驱动相关的路径命名
CgroupNameToCgroupPath根据驱动选择命名方案(见 cgroup.go):
- cgroupfs 驱动:
ToCgroupfs()返回/+ 各段用/拼接的简单路径; - systemd 驱动:
ToSystemd()将各段以-连接并追加.slice后缀,再调用cgroupsystemd.ExpandSlice展开为完整切片路径;名称中的-会被转义为_(escapeSystemdCgroupName)。
容器级 cgroup 目录名由BuildContainerCgroupName生成(见 cgroup.go):输入为runtime://id格式的容器 ID。cgroupfs 驱动下直接返回纯 ID;systemd 驱动下按运行时类型生成 scope 名称,例如 containerd 对应cri-containerd-<id>.scope、docker 对应docker-<id>.scope、cri-o 对应crio-<id>.scope。这一行为有专门的单元测试覆盖(见 cgroup_test.go)。
控制文件映射:v1 与 v2 的桥接表
系统在 v1 与 v2 控制文件之间建立了明确的映射关系:
| 功能 | Cgroup v1 | Cgroup v2 | 格式变化 |
|---|---|---|---|
| CPU Shares/Weight | cpu.shares | cpu.weight | shares(1024 基准) → weight(100 基准) |
| CPU Quota | cpu.cfs_quota_us | cpu.max | "200000"→"200000 100000" |
| CPU Burst | cpu.cfs_burst_us | cpu.max.burst | 格式相同 |
| Memory Limit | memory.limit_in_bytes | memory.max | 字节数 → "max" 表示不设限 |
| Memory High | 无 | memory.high | v2 新增 |
| Memory Low | 无 | memory.low | v2 新增 |
| Memory Min | 无 | memory.min | v2 新增 |
这些文件名的常量定义集中在 cgroup.go:v1 侧有cpu.qos_level、cpuacct.usage、cpu.cfs_burst_us、cpu.cfs_quota_us、net_cls.classid、cpu.shares,v2 侧有cpu.weight、cpu.stat、cpu.max.burst、cpu.max、cpu.idle等。
CPU 权重与限额的换算规则
在 pkg/agent/utils/pod/resources.go 中可以看到两代版本对应的换算函数:
- v1 shares:
milliCPUToShares将 milliCPU 转换为 CFS shares,基准sharesPerCPU = 1024,并夹在minShares = 2与maxShares = 262144之间; - v2 weight:
milliCPUToWeight采用weight = milliCPU / 10,夹在 1 与 10000 之间(对应内核 v2 权重范围); - v2 quota:
milliCPUToMax生成"quota period"格式字符串,其中quotaPeriod = 100000(等价 100ms),quota 最小值为minQuotaPeriod = 1000(1ms);milliCPU 为 0 时返回"max 100000"表示不设限; - v2 memory:
memoryLimitToMax在内存字节数为 0 时返回"max",否则返回十进制字节数。
内存子系统接口抽象
[v2 独有的memory.high/memory.low/memory.min能力通过MemorySubsystem接口暴露(见 memory.go):
NewMemorySubsystem(cgroupVersion)按版本构建接口映射:v1 仅提供Max(对应memory.limit_in_bytes),v2 提供Max/High/Low/Min四个接口,分别对应memory.max/memory.high/memory.low/memory.min;- 每个
MemoryInterface封装Get/Set操作,并以字节为单位读写;"不设限"的表示方式因版本而异:v1 用-1(valueUnlimitedV1),v2 用字符串"max"(valueUnlimitedV2),读取时统一归一化为MemoryUnlimited = -1常量(见 memory.go)。
事件处理器适配:四大处理器的 v2 改造
每个事件处理器在注册时通过cgroupMgr与底层交互(注册逻辑见 pkg/agent/events/handlers/registry.go)。
Resources Handler:资源限额
用途:管理容器的 CPU 与内存资源限额。其实现位于 pkg/agent/events/handlers/resources/resources.go,会在 Pod 事件到来时:
- 根据
cgroupMgr.GetCgroupVersion()分流:v1 调用CalculateExtendResources(pod),v2 调用CalculateExtendResourcesV2(pod)(见 resources.go); - v2 关键变化:使用
cpu.weight替代cpu.shares;cpu.max需要写入"quota period"格式——当值为 -1(不设限)时写入"max 100000",正值时写入"%d 100000"(quota 后跟固定 period,见 resources.go);内存不设限时向memory.max写入"max"; - 路径统一通过
GetPodCgroupPath+BuildContainerCgroupName计算,Pod 级配置直接写 Pod 目录,容器级配置写容器子目录。
CPU Burst Handler:CPU 突发能力
用途:管理 CPU burst 能力(通过 Pod 注解启用)。其实现位于 pkg/agent/events/handlers/cpuburst/cpu_burst_linux.go:
- v2 变化:使用
cpu.max.burst替代cpu.cfs_burst_us;从cpu.max读取配额; - 配额解析函数
readCPUQuotaV2会解析cpu.max的两种格式:"max period"(返回 -1 表示不设限)与"quota period"(返回 quota 数值,见 cpu_burst_linux.go); walkFunc会递归遍历 Pod cgroup 目录下所有容器目录,对每个容器计算并写入 burst 值,最后再汇总设置 Pod 级 cgroup;若 burst 时间大于 quota 总时间则截断为 quota 值,quota 为 -1(不设限)时跳过(见 cpu_burst_linux.go)。
CPU QoS Handler 与 Memory QoS Handler
- CPU QoS(cpuqos_linux.go):设置 CPU QoS 等级,目前沿用厂商特有的
cpu.qos_level文件,v2 下暂无变化; - Memory QoS(memoryqos_linux.go):设置内存 QoS 等级,同样沿用厂商特有的
memory.qos_level文件。
MemoryQoS V2 Handler:v2 独有能力
针对 cgroup v2 专门新增了 memoryqosv2_linux.go 处理器,利用 v2 独有的memory.high/memory.low/memory.min实现精细的内存 QoS 控制:
- 从 Pod 注解
ColocationConfigKey解析混部配置中的MemoryQos(HighRatio/LowRatio/MinRatio),缺省时使用默认值HighRatio=100, LowRatio=0, MinRatio=0(见 memoryqosv2_linux.go); memory.high依据容器 Limits 按HighRatio百分比计算;memory.low与memory.min依据容器 Requests 按对应比例计算;比率达到 100% 或数量为 0 时写MemoryUnlimited(即 v2 的 "max");- 通过
cgroupMgr.Memory()获取的MemorySubsystem判断各接口是否supported,保证在 v1 环境下该处理器自然失效。
测试策略:双版本覆盖
仓库为 cgroup 适配提供了较为完整的测试支撑:
- 驱动与容器命名:
TestBuildContainerCgroupName覆盖 cgroupfs / systemd 两种驱动 × containerd / docker / cri-o / 纯 ID 四种输入的组合(见 cgroup_test.go); - 版本注入:
DetectCgroupVersion支持VOLCANO_TEST_CGROUP_VERSION环境变量强制指定版本,使测试无需真实 cgroup 挂载即可覆盖 v1/v2 两条代码路径; - 事件处理器测试:
resources、cpuqos、cpuburst、cputhrottle、memoryqos、memoryqosv2、networkqos各处理器目录下均配有*_test.go,测试中通过cgroup.NewCgroupManager("cgroupfs", tmpDir, "")在临时目录构造管理器(例如 cpu_burst_linux_test.go),结合测试注解验证 cgroup 文件的实际写入结果。
向后兼容与未来展望
向后兼容
- 未显式配置驱动时,
GetCgroupDriver()默认回退到cgroupfs,与既有部署习惯保持一致; buildCgroupPath对未知版本回退到 v1 行为,保证异常情况下的稳健性;- 未请求任何扩展资源的 Pod 会被跳过,不产生任何 cgroup 写入,不影响普通工作负载;
- Pod 级限额只有在所有容器都声明了对应限额时才写入,避免 v1 语义下的误写(见 resources.go)。
未来工作
- 本设计不包含v1 到 v2 的迁移工具,也不支持单集群内混合 v1/v2 部署;
- CPU QoS 与 Memory QoS 目前仍使用厂商特有的
cpu.qos_level/memory.qos_level文件,后续可探索迁移到 v2 标准接口; - 本次工作聚焦兼容性而非性能优化,未来可在双版本兼容的基础上针对 v2 统一层级的优势做进一步性能调优。
总结
Volcano Agent 的 cgroup v2 适配通过"底层探测 + 统一接口 + 版本感知路径构造 + 处理器分流"的分层设计,让 Agent 在两代 cgroup 之上都能正确工作:底层以statfs魔数完成版本探测、以多来源回退完成驱动识别;中间层以CgroupManager/MemorySubsystem抽象出版本无关的操作原语;上层各事件处理器只需根据GetCgroupVersion()选择对应的资源换算与文件写入格式即可。这一设计既保证了既有 v1 集群的平滑兼容,也为容器运行时全面转向 cgroup v2 做好了准备。相关实现与测试可直接在 pkg/agent/utils/cgroup 与 pkg/agent/events/handlers 目录中查阅验证。
【免费下载链接】volcanoA Cloud Native Batch System (Project under CNCF)项目地址: https://gitcode.com/GitHub_Trending/vol/volcano
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考