searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

集群节点自动扩缩容-cluster autoscaler

2023-07-21 01:32:25
154
0

一、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-delete3、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根据参数选择策略

 

 
0条评论
作者已关闭评论
望****杰
2文章数
0粉丝数
望****杰
2 文章 | 0 粉丝
望****杰
2文章数
0粉丝数
望****杰
2 文章 | 0 粉丝
原创

集群节点自动扩缩容-cluster autoscaler

2023-07-21 01:32:25
154
0

一、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-delete3、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根据参数选择策略

 

 
文章来自个人专栏
文章 | 订阅
0条评论
作者已关闭评论
作者已关闭评论
1
1