Pod 水平自动伸缩(Horizontal Pod Autoscaler)是k8s的kube-controller-manager
中已经集成的一个controller
,主要功能是根据pod当前的资源使用率来对deploy/rs自动扩缩容。详细介绍可以参考社区的官方文档 。
对于Pod 水平自动伸缩而言,最重要的就是其扩缩容的算法,社区原生的算法主要就是根据当前实例个数、当前metric指标和hpa中设置的期望指标来计算的,具体可以看2.2章节。
期望副本数 = ceil[当前副本数 * ( 当前指标 / 期望指标 )]
1、启动参数 kube-controller-manager
中包含了多个跟Pod 水平自动伸缩相关的启动参数:
参数名
参数解释
horizontal-pod-autoscaler-sync-period
controller控制循环的检查周期(默认值为15秒)
horizontal-pod-autoscaler-upscale-delay
上次扩容之后,再次扩容需要等待的时间,默认
horizontal-pod-autoscaler-downscale-stabilization
上次缩容执行结束后,再次执行缩容的间隔,默认5分钟
horizontal-pod-autoscaler-downscale-delay
上次扩容之后,再次扩容需要等待的时间,
horizontal-pod-autoscaler-tolerance
缩放比例的容忍值,默认为0.1,即在0.9~1.1不会触发扩缩容
horizontal-pod-autoscaler-use-rest-clients
使用rest client获取metric数据,支持custom metric时需要使用
horizontal-pod-autoscaler-cpu-initialization-period
pod 的初始化时间, 在此时间内的 pod,CPU 资源指标将不会被采纳
horizontal-pod-autoscaler-initial-readiness-delay
pod 准备时间, 在此时间内的 pod 统统被认为未就绪
2、处理逻辑 2.1、资源使用率来源 控制器将从一系列的聚合 API(metrics.k8s.io
、custom.metrics.k8s.io
和external.metrics.k8s.io
) 中获取指标数据。 metrics.k8s.io
API 通常由 metrics-server
(需要额外部署)提供。通过 metrics-server
获取到的数据如下,其中包含有cpu和memory的使用量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 [paas@192-168-199-200 ~]$ curl 127.0.0.1:8080/apis/metrics.k8s.io/v1beta1/namespaces/default/pods/test-delete-bcf859ddb-b7mjd { "kind": "PodMetrics", "apiVersion": "metrics.k8s.io/v1beta1", "metadata": { "name": "test-delete-bcf859ddb-b7mjd", "namespace": "default", "selfLink": "/apis/metrics.k8s.io/v1beta1/namespaces/default/pods/test-delete-bcf859ddb-b7mjd", "creationTimestamp": "2020-05-22T02:58:34Z" }, "timestamp": "2020-05-22T02:58:01Z", "window": "30s", "containers": [ { "name": "container-0", "usage": { "cpu": "0", "memory": "1396Ki" } } ] }
可以看到通过metrics-server
获取数据是以pod为单位的,在Pod Autoscaler
中,也是以pod为单位来做的自动伸缩。
custom.metrics.k8s.io
需要配合prometheus
等组件一起实现,暂且不表,后续介绍完了prometheus
之后再慢慢讲。
2.2、算法细节(来自社区文档) 从最基本的角度来看,pod 水平自动缩放控制器跟据当前指标和期望指标来计算缩放比例。其中当前指标就是通过2.1节中的接口来获取,期望指标则是hpa资源中设置的值。
1 期望副本数 = ceil [当前副本数 * ( 当前指标 / 期望指标 )]
例如,当前指标为200m
,目标设定值为100m
,那么由于200.0 / 100.0 == 2.0
, 副本数量将会翻倍。 如果当前指标为50m
,副本数量将会减半,因为50.0 / 100.0 == 0.5
。 如果计算出的缩放比例接近1.0(跟据--horizontal-pod-autoscaler-tolerance
参数全局配置的容忍值,默认为0.1), 将会放弃本次缩放。
如果 HorizontalPodAutoscaler 指定的是targetAverageValue
或 targetAverageUtilization
, 那么将会把指定pod的平均指标做为currentMetricValue
。 然而,在检查容忍度和决定最终缩放值前,我们仍然会把那些无法获取指标的pod统计进去。
所有被标记了删除时间戳(Pod正在关闭过程中)的 pod 和 失败的 pod 都会被忽略。
如果某个 pod 缺失指标信息,它将会被搁置,只在最终确定缩值时再考虑。
当使用 CPU 指标来缩放时,任何还未就绪(例如还在初始化)状态的 pod 或 最近的指标为就绪状态前的 pod, 也会被搁置
由于受技术限制,pod 水平缩放控制器无法准确的知道 pod 什么时候就绪, 也就无法决定是否暂时搁置该 pod。 --horizontal-pod-autoscaler-initial-readiness-delay
参数(默认为30s),用于设置 pod 准备时间, 在此时间内的 pod 统统被认为未就绪。 --horizontal-pod-autoscaler-cpu-initialization-period
参数(默认为5分钟),用于设置 pod 的初始化时间, 在此时间内的 pod,CPU 资源指标将不会被采纳。
在排除掉被搁置的 pod 后,缩放比例就会跟据currentMetricValue / desiredMetricValue
计算出来。
如果有任何 pod 的指标缺失,我们会更保守地重新计算平均值, 在需要缩小时假设这些 pod 消耗了目标值的 100%, 在需要放大时假设这些 pod 消耗了0%目标值。 这可以在一定程度上抑制伸缩的幅度。
此外,如果存在任何尚未就绪的pod,我们可以在不考虑遗漏指标或尚未就绪的pods的情况下进行伸缩, 我们保守地假设尚未就绪的pods消耗了试题指标的0%,从而进一步降低了伸缩的幅度。
在缩放方向(缩小或放大)确定后,我们会把未就绪的 pod 和缺少指标的 pod 考虑进来再次计算使用率。 如果新的比率与缩放方向相反,或者在容忍范围内,则跳过缩放。 否则,我们使用新的缩放比例。
注意,平均利用率的原始 值会通过 HorizontalPodAutoscaler 的状态体现( 即使使用了新的使用率,也不考虑未就绪 pod 和 缺少指标的 pod)。
如果创建 HorizontalPodAutoscaler 时指定了多个指标, 那么会按照每个指标分别计算缩放副本数,取最大的进行缩放。 如果任何一个指标无法顺利的计算出缩放副本数(比如,通过 API 获取指标时出错), 那么本次缩放会被跳过。
最后,在 HPA 控制器执行缩放操作之前,会记录缩放建议信息(scale recommendation)。 控制器会在操作时间窗口中考虑所有的建议信息,并从中选择得分最高的建议。 这个值可通过 kube-controller-manager 服务的启动参数 --horizontal-pod-autoscaler-downscale-stabilization
进行配置, 默认值为 5min。 这个配置可以让系统更为平滑地进行缩容操作,从而消除短时间内指标值快速波动产生的影响。
3、代码解析 3.1、重要的数据结构 在进入代码之前,我们需要先说明一下与Pod Autoscaler
相关的几个重要的k8s
资源,这些资源是弹性伸缩实现的基石。
3.1.1、HorizontalPodAutoscaler
(v2beta1) 在最早期的v1
版本的HorizontalPodAutoscaler
中,只能指定targetCPUUtilizationPercentage
,也就是说只能通过pod
的cpu
使用率来做弹性伸缩,功能比较局限。后面的v2beta1
将这些资源做了拓展,已经不局限于cpu
了。我们先举一个v2beta1
的样例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 kind: HorizontalPodAutoscaler apiVersion: autoscaling/v2beta2 metadata: name: fff namespace: default selfLink: "/apis/autoscaling/v2beta2/namespaces/default/horizontalpodautoscalers/fff" uid: 698d7d88-82f7-4b2d-ad1a-467e07a45edb resourceVersion: '44279715' creationTimestamp: '2020-05-14T06:51:45Z' spec: scaleTargetRef: kind: Deployment name: fffff apiVersion: apps/v1beta2 minReplicas: 1 maxReplicas: 10 metrics: - type: Resource resource: name: memory target: type: Utilization averageUtilization: 70 - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 status: lastScaleTime: '2020-05-14T06:57:01Z' currentReplicas: 1 desiredReplicas: 1 currentMetrics: - type: Resource resource: name: memory current: averageValue: '1433600' averageUtilization: 1 - type: Resource resource: name: cpu current: averageValue: '0' averageUtilization: 0 conditions: - type: AbleToScale status: 'True' lastTransitionTime: '2020-05-14T06:52:00Z' reason: ReadyForNewScale message: recommended size matches current size - type: ScalingActive status: 'True' lastTransitionTime: '2020-05-25T11:27:20Z' reason: ValidMetricFound message: the HPA was able to successfully calculate a replica count from memory resource utilization (percentage of request) - type: ScalingLimited status: 'False' lastTransitionTime: '2020-05-14T06:52:00Z' reason: DesiredWithinRange message: the desired count is within the acceptable range
在HorizontalPodAutoscaler
中,很关键的一个字段就是spec.metrics
,用来指定自动伸缩所根据的指标,分别有4种类型:
Resource
:资源使用率,可以是pod
的cpu
、memory
,对应的指标可以是使用率、使用量、平均使用量,前面的样例中使用的就是这种类型
Pods
:表示指定的指标在不同Pod之间进行平均,并通过与一个目标值比对来确定副本的数量。 它们的工作方式与资源度量指标非常相像,差别是它们仅支持target
类型为AverageValue
。样例如下:
1 2 3 4 5 6 7 type: Pods pods: metric: name: packets-per-second target: type: AverageValue averageValue: 1k
Object
:k8s
中某一个可以描述资源指标的对象,例如有hits-per-second
指标的Ingress
对象,表示requests-per-second
的度量指标样例如下。需要注意的是,这个资源对象需要与其作用的pod
在相同namespace
中。
1 2 3 4 5 6 7 8 9 10 11 type: Object object: metric: name: requests-per-second describedObject: apiVersion: networking.k8s.io/v1beta1 kind: Ingress name: main-route target: type: Value value: 2k
External
:使用外部的度量指标,这些指标依赖于外部的监控系统,这种方式不太推荐,更建议使用custom metric
的方式。
由于spec.metrics
是一个slice,因此可以同时指定多个指标类型,HorizontalPodAutoscaler
会计算每一个指标所提议的副本数量,然后最终选择一个最高值。
3.2、代码目录 跟k8s中其他官方的controller一样,Pod Autoscaler
控制器的代码主要都位于k8s中,主要代码逻辑在horizontal.go
中,其他文件中的代码见下面的说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 # pkg/controller/podautoscaler/ ├── config # HPAController的启动参数相关 │ ├── BUILD │ ├── doc.go │ ├── OWNERS │ ├── types.go │ ├── v1alpha1 │ │ ├── BUILD │ │ ├── conversion.go │ │ ├── defaults.go │ │ ├── doc.go │ │ ├── register.go │ │ ├── zz_generated.conversion.go │ │ └── zz_generated.deepcopy.go │ └── zz_generated.deepcopy.go ├── doc.go ├── horizontal.go # 主代码逻辑 ├── horizontal_test.go ├── legacy_horizontal_test.go ├── legacy_replica_calculator_test.go ├── metrics │ ├── BUILD │ ├── interfaces.go # 获取metric数值的接口 │ ├── legacy_metrics_client.go # 调用heapster获取metric的实现 │ ├── legacy_metrics_client_test.go │ ├── rest_metrics_client.go # 调用metric-server获取metric的实现 │ ├── rest_metrics_client_test.go │ ├── utilization.go # 计算当前使用率的公共函数 │ └── utilization_test.go ├── OWNERS ├── rate_limiters.go # HPAController限流器 ├── replica_calculator.go # 根据当前使用率计算pod期望个数的函数实现 └── replica_calculator_test.go
3.3、代码逻辑 为了方便理解HorizontalController
本身的逻辑,这里不去细究kube-controller-manager
本身的框架,也不去细究k8s
中list-watch
机制中的informer
相关的具体细节,遇到时也仅解析其功能和用法。
3.3.1、创建HorizontalController
创建HorizontalController
时一个十分关键的参数是resyncPeriod
,即启动参数horizontal-pod-autoscaler-sync-period
,controller
中并不是通过Ticker
的方式来定时去做检查,而是在informer
中定时把所有资源发送到controller
的处理队列中,这种方式的好处是不需要给每个hpa
资源都创建一个Ticker
。这个功能主要是informer
自己去实现的,对这部分感兴趣的话可以去瞅瞅staging/src/k8s.io/client-go/tools/cache
里面的代码,非常值得研究。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func NewHorizontalController ( evtNamespacer v1core.EventsGetter, scaleNamespacer scaleclient.ScalesGetter, hpaNamespacer autoscalingclient.HorizontalPodAutoscalersGetter, mapper apimeta.RESTMapper, metricsClient metricsclient.MetricsClient, hpaInformer autoscalinginformers.HorizontalPodAutoscalerInformer, podInformer coreinformers.PodInformer, resyncPeriod time.Duration, downscaleStabilisationWindow time.Duration, tolerance float64 , cpuInitializationPeriod, delayOfInitialReadinessStatus time.Duration, ) *HorizontalController { hpaInformer.Informer().AddEventHandlerWithResyncPeriod( cache.ResourceEventHandlerFuncs{ AddFunc: hpaController.enqueueHPA, UpdateFunc: hpaController.updateHPA, DeleteFunc: hpaController.deleteHPA, }, resyncPeriod, ) return hpaController }
3.3.2、运行controller
相比于创建controller
时的各种涉及到informer
参数的设置,运行controller
的逻辑明显清晰很多,入口就是其Run
函数,其中会起一个携程来运行实际的处理逻辑。在这个协程中,会不断从informer的队列中取出需要处理的HPA对象的key,然后使用这个key去缓存中获取对应的HPA对象的完整对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 func (a *HorizontalController) Run (stopCh <-chan struct {}) { defer utilruntime.HandleCrash() defer a.queue.ShutDown() klog.Infof("Starting HPA controller" ) defer klog.Infof("Shutting down HPA controller" ) if !controller.WaitForCacheSync("HPA" , stopCh, a.hpaListerSynced, a.podListerSynced) { return } go wait.Until(a.worker, time.Second, stopCh) <-stopCh } func (a *HorizontalController) worker () { for a.processNextWorkItem() { } klog.Infof("horizontal pod autoscaler controller worker shutting down" ) } func (a *HorizontalController) processNextWorkItem () bool { key, quit := a.queue.Get() if quit { return false } defer a.queue.Done(key) deleted, err := a.reconcileKey(key.(string )) if err != nil { utilruntime.HandleError(err) } if !deleted { a.queue.AddRateLimited(key) } return true } func (a *HorizontalController) reconcileKey (key string ) (deleted bool , err error) { namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { return true , err } hpa, err := a.hpaLister.HorizontalPodAutoscalers(namespace).Get(name) if errors.IsNotFound(err) { klog.Infof("Horizontal Pod Autoscaler %s has been deleted in %s" , name, namespace) delete (a.recommendations, key) return true , nil } return false , a.reconcileAutoscaler(hpa, key) }
在处理HPA对象时,主要分为3步:
获取目标deploy
或者rs
的scale
数据,包含deploy
或者rs
中spec.replicas
指定的实例个数
根据当前的情况计算是否需要扩缩容以及期望的实例个数
需要scale
的情况则调用接口对改资源进行弹性伸缩,否则保持不变
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 func (a *HorizontalController) reconcileAutoscaler (hpav1Shared *autoscalingv1.HorizontalPodAutoscaler, key string ) error { hpav1 := hpav1Shared.DeepCopy() hpaRaw, err := unsafeConvertToVersionVia(hpav1, autoscalingv2.SchemeGroupVersion) if err != nil { a.eventRecorder.Event(hpav1, v1.EventTypeWarning, "FailedConvertHPA" , err.Error()) return fmt.Errorf("failed to convert the given HPA to %s: %v" , autoscalingv2.SchemeGroupVersion.String(), err) } hpa := hpaRaw.(*autoscalingv2.HorizontalPodAutoscaler) hpaStatusOriginal := hpa.Status.DeepCopy() reference := fmt.Sprintf("%s/%s/%s" , hpa.Spec.ScaleTargetRef.Kind, hpa.Namespace, hpa.Spec.ScaleTargetRef.Name) targetGV, err := schema.ParseGroupVersion(hpa.Spec.ScaleTargetRef.APIVersion) targetGK := schema.GroupKind{ Group: targetGV.Group, Kind: hpa.Spec.ScaleTargetRef.Kind, } mappings, err := a.mapper.RESTMappings(targetGK) scale, targetGR, err := a.scaleForResourceMappings(hpa.Namespace, hpa.Spec.ScaleTargetRef.Name, mappings) currentReplicas := scale.Spec.Replicas a.recordInitialRecommendation(currentReplicas, key) var ( metricStatuses []autoscalingv2.MetricStatus metricDesiredReplicas int32 metricName string ) desiredReplicas := int32 (0 ) rescaleReason := "" rescale := true if scale.Spec.Replicas == 0 { desiredReplicas = 0 rescale = false setCondition(hpa, autoscalingv2.ScalingActive, v1.ConditionFalse, "ScalingDisabled" , "scaling is disabled since the replica count of the target is zero" ) } else if currentReplicas > hpa.Spec.MaxReplicas { rescaleReason = "Current number of replicas above Spec.MaxReplicas" desiredReplicas = hpa.Spec.MaxReplicas } else if hpa.Spec.MinReplicas != nil && currentReplicas < *hpa.Spec.MinReplicas { rescaleReason = "Current number of replicas below Spec.MinReplicas" desiredReplicas = *hpa.Spec.MinReplicas } else if currentReplicas == 0 { rescaleReason = "Current number of replicas must be greater than 0" desiredReplicas = 1 } else { var metricTimestamp time.Time metricDesiredReplicas, metricName, metricStatuses, metricTimestamp, err = a.computeReplicasForMetrics(hpa, scale, hpa.Spec.Metrics) rescaleMetric := "" if metricDesiredReplicas > desiredReplicas { desiredReplicas = metricDesiredReplicas rescaleMetric = metricName } desiredReplicas = a.normalizeDesiredReplicas(hpa, key, currentReplicas, desiredReplicas) rescale = desiredReplicas != currentReplicas } if rescale { scale.Spec.Replicas = desiredReplicas _, err = a.scaleNamespacer.Scales(hpa.Namespace).Update(targetGR, scale) } else { klog.V(4 ).Infof("decided not to scale %s to %v (last scale time was %s)" , reference, desiredReplicas, hpa.Status.LastScaleTime) desiredReplicas = currentReplicas } a.setStatus(hpa, currentReplicas, desiredReplicas, metricStatuses, rescale) return a.updateStatusIfNeeded(hpaStatusOriginal, hpa) }
前面的逻辑中,最重要的一步就是根据当前的资源使用情况来计算期望的实例个数,也就是computeReplicasForMetrics
函数。在这个函数中,主要就是遍历所有的metric
策略(3.1.1节中有提到不同类型),并计算各个策略下的期望实例个数,最终取最大的那个。这段逻辑最终进入到计算单个策略的函数computeReplicasForMetric
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func (a *HorizontalController) computeReplicasForMetrics (hpa *autoscalingv2.HorizontalPodAutoscaler, scale *autoscalingv1.Scale, metricSpecs []autoscalingv2.MetricSpec) (replicas int32 , metric string , statuses []autoscalingv2.MetricStatus, timestamp time.Time, err error) { specReplicas := scale.Spec.Replicas statusReplicas := scale.Status.Replicas statuses = make ([]autoscalingv2.MetricStatus, len (metricSpecs)) selector, err := labels.Parse(scale.Status.Selector) invalidMetricsCount := 0 var invalidMetricError error for i, metricSpec := range metricSpecs { replicaCountProposal, metricNameProposal, timestampProposal, err := a.computeReplicasForMetric(hpa, metricSpec, specReplicas, statusReplicas, selector, &statuses[i]) if err == nil && (replicas == 0 || replicaCountProposal > replicas) { timestamp = timestampProposal replicas = replicaCountProposal metric = metricNameProposal } } return replicas, metric, statuses, timestamp, nil }
对于每一个策略而言,需要根据其类型做不同的处理,computeReplicasForMetric
中主要就是一个case
语句,由于有4中类型,其中Resource
较为常用,因此后面就只对这种类型进行代码分析。Resource
类型的计算是由computeStatusForResourceMetric
来处理的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func (a *HorizontalController) computeReplicasForMetric (hpa *autoscalingv2.HorizontalPodAutoscaler, spec autoscalingv2.MetricSpec, specReplicas, statusReplicas int32 , selector labels.Selector, status *autoscalingv2.MetricStatus) (replicaCountProposal int32 , metricNameProposal string , timestampProposal time.Time, err error) { switch spec.Type { case autoscalingv2.ObjectMetricSourceType: metricSelector, err := metav1.LabelSelectorAsSelector(spec.Object.Metric.Selector) replicaCountProposal, timestampProposal, metricNameProposal, err = a.computeStatusForObjectMetric(specReplicas, statusReplicas, spec, hpa, selector, status, metricSelector) case autoscalingv2.PodsMetricSourceType: metricSelector, err := metav1.LabelSelectorAsSelector(spec.Pods.Metric.Selector) replicaCountProposal, timestampProposal, metricNameProposal, err = a.computeStatusForPodsMetric(specReplicas, spec, hpa, selector, status, metricSelector) case autoscalingv2.ResourceMetricSourceType: replicaCountProposal, timestampProposal, metricNameProposal, err = a.computeStatusForResourceMetric(specReplicas, spec, hpa, selector, status) case autoscalingv2.ExternalMetricSourceType: replicaCountProposal, timestampProposal, metricNameProposal, err = a.computeStatusForExternalMetric(specReplicas, statusReplicas, spec, hpa, selector, status) default : errMsg := fmt.Sprintf("unknown metric source type %q" , string (spec.Type)) } return replicaCountProposal, metricNameProposal, timestampProposal, nil }
在computeStatusForResourceMetric
中,主要是针对Resource
类型的策略进行处理,在3.1.1章节中已经说明Resource
类型的策略对应的指标可以是:
平均使用率AverageUtilization
使用量Value
平均使用量AverageValue
但是在computeStatusForResourceMetric
函数中,似乎并不支持使用量Value
这种类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 func (a *HorizontalController) computeStatusForResourceMetric (currentReplicas int32 , metricSpec autoscalingv2.MetricSpec, hpa *autoscalingv2.HorizontalPodAutoscaler, selector labels.Selector, status *autoscalingv2.MetricStatus) (int32 , time.Time, string , error) { if metricSpec.Resource.Target.AverageValue != nil { var rawProposal int64 replicaCountProposal, rawProposal, timestampProposal, err := a.replicaCalc.GetRawResourceReplicas(currentReplicas, metricSpec.Resource.Target.AverageValue.MilliValue(), metricSpec.Resource.Name, hpa.Namespace, selector) metricNameProposal := fmt.Sprintf("%s resource" , metricSpec.Resource.Name) *status = autoscalingv2.MetricStatus{ Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricStatus{ Name: metricSpec.Resource.Name, Current: autoscalingv2.MetricValueStatus{ AverageValue: resource.NewMilliQuantity(rawProposal, resource.DecimalSI), }, }, } return replicaCountProposal, timestampProposal, metricNameProposal, nil } else { if metricSpec.Resource.Target.AverageUtilization == nil { errMsg := "invalid resource metric source: neither a utilization target nor a value target was set" a.eventRecorder.Event(hpa, v1.EventTypeWarning, "FailedGetResourceMetric" , errMsg) setCondition(hpa, autoscalingv2.ScalingActive, v1.ConditionFalse, "FailedGetResourceMetric" , "the HPA was unable to compute the replica count: %s" , errMsg) return 0 , time.Time{}, "" , fmt.Errorf(errMsg) } targetUtilization := *metricSpec.Resource.Target.AverageUtilization var percentageProposal int32 var rawProposal int64 replicaCountProposal, percentageProposal, rawProposal, timestampProposal, err := a.replicaCalc.GetResourceReplicas(currentReplicas, targetUtilization, metricSpec.Resource.Name, hpa.Namespace, selector) metricNameProposal := fmt.Sprintf("%s resource utilization (percentage of request)" , metricSpec.Resource.Name) *status = autoscalingv2.MetricStatus{ Type: autoscalingv2.ResourceMetricSourceType, Resource: &autoscalingv2.ResourceMetricStatus{ Name: metricSpec.Resource.Name, Current: autoscalingv2.MetricValueStatus{ AverageUtilization: &percentageProposal, AverageValue: resource.NewMilliQuantity(rawProposal, resource.DecimalSI), }, }, } return replicaCountProposal, timestampProposal, metricNameProposal, nil } }
由于篇幅有限,本文只说明一下平均使用率AverageUtilization
这种情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 func (c *ReplicaCalculator) GetResourceReplicas (currentReplicas int32 , targetUtilization int32 , resource v1.ResourceName, namespace string , selector labels.Selector) (replicaCount int32 , utilization int32 , rawUtilization int64 , timestamp time.Time, err error) { metrics, timestamp, err := c.metricsClient.GetResourceMetric(resource, namespace, selector) podList, err := c.podLister.Pods(namespace).List(selector) itemsLen := len (podList) readyPodCount, ignoredPods, missingPods := groupPods(podList, metrics, resource, c.cpuInitializationPeriod, c.delayOfInitialReadinessStatus) removeMetricsForPods(metrics, ignoredPods) requests, err := calculatePodRequests(podList, resource) usageRatio, utilization, rawUtilization, err := metricsclient.GetResourceUtilizationRatio(metrics, requests, targetUtilization) rebalanceIgnored := len (ignoredPods) > 0 && usageRatio > 1.0 if !rebalanceIgnored && len (missingPods) == 0 { if math.Abs(1.0 -usageRatio) <= c.tolerance { return currentReplicas, utilization, rawUtilization, timestamp, nil } return int32 (math.Ceil(usageRatio * float64 (readyPodCount))), utilization, rawUtilization, timestamp, nil } if len (missingPods) > 0 { if usageRatio < 1.0 { for podName := range missingPods { metrics[podName] = metricsclient.PodMetric{Value: requests[podName]} } } else if usageRatio > 1.0 { for podName := range missingPods { metrics[podName] = metricsclient.PodMetric{Value: 0 } } } } if rebalanceIgnored { for podName := range ignoredPods { metrics[podName] = metricsclient.PodMetric{Value: 0 } } } newUsageRatio, _, _, err := metricsclient.GetResourceUtilizationRatio(metrics, requests, targetUtilization) if math.Abs(1.0 -newUsageRatio) <= c.tolerance || (usageRatio < 1.0 && newUsageRatio > 1.0 ) || (usageRatio > 1.0 && newUsageRatio < 1.0 ) { return currentReplicas, utilization, rawUtilization, timestamp, nil } return int32 (math.Ceil(newUsageRatio * float64 (len (metrics)))), utilization, rawUtilization, timestamp, nil }
4、参考文档 https://kubernetes.io/zh/docs/tasks/run-application/horizontal-pod-autoscale/
https://kubernetes.io/zh/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough/
https://docs.aws.amazon.com/zh_cn/eks/latest/userguide/horizontal-pod-autoscaler.html