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

我记得那是今年 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 热力图慢慢从“血红色”变成绿色梯度,那是我用代码和策略把机房的风扇声变得更“均匀”的瞬间。