一、CA(cluster-autoscaler)是什么
当满足以下条件时,自动扩容和缩容Kubernetes集群Node。
1、存在当集群容量不足,无法在集群中运行的pod时,它会自动去Cloud Provider (支持 GCE、GKE 和 AWS)创建新的Node,
2、当集群存在长时间未充分利用的节点,会将该节点上pod驱逐到其他节点(该节点上pod可以移动到其他节点),将其删除以节省开支
二、架构
1、
2、组件
• 1、autoscaler–核心模块,包括Scale Up和Scale Down功能
• 2、Estimator–评估计算扩容节点
• 3、expander–扩容时,选择合适的算法—可增加或定制算法
• 4、Simulator–负责模拟调度,计算缩容节点(为缩容节点找下家)
• 5、CA Cloud-Provider–与iaas交互
三、工作原理
1、扩容
1、创建监听所有pod的状态,默认10s(通过参数--scan-interval设置)检测当前集群状态下是否存在PodCondition的unschedulable 为false的Pod,然后经过计算,判断需要扩容几个节点,最终通过cloud provider从node group(若有多个,可根据expander选择不同的策略)中进行节点的扩容。
2、扩容的速度依赖于iaas的速度,CA期望15分钟(--max-node-provision-time)完成,超过了这个时间,节点还未注册,将不会在模拟调用中考虑该节点,并尝试用不同的group扩容。ca将会删除这些未注册的节点
3、ca不负责注册,需要在脚本中kubeadm注册节点
4、Node Group 就对应伸缩组的概念,可以支持配置支持多个伸缩组,通过策略来进行选择,目前支持的策略为:
random:随机选择
most-pods:选择能够创建Pod最多的Node Group
least-waste:以最小浪费原则选择,即选择有最少可用资源的 Node Group
price:根据主机的价格选择,选择最便宜的
priority:根据优先级进行选择,通过配置cluster-autoscaler-priority-expander configMap,
2、缩容
1、每10s(--scan-interval),会检查满足以下全部条件为空闲的节点
1、该节点上所有pod(默认包括DaemonSet pods and Mirror pods ,可配置忽略--ignore-daemonsets-utilization and --ignore-mirror-pods-utilization)的cpu、内存的request小于节点可分配的50%(--scale-down-utilization-threshold)
2、节点所有的pod(除了manifest-run pods or daemonsets pods )),可在其他节点运行。当检查这个条件时,可被运行的新节点将被记住,因此ca就知道每个pod可以被move,调度器也会据此重新调度到对应节点
3、没有禁止缩容的注释
2、节点的空闲时间超过10分钟,将被终止,ca一次只终止一个非空的空闲节点,降低产生未调度pod的风险。当下一个节点空闲超时10分钟且模拟调度中不依赖相同节点,可在前一个节点终止后终止。但空节点可以批量终止,一次最多10个(--max-empty-bulk-delete)
3、ca先驱逐该节点的pod,给节点打污点,
四、源码解析
func ScaleUp(context *context.AutoscalingContext, processors *ca_processors.AutoscalingProcessors, clusterStateRegistry *clusterstate.ClusterStateRegistry, unschedulablePods []*apiv1.Pod, nodes []*apiv1.Node, daemonSets []*appsv1.DaemonSet, nodeInfos map[string]*schedulernodeinfo.NodeInfo, ignoredTaints taints.TaintKeySet) (*status.ScaleUpStatus, errors.AutoscalerError) {
......
// 验证当前集群中所有 ready node 是否来自于 nodeGroups,取得所有非组内的 node
nodesFromNotAutoscaledGroups, err := utils.FilterOutNodesFromNotAutoscaledGroups(nodes, context.CloudProvider)
if err != nil {
return &status.ScaleUpStatus{Result: status.ScaleUpError}, err.AddPrefix("failed to filter out nodes which are from not autoscaled groups: ")
}
nodeGroups := context.CloudProvider.NodeGroups()
gpuLabel := context.CloudProvider.GPULabel()
availableGPUTypes := context.CloudProvider.GetAvailableGPUTypes()
// 资源限制对象,会在 build cloud provider 时传入
// 如果有需要可在 CloudProvider 中自行更改,但不建议改动,会对用户造成迷惑
resourceLimiter, errCP := context.CloudProvider.GetResourceLimiter()
if errCP != nil {
return &status.ScaleUpStatus{Result: status.ScaleUpError}, errors.ToAutoscalerError(
errors.CloudProviderError,
errCP)
}
// 计算资源限制
// nodeInfos 是所有拥有节点组的节点与示例节点的映射
// 示例节点会优先考虑真实节点的数据,如果 NodeGroup 中还没有真实节点的部署,则使用 Template 的节点数据
scaleUpResourcesLeft, errLimits := computeScaleUpResourcesLeftLimits(context.CloudProvider, nodeGroups, nodeInfos, nodesFromNotAutoscaledGroups, resourceLimiter)
if errLimits != nil {
return &status.ScaleUpStatus{Result: status.ScaleUpError}, errLimits.AddPrefix("Could not compute total resources: ")
}
// 根据当前节点与 NodeGroups 中的节点来计算会有多少节点即将加入集群中
// 由于云服务商的伸缩组 increase size 操作并不是同步加入 node,所以将其统计,以便于后面计算节点资源
upcomingNodes := make([]*schedulernodeinfo.NodeInfo, 0)
for nodeGroup, numberOfNodes := range clusterStateRegistry.GetUpcomingNodes() {
......
}
klog.V(4).Infof("Upcoming %d nodes", len(upcomingNodes))
// 最终会进入选择的节点组
expansionOptions := make(map[string]expander.Option, 0)
......
// 出于某些限制或错误导致不能加入新节点的节点组,例如节点组已达到 MaxSize
skippedNodeGroups := map[string]status.Reasons{}
// 综合各种情况,筛选出节点组
for _, nodeGroup := range nodeGroups {
......
}
if len(expansionOptions) == 0 {
klog.V(1).Info("No expansion options")
return &status.ScaleUpStatus{
Result: status.ScaleUpNoOptionsAvailable,
PodsRemainUnschedulable: getRemainingPods(podEquivalenceGroups, skippedNodeGroups),
ConsideredNodeGroups: nodeGroups,
}, nil
}
......
// 选择一个最佳的节点组进行扩容,expander 用于选择一个合适的节点组进行扩容,默认为 RandomExpander,flag: expander
// random 随机选一个,适合只有一个节点组
// most-pods 选择能够调度最多 pod 的节点组,比如有 noSchedulerPods 是有 nodeSelector 的,它会优先选择此类节点组以满足大多数 pod 的需求
// least-waste 优先选择能满足 pod 需求资源的最小资源类型的节点组
// price 根据价格模型,选择最省钱的
// priority 根据优先级选择
bestOption := context.ExpanderStrategy.BestOption(options, nodeInfos)
if bestOption != nil && bestOption.NodeCount > 0 {
......
newNodes := bestOption.NodeCount
// 考虑到 upcomingNodes, 重新计算本次新加入节点
if context.MaxNodesTotal > 0 && len(nodes)+newNodes+len(upcomingNodes) > context.MaxNodesTotal {
klog.V(1).Infof("Capping size to max cluster total size (%d)", context.MaxNodesTotal)
newNodes = context.MaxNodesTotal - len(nodes) - len(upcomingNodes)
if newNodes < 1 {
return &status.ScaleUpStatus{Result: status.ScaleUpError}, errors.NewAutoscalerError(
errors.TransientError,
"max node total count already reached")
}
}
createNodeGroupResults := make([]nodegroups.CreateNodeGroupResult, 0)
// 如果节点组在云服务商端处不存在,会尝试创建根据现有信息重新创建一个云端节点组
// 但是目前所有的 CloudProvider 实现都没有允许这种操作,这好像是个多余的方法
// 云服务商不想,也不应该将云端节点组的创建权限交给 ClusterAutoscaler
if !bestOption.NodeGroup.Exist() {
oldId := bestOption.NodeGroup.Id()
createNodeGroupResult, err := processors.NodeGroupManager.CreateNodeGroup(context, bestOption.NodeGroup)
......
}
// 得到最佳节点组的示例节点
nodeInfo, found := nodeInfos[bestOption.NodeGroup.Id()]
if !found {
// This should never happen, as we already should have retrieved
// nodeInfo for any considered nodegroup.
klog.Errorf("No node info for: %s", bestOption.NodeGroup.Id())
return &status.ScaleUpStatus{Result: status.ScaleUpError, CreateNodeGroupResults: createNodeGroupResults}, errors.NewAutoscalerError(
errors.CloudProviderError,
"No node info for best expansion option!")
}
// 根据 CPU、Memory及可能存在的 GPU 资源(hack: we assume anything which is not cpu/memory to be a gpu.),计算出需要多少个 Nodes
newNodes, err = applyScaleUpResourcesLimits(context.CloudProvider, newNodes, scaleUpResourcesLeft, nodeInfo, bestOption.NodeGroup, resourceLimiter)
if err != nil {
return &status.ScaleUpStatus{Result: status.ScaleUpError, CreateNodeGroupResults: createNodeGroupResults}, err
}
// 需要平衡的节点组
targetNodeGroups := []cloudprovider.NodeGroup{bestOption.NodeGroup}
// 如果需要平衡节点组,根据 balance-similar-node-groups flag 设置。
// 检测相似的节点组,并平衡它们之间的节点数量
if context.BalanceSimilarNodeGroups {
......
}
// 具体平衡策略可以看 (b *BalancingNodeGroupSetProcessor) BalanceScaleUpBetweenGroups 方法
scaleUpInfos, typedErr := processors.NodeGroupSetProcessor.BalanceScaleUpBetweenGroups(context, targetNodeGroups, newNodes)
if typedErr != nil {
return &status.ScaleUpStatus{Result: status.ScaleUpError, CreateNodeGroupResults: createNodeGroupResults}, typedErr
}
klog.V(1).Infof("Final scale-up plan: %v", scaleUpInfos)
// 开始扩容,通过 IncreaseSize 扩容
for _, info := range scaleUpInfos {
typedErr := executeScaleUp(context, clusterStateRegistry, info, gpu.GetGpuTypeForMetrics(gpuLabel, availableGPUTypes, nodeInfo.Node(), nil), now)
if typedErr != nil {
return &status.ScaleUpStatus{Result: status.ScaleUpError, CreateNodeGroupResults: createNodeGroupResults}, typedErr
}
}
......
}
......
}
五、其他
1、如何控制某些Node不被CA在缩容时删除
1、Pod 配置了 PodDisruptionBudget (PDB)
2、kube-system下pod运行所在的节点,
3、用户可通过pod spec配置priorityClassName: system-cluster-critical,阻止该pod被驱逐
4、模拟调度时,节点上pod不满足可调度到其他节点(标签、污点、亲和等等)
5、节点上有不受副本控制器管理的pod
6、具有注释:"cluster-autoscaler.kubernetes.io/safe-to-evict": "false"的pod
7、具有注释 "cluster-autoscaler.kubernetes.io/scale-down-disabled": "true"的节点
8、具有本地存储的pod
2、防止非空节点被缩容--将--scale-down-utilization-threshold设置为0
3、开启或关闭特定daemonset的驱逐---daemonset 的pod具有"cluster-autoscaler.kubernetes.io/enable-ds-eviction": "true"
4、当有多个伸缩组时,expander根据参数选择策略