揭秘云原生混布资源调度器Koordinator (二)核心 CRD 与 API 设计

30 阅读15分钟

API 体系概览

2.1 What - Koordinator API 设计的核心理念

Koordinator 的所有功能都通过 Kubernetes CRD(Custom Resource Definition) 暴露,遵循云原生设计哲学。而不是通过私有 API 或配置文件。

为什么选择 CRD?

  1. ✅ 与 Kubernetes 生态统一:用户可以使用 kubectl 管理 Koordinator 资源
  2. ✅ 声明式配置:一切都是 YAML,易于版本控制和自动化
  3. ✅ 原生支持 RBAC:权限管理原生集成
  4. ✅ Watch 机制:其他组件可以实时监听资源变化
  5. ✅ Validation 和 Conversion:API Server 原生支持数据验证和版本转换

2.2 Why - 为什么需要这些 CRD?

问题传统方案的不足Koordinator CRD 的解决
节点配置差异ConfigMap 无法动态计算,所有节点配置相同NodeSLO:为每个节点计算不同的 QoS 参数
指标数据查询无法统一查询节点和 Pod 的性能指标NodeMetric:提供标准化的指标查询接口
资源预留难以提前为批量 Pod 预留资源Reservation:支持资源预留,批量调度
设备管理GPU/FPGA 调度无法精细化Device:统一的设备管理接口
弹性配额ResourceQuota 过于静态ElasticQuota:支持动态配额扩展
Pod 迁移无法管理 Pod 迁移任务PodMigrationJob:正式的迁移任务对象

2.3 How - API 的数据流

用户操作(kubectl apply)
          ↓
    Webhook 验证和变更
          ↓
    存储到 etcd
          ↓
    各个组件通过 Watch 监听变化
          ↓
    组件根据资源内容做出决策
          ↓
    执行动作并更新资源状态

核心 CRD 详解

2.4 NodeSLO(节点服务级别目标)

2.4.1 What - NodeSLO 是什么?

NodeSLO 定义了单个节点的 QoS 策略配置和资源阈值策略。由 koord-manager 根据集群级策略动态计算和创建,Koordlet 监听并在节点上应用。

核心特点

  • 一个节点一个 NodeSLO(同名)
  • 由控制面自动管理,用户通常不直接修改
  • 支持节点级的差异化配置

2.4.2 完整结构示例

apiVersion: slo.koordinator.sh/v1alpha1
kind: NodeSLO
metadata:
  name: node-001  # 与节点同名,一一对应
spec:
  # 1. 资源阈值策略:定义各类 Pod 的资源保护边界
  resourceThresholdStrategy:
    enable: true
    thresholds:
      # CPU 相关
      cpu-suppress-percent: 65        # CPU 使用率 > 65% 触发 BE 抑制
      cpu-evict-threshold-percent: 85 # CPU 调度延迟 > 85% 触发驱逐
      # 内存相关
      memory-evict-threshold-percent: 75      # 内存 > 75% 触发驱逐
      memory-request-utilization-percent: 80  # 内存请求利用率阈值

  # 2. 资源 QoS 策略:针对每个 QoS 等级的详细参数
  resourceQOSStrategy:
    # LSR 级别:最强保护
    lsrClass:
      cpuQOS:
        enable: true
        groupIdentity: 0              # group identity,值越高优先级越高
      memoryQOS:
        enable: true
        minLimitPercent: 100          # memory.min = request * 100%,完全保护
        lowLimitPercent: 100          # memory.low = request * 100%
        throttlingPercent: 120        # memory.high = limit * 120%
        wmarkRatio: 95                # 异步回收水位(0-100)
        wmarkScalePermill: 50         # 水位调整系数(1-1000)
        wmarkMinAdj: -25              # 全局回收时的调整(-25 到 50)
      blkIOQOS:
        enable: false

    # LS 级别:中等保护
    lsClass:
      cpuQOS:
        enable: true
        groupIdentity: 2
      memoryQOS:
        enable: true
        minLimitPercent: 0
        lowLimitPercent: 0
        throttlingPercent: 100
        wmarkRatio: 90
        wmarkScalePermill: 30
        wmarkMinAdj: -25

    # BE 级别:最弱保护
    beClass:
      cpuQOS:
        enable: true
        groupIdentity: -1             # 最低优先级
      memoryQOS:
        enable: true
        minLimitPercent: 0
        lowLimitPercent: 0
        throttlingPercent: 100
        wmarkRatio: 80
        wmarkScalePermill: 30
        wmarkMinAdj: 50               # 最容易被全局内存回收

