[kubernetes系列]HPA模块深度讲解

4,831 阅读8分钟

一,前言

对于kubernetes基础性的知识,目前有很多资料,于是不会重复展开,想做一个对每个模块都深入讲解的系列,包括基础使用,源码解读,和实践中遇到的问题等,所以篇幅很比较长。

二,HPA模块

1,相关说明

(1) kubernetes版本:v1.9.2

(2) 适合对kubernetes基础有一定了解的人群

2,基本概念和使用

(1) 概念

HPA是kubernetes中自动水平扩展模块,基于用户设定和获取到的指标(CPU,Memory,自定义metrics),对Pod进行伸缩(不是直接操作pod)。HPA controller属于Controller Manager的一个controller。

(2) 基本使用

我们可以在pkg/apis/autoscaling 下可以看到,目前是有两个版本:v1(仅支持CPU指标),v2beta1(支持CPU和Memory和自定义指标)。下面看一下,一个比较全面的hpa的写法。

kind: HorizontalPodAutoscaler
apiVersion: autoscaling/v2beta1
metadata:
  name: example-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: example-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      targetAverageUtilization: 50

  - type: Resource
    resource:
      name: memory
      targetAverageUtilization: 50

  - type: Pods
    pods:
      metricName: receive_bytes_total
      targetAverageValue: 100

  - type: Object
    object:
      target:
        kind: endpoints
        name: example-app
      metricName: request_total
      targetValue: 500m

3,源码分析

kubernetes中的代码都是有一定的“套路”(后面会专门写一篇来深入分析这种“套路”),我们首先从api入手,再到controller

(1) api

这是一个标准的kubernetes的api写法(可使用官方工具生成),register.go中添加了三个type:HorizontalPodAutoscaler/HorizontalPodAutoscalerList/Scale。接下来看types.go中关于这几个的定义。对应上面的yaml定义来看。

// 1,HorizontalPodAutoscaler
type HorizontalPodAutoscaler struct {
    ......
	Spec HorizontalPodAutoscalerSpec
	Status HorizontalPodAutoscalerStatus
	......
}
// 用户设置的值
type HorizontalPodAutoscalerSpec struct {
	MinReplicas *int32  //设置的最小的replicas
	MaxReplicas int32   //设置的最大的replicas
	Metrics []MetricSpec
}
// hpa的目前的状态
type HorizontalPodAutoscalerStatus struct {
	ObservedGeneration *int64   //观察的最近的generaction
	LastScaleTime *metav1.Time  //上次伸缩的时间
	CurrentReplicas int32       //目前的replicas数量
	DesiredReplicas int32       //期望的replicas数量
	CurrentMetrics []MetricStatus   //最近一次观察到的metrics数据
	Conditions []HorizontalPodAutoscalerCondition   //在某个特定点的hpa状态
}
// Metrics定义
type MetricSpec struct {
	Type MetricSourceType   //metrics type
	Object *ObjectMetricSource  // Object类型的metrics定义
	Pods *PodsMetricSource  // pods类型的metrics定义
	Resource *ResourceMetricSource // Resource类型的metrics定义
}

// 2,Scale 是resources的一次scaling请求,最后hpa都是要使用这个来实现
type Scale struct {
	Spec ScaleSpec         // 期望到达的状态
	Status ScaleStatus     // 目前的状态
}

(2) controller

api定义完后,需要有一个controller来保证系统的状态能符合我们定义的要求,这时候就需要hpa controller了,hpa controller通过从apiserver中获取各个指标的值,根据特定的伸缩算法,来维持在预期的状态。

上面说过,hpa controller属于controller manager,于是我们去cmd/kube-controller-manager下,经过一路跟踪,可以看到hpa controller的启动逻辑在options/autoscaling.go中

func startHPAController(ctx ControllerContext) (bool, error) {
    // 需要包含v1版本
	if !ctx.AvailableResources[schema.GroupVersionResource{Group: "autoscaling", Version: "v1", Resource: "horizontalpodautoscalers"}] {
		return false, nil
	}
    // 如果要使用自定义metrics,需要开启该选项
	if ctx.Options.HorizontalPodAutoscalerUseRESTClients {
		return startHPAControllerWithRESTClient(ctx)
	}
    // 从Heapster拉取数据
	return startHPAControllerWithLegacyClient(ctx)
}

