Kubernetes GPU 调度:拓扑感知与多租户隔离
Kubernetes GPU 调度:拓扑感知与多租户隔离
一、6 月 14 日集群事故
凌晨,生产集群的 GPU 资源开始打架。几个高优先级的 AI 训练任务同时提交,老调度器只认 CPU 和内存,对 GPU 内部的 NVLink、NVSwitch 连接关系一无所知。
结果就是:需要高带宽通信的分布式训练 Pod,被调度器扔到了物理距离远、拓扑连接差的节点上。NCCL 通信延迟直接飙升,训练吞吐量掉了 40% 以上。更糟的是,低优先级任务的脏数据堵在高速缓存里,部分 GPU 显存溢出,驱动直接重置。
问题很明确:调度器不知道硬件长什么样,多租户之间也没有隔离。
二、拓扑感知怎么做的
调度器需要在决策时算一个亲和性得分。思路不复杂:
- 通过 Device Plugin 拿到所有节点的 GPU 拓扑图,包括连接类型和带宽
- 新 Pod 进来时,遍历可调度节点,算它和现有负载之间的拓扑距离
- 连接带宽越高、跳数越少,距离权重越低
- 多租户共享时,给每个租户配独立的资源池,防止一家独大
数据流是这样的:调度器从 API Server 拿 Pod 信息,用 Informer 监听节点状态,查拓扑数据库打分,最后把最优节点绑定回 API Server。
sequenceDiagram participant Pod as 训练任务 Pod participant Scheduler as K8s 调度器 participant TopoDB as 拓扑感知数据库 participant Node as 计算节点 GPU participant API as API Server Pod->>Scheduler: 提交资源请求 (含 GPU 拓扑标签) Scheduler->>TopoDB: 查询节点 GPU 拓扑状态 TopoDB-->>Scheduler: 返回拓扑图 (NVLink/PCIe 关系) Scheduler->>Scheduler: 执行过滤与评分算法 Note over Scheduler: 计算拓扑距离与租户权重 Scheduler->>Node: 选择最优节点进行绑定 Node-->>Scheduler: 确认资源预留 Scheduler->>API: 更新 Pod 绑定状态 API-->>Pod: 调度成功,开始初始化评分时如果两个节点资源都够,优先选拓扑距离近、且当前租户占用率没超阈值的。
三、Go 模拟实现
用标准库写了个脚本,sync、time、fmt,没碰外部依赖。核心是SelectBestNode,逻辑分三步:资源过滤、拓扑打分、租户负载惩罚。
package main import ( "fmt" "sync" time" ) type GPUNode struct { ID string GPUCount int TopologyMap map[string]int TenantLoad float64 } type PodRequest struct { ID string RequiredGPU int TenantID string } type Scheduler struct { nodes map[string]*GPUNode mu sync.RWMutex } func NewScheduler() *Scheduler { return &Scheduler{nodes: make(map[string]*GPUNode)} } func (s *Scheduler) AddNode(node *GPUNode) { s.mu.Lock() defer s.mu.Unlock() s.nodes[node.ID] = node } func (s *Scheduler) SelectBestNode(req *PodRequest) string { s.mu.RLock() defer s.mu.RUnlock() var bestNodeID string maxScore := -1.0 for _, node := range s.nodes { if node.GPUCount < req.RequiredGPU { continue } topoScore := 0.0 if node.TopologyMap != nil { for _, weight := range node.TopologyMap { if weight < 2 { topoScore += 10.0 } } } loadPenalty := node.TenantLoad * 20.0 finalScore := topoScore - loadPenalty if finalScore > maxScore { maxScore = finalScore bestNodeID = node.ID } } return bestNodeID } func main() { sched := NewScheduler() sched.AddNode(&GPUNode{ID: "Node-A", GPUCount: 8, TopologyMap: map[string]int{"Node-B": 1}, TenantLoad: 0.2}) sched.AddNode(&GPUNode{ID: "Node-B", GPUCount: 8, TopologyMap: map[string]int{"Node-A": 1}, TenantLoad: 0.8}) req := &PodRequest{ID: "Train-001", RequiredGPU: 4, TenantID: "Tenant-X"} selected := sched.SelectBestNode(req) fmt.Printf("为任务 %s 选择的最优节点: %s\n", req.ID, selected) }生产环境里TopologyMap来自真实的拓扑发现插件,TenantLoad靠实时监控指标更新。这个模拟只是为了验证过滤和评分逻辑跑得通。
四、故障恢复步骤
运维团队按这个顺序处理的:
先查 Device Plugin。kubectl get pods -n kube-system看 NVIDIA Device Plugin 是否正常。如果插件重启频繁,检查/var/lib/kubelet/device-plugins下的 socket 文件,这通常是 GPU 资源上报失败的原因。
调调度器权重。把TopologyAwareScore插件权重调高,纯资源请求权重调低。ConfigMap 里把拓扑亲和性评分系数从默认值提到 0.8,强制调度器优先选拓扑邻近节点。
清理脏数据。对负载高的节点执行kubectl drain,配合kubectl delete pod清理Error或OOMKilled状态的残留 Pod。节点上的/tmp和共享内存目录也手动清一遍,释放被占用的 I/O 资源。
验证效果。提交测试任务,看kubectl describe pod的 Events 部分有没有拓扑评分。节点上用nvidia-smi topo -m确认 GPU 通信路径,NCCL 环境配置也得对。
加租户配额。Namespace 级别上ResourceQuota和LimitRange,限制单个租户能占的 GPU 显存总量。
折腾了两个小时,集群恢复正常,训练吞吐量回到基准线。
五、几点经验
这次事故暴露了两个问题:调度器不知道 GPU 拓扑长什么样,多租户之间缺乏隔离。解决思路也不复杂——让调度器能算拓扑距离,给每个租户配资源池。
Go 模拟验证了评分逻辑可行,后续可以往 K8s Scheduler Framework 里集成。排障流程也梳理出来了:查 Device Plugin → 调权重 → 清脏数据 → 验证 → 加配额。
这套方案对大规模 AI 训练集群有参考价值,但具体参数还得根据实际硬件和负载调整。