status:
  # 节点上 NodeSLO 的应用状态
  nodeMetric:
    # 最后一次更新时间
    updateTime: "2024-01-01T12:00:00Z"

2.4.3 Memory QoS 参数详解与生产调优

内存保护的三级机制

当内存使用率上升时的处理流程:

1. 使用量 < memory.min
   └─ 完全保护,不会被任何回收(全局回收也不行)
   └─ 用于 LSR/LSE,确保关键数据在内存中

2. memory.min < 使用 < memory.low
   └─ 尽力保护,优先级 > 其他 Pod
   └─ 全局内存紧张时,其他低优先级 Pod 优先被回收
   └─ 用于 LS,平衡保护和灵活性

3. memory.low < 使用 < memory.high
   └─ 开始异步直接回收(不阻塞应用)
   └─ 内核后台线程执行,应用继续运行
   └─ 影响较小

4. memory.high < 使用 < limit
   └─ 同步直接回收(可能阻塞应用)
   └─ 应用执行内存分配时会被阻塞进行回收
   └─ 可能导致延迟增加

5. 使用 > limit
   └─ OOM Kill:进程被内核杀死

参数调优表

参数LSR 推荐值LS 推荐值BE 推荐值含义
minLimitPercent1000-200memory.min = request * 百分比
lowLimitPercent1000-200memory.low = request * 百分比
throttlingPercent12010080memory.high = limit * 百分比
wmarkRatio959075异步回收水位(高→晚回收→更多冲突)
wmarkMinAdj-25-2550全局回收调整(低→不容易被回收)

生产调优案例

# 案例:在线数据库(LSR)的配置
lsrClass:
  memoryQOS:
    enable: true
    minLimitPercent: 100      # 数据库热数据必须在内存中
    lowLimitPercent: 100      # 缓冲池完全保护
    throttlingPercent: 120    # 允许超过 limit 一些(缓冲队列)
    wmarkRatio: 95            # 水位很高,尽量避免回收
    wmarkMinAdj: -25          # 全局回收时尽量不回收

# 案例:批处理任务(BE)的配置
beClass:
  memoryQOS:
    enable: true
    minLimitPercent: 0        # 没有最小保护
    lowLimitPercent: 0        # 也没有低保护
    throttlingPercent: 80     # limit 的 80% 开始回收
    wmarkRatio: 75            # 水位低,容易回收
    wmarkMinAdj: 50           # 全局回收时最先被回收

2.4.4 CPU QoS 参数详解

CPU Group Identity 的工作原理

Linux CGroup v2 中的 cpu.group_identity 机制:

内核调度器根据 group_identity 分配 CPU 时间片:

groupIdentity = 0  (LSR)
│   └─ 平时能占用 90% CPU 时间
│
groupIdentity = 2  (LS)
│   └─ 平时能占用 80% CPU 时间
│
groupIdentity = -1 (BE)
    └─ 平时能占用 70% CPU 时间

当某个 group 的 Pod 被抑制时:
LSR groupIdentity 0  → 不受抑制,继续 90%
LS  groupIdentity 2  → 被降低到 50%
BE  groupIdentity -1 → 被降低到 30%

结果:高优先级 Pod 被保护,低优先级被抑制

2.5 NodeMetric(节点指标)

2.5.1 What - NodeMetric 是什么?

NodeMetric 包含节点和 Pod 的实时性能指标数据,由 Koordlet 采集并定期上报。调度器和其他组件通过查询 NodeMetric 来做出决策。

2.5.2 完整结构示例

apiVersion: slo.koordinator.sh/v1alpha1
kind: NodeMetric
metadata:
  name: node-001  # 与节点同名
spec:
  nodeName: node-001
  collectionIntervalSeconds: 60  # 采集间隔

