背景介绍
在AI算力系统中,为了提高资源利用率,我们通常会将集群节点划分为不同的资源池:
- 推理资源池:专门用于部署在线推理服务,保证推理服务的稳定性和响应速度
- 训练资源池:专门用于运行离线训练任务
- 混部资源池:同时运行在线推理服务和离线训练任务,实现资源的混合部署
调度策略
在线推理服务在部署时采用以下调度策略:
- 高优先级服务:优先调度到推理资源池,资源不足时溢出到混部资源池
- 中低优先级服务:优先调度到混部资源池,资源不足时溢出到推理资源池
这种调度策略确保了高优服务的稳定性,同时通过混部资源池提高了整体资源利用率。
缩容需求
当在线推理服务需要缩容时,我们希望实现以下目标:
- 优先缩容混部资源池中的Pod:释放混部资源池的资源,让给离线训练任务使用
- 保留推理资源池中的Pod:保证推理服务的稳定性,避免影响核心业务
核心问题:Kubernetes原生的缩容逻辑是如何工作的?我们如何精确控制缩容时删除哪些Pod?
源码分析
1. Pod删除排序机制
Kubernetes在进行ReplicaSet或Deployment缩容时,需要决定删除哪些Pod。这个决策逻辑在pkg/controller/controller_utils.go文件的ActivePodsWithRanks结构中实现。
1.1 核心排序规则
根据源码controller_utils.go#780-812,Pod删除的优先级排序遵循以下规则(按优先级从高到低):
- 未分配节点的Pod优先删除:未被调度到节点的
Pod会优先于已分配节点的Pod被删除 - 按Pod阶段排序:
Pending < Unknown < Running,即Pending状态的Pod最先被删除 - 未就绪的Pod优先删除:未
Ready的Pod会优先于Ready的Pod被删除 - Pod删除成本(pod-deletion-cost):成本值越低的
Pod越优先被删除 - 节点上Pod密度:同一节点上相同
ReplicaSet的Pod越多,该节点上的Pod越优先被删除(避免单点故障) - Ready时间:
Ready时间越短的Pod越优先被删除 - 容器重启次数:重启次数越多的
Pod越优先被删除 - 创建时间:创建时间越晚的
Pod越优先被删除
// Source: pkg/controller/controller_utils.go#780-812
// ActivePodsWithRanks is a sortable list of pods and a list of corresponding
// ranks which will be considered during sorting.
type ActivePodsWithRanks struct {
Pods []*v1.Pod
Rank []int
Now metav1.Time
}
// Less compares two pods with corresponding ranks and returns true if the first
// one should be preferred for deletion.
func (s ActivePodsWithRanks) Less(i, j int) bool {
// 1. Unassigned < assigned
if s.Pods[i].Spec.NodeName != s.Pods[j].Spec.NodeName &&
(len(s.Pods[i].Spec.NodeName) == 0 || len(s.Pods[j].Spec.NodeName) == 0) {
return len(s.Pods[i].Spec.NodeName) == 0
}
// 2. PodPending < PodUnknown < PodRunning
if podPhaseToOrdinal[s.Pods[i].Status.Phase] != podPhaseToOrdinal[s.Pods[j].Status.Phase] {
return podPhaseToOrdinal[s.Pods[i].Status.Phase] < podPhaseToOrdinal[s.Pods[j].Status.Phase]
}
// 3. Not ready < ready
if podutil.IsPodReady(s.Pods[i]) != podutil.IsPodReady(s.Pods[j]) {
return !podutil.IsPodReady(s.Pods[i])
}
// 4. lower pod-deletion-cost < higher pod-deletion cost
if utilfeature.DefaultFeatureGate.Enabled(features.PodDeletionCost) {
pi, _ := helper.GetDeletionCostFromPodAnnotations(s.Pods[i].Annotations)
pj, _ := helper.GetDeletionCostFromPodAnnotations(s.Pods[j].Annotations)
if pi != pj {
return pi < pj // 成本低的优先删除
}
}
// 5-8. 其他规则...
}
1.2 Pod删除成本(pod-deletion-cost)
pod-deletion-cost是Kubernetes提供的一个annotation,用于控制Pod的删除优先级。源码位于pkg/apis/core/helper/helpers.go#489-506:
// GetDeletionCostFromPodAnnotations returns the integer value of pod-deletion-cost.
// Returns 0 if not set or the value is invalid.
func GetDeletionCostFromPodAnnotations(annotations map[string]string) (int32, error) {
if value, exist := annotations[core.PodDeletionCost]; exist {
// values that start with plus sign (e.g, "+10") or leading zeros (e.g., "008") are not valid.
if !validFirstDigit(value) {
return 0, fmt.Errorf("invalid value %q", value)
}
i, err := strconv.ParseInt(value, 10, 32)
if err != nil {
return 0, err
}
return int32(i), nil
}
return 0, nil
}
关键特性:
Annotation键名:controller.kubernetes.io/pod-deletion-cost- 值类型:
int32范围的整数(-2147483648到2147483647) - 默认值:
0(未设置时) - 删除规则:成本值越低,越优先被删除
- 无效值会被忽略,视为
0
2. ReplicaSet缩容流程
在ReplicaSet控制器中,缩容流程如下(源码位于pkg/controller/replicaset/replica_set.go#869-878):
func getPodsToDelete(filteredPods, relatedPods []*v1.Pod, diff int) []*v1.Pod {
// No need to sort pods if we are about to delete all of them.
if diff < len(filteredPods) {
podsWithRanks := getPodsRankedByRelatedPodsOnSameNode(filteredPods, relatedPods)
sort.Sort(podsWithRanks) // 按照ActivePodsWithRanks.Less规则排序
reportSortingDeletionAgeRatioMetric(filteredPods, diff)
}
return filteredPods[:diff] // 返回排序后前diff个Pod进行删除
}
3. Deployment滚动更新中的缩容
在Deployment滚动更新过程中,旧ReplicaSet的缩容逻辑位于pkg/controller/deployment/rolling.go#190-236:
func (dc *DeploymentController) scaleDownOldReplicaSetsForRollingUpdate(
ctx context.Context, allRSs []*apps.ReplicaSet, oldRSs []*apps.ReplicaSet,
deployment *apps.Deployment) (int32, error) {
maxUnavailable := deploymentutil.MaxUnavailable(*deployment)
minAvailable := *(deployment.Spec.Replicas) - maxUnavailable
availablePodCount := deploymentutil.GetAvailableReplicaCountForReplicaSets(allRSs)
if availablePodCount <= minAvailable {
return 0, nil // 不能缩容,会违反可用性要求
}
// 按创建时间排序旧的ReplicaSet
sort.Sort(controller.ReplicaSetsByCreationTimestamp(oldRSs))
// 依次缩容旧的ReplicaSet
for _, targetRS := range oldRSs {
if totalScaledDown >= totalScaleDownCount {
break
}
// 缩容逻辑...
}
return totalScaledDown, nil
}
实现方案
基于以上源码分析,我们可以通过pod-deletion-cost注解来精确控制混部调度场景下的Pod删除优先级。
方案一:手动设置删除成本
对推理服务缩容之前,获取该服务的所有Pod,根据Pod绑定的节点名称,获取节点对应的标签信息。并根据用途标签设置Pod的pod-deletion-cost注解,只需要设置混部节点的Pod其删除成本为负数(如-100)即可。然后再执行缩容。
方案二:自动化设置删除成本
通过Controller监听Pod创建和修改事件,根据Pod实际调度到的节点资源池动态调整Pod的删除成本注解:
// Pod Controller
func (c *PodController) syncPod(pod *corev1.Pod) error {
// 只处理已调度的Pod
if pod.Spec.NodeName == "" {
return nil
}
// 获取Pod所在节点
node, err := c.nodeLister.Get(pod.Spec.NodeName)
if err != nil {
return err
}
// 获取节点的资源池标签
resourcePool := node.Labels["node.usage"]
// 根据资源池设置删除成本
var deletionCost string
switch resourcePool {
case "inference":
// 推理资源池:高删除成本
deletionCost = "1000"
case "hybrid":
// 混部资源池:低删除成本
deletionCost = "-100"
default:
// 默认资源池
deletionCost = "0"
}
// 检查是否需要更新
currentCost := pod.Annotations["controller.kubernetes.io/pod-deletion-cost"]
if currentCost == deletionCost {
return nil // 无需更新
}
// 更新Pod annotation
podCopy := pod.DeepCopy()
if podCopy.Annotations == nil {
podCopy.Annotations = make(map[string]string)
}
podCopy.Annotations["controller.kubernetes.io/pod-deletion-cost"] = deletionCost
_, err = c.kubeClient.CoreV1().Pods(pod.Namespace).Update(
context.TODO(), podCopy, metav1.UpdateOptions{},
)
return err
}