上一篇 下一篇 分享链接 返回 返回顶部

如何在Kubernetes原生调度器上实现 基于GPU利用率的动态迁移与负载均衡

发布人:Minchunlin 发布时间:2025-08-04 21:37 阅读量:814


我记得那是今年 3 月的一个凌晨,我独自在香港葵涌的数据中心里调度 GPU 集群。机房里弥漫着空调的冷风和风扇的低鸣声。Grafana 的 GPU 热力图上,一块 A100 的负载 96%,相邻三块才 20% 左右,导致客户的推理请求 P99 延迟飙升到 400ms。

Kubernetes 默认的调度器在 GPU 任务上几乎是“盲的”——它只看节点资源请求,不看实际利用率。当训练任务长时间占满一张卡时,新的推理 Pod 往往被分配到负载已经高得发烫的 GPU 上,最终导致整体吞吐下降。

那一夜,我完成了第一版 GPU 负载感知调度插件 和 动态迁移策略,让推理任务自动避开高负载 GPU,训练任务按需迁移到空闲节点,P95 延迟稳定在 150ms。以下是我的完整实操经验。

1. 香港机房集群架构与痛点

我维护的 GPU 集群部署在香港葵涌机房,硬件和软件环境如下:

GPU 节点:

  • 8 × Supermicro 4029GP-TRT
  • 每台 4 × NVIDIA A100 80GB SXM4(启用 NVSwitch)
  • 双 6248R + 512GB RAM

网络与存储:

  • 双 100Gb RDMA + 25Gb 管理网络
  • CephFS 分布式存储,吞吐约 50GB/s

Kubernetes 环境:

  • K8s v1.27 + kube-scheduler 自定义 Extender
  • NVIDIA GPU Operator + DCGM Exporter

遇到的痛点

GPU 利用率不均:

推理任务被随机调度到高负载 GPU,导致延迟暴涨。

显存碎片化:

推理任务频繁创建销毁,显存利用率高但分布零散。

缺乏负载感知:

默认 K8s 只按 GPU 数量调度,不感知显存和算力利用率。

2. 构建 GPU 负载感知调度器

我选择了 K8s Scheduler Extender 的方式,而不是完全重写调度器,核心思路:

  • DCGM 采集 GPU 指标(显存占用、GPU 利用率、MIG 实例利用率)
  • 调度打分插件基于 GPU 负载给节点评分
  • 低分节点拒绝分配,保证新任务落在空闲 GPU 上

2.1 部署 DCGM Exporter

在每台 GPU 节点上部署 NVIDIA 官方 DCGM Exporter:

helm repo add nvidia https://nvidia.github.io/gpu-helm-charts
helm install dcgm-exporter nvidia/dcgm-exporter

确认能采集到核心指标:

  • DCGM_FI_DEV_GPU_UTIL —— GPU 利用率
  • DCGM_FI_DEV_FB_USED —— 显存使用量(MiB)
  • DCGM_FI_DEV_FB_TOTAL —— 显存总量

2.2 编写调度插件(Extender)

我用 Python + Flask 写了一个轻量的调度器 Extender 服务,核心逻辑:

  • 接收 K8s 的 Pod 调度请求(包含候选节点)
  • 查询 Prometheus/PushGateway 的 GPU 负载数据
  • 根据 GPU 空闲率给节点打分
  • 返回按得分排序的节点列表

核心打分逻辑示例:

from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

PROMETHEUS_URL = "http://prometheus:9090/api/v1/query"

def get_gpu_metrics(node):
    # 查询 GPU 利用率和显存占用
    util_query = f'DCGM_FI_DEV_GPU_UTIL{{instance="{node}"}}'
    mem_query = f'DCGM_FI_DEV_FB_USED{{instance="{node}"}}'
    util = float(requests.get(PROMETHEUS_URL, params={"query": util_query}).json()["data"]["result"][0]["value"][1])
    mem = float(requests.get(PROMETHEUS_URL, params={"query": mem_query}).json()["data"]["result"][0]["value"][1])
    return util, mem

@app.route('/score', methods=['POST'])
def score_nodes():
    data = request.get_json()
    scores = []
    for node in data["nodes"]["items"]:
        node_name = node["metadata"]["name"]
        gpu_util, gpu_mem = get_gpu_metrics(node_name)
        score = max(0, 100 - gpu_util - gpu_mem/1000)  # 简单权重
        scores.append({"name": node_name, "score": score})
    return jsonify({"scores": scores})

 

通过这种方式,K8s 原生调度器会优先分配 GPU 负载低的节点。

3. 动态迁移与负载均衡策略

调度器只能解决新任务分配问题,但老任务的负载可能导致节点不均衡。因此,我在生产环境中实现了 动态迁移策略:

持续监控 GPU 热力图

若单卡利用率 >90% 且持续 5 分钟

且集群存在空闲 GPU <30% 利用率

触发 Live Migration(KVM 层)

对训练 VM 使用 qm migrate 在线迁移到低负载节点

利用 NVSwitch + RDMA 网络,迁移时延控制在 3~5 秒

结合 Pod Eviction & Reschedule

对非关键推理 Pod 使用 K8s Eviction API 驱逐

重新由调度器分配到空闲节点

示例迁移脚本(shell):

# 查询负载最高的虚拟机并迁移
high_vm=$(gpu_monitor | sort -k2 -nr | head -1 | awk '{print $1}')
qm migrate $high_vm gpu-node-3 --online

实际操作中,我曾在凌晨 2 点手动触发迁移,当时 3 台节点满载,其他 5 台闲置,迁移完成后集群利用率从极度倾斜恢复到 70% 均衡。

4. 监控与告警体系

在香港机房,我配置了完整的 GPU 可观测体系:

Prometheus + Grafana

GPU 热力图(按 MIG 实例显示显存与算力占用)

节点均衡度(最大负载与最小负载比值)

Slack / PagerDuty 告警

单卡 >90% 且持续 5min 自动告警

触发自动迁移或通知运维值班

这种“闭环”保证了训练和推理任务混合场景下的低延迟与高吞吐。

5. 实战总结

经过多轮调优,我们的香港 GPU 集群实现了:

  • 新任务智能落位:负载感知调度器避免了盲目分配
  • 老任务动态迁移:结合 KVM live migration 和 Pod eviction
  • 集群利用率均衡:单卡负载差异从 70% 缩小到 20%
  • 延迟稳定性提升:推理任务 P95 延迟降低 60%+

最让我有成就感的,是深夜看着 Grafana 热力图慢慢从“血红色”变成绿色梯度,那是我用代码和策略把机房的风扇声变得更“均匀”的瞬间。

目录结构
全文