核心使命与设计理念
11.1 CGroup 管理是什么?
CGroup 管理是 Koordinator 对 Linux CGroup 的抽象封装和生命周期管理,提供统一的 API 来控制 Pod 的资源使用。
核心职责:
- 版本适配:支持 CGroup v1 和 v2,提供统一接口
- 生命周期:管理 Pod 对应的 CGroup 的创建、更新、删除
- 参数管理:提供 CPU、内存、I/O 等资源的参数访问接口
- 错误检测:检测 CGroup 操作的错误,提供诊断信息
- 性能监控:统计 CGroup 操作的性能,发现瓶颈
CGroup 的版本差异:
┌─────────────────────────────────────────────┐
│ CGroup v1 vs v2 的主要差异 │
├─────────────────────────────────────────────┤
│ │
│ 特性 | CGroup v1 | CGroup v2 │
│ ─────────────────────────────────────────── │
│ 结构 | 多个独立子系统 | unified │
│ 文件路径 | /sys/fs/cgroup/cpu | /sys/fs/cgroup/cpu.max │
│ CPU 限制 | cpu.cfs_quota_us | cpu.max │
│ 内存保护 | 无 | memory.min/low/high │
│ I/O 权重 | blkio.weight | io.weight │
│ CPU 权重 | cpu.shares | cpu.weight │
│ PSI 支持 | 有 | 完整支持 │
│ │
│ Koordinator 的策略: │
│ ├─ 同时支持 v1 和 v2 │
│ ├─ 检测系统版本,自动选择 │
│ ├─ 提供统一的抽象接口 │
│ └─ 自动转换参数格式 │
│ │
└─────────────────────────────────────────────┘
11.2 为什么需要 CGroup 管理?
问题 1:v1 和 v2 的 API 完全不同
使用 CGroup 的复杂性:
场景:更新 Pod 的 CPU 限制
CGroup v1 方式:
├─ 路径: /sys/fs/cgroup/cpu,cpuacct/kubepods/pod-uid/
├─ 操作: echo 400000 > cpu.cfs_quota_us
├─ 含义: 设置 CPU 限制为 4 CPU
CGroup v2 方式:
├─ 路径: /sys/fs/cgroup/kubepods/pod-uid/
├─ 操作: echo "4000000 100000" > cpu.max
├─ 含义: 设置 CPU 限制为 4 CPU(时间/周期)
问题:
├─ 参数完全不同(cfs_quota_us vs cpu.max)
├─ 路径也不同
├─ 格式也不同(单个值 vs 两个值)
├─ 单位也不同(微秒 vs 数值)
└─ 代码需要判断版本,分别处理
CGroup 管理层的价值:
└─ 隐藏这些差异
└─ 提供统一的 UpdateCPUQuota(pod, 4.0) 接口
└─ 内部自动处理版本差异和格式转换
问题 2:CGroup 操作的陷阱很多
常见陷阱和错误:
陷阱 1: 内存 min/max 的顺序依赖
├─ 错误: min=5G, max=4G (min > max,失败)
├─ 正确: 先确保 max >= min
└─ 处理: CGroup 管理层自动处理顺序
陷阱 2: CPU quota 的特殊值
├─ quota=-1 表示无限制(v1)
├─ quota=0 是无效值,应该用很大的值
├─ v2 中用负数表示无限制
└─ 处理: 封装处理这些特殊值
陷阱 3: CGroup 路径的动态变化
├─ 不同 Kubernetes 版本的路径格式不同
├─ systemd 可能改变路径结构
├─ 需要动态查询正确的路径
└─ 处理: CGroup 管理层查询并缓存路径
陷阱 4: 权限问题
├─ 普通进程无法修改 CGroup
├─ 需要 root 权限
├─ 某些特殊 CGroup 可能有特殊权限要求
└─ 处理: CGroup 管理层检查权限,提供错误信息
陷阱 5: 文件系统缓存
├─ 写入 CGroup 文件后,可能需要等待内核处理
├─ 立即读取可能得不到新值
├─ 需要重试或延迟
└─ 处理: CGroup 管理层使用重试和延迟
Koordinator 的 CGroup 管理层的好处:
└─ 统一处理所有这些陷阱
└─ 提供简单、安全的 API
└─ 减少 Bug 的可能性
问题 3:需要灵活支持多种 CGroup 配置
场景:混合 v1 和 v2 的系统
某些企业环境:
├─ 容器运行时使用 v1(如 Docker)
├─ 某些控制器使用 v2
└─ 需要同时支持两种
或者:
├─ 某些子系统使用 v1
├─ 某些子系统使用 v2
└─ 需要自动检测和处理
Koordinator 的方案:
├─ 检测每个子系统的版本
├─ 为不同子系统选择合适的 API
├─ 提供统一的高层接口
└─ 用户无需关心底层版本
11.3 CGroup 管理的实现架构
┌──────────────────────────────────────────────┐
│ Koordinator CGroup 管理架构 │
├──────────────────────────────────────────────┤
│ │
│ 应用层(ResourceExecutor) │
│ UpdateCPUQuota(pod, 4.0) │
│ ↓ │
│ CGroup 管理层(本章内容) │
│ ├─ 版本检测 │
│ ├─ 参数转换 │
│ ├─ 路径查询 │
│ ├─ 权限检查 │
│ └─ 重试和错误处理 │
│ ↓ │
│ CGroup 操作层 │
│ 写入 /sys/fs/cgroup/... 文件 │
│ ↓ │
│ Linux Kernel │
│ 实际执行资源限制 │
│ │
└──────────────────────────────────────────────┘
CGroup 管理的核心实现
11.4 版本检测和自适应
CGroup 版本检测:
// 检测 CGroup 版本
func DetectCGroupVersion() (CGroupVersion, error) {
// 方法 1: 检查 /sys/fs/cgroup 的结构
if exists("/sys/fs/cgroup/cgroup.controllers") {
return CGroupV2, nil // v2 unified 模式
}
if exists("/sys/fs/cgroup/cpu") {
return CGroupV1, nil // v1 有 cpu 子目录
}
// 方法 2: 检查挂载方式
mounts := readProcMounts()
for mount in mounts {
if mount.type == "cgroup2" {
return CGroupV2, nil
}
if mount.type == "cgroup" {
return CGroupV1, nil
}
}
return Unknown, errors.New("cannot detect cgroup version")
}
// 为每个子系统检测版本
type SubsystemVersion struct {
Subsystem string // "cpu", "memory", "io"
Version CGroupVersion // v1 or v2
Path string // 完整路径
}
func DetectSubsystemVersions() map[string]SubsystemVersion {
result := make(map[string]SubsystemVersion)
// CPU 子系统
if exists("/sys/fs/cgroup/cpu.max") {
result["cpu"] = SubsystemVersion{
Subsystem: "cpu",
Version: CGroupV2,
Path: "/sys/fs/cgroup",
}
} else if exists("/sys/fs/cgroup/cpu") {
result["cpu"] = SubsystemVersion{
Subsystem: "cpu",
Version: CGroupV1,
Path: "/sys/fs/cgroup/cpu,cpuacct",
}
}
// 类似处理其他子系统
return result
}
生产案例:混合 v1 和 v2 系统的处理
场景:企业环境,有的机器 v1,有的机器 v2
检测结果示例:
机器 A(CGroup v1):
├─ CPU: /sys/fs/cgroup/cpu,cpuacct/pod-uid/
├─ Memory: /sys/fs/cgroup/memory/pod-uid/
├─ I/O: /sys/fs/cgroup/blkio/pod-uid/
└─ 所有子系统都是 v1
机器 B(CGroup v2):
├─ CPU: /sys/fs/cgroup/pod-uid/
├─ Memory: /sys/fs/cgroup/pod-uid/
├─ I/O: /sys/fs/cgroup/pod-uid/
└─ 所有子系统统一在 v2
机器 C(混合):
├─ CPU: /sys/fs/cgroup/cpu.max(v2)
├─ Memory: /sys/fs/cgroup/memory/pod-uid/(v1)
├─ I/O: /sys/fs/cgroup/pod-uid/(v2)
└─ 不同子系统用不同版本
Koordinator 的处理:
├─ 启动时检测所有子系统的版本
├─ 对每个子系统,使用对应的 API
├─ 应用层无需关心版本差异
└─ 自动适应不同机器的配置
代码示例:
// 更新 CPU quota
func UpdateCPUQuota(pod, cpuValue) error {
subsys := subsystemVersions["cpu"]
if subsys.Version == CGroupV1 {
quota := float64ToQuota(cpuValue)
path := subsys.Path + pod.uid + "/cpu.cfs_quota_us"
return WriteFile(path, quota)
} else {
// v2
quota := float64ToV2Quota(cpuValue)
path := subsys.Path + pod.uid + "/cpu.max"
return WriteFile(path, quota)
}
}
// 应用层调用
UpdateCPUQuota(pod, 4.0)
// 内部自动选择 v1 还是 v2 的 API
11.5 参数格式转换
CPU 参数的转换:
CGroup v1 的 CPU 参数:
├─ cpu.cfs_period_us: 调度周期(默认 100000 us = 100ms)
├─ cpu.cfs_quota_us: 该周期内的 CPU 时间(微秒)
│
├─ 计算: CPU limit = quota / period
│
└─ 示例:
├─ 4 CPU → quota = 4 × 100000 = 400000
├─ 2 CPU → quota = 2 × 100000 = 200000
└─ 0.5 CPU → quota = 0.5 × 100000 = 50000
CGroup v2 的 CPU 参数:
├─ cpu.max: "quota period" 格式
│
├─ 示例:
│ ├─ 4 CPU → "400000 100000"
│ ├─ 2 CPU → "200000 100000"
│ └─ 无限制 → "max 100000"
│
└─ 特殊值:
├─ "max" 表示无限制(不是 -1)
└─ 单位都是微秒
转换函数:
func CPUValueToCGroupV1(cpuValue float64) string {
// cpuValue 单位: CPU 核数
period := 100000 // 默认 100ms
quota := int64(cpuValue * float64(period))
if quota < 1000 {
quota = 1000 // 最小值
}
if quota > math.MaxInt64 {
quota = math.MaxInt64 // 最大值
}
return strconv.FormatInt(quota, 10)
}
func CPUValueToCGroupV2(cpuValue float64) string {
if cpuValue <= 0 {
return "max 100000" // 无限制
}
period := 100000
quota := int64(cpuValue * float64(period))
return fmt.Sprintf("%d %d", quota, period)
}
内存参数的转换:
CGroup v1 的内存参数:
├─ memory.limit_in_bytes: 硬限制
├─ memory.soft_limit_in_bytes: 软限制
├─ memory.swappiness: Swap 使用倾向
│
└─ 单位: 字节
CGroup v2 的内存参数:
├─ memory.max: 硬限制
├─ memory.high: 软限制
├─ memory.min: 硬保证
├─ memory.low: 软保证
│
└─ 单位: 字节
参数映射:
v1 | v2
─────────────────────────────────────
memory.limit_in_bytes → memory.max
memory.soft_limit → memory.high
(无对应) ← memory.min
(无对应) ← memory.low
转换示例:
用户指定: memory_limit = 8GB
v1 实现:
├─ memory.limit_in_bytes = 8 × 1024^3 = 8589934592
v2 实现:
├─ memory.max = 8589934592
├─ memory.high = 8589934592 × 0.9 = 7730840371
├─ memory.min = 8589934592 × 0.5 = 4294967296
└─ memory.low = 8589934592 × 0.8 = 6871947673
I/O 参数的转换:
CGroup v1 的 I/O 参数:
├─ blkio.weight: I/O 权重(100-1000)
├─ blkio.throttle.read_bps_device: 读取限速(字节/秒)
├─ blkio.throttle.write_bps_device: 写入限速
│
└─ 格式: "major:minor value"
CGroup v2 的 I/O 参数:
├─ io.weight: I/O 权重(v2 格式)
├─ io.max: 限速规则
│
└─ 格式: "major:minor rbps=value wbps=value"
转换例子:
设置读取限速 500 MB/s:
v1:
├─ 获取设备: blkstat -d sda
├─ 主设备号: 8, 次设备号: 0
├─ 写入: echo "8:0 524288000" > blkio.throttle.read_bps_device
│ (524288000 = 500 × 1024 × 1024)
v2:
├─ echo "8:0 rbps=524288000" > io.max
转换函数:
func MBpsToBytes(mbps float64) int64 {
return int64(mbps * 1024 * 1024)
}
func SetIOReadLimit(path, device string, mbps float64) error {
bytes_per_sec := MBpsToBytes(mbps)
if version == CGroupV1 {
content := fmt.Sprintf("%s %d", device, bytes_per_sec)
return WriteFile(path + "/blkio.throttle.read_bps_device", content)
} else {
content := fmt.Sprintf("%s rbps=%d", device, bytes_per_sec)
return WriteFile(path + "/io.max", content)
}
}
11.6 路径管理和查询
动态路径计算:
CGroup 路径的复杂性:
Kubernetes 创建 Pod 时,会为其创建 CGroup
CGroup 的路径取决于:
1. CGroup 驱动 (cgroupDriver)
├─ cgroupfs: 直接使用 cgroup 文件系统
├─ systemd: 使用 systemd 来管理 cgroup
└─ 路径完全不同
2. Pod 的 QoS 等级
├─ Guaranteed: /sys/fs/cgroup/kubepods/pod-uuid/
├─ Burstable: /sys/fs/cgroup/kubepods/burstable/pod-uuid/
└─ BestEffort: /sys/fs/cgroup/kubepods/besteffort/pod-uuid/
3. systemd 的特殊处理
├─ systemd 会将路径转换为 slice 和 scope
├─ 路径格式: /kubepods.slice/kubepods-pod-xxx.slice/...
└─ 非常复杂
Path 查询算法:
func GetCGroupPath(pod, subsystem) string {
// 步骤 1: 检查 CGroup 驱动
driver := GetCGroupDriver() // cgroupfs or systemd
if driver == "cgroupfs" {
// 直接路径
return ConstructCGPathCgroupFS(pod, subsystem)
} else if driver == "systemd" {
// systemd 路径
return ConstructCGPathSystemd(pod, subsystem)
}
}
func ConstructCGPathCgroupFS(pod, subsystem) string {
// 格式: /sys/fs/cgroup/{subsystem}/kubepods/{qos}/pod-{uuid}/
qos := pod.QoSClass
if qos == "Guaranteed" {
qos = "" // Guaranteed 没有 qos 目录
} else {
qos = "/" + strings.ToLower(qos)
}
return fmt.Sprintf(
"/sys/fs/cgroup/%s/kubepods%s/pod-%s/",
subsystem,
qos,
pod.UID,
)
}
func ConstructCGPathSystemd(pod, subsystem) string {
// systemd 的路径非常复杂
// 需要转换 UID 格式,添加 slice 等
// 简化示例(实际更复杂):
slice := fmt.Sprintf("kubepods-pod-%s.slice", pod.UID)
return fmt.Sprintf("/sys/fs/cgroup/%s/%s", subsystem, slice)
}
路径缓存:
为了避免频繁计算路径,使用缓存:
type PathCache struct {
mu sync.RWMutex
cache map[string]string // pod_uid -> path
ttl time.Duration // 缓存时间
}
func (pc *PathCache) Get(pod string) string {
pc.mu.RLock()
defer pc.mu.RUnlock()
return pc.cache[pod]
}
func (pc *PathCache) Set(pod, path string) {
pc.mu.Lock()
defer pc.mu.Unlock()
pc.cache[pod] = path
}
生产案例:路径查询和缓存的重要性
场景:计算 1000 个 Pod 的 CGroup 路径
不使用缓存:
┌─────────────────────────────────────┐
│ 每次查询都需要计算路径 │
├─────────────────────────────────────┤
│ 步骤 1: 读取 /proc/self/cgroup │
│ 步骤 2: 解析 cgroup 信息 │
│ 步骤 3: 根据驱动计算路径 │
│ 步骤 4: 验证路径存在 │
│ │
│ 每个 Pod: 5ms │
│ 1000 个 Pod: 5000ms(5 秒) │
│ QOSManager 的 1s 周期无法完成 │
└─────────────────────────────────────┘
使用缓存:
┌─────────────────────────────────────┐
│ 第一次查询 │
├─────────────────────────────────────┤
│ 计算 + 缓存: 5ms × 1000 = 5s │
│ 缓存 TTL: 10 分钟 │
│ │
│ 后续查询(缓存命中) │
├─────────────────────────────────────┤
│ 直接返回缓存值: 0.1ms × 1000 = 100ms│
│ 比计算快 50 倍! │
│ QOSManager 轻松完成 │
└─────────────────────────────────────┘
缓存失效处理:
Pod 被删除时:
├─ 缓存项不会自动删除(TTL 处理)
├─ 访问不存在的 CGroup 会失败
├─ ResourceExecutor 捕获 ENOENT,跳过
└─ 缓存自动失效(TTL 或手动清理)
Pod 重新调度时:
├─ 新 Pod 的 CGroup 路径可能不同
├─ 旧缓存失效,查询计算新路径
├─ 缓存新值
└─ 继续使用新路径
11.7 错误检测和诊断
CGroup 操作的错误分类:
CGroup 操作常见错误及诊断:
错误 1: ENOENT (No such file or directory)
├─ 原因: Pod 已删除或路径错误
├─ 诊断: 检查 StatesInformer 中是否有该 Pod
├─ 处理: 跳过该 Pod,移除缓存条目
└─ 可恢复: 是
错误 2: EACCES (Permission denied)
├─ 原因: 权限不足,通常是权限配置问题
├─ 诊断: 检查进程权限(id -u),检查 CGroup 文件权限
├─ 处理: 需要提权或修改 CGroup 配置
└─ 可恢复: 否(需要人工干预)
错误 3: EINVAL (Invalid argument)
├─ 原因: 参数无效(超出范围、格式错误)
├─ 诊断: 检查参数范围,对比 CGroup 接受的值
├─ 处理: 调整参数后重试
└─ 可恢复: 是
错误 4: EIO (Input/output error)
├─ 原因: 内核或文件系统故障
├─ 诊断: 检查系统日志 (dmesg),检查文件系统状态
├─ 处理: 重试,如果持续失败需要检查系统
└─ 可恢复: 可能(取决于故障情况)
错误 5: ERANGE (Numerical result out of range)
├─ 原因: 数值超出范围(如 memory.min > memory.max)
├─ 诊断: 检查当前的 min/max 值,确保 min <= max
├─ 处理: 调整参数顺序,先增大 max,再减小 min
└─ 可恢复: 是
诊断工具函数:
type CGError struct {
Errno int
Path string
Value string
Message string
}
func DiagnoseCGError(err CGError) {
switch err.Errno {
case ENOENT:
log.Info("CGroup not found, pod may be deleted")
case EACCES:
log.Error("Permission denied, check process privilege")
log.Error("Current UID:", os.Getuid())
log.Error("CGroup file permissions:", GetFilePermissions(err.Path))
case EINVAL:
log.Error("Invalid argument")
log.Error("Requested value:", err.Value)
log.Error("Valid range:", GetValidRange(err.Path))
log.Error("Current value:", ReadCurrentValue(err.Path))
case EIO:
log.Error("I/O error, check system logs")
log.Error("dmesg:", ExecuteCommand("dmesg | tail -20"))
case ERANGE:
log.Error("Value out of range")
log.Error("Constraint:", "memory.min must be <= memory.max")
log.Error("Current min:", Read(path + "/memory.min"))
log.Error("Current max:", Read(path + "/memory.max"))
}
}
生产调优指南
11.8 CGroup 管理配置
# CGroup 管理配置
apiVersion: v1
kind: ConfigMap
metadata:
name: koordlet-config
data:
cgroup-config.yaml: |
cgroupManager:
# 版本检测
versionDetection:
enabled: true
cacheTTL: 3600s # 检测结果缓存 1 小时
# 路径管理
pathManager:
enablePathCache: true
pathCacheTTL: 600s # 路径缓存 10 分钟
# 支持不同的驱动
supportedDrivers:
- cgroupfs
- systemd
# 参数转换
parameterConversion:
enableSmartConversion: true # 自动选择 v1/v2 API
validateRange: true # 验证参数范围
# 错误处理
errorHandling:
retryableErrors:
- EIO
- EAGAIN
- EINTR
maxRetries: 3
retryDelay: 100ms
nonRetryableErrors:
- EACCES
- EINVAL
11.9 故障排查
问题 1:路径计算错误
诊断:
1. 检查 CGroup 驱动
$ kubectl get nodes -o jsonpath='{.items[].status.nodeInfo.kubeletVersion}'
$ cat /var/lib/kubelet/kubeadm-flags.env | grep cgroup
2. 检查实际的 CGroup 路径
$ find /sys/fs/cgroup -name "pod-*" | head -5
3. 比对 Koordinator 计算的路径
$ kubectl logs koordlet-xxx | grep "cgroup path"
解决方案:
├─ 更新版本检测逻辑
├─ 增加对新驱动的支持
└─ 手动调整路径计算
问题 2:参数转换错误
诊断:
1. 检查错误日志
$ kubectl logs koordlet-xxx | grep "EINVAL\|ERANGE"
2. 验证参数值
$ cat /sys/fs/cgroup/memory/kubepods/pod-xxx/memory.min
$ cat /sys/fs/cgroup/memory/kubepods/pod-xxx/memory.max
3. 对比预期值
$ kubectl describe pod pod-xxx
解决方案:
├─ 修复参数转换公式
├─ 增加参数范围校验
└─ 调整参数顺序(如 min < max)
11.10 监控指标
# CGroup 管理监控
# 版本检测结果
koordlet_cgroup_version{subsystem="cpu"}
koordlet_cgroup_version{subsystem="memory"}
# 路径查询缓存命中率
koordlet_cgroup_path_cache_hit_ratio
# 参数转换成功率
koordlet_cgroup_parameter_conversion_success_total
koordlet_cgroup_parameter_conversion_errors_total{error="EINVAL"}
# 错误分布
koordlet_cgroup_operation_errors{errno="ENOENT"}
koordlet_cgroup_operation_errors{errno="EACCES"}