Volcano Agent Cgroup V2 适配设计深度解析:双版本检测、统一管理与事件处理器改造
2026/9/17 19:04:46 网站建设 项目流程

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.sharescpu.weightmemory.limit_in_bytesmemory.max),以及仓库源码中对应的实现位置与测试验证方式。

背景与动机:容器运行时向 cgroup v2 迁移的必然性

主流的 Kubernetes 发行版与容器运行时正在将 cgroup v2 作为默认配置。相比 cgroup v1 将各子系统分散挂载的方式,cgroup v2 提供了**统一层级(unified hierarchy)**与更强的资源管理能力。作为运行在节点侧、负责按扩展资源与 QoS 级别配置 cgroup 的组件,Volcano Agent 必须同时兼容两代接口。该适配工作的核心挑战可归纳为四点:

  1. 层级结构不同:cgroup v1 使用分离的子系统挂载(/sys/fs/cgroup/cpu/sys/fs/cgroup/memory),而 cgroup v2 使用统一的挂载点(/sys/fs/cgroup),不再按子系统拆分目录;
  2. 控制文件改名:两代版本的控制文件名称与格式不同,例如cpu.shares对应 v2 的cpu.weightmemory.limit_in_bytes对应 v2 的memory.max
  3. 资源表示方式不同:CPU 与内存的限额在 v2 中采用新的表达形式(cpu.max的 "quota period" 格式、"max" 表示不设限);
  4. 驱动兼容性: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 下的resourcescpuqosmemoryqosmemoryqosv2cpuburst等目录),中间是CgroupManager接口与CgroupManagerImpl实现,底层是IsCgroupsV2GetCgroupDriverDetectCgroupVersion等探测函数。

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_MAGIC0x63677270)用于标识 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)按照以下优先级依次探测:

  1. 环境变量CGROUP_DRIVER:若取值为systemdcgroupfs则直接返回;
  2. Kubelet 配置文件:依次解析/var/lib/kubelet/config.yaml/etc/kubernetes/kubelet.conf/var/lib/kubelet/kubeadm-flags.env,从中提取 YAML 形式的cgroupDriver:字段或--cgroup-driver命令行参数;
  3. Kubelet 进程参数:若配置文件不存在或未包含驱动信息,则扫描/proc目录找到名为kubelet的进程,读取/proc/<pid>/cmdline,解析--cgroup-driver参数(同时兼容--cgroup-driver=value--cgroup-driver value两种写法),若进程带--config参数还会回读对应配置文件(见 cgroup.go);
  4. 系统探测:通过文件系统特征判断——存在/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);
  5. 默认回退:以上均失败时返回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 v1Cgroup v2格式变化
CPU Shares/Weightcpu.sharescpu.weightshares(1024 基准) → weight(100 基准)
CPU Quotacpu.cfs_quota_uscpu.max"200000""200000 100000"
CPU Burstcpu.cfs_burst_uscpu.max.burst格式相同
Memory Limitmemory.limit_in_bytesmemory.max字节数 → "max" 表示不设限
Memory Highmemory.highv2 新增
Memory Lowmemory.lowv2 新增
Memory Minmemory.minv2 新增

这些文件名的常量定义集中在 cgroup.go:v1 侧有cpu.qos_levelcpuacct.usagecpu.cfs_burst_uscpu.cfs_quota_usnet_cls.classidcpu.shares,v2 侧有cpu.weightcpu.statcpu.max.burstcpu.maxcpu.idle等。

CPU 权重与限额的换算规则

在 pkg/agent/utils/pod/resources.go 中可以看到两代版本对应的换算函数:

  • v1 sharesmilliCPUToShares将 milliCPU 转换为 CFS shares,基准sharesPerCPU = 1024,并夹在minShares = 2maxShares = 262144之间;
  • v2 weightmilliCPUToWeight采用weight = milliCPU / 10,夹在 1 与 10000 之间(对应内核 v2 权重范围);
  • v2 quotamilliCPUToMax生成"quota period"格式字符串,其中quotaPeriod = 100000(等价 100ms),quota 最小值为minQuotaPeriod = 1000(1ms);milliCPU 为 0 时返回"max 100000"表示不设限;
  • v2 memorymemoryLimitToMax在内存字节数为 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 用-1valueUnlimitedV1),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.sharescpu.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解析混部配置中的MemoryQosHighRatio/LowRatio/MinRatio),缺省时使用默认值HighRatio=100, LowRatio=0, MinRatio=0(见 memoryqosv2_linux.go);
  • memory.high依据容器 Limits 按HighRatio百分比计算;memory.lowmemory.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 两条代码路径;
  • 事件处理器测试resourcescpuqoscpuburstcputhrottlememoryqosmemoryqosv2networkqos各处理器目录下均配有*_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询