WEBKT

Kubernetes 混部实践:基于 CPU Manager 扩展的在离线容器高精度隔离方案

9 0 0 0

在企业级 Kubernetes 集群中,为了提升资源利用率,“在离线混部(Co-location)”已成为降低算力成本的标配手段。然而,简单的将延迟敏感型(Latency-Sensitive, 在线)与高吞吐非实时型(Best-Effort/Batch, 离线)业务混合部署,极易引发严重的“噪邻效应(Noisy Neighbor)”。

Kubernetes 原生的 CPU Manager 提供了 static 策略来解决在线业务的绑核问题,但在复杂的混部场景下,其刚性的分配机制暴露出了巨大的局限性。本文将深入探讨如何基于 CPU Manager 进行深度扩展,结合 Linux 内核特性,构建一套精细化的在离线容器混部 CPU 调度与隔离策略。

原生 CPU Manager 应对混部时的痛点

Kubernetes 的 CPU Manager 通过 static 策略,允许将 Guaranteed Pod 独占绑定到特定的 CPU 核心上(排他性分配),其初衷是为了减少上下文切换和 Cache Miss。

                    ┌──────────────────────────────────────────┐
                    │               Node CPU Cores             │
                    ├──────────────┬─────────────┬─────────────┤
                    │    Core 0    │   Core 1    │   Core 2    │
                    ├──────────────┴─────────────┼─────────────┤
                    │   Exclusive (Guaranteed)   │ Shared Pool │
                    │       Online Pod A         │ Offline Pod │
                    └────────────────────────────┴─────────────┘

但在混部场景下,这种刚性设计存在两个致命缺陷:

  1. 资源碎片与浪费:当在线业务(Online)处于低谷期时,其独占的 CPU 核心只能闲置,离线业务(Offline)哪怕有高并发计算需求,也无法利用这些被绑定的空闲 CPU。
  2. 共享池性能退化:所有的 BurstableBestEffort 容器(包括部分次要在线和全部离线容器)都被塞进剩余的共享 CPU 池中(Shared Pool)。离线计算的突发流量极易在共享池内引发 CPU 争抢,从而间接通过总线锁、L3 Cache 污染等硬件级干扰,拖慢共享池内在线业务的响应时延。

为了解决这些问题,我们必须在 CPU Manager 的基础上,引入**动态回收(Reclaim)分级压制(Throttling)**机制。

在离线混部 CPU 调度方案架构

一个高可用的在离线混部系统,需要打通 Kubernetes 调度层、Node 代理(Kubelet/DaemonSet)以及 Linux 内核控制组(Cgroups)。其核心架构设计如下:

   +-------------------------------------------------------------+
   |                      Kubernetes Master                      |
   |   - Kube-Scheduler (混部感知、优先级调度、在离线拓扑对齐)     |
   +-------------------------------------------------------------+
                                  |
                                  v
   +-------------------------------------------------------------+
   |                         Worker Node                         |
   |                                                             |
   |  +-------------------------------------------------------+  |
   |  |                  Dynamic Agent (Colo-Agent)           |  |
   |  |  - 实时负载监控 (在线 CPU 实际使用率)                    |  |
   |  |  - 动态计算可回收 CPU (Reclaimed CPUs)                  |  |
   |  |  - Cgroups / Cpuset 动态调整引擎                      |  |
   |  +-------------------------------------------------------+  |
   |                              |                              |
   |                              v                              |
   |  +-------------------------------------------------------+  |
   |  |                 OS Kernel (Cgroups v2 / BPF)          |  |
   |  |  - Online Pods: Exclusive Cores (cpu.weight = 10000)  |  |
   |  |  - Offline Pods: cpuset.cpus = Reclaimed Cores         |  |
   |  |                  cpu.weight = 1 (低优先级压制)        |  |
   |  +-------------------------------------------------------+  |
   +-------------------------------------------------------------+

1. 动态 CPU 回收算法(Reclaim)

核心思路是:在线业务不用,离线业务借用;在线业务一旦需要,瞬间归还。

