k8s那点事儿Kubernetes

k8s源码学习——autoscaler

2020-04-28  本文已影响0人  霹雳五号_297e

autoscaler是k8s的controller中非常重要的一个controller,它提供了微服务的弹性能力,并且和serverless密切相关。弹性伸缩本身是很好理解的一个概念,当微服务负载高(cpu/内存使用率过高)的时候就水平扩容,增加pod的数量降低负载,当负载降低时就减少pod的数量,减少资源的消耗,通过这种方式使得微服务始终稳定在一个理想的状态。理论上,这么一个逻辑单纯的功能实现起来应该并不复杂,然而,由于实际情况的一些限制,autoscaler要处理的场景远比想象中要复杂得多,它要处理以下几个问题:

1)cpu/mem的监控数据是抽样采集的,波动是非常迅速的,而实际容器增减的速度显然跟不上监控数据波动的速度,你的伸缩相对实际负载来说是有滞后性的。

2)扩容的时候,扩容的pod数量超过了集群容量怎么办?而缩容的时候,要不要在没有请求的时候把集群大小缩为0,缩为0就意味着你的服务实际上已经下线了,而这个时候再有请求过来就会请求失败。

3)监控数据并不是稳定可靠的,如果采集到的数据经常有缺失怎么办。如果你的集群机器数量很大的话,某次采集缺失几个pod的数据发生的概率不会低,这种情况还要不要继续伸缩。

4)如果负载存在大范围波动的情况下该怎么办,比如说监控数据一直在某个值附近反复横跳,导致你的集群大小也会反复横跳,但是从更长的时间角度看,所有的波动其实都发生在某个值的附近,这个时候是不是应该优化。

5)一些数值处理该怎么做?比如你规定了80%cpu负载的情况下应该扩容,但是监控数据是78.5%的cpu使用率,这个时候你应不应该扩容?

要找到这些问题的答案,首先来看一下autoscaler的数据结构,看看有哪些属性。在pkg的apis目录下面,我们可以找到types.go这个文件,里面是autoscaler的主要的structs。比如HorizontalPodAutoscalerSpec这个结构体里面,我们发现了MinReplicas和MaxReplicas这两个字段:

code snippet

这两个字段一个规定了autoscale的上限,一个规定了autoscale的下限,说明了伸缩永远是发生在一个有限范围之内,很好地回答了伸缩规模的问题。但是很神奇的是,MaxReplicas使用的是值,而MinReplicas使用的却是指针,这是怎么一回事?从类型上理解,MaxReplicas是一个配置好就不会改变的值,而MinReplicas在运行时会被修改。阅读了注释,我们了解到MinReplicas默认设为1,只有当HPAScaleToZero这个配置设为true的时候,这个值才能被设置为0。这里面又有一个新的问题,为什么要单独设置scale为0的情况,这种情况有什么特殊的吗?是不是因为我们上面所说的集群大小为0了,相当于服务不可用了,这种情况需要单独处理?带着刨根问底的精神,我找到了这个pr https://github.com/kubernetes/kubernetes/pull/74526,里面解释了为什么要支持HPA伸缩到0:

why scale to 0

说得很明白了,支持HPA伸缩到0的目的是支持用户去定义一个metric,让HPA可以根据这个metric去伸缩replica到0来节约资源,主要针对某些批量任务。所以说,这个策略本身和服务可用性没有关系,也不支持serverless式的冷启动,它只实现了scale to zero的功能,但是没有处理请求的能力,要真正实现服务scale to zero还是需要knative提供的KPA和istio组件。

回过头我们来看一下伸缩规则,HPA的伸缩规则有两组,一组针对scale up,一组针对scale down,如下所示:

scaling rules

这里面提到了stabilization window(稳定窗口)这么一个概念,这是怎么一回事?而且注释说扩容默认没有稳定窗口,但是缩容有稳定窗口。TCP协议里面我们碰到过窗口这么个概念,一般来说,窗口是客户端和服务端用来做流控的一种机制,这里面这个窗口是不是也是这种功能。话不多说,直奔相关代码。

Autoscaler的控制逻辑都在pkg/controller/podautoscaler/horizontal.go这个文件里面。阅读代码,我们会发现,autoscaler的控制逻辑还是很清晰的,基本上就是一条直线。主控逻辑是一个1s执行一次的for循环,循环一个队列里面的HPA任务,先获取当前状态,然后计算出如果平衡到理想状态,需要scale_up/scale_down多少pod,然后再执行动作。里面有一个normalizeDesiredReplicasWithBehaviors方法,会对伸缩结果做平衡,它做了以下几个事情:

1)限制伸缩的值在最大最小范围之内。(避免超过资源限制)

2)限制一次伸缩的pod数量和每分钟伸缩的pod数量。(钝化伸缩动作,避免反复横跳)

3)选择5分钟之内的最小推荐值作为伸缩值。

OK,这下我们了解了,autoscaler根据当前metrics计算出一个值并不会直接拿来伸缩,而是作为推荐值和稳定窗口内的所有推荐值比较,取最小值作为伸缩依据,如果这个值一直不变的话,autoscaler不会做任何动作。这样的话,一方面autoscaler对于负载的变动是敏感的(每秒推荐一次),另一方面autoscaler对于变化的反应是迟钝的,你必须要连续5分钟的持续变动才能造成伸缩动作(当然你可以调这个窗口大小),这样提高了整个集群pod数量的稳定性。那么,为什么要取最小值而不是最大值(伸和缩都取最小)呢?因为HPA整个的设计思路就是节约资源,所以能少用点资源就少用点资源,这样整个k8s集群能够容纳的服务才能变多。

最后,还有一个问题,我们还没解决:对于微服务来说,scale到0之后,有新的请求进来了,怎么办?单纯靠HPA并不能解决这个问题,这个时候我们就需要Knative了,Knative解决这个问题的方式是先用proxy拦截掉这部分请求,然后由KPA(Knative的autoscaler)通过activator去激活一个pod,激活之后再把流量放进来,这样保证0pod的时候服务仍然看起来可用,具体代码实现,我们后面学习Knative源码的时候可以再看。

上一篇下一篇

猜你喜欢

热点阅读