func startHPAControllerWithMetricsClient(ctx ControllerContext, metricsClient metrics.MetricsClient) (bool, error) {
    .......
    // 核心参数,根据metrics计算相应的replicas
    replicaCalc := podautoscaler.NewReplicaCalculator(
		metricsClient,
		hpaClient.CoreV1(),
		ctx.Options.HorizontalPodAutoscalerTolerance,
	)
	// 新建HorizontalController
	go podautoscaler.NewHorizontalController(
		hpaClientGoClient.CoreV1(),
		scaleClient, // scale相关客户端,实现最终的pod伸缩
		hpaClient.AutoscalingV1(),
		restMapper,
		replicaCalc, // 副本计算器
		ctx.InformerFactory.Autoscaling().V1().HorizontalPodAutoscalers(), //infomer
		ctx.Options.HorizontalPodAutoscalerSyncPeriod.Duration, // hpa获取数据的间隔
		ctx.Options.HorizontalPodAutoscalerUpscaleForbiddenWindow.Duration, // hpa扩容最低间隔
		ctx.Options.HorizontalPodAutoscalerDownscaleForbiddenWindow.Duration, // hpa缩容最低间隔
	).Run(ctx.Stop)
	return true, nil
}

// 接下来看HorizontalController的定义
type HorizontalController struct {
	scaleNamespacer scaleclient.ScalesGetter   // 负责scale的get和update
	hpaNamespacer   autoscalingclient.HorizontalPodAutoscalersGetter // 负责HorizontalPodAutoscaler的Create, Update, UpdateStatus, Delete, Get, List, Watch等
	mapper          apimeta.RESTMapper 
	replicaCalc   *ReplicaCalculator // 负责根据指标计算replicas
	eventRecorder record.EventRecorder  //event记录
	upscaleForbiddenWindow   time.Duration
	downscaleForbiddenWindow time.Duration
    // 从informer中list/get数据
	hpaLister       autoscalinglisters.HorizontalPodAutoscalerLister
	hpaListerSynced cache.InformerSynced

	queue workqueue.RateLimitingInterface
}

开始Run后,就是controller开发的那一套流程了,设计到相关的informer,workerqueue就不展开了,最关键的是下面的reconcileAutoscaler,其实就是通过一系列算法调节当前副本数,期望副本数,边界(最大最小)副本数三者的关系。(接下来分析可能比较长,只截取部分关键代码,注意看注释)

func (a *HorizontalController) reconcileAutoscaler(hpav1Shared *autoscalingv1.HorizontalPodAutoscaler) error {

    // 通过namespace和name获取对应的scale
	scale, targetGR, err := a.scaleForResourceMappings(hpa.Namespace, hpa.Spec.ScaleTargetRef.Name, mappings)
	// 获取当前副本
	currentReplicas := scale.Status.Replicas

	rescale := true
    // 副本为0,则不进行scale操作
	if scale.Spec.Replicas == 0 {
		desiredReplicas = 0
		rescale = false
	// 当前副本大于期望的最大副本数量,不进行操作
	} 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
	} 
	// 当前副本为0也不进行操作
	else if currentReplicas == 0 {
		rescaleReason = "Current number of replicas must be greater than 0"
		desiredReplicas = 1
	} 
	// 当前副本数量处于设置的Min和Max之间才进行操作
	else { 
	    // 根据metrics指标计算对应的副本数
		metricDesiredReplicas, metricName, metricStatuses, metricTimestamp, err = a.computeReplicasForMetrics(hpa, scale, hpa.Spec.Metrics)
		rescaleMetric := ""
		// 这里是防止扩容过快,限制了最大只能扩当前实例的两倍
		desiredReplicas = a.normalizeDesiredReplicas(hpa, currentReplicas, desiredReplicas)
        // 限制扩容和缩容间隔,默认是两次缩容的间隔不得小于5min,两次扩容的间隔不得小于3min
		rescale = a.shouldScale(hpa, currentReplicas, desiredReplicas, timestamp)
	}

    // 如果上面的限制条件都通过,则进行扩容,扩容注意通过scale实现
	if rescale {
		scale.Spec.Replicas = desiredReplicas
		_, err = a.scaleNamespacer.Scales(hpa.Namespace).Update(targetGR, scale)
	} else {
		desiredReplicas = currentReplicas
	}
}