Colo-Agent 运行在每个 Node 上,以毫秒级周期监控在线 Pod(Guaranteed)的 CPU 实际使用率。假设某个在线 Pod 被绑定了 Core 0-3,但实际仅使用了 15% 的 CPU 算力。

$$\text{Reclaimed_CPUs} = \text{Total_Exclusive_CPUs} \times (1 - \text{Usage_Rate}) - \text{Safety_Margin}$$

通过设置一个安全水位线(Safety Margin),计算出当前可回收的物理核心。如果计算结果表明 Core 2 和 Core 3 处于极低负载状态,Colo-Agent 会动态将 Core 2-3 加入到离线业务的 cpuset.cpus 允许列表中。

2. 毫秒级弹性压制(Throttling)

当在线业务流量突增,需要重新使用 Core 2-3 时,如果离线业务不能瞬间退让,就会造成在线业务的 P99 延迟抖动。我们需要多重机制保障“快速退让”:

  • Cgroups Cpuset 快速剔除:Colo-Agent 必须在 10ms 内重写离线 Pod 的 cpuset.cpus,将 Core 2-3 从离线容器的可用核心中抹去。
  • CPU Sched Share 绝对压制:在内核层,离线 Pod 的 cpu.shares (cgroups v1) 或 cpu.weight (cgroups v2) 设为最小值(例如 1),而在线 Pod 保持默认或设为最大值。这样,即使离线业务还未来得及被移出核心,在 CPU 调度器(CFS)分配时间片时,在线业务也会以压倒性优势优先获得执行权。

核心技术实现:基于 Go 扩展 Cpuset 动态调控

在落地的实施中,我们可以编写一个轻量级的 Node Agent(利用 Go 语言),通过原生 cgroups 接口或 containerd 的 API,动态调整容器的物理绑核。

以下是实现动态分配回收逻辑的核心伪代码:

package main

import (
    "fmt"
    "io/ioutil"
    "path/filepath"
    "strconv"
    "strings"
    "time"
)

const (
    CgroupV2BasePath   = "/sys/fs/cgroup"
    OnlinePodCgroup    = "kubepods.slice/kubepods-burstable.slice" // 示例路径
    OfflinePodCgroup   = "kubepods.slice/kubepods-besteffort.slice"
    SafetyThresholdPct = 0.3 // 在线业务使用率低于30%时启动回收
)

// MonitorAndReclaim 监控并回收
func MonitorAndReclaim() {
    ticker := time.NewTicker(100 * time.Millisecond) // 100ms 高频探测
    for range ticker.C {
        // 1. 获取在线业务的 CPU 实际使用情况
        onlineUsage, err := getCPUUsage(OnlinePodCgroup)
        if err != nil {
            fmt.Printf("Failed to get online CPU usage: %v\n", err)
            continue
        }

        // 2. 根据负载决定是否释放或收回核心
        if onlineUsage < SafetyThresholdPct {
            // 在线业务空闲,将闲置核心分配给离线
            err = applyCpuset(OfflinePodCgroup, "2-3") // 动态允许离线业务使用 2-3 号核
            if err != nil {
                fmt.Printf("Failed to expand offline cpuset: %v\n", err)
            }
        } else {
            // 在线业务负载回升,瞬间收缩离线核心,仅保留 0 号避难核
            err = applyCpuset(OfflinePodCgroup, "0")
            if err != nil {
                fmt.Printf("Failed to shrink offline cpuset: %v\n", err)
            }
        }
    }
}

func getCPUUsage(cgroupPath string) (float64, error) {
    // 读取 cgroup 中的 stat 数据,计算 CPU 使用率
    // 此处省略具体的读取和差分计算逻辑,返回 0.0 ~ 1.0 的比例值
    return 0.15, nil 
}

func applyCpuset(cgroupPath, cores string) error {
    targetPath := filepath.Join(CgroupV2BasePath, cgroupPath, "cpuset.cpus")
    return ioutil.WriteFile(targetPath, []byte(cores), 0644)
}

func main() {
    fmt.Println("Starting High-precision Co-location CPU Controller...")
    MonitorAndReclaim()
}

内核级高精度隔离防护