status:
  updateTime: "2024-01-01T12:00:00Z"
  
  # 节点级别的资源使用情况
  nodeMetric:
    usage:
      cpu: "64000m"            # 当前实际使用 64 core
      memory: "64Gi"           # 当前实际使用 64 GB
    request:
      cpu: "100000m"           # 所有 Pod request 总和 100 core
      memory: "100Gi"          # 所有 Pod request 总和 100 GB
    limit:
      cpu: "128000m"           # 所有 Pod limit 总和 128 core
      memory: "128Gi"          # 所有 Pod limit 总和 128 GB

  # 统计汇总数据
  beMetric:                    # BE Pod 的统计
    usage:
      cpu: "20000m"            # BE Pod 实际使用 20 core
      memory: "20Gi"           # BE Pod 实际使用 20 GB
    request:
      cpu: "30000m"
      memory: "30Gi"

  lsMetric:                    # LS Pod 的统计
    usage:
      cpu: "44000m"
      memory: "44Gi"
    request:
      cpu: "70000m"
      memory: "70Gi"

  # 各 Pod 的资源使用明细
  podMetrics:
  - podRef:
      namespace: default
      name: web-server-1
    containers:
    - name: app
      usage:
        cpu: "2000m"           # 该容器使用 2 core
        memory: "2Gi"          # 该容器使用 2 GB
      limit:
        cpu: "4000m"           # limit 4 core
        memory: "4Gi"          # limit 4 GB

2.5.3 调度中的应用示例

场景:koord-scheduler 在调度 Pod A(request 10 core)时

候选节点 1:
├─ 物理 CPU: 128 core
├─ 当前实际使用(来自 NodeMetric): 80 core
├─ 当前 request 总和: 100 core
├─ 可超卖比例: 30%
├─ 可超卖容量: (128 - 100) * 30% = 8.4 core
└─ Pod A 所需: 10 core
    └─ 结果:8.4 < 10,不满足条件,Filter 阶段排除

候选节点 2:
├─ 物理 CPU: 128 core
├─ 当前实际使用: 50 core
├─ 当前 request 总和: 80 core
├─ 可超卖容量: (128 - 80) * 30% = 14.4 core
└─ Pod A 所需: 10 core
    └─ 结果:14.4 > 10,满足条件,可以调度

优势:
✅ 基于实时使用情况(NodeMetric)调度,而不是静态的 request/limit
✅ 避免把 Pod 调到 request 充足但实际使用率很高的节点
✅ 更精确的资源利用决策

2.6 Reservation(资源预留)

2.6.1 What - Reservation 是什么?

Reservation 用于提前为某些工作负载预留资源。解决的问题是:批量 Pod(如 Spark Job、分布式训练)需要集群中有足够的连续空闲资源,才能同时调度所有副本。

2.6.2 完整结构示例

apiVersion: scheduling.koordinator.sh/v1alpha1
kind: Reservation
metadata:
  name: spark-job-reservation-202401
spec:
  # 定义要预留的资源模板
  template:
    spec:
      containers:
      - name: spark-executor
        resources:
          requests:
            cpu: "8"            # 预留 8 core
            memory: "16Gi"      # 预留 16 GB
          limits:
            cpu: "8"
            memory: "16Gi"

  # 定义谁可以使用这个预留
  owners:
  - labelSelector:
      matchLabels:
        job-id: "spark-123"     # 匹配这些 Label 的 Pod 可以使用
        app: "spark-executor"

  # 预留的有效期
  ttl: 10m                       # 10 分钟后过期
  
  # 预分配:等待足够空闲资源后再预留
  preAllocation: true
  
  # 一次性使用:只有第一个 owner 可以使用
  allocateOnce: true

status:
  # 预留的状态阶段
  phase: Available               # Pending / Available / Succeeded / Failed
  
  # 预留被调度到的节点
  nodeName: node-001
  
  # 当前使用者
  currentOwners:
  - name: spark-executor-1
    namespace: default
  - name: spark-executor-2
    namespace: default
  
  # 预留的资源和已分配资源
  allocatable:
    cpu: "8"
    memory: "16Gi"
  allocated:
    cpu: "8"
    memory: "16Gi"              # 已有 1 个 Pod 使用,再有 7 core 可用