(3) 整套流程如下

4,HPA实战经验

虽然说hpa能根据指标对pod进行弹性伸缩,达到根据使用量扩展机器的功能,但是,在实际运用中,我发现了以下的问题,希望能给要使用该模块的人带来一些启发。

(1) 问题详情

我们遇到了这样的一个业务场景:在某个时间段会突然流量剧增十倍,此时由于之前是处于低流量状态,replicas一直处于较低值,那么此时扩容由于扩容算法的限制(最多为2倍),此时扩容的数量是不足够的。然后,同样由于扩容算法的限制,两次扩容周期默认为不低于三分钟,那么将无法在短期内到达一个理想的副本数。此时从监控上看pod的数量图如下:

那么这样将会造成很大的问题,无法及时处理这种实时性高的业务场景。同时,我们还遇到了这样的业务情况,在一次大量扩容后,流量剧减,pod数量降到了一个极低值,但是由于出现业务流量的抖动,在接下来很短时间内,再一次出现大流量,此时pod数量无法处理如此高的流量,影响业务的SLA等。

(2) 问题解决思路

1,利用多指标 如果只使用单一指标,例如CPU,整个hpa将严重依赖于这项指标,该指标的准确性等直接影响整个hpa。在这里,我们使用CRD进行了多指标的开发,结合某个业务的具体场景,开发合适的指标,然后结合着CPU等指标一起使用。

2,调整默认参数 默认的扩容和缩容周期不一定是最合适你们的业务的,所以可以根据业务自身的情况进行调整。

3,自行开发hpa controller 这里还有一个思路是修改hpa controller,但是这样将会不利于以后的升级。所以可以自行开发hpa controller,自行定义最使用你们业务的扩容缩容算法即可。但是这样的开发成本就稍微有点大。

三,注意事项

(1) 支持deployment的滚动升级,不支持RC的滚动升级

四,总结

(1) 如果是我,会如何来设计

提出自己的愚蠢的思路:如果我是hpa这块的负责人,那么我将会将扩容和缩容算法这块写成是可扩展的,用户可自定义的扩容缩容步长,可针对每个hpa自定义扩容缩容的时间间隔,这样将会大大方便使用。

五,新版本特性研究(持续更新)

(1) v1.12.1

该版本对hpa这块改动挺大,总之增加了很多灵活性和可用性

1,可通过设置HorizontalPodAutoscalerDownscaleStabilizationWindow来防止流量抖动造成pod突然的减少。该版本中会记录HorizontalPodAutoscalerDownscaleStabilizationWindow时间内的每次扩展pod数量,每次取该时间内最大值,然后尝试尽量到达该值

2,目前对扩容这块还是有所限制,还是存在两倍大小的限制,但是取消了扩容和缩容的时间间隔限制

3,可自行设定误差容忍度

4,增加了pod readiness和missing metrics的考量。如何pod处于not ready状态或者有丢失的metrics,那么会将该pod闲置不处理。处理逻辑如下:如果是Resource和Pods的类型,那么判断pod是否ready的时候,首先pod的startTime加上cpuInitializationPeriod(可设置)大于当前时间,再判断pod的状态和metric获取的时间,从而来判断pod是否ready。pod的startTime加上cpuInitializationPeriod小于当前时间的情况下,则根据pod的状态和pod的启动时间加上readiness-delay(可设置)来判断pod是否ready。对于Object和External类型,则直接判断pod的状态

六,参考文献

(1) imkira.com/a24.html

(2) kubernetes.io/docs/tasks/…