Kubernetes Pod 反亲和:不要把同一模型的副本都放在一个节点 — 方向2
一、三个副本都在同一台 GPU 节点上的概率有多大
在一个有 12 个 GPU 节点的集群中,部署 3 个推理副本。Kubernetes 默认调度器的最优解是什么?在节点资源充裕的情况下,它可能会把 3 个副本全部调度到同一台 GPU 节点上——因为那台节点的资源评分最高、网络跳数最少。
然后这台节点因为 GPU 驱动故障重启了。3 个副本全部不可用,服务中断。
这就是默认调度策略与高可用需求之间的矛盾。Kubernetes 的默认调度目标是最优资源匹配,而非分散风险。在一个没有亲和性约束的部署中,同一个模型的多个副本被调度到同一个节点上的概率取决于节点数量与副本数量,不是可以忽略的小概率事件。
生产环境的对比数据印证了这一点。在一个 8 节点、每节点 4 副本的典型推理部署中,如果不加反亲和约束,约 30% 的节点故障会导致超过 50% 的副本同时不可用。这意味着表面上"做了多副本",但故障域高度重叠,可用性增益远低于预期。
二、反亲和策略的层次与调度决策
Pod 反亲和通过podAntiAffinity来控制调度器避免将特定标签的 Pod 放在同一拓扑域中。但不同的topologyKey决定了反亲和的粒度:
节点级反亲和(kubernetes.io/hostname)是最实用的选择。它确保同一模型的副本分布在不同的物理节点上。当 GPU 节点因为驱动故障、内核崩溃、显存硬件错误而宕机时,只影响一个副本。
但有一个陷阱:如果节点数量少于副本数量,requiredDuringScheduling级别的反亲和会导致部分副本无法调度。此时需要使用preferredDuringScheduling让调度器在无法满足反亲和时仍然调度 Pod,并配合告警提示运维扩容节点。
三、生产级反亲和配置与批量调度的冲突处理
以下是针对推理服务的高可用反亲和配置:
apiVersion: apps/v1 kind: Deployment metadata: name: llama-inference spec: replicas: 3 selector: matchLabels: app: llama-inference model: llama-70b template: metadata: labels: app: llama-inference model: llama-70b spec: affinity: podAntiAffinity: # 硬性约束:必须分散在不同节点 requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: model operator: In values: - llama-70b topologyKey: kubernetes.io/hostname # 软性约束:优先分散在不同防火墙域(如果有机架感知需求) preferredDuringSchedulingIgnoredDuringExecution: - weight: 50 podAffinityTerm: labelSelector: matchExpressions: - key: model operator: In values: - llama-70b topologyKey: topology.kubernetes.io/zone # 配合拓扑分布约束,适用于更大规模部署 topologySpreadConstraints: - maxSkew: 1 topologyKey: kubernetes.io/hostname whenUnsatisfiable: DoNotSchedule labelSelector: matchLabels: app: llama-inference containers: - name: inference image: llama-inference:v2.1 resources: limits: nvidia.com/gpu: "1"配置中同时使用了podAntiAffinity和topologySpreadConstraints。两者的区别是:反亲和关注"不要把相同标签的 Pod 放一起",而拓扑分布约束关注"各域的 Pod 数量要均衡"。在大多数场景下,两者可以协同工作。但在 GPU 资源紧张的集群中,如果两者都设为硬约束,可能导致调度卡死。
当多个推理模型的部署同时进行滚动更新时,调度器会面临一个复杂问题:每个模型的副本都需要分散在不同节点,但节点上的 GPU 插槽是有限的。这可能导致"调度僵局"——所有部署都在等待对方腾出节点。
// ScheduleGuard 批量调度时的僵局检测与缓解 type ScheduleGuard struct { client kubernetes.Interface } // DetectStalemate 检测调度僵局 func (g *ScheduleGuard) DetectStalemate(namespace string) ([]string, bool) { // 查找所有因反亲和约束而 Pending 超过 5 分钟的 Pod pods, err := g.client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{ FieldSelector: "status.phase=Pending", }) if err != nil { return nil, false } var stalePods []string for _, pod := range pods.Items { if time.Since(pod.CreationTimestamp.Time) < 5*time.Minute { continue } // 检查是否因反亲和而 Pending for _, condition := range pod.Status.Conditions { if condition.Type == corev1.PodScheduled && condition.Reason == corev1.PodReasonUnschedulable && strings.Contains(condition.Message, "anti-affinity") { stalePods = append(stalePods, pod.Name) } } } return stalePods, len(stalePods) > 2 // 超过2个僵死Pod视为僵局 }僵局检测的目的不是自动化解决——调度僵局通常意味着物理资源不足,唯一的出路是扩容节点或降低约束级别。
四、反亲和的代价与场景选择
反亲和约束并非无成本。
调度灵活性降低:硬性反亲和约束减少了调度器的选择空间。在资源紧张的情况下,这可能导致 Pod 长时间 Pending,而一个有反亲和约束的 Pending Pod 比一个调度到同节点的 Pod 更糟糕。
故障域与资源利用率的博弈:如果只有 2 个 GPU 节点,却要求 3 个副本硬约束反亲和,至少 1 个副本永远无法调度。这是配置错误,但很容易被忽视。
GPU 型号差异的复杂性:如果集群混部了 A100、A800 和 T4,反亲和配置可能导致推理副本被调度到性能差异很大的 GPU 上。这时候需要配合nodeSelector或节点亲和来约束 GPU 型号范围。
适用边界:
- 适合:GPU 节点数 ≥ 副本数 × 2 的集群,有足够的冗余空间。
- 适合:在线推理服务,对单点故障零容忍。
- 不适合:GPU 节点数与副本数接近的场景,硬性反亲和会导致 Pod Pending。
- 不适合:批处理推理任务,任务可重试,对故障不敏感。
五、总结
Pod 反亲和的本质是将故障域分散到不同的物理节点。对于推理服务,节点级反亲和应用kubernetes.io/hostname是最核心的配置。
落地方案:第一步,使用requiredDuringScheduling强制同模型副本分布在不同节点。第二步,叠加topologySpreadConstraints保持整体分布均衡。第三步,当节点数不足时,降级为preferredDuringScheduling,并配套资源不足的告警。
基础设施不需要漂亮话,需要的是在一台 GPU 节点宕机后,其他副本还能接住所有推理请求。