2.6.3 工作流程示例

时间轴:

T0:00 用户创建 Spark Job
      └─ 同时创建一个 Reservation
         ├─ 预留 20 个 executor × 8 core = 160 core
         └─ TTL = 10 分钟

T0:05 koord-scheduler 评估集群
      ├─ 找到 3 个节点,每个可以容纳 6-7 个 executor
      ├─ 总容量足够 20 个 executor
      └─ Reservation 状态变为 Available

T0:10 Spark Job 的 Pod 开始创建(20 个 executor Pod)
      ├─ 第 1-10 个 Pod 创建
      ├─ 调度器识别 owner 匹配
      ├─ 优先在已预留的节点上调度
      ├─ Pod 绑定,从预留中分配资源
      └─ Reservation.allocated 增加

T1:00 所有 20 个 Pod 都被调度成功
      └─ Reservation 状态变为 Succeeded

T2:00 Spark Job 执行完成,Pod 被删除
      ├─ Reservation 自动清理
      └─ 预留的资源释放回集群

优势:
✅ 保证批量 Pod 的调度成功率
✅ 避免部分 Pod 无法调度的尴尬局面
✅ 支持时间限制和预分配策略

2.6.4 生产案例

# 案例:Flink 离线处理 Job
apiVersion: scheduling.koordinator.sh/v1alpha1
kind: Reservation
metadata:
  name: flink-job-batch-20240101
spec:
  template:
    spec:
      containers:
      - name: flink-taskmanager
        resources:
          requests:
            cpu: "4"
            memory: "8Gi"
  
  owners:
  - labelSelector:
      matchLabels:
        flink-job: "batch-20240101"
        app: "flink-taskmanager"
  
  ttl: 2h  # Flink Job 预计运行 2 小时
  preAllocation: true
  allocateOnce: false  # 可以多个 Job 共享(不是一次性)

---
# 对应的 Flink Job Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flink-batch-job
spec:
  replicas: 100  # 100 个 TaskManager
  selector:
    matchLabels:
      app: flink-taskmanager
  template:
    metadata:
      labels:
        flink-job: "batch-20240101"
        app: "flink-taskmanager"
    spec:
      containers:
      - name: flink-taskmanager
        resources:
          requests:
            cpu: "4"
            memory: "8Gi"

2.7 ElasticQuota(弹性配额)

2.7.1 What - ElasticQuota 是什么?

传统 ResourceQuota 是静态的,无法根据集群空闲资源动态调整。ElasticQuota 支持基础配额保证 + 弹性扩展的模式。

2.7.2 完整结构示例

apiVersion: quota.koordinator.sh/v1alpha1
kind: ElasticQuota
metadata:
  name: team-a-quota
  namespace: team-a
spec:
  # 硬配额:即使集群资源充足也不能超过
  hard:
    cpu: "100"
    memory: "100Gi"
    pods: "100"
  
  # 最小配额:集群必须保证
  min:
    cpu: "50"
    memory: "50Gi"
    pods: "50"
  
  # 最大可扩展配额:集群空闲时可以扩展到这个值
  max:
    cpu: "200"
    memory: "150Gi"
    pods: "150"
  
  # 配额分配优先级(数字越大优先级越高)
  priority: 50

status:
  # 当前实际可用配额(动态变化)
  allocatedQuota:
    cpu: "150"
    memory: "120Gi"
    pods: "100"
  
  # 当前使用量
  used:
    cpu: "120"
    memory: "100Gi"
    pods: "80"
  
  # 已分配给 Pod 的配额
  podAllocatedQuota:
    cpu: "120"
    memory: "100Gi"
    pods: "80"

2.7.3 动态扩展的工作原理

场景:两个 Team 的配额管理

集群总资源:500 core, 500 GB

Team A 配置:
├─ min: 100 core (硬保证)
├─ max: 300 core (最多可获得)
└─ hard: 200 core (最高不能超过)

Team B 配置:
├─ min: 100 core (硬保证)
├─ max: 300 core (最多可获得)
└─ hard: 200 core (最高不能超过)

