揭秘云原生混布资源调度器Koordinator (十一)CGroup 管理机制

51 阅读11分钟

核心使命与设计理念

11.1 CGroup 管理是什么?

CGroup 管理是 Koordinator 对 Linux CGroup 的抽象封装和生命周期管理,提供统一的 API 来控制 Pod 的资源使用。

核心职责

  1. 版本适配:支持 CGroup v1 和 v2,提供统一接口
  2. 生命周期:管理 Pod 对应的 CGroup 的创建、更新、删除
  3. 参数管理:提供 CPU、内存、I/O 等资源的参数访问接口
  4. 错误检测:检测 CGroup 操作的错误,提供诊断信息
  5. 性能监控:统计 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"}