仅仅在用户态进行 CPU 核心的挪移是不够的。由于 CPU 架构存在超线程(Hyper-Threading)、共享 L3 Cache 等特性,离线业务在运行过程中仍会干扰在线业务。我们必须开启以下内核级特性:

1. 内核 Core Scheduling (核心调度)

超线程(HT)共享同一个物理核心的执行单元和 L1/L2 缓存。如果一个物理核的 Thread 0 跑在线,Thread 1 跑离线,离线业务会通过侧信道或执行单元争抢干扰在线业务。

Linux 5.14+ 引入了 Core Scheduling。开启后,内核会确保:如果在线 Pod 正在某个超线程上运行,该物理核心的另一个超线程绝不会调度执行不相关的离线 Pod。 它要么运行同组的在线 Pod,要么强制进入 Idle(空闲)状态,从而实现硬件级别的强隔离。

在 Kubernetes 中,可通过为 Pod 写入特定的 Annotation 配合 runtime(如 Containerd)来启用该特性。

metadata:
  annotations:
    scheduling.sigs.k8s.io/core-scheduling-group: "online-latency-sensitive"

2. cgroups v2 的 cpu.idle 特性

在 cgroups v2 中,Linux 内核引入了 cpu.idle 文件。这是一个为混部场景量身定制的特性。

当把离线 Cgroup 的 cpu.idle 设置为 1 时,该控制组内的所有进程都将被标记为 SCHED_IDLE 调度优先级。这意味着:

  • 只要系统中有任何非 idle 进程(即在线业务)需要 CPU 资源,这些离线进程会无条件立即被抢占
  • 其抢占延迟在微秒级,远比调整 cpu.weightcpu.shares 更加彻底。
# 手动激活离线 Cgroup 的极低优先级属性
echo 1 > /sys/fs/cgroup/kubepods.slice/kubepods-besteffort.slice/cpu.idle

生产环境落地的部署拓扑

要在实际生产集群中实施此方案,建议通过标准的 Kubernetes 组件与 DaemonSet 进行渐进式落地:

方案层级 采用技术/组件 解决的核心问题
调度层 Volcano / Koordinator / Crane 负责在离线应用混合排程,确保单机不出现极端过载情况。
控制层 自研 Colo-Agent (或开源 Agent 扩展) 负责单机高频(10-50ms)扫描在线负载,执行动态 Cgroup 重新配置。
内核层 cgroups v2, Core Scheduling, Group Identity 负责底层硬件级资源物理隔离(Cache, 超线程, 内存带宽限制)。

生产配置参考:在线 Pod 与离线 Pod 声明

在线业务 Pod 声明,保证其归属于 Guaranteed 级别,并打上高优先级标签:

apiVersion: v1
kind: Pod
metadata:
  name: online-web-service
  labels:
    kollect.io/qos-class: latency-sensitive
spec:
  containers:
  - name: web
    image: nginx:latest
    resources:
      limits:
        cpu: "4"
        memory: "8Gi"
      requests:
        cpu: "4"
        memory: "8Gi" # limits=requests 触发 Kubelet CPU Manager 绑核

离线业务 Pod 声明,采用 BestEffort 级别,并声明其可以被动态回收核心抢占:

apiVersion: v1
kind: Pod
metadata:
  name: offline-batch-job
  labels:
    kollect.io/qos-class: best-effort
spec:
  containers:
  - name: compute-worker
    image: spark-worker:latest
    resources:
      limits:
        cpu: "16"       # 允许其在空闲时最大冲高到16核
        memory: "4Gi"
      requests:
        cpu: "100m"     # 极低的 request,保障调度弹性
        memory: "1Gi"

总结

基于 CPU Manager 扩展的在离线混合调度策略,不是单纯的静态隔离,而是一套**“用户态动态调整 + 内核级强力压制”**的联动控制系统。通过动态回收在线业务闲置的 CPU 核心、利用 cpu.idleCore Scheduling 进行硬件级避让,企业能够在保证在线业务 P99 时延不受损的前提下,将集群的整体 CPU 利用率从传统的 15% 提升至 45% 以上,实现真正的降本增效。

云原生架构深客 Kubernetes在离线混部

评论点评