场景 1:集群充足(可用 300 core)
┌─────────────────────────────────┐
│ Team A 当前用: 80 core          │
│ Team B 当前用: 80 core          │
│ 可用资源: 340 core              │
├─────────────────────────────────┤
│ Team A 可分配: 100(min) + 100(扩展) = 200 core  │
│ Team B 可分配: 100(min) + 100(扩展) = 200 core  │
│ → 都可以申请新 Pod              │
└─────────────────────────────────┘

场景 2:集群紧张(只可用 50 core)
┌─────────────────────────────────┐
│ Team A 当前用: 90 core          │
│ Team B 当前用: 100 core         │
│ Team A 新增需求: +30 core       │
│ 可用资源: 50 core               │
├─────────────────────────────────┤
│ 保证最少配额:                   │
│ ├─ Team A min: 100 core (已用 90,还有 10)  │
│ └─ Team B min: 100 core (已用 100,没有)    │
│                                 │
│ Team APod 只能分配: 10 core │
│ (min 剩余) + 40 core (扩展的一部分) │
│ = 50 core,但优先级更高的先拿  │
└─────────────────────────────────┘

场景 3:有一个 Team 优先级更高
┌─────────────────────────────────┐
│ Team A priority: 100 (高)      │
│ Team B priority: 50 (低)       │
│                                 │
│ 集群只剩 50 core 可用           │
│ Team A 请求 +50 core           │
│ Team B 请求 +40 core           │
│                                 │
│ 结果:Team A 获得全部 50 core  │
│      Team B 无法获得扩展配额    │
└─────────────────────────────────┘

2.7.4 生产案例

# 案例:高价值业务 Team(在线服务)
apiVersion: quota.koordinator.sh/v1alpha1
kind: ElasticQuota
metadata:
  name: critical-team-quota
  namespace: critical-team
spec:
  hard:
    cpu: "500"
    memory: "500Gi"
  min:
    cpu: "300"       # 最少保证 300 core
    memory: "300Gi"
  max:
    cpu: "500"       # 最多可扩展到 500 core
    memory: "500Gi"
  priority: 100      # 最高优先级

---
# 案例:低优先级业务 Team(离线计算)
apiVersion: quota.koordinator.sh/v1alpha1
kind: ElasticQuota
metadata:
  name: batch-team-quota
  namespace: batch-team
spec:
  hard:
    cpu: "200"
    memory: "200Gi"
  min:
    cpu: "100"       # 最少保证 100 core
    memory: "100Gi"
  max:
    cpu: "300"       # 可扩展到 300 core
    memory: "300Gi"
  priority: 10       # 低优先级

2.8 Device(设备管理)

2.8.1 What - Device 是什么?

Device 用于管理 GPU、FPGA、RDMA 等特殊硬件设备。支持设备级的精细化调度和共享。

2.8.2 结构示例

apiVersion: scheduling.koordinator.sh/v1alpha1
kind: Device
metadata:
  name: node-001
spec:
  devices:
  - deviceType: "gpu"
    deviceModel: "nvidia-a100"
    uuid: "gpu-uuid-001"
    totalMemory: "40Gi"           # GPU 显存 40GB
    health: true                   # 设备健康状态
    
  - deviceType: "gpu"
    deviceModel: "nvidia-a100"
    uuid: "gpu-uuid-002"
    totalMemory: "40Gi"
    health: true
    
  - deviceType: "rdma"
    deviceModel: "mellanox-ib"
    uuid: "rdma-uuid-001"
    totalMemory: "0"               # RDMA 不需要显存
    health: true
    
  - deviceType: "fpga"
    deviceModel: "xilinx-u250"
    uuid: "fpga-uuid-001"
    totalMemory: "0"
    health: true

status:
  # 各设备的使用情况
  deviceAllocations:
  - device:
      deviceType: "gpu"
      uuid: "gpu-uuid-001"
    allocations:
    - pod:
        namespace: default
        name: ml-training-1
      container: training
      allocated: "50%"             # 该 Pod 分配了 50% 的 GPU 资源
  
  - device:
      deviceType: "gpu"
      uuid: "gpu-uuid-002"
    allocations:
    - pod:
        namespace: default
        name: ml-training-2
      container: training
      allocated: "100%"            # 独占该 GPU

总 - API 的完整数据流与交互

2.9 API 的端到端数据流

集群级配置
└─ ConfigMap (slo-controller-config)
    │ 定义全局 QoS 策略
    │
    ↓
koord-manager (NodeSLO Controller)
    │ 监听 ConfigMap
    │ 为每个节点计算对应参数
    │
    ↓
NodeSLO CRD (节点级 QoS 配置)
    │ 每个节点一个
    │
    ├────────────→ Koordlet (StatesInformer 监听)
    │               │ 应用 NodeSLO 配置
    │               │
    │               ├─→ QOSManager
    │               │    计算策略
    │               │
    │               ├─→ ResourceExecutor
    │               │    执行 CGroup 更新
    │               │
    │               └─→ 上报 NodeMetric
    │
    ↓
NodeMetric CRD (节点实时指标)
    │ 每个节点一个,每分钟更新
    │
    ├────────────→ koord-scheduler
    │              ├─ LoadAware Plugin
    │              └─ 负载感知调度决策
    │
    ├────────────→ koord-manager
    │              └─ 聚合计算可超卖资源量
    │
    └────────────→ koord-descheduler
                   └─ 发现不合理调度

2.10 关键 API 调用示例

创建预留资源

kubectl apply -f reservation.yaml

创建弹性配额

kubectl apply -f elastic-quota.yaml

查询节点指标

kubectl get nodemetric node-001 -o yaml
# 查看该节点的实时 CPU/内存使用情况

查询节点 SLO 配置

kubectl get nodeslo node-001 -o yaml
# 查看该节点的 QoS 策略参数

总结 - CRD 设计的最佳实践

2.11 生产中的常见问题与解决

问题 1:NodeSLO 配置更新后没有立即生效

原因:Koordlet 的 Watch 有延迟(通常 1-10 秒)

解决:
1. 查看 Koordlet 日志确认是否监听到变化
2. 手动重启 Koordlet 加快生效
3. 增加 Watch 超时时间(如果是网络问题)

验证:
kubectl get nodeslo <node-name> -o yaml | grep -i status

问题 2:Reservation 一直处于 Pending 状态

原因:集群中没有足够的连续空闲资源

解决:
1. 检查集群可用资源
   kubectl top nodes
2. 调整 Reservation 的 preAllocation 参数
3. 减少预留资源量
4. 等待其他 Pod 释放资源

查看细节:
kubectl describe reservation <name>

问题 3:ElasticQuota 的实际可用配额与预期不符

原因:其他 Team 的 Pod 占用了扩展配额

解决:
1. 查看各 Team 的实际配额使用
   kubectl get elasticquota -A
2. 调整 hard 和 max 参数
3. 根据实际需求调整优先级

优化建议:
- 关键业务设置更高的 min(硬保证)
- 设置合理的 hard(防止单个 Team 过度使用)
- 根据业务特性调整 max 和 priority

2.12 API 版本管理与向后兼容

Koordinator API 版本历程:

v1alpha1 (当前稳定版本)
├─ NodeSLO
├─ NodeMetric
├─ Reservation
├─ ElasticQuota
├─ Device
└─ PodMigrationJob

向后兼容原则:
✅ 新增字段总是 Optional
✅ 删除字段通过 Deprecated 标记,保留一个大版本
✅ 字段类型更改通过新字段实现
✅ 支持版本间的自动转换(Conversion Webhook)

总结 - 章节关键要点

2.13 快速查询表

CRD作用域管理者消费者更新频率
NodeSLOClusterkoord-managerKoordlet不频繁(配置变化时)
NodeMetricClusterKoordletkoord-scheduler, koord-descheduler, koord-manager每分钟
ReservationClusterkoord-schedulerkoord-scheduler (调度)创建/销毁时
ElasticQuotaNamespacedkoord-managerWebhook (准入), koord-scheduler动态
DeviceCluster用户维护或自动发现koord-scheduler不频繁

本章要点

  • 理解 Koordinator API 的设计哲学
  • 掌握各核心 CRD 的结构和使用方法
  • 学会根据生产场景配置 NodeSLO、Reservation、ElasticQuota
  • 了解 API 的完整数据流和最佳实践