云原生弹性伸缩控制器实现细节
什么是弹性伸缩
基本原理
弹性伸缩(Horizontal Pod Autoscaler)是kubernetes内置的一种副本控制器,主要功能是检测分析pod的负载变化情况来判断是否需要调整服务pod个数并自动将服务pod数扩缩至伸缩算法的预期值以满足服务正常运行,目前逐渐成为了各大服务器厂商的标准配置。其基本原理如下图:
由图可看出,弹性伸缩调度是一个间歇运行的闭环系统,主要有以下三个组件构成:
Metrics Server
主要功能是从RC/deployment处搜刮服务运行的负载指标,为hpa调度提供数据源
Rc/Deployment
hpa具体调度的资源,目前Hpa支持Deployment、StateFulSet和RC等包含了Scale子资源的所有资源
Hpa控制器
弹性伸缩系统的大脑,通过从Metrics Server中获取服务的实际负载,按照弹性伸缩算法对Rc/Deployment资源进行调度,弹性伸缩核心算法如下:
主要调度过程如下图:
应用场景
弹性伸缩主要是用于解决容量规划和实际负载的矛盾,其最典型的应用场景就是应对实际负载激增而容量规划还未来得及反应的情况,如下图:
举个简单的栗子——微博热搜,热搜时访问量激增,需要快速扩容机器,在热点过后,又需要快速回收机器降低成本,一方面是可能扩容不及时,导致服务存在部分不可用,另一方面费时费力,需要人力操作。对于这种场景使用弹性伸缩就很合适,控制器持续检测服务使用率,当使用率超过配置的阈值时会动态扩容,保证服务可用和稳定;当热点消失,服务使用率下降,此时控制器会动态缩容,降低资源浪费,全程实现自动化,智能化。
优势
从弹性伸缩的应用场景中可以看到,弹性伸缩至少具备如下三个优势:
自动
接入弹性伸缩的服务扩缩操作全程由弹性伸缩控制器控制,无需人力干预,全程操作闭环自动。
稳定
弹性伸缩会在服务负载高于配置阈值时触发扩容,扩容后服务有更多的实例支撑服务访问,保证服务的稳定。
-
降本
弹性伸缩会在服务负载降低到配置阈值时触发缩容,缩容后服务会回收多余的实例,降低资源浪费。
云原生平台弹性伸缩实现细节
以上主要介绍kubernetes原生弹性伸缩hpa的设计原理和优势。原生的弹性伸缩部署和使用都很简单,但是不能直接应用于哈啰的业务,因为hpa无法感知soa拉出场景。我们的预期是实现基于负载和soa注册状态的双重检测。所以我们借鉴hpa的设计思路自研了一套能够实现哈啰业务场景的水位弹性控制器WPA(Water Pod Autoscaler),即实现soa拉入、拉出和应用负载的组合推荐计算,将soa注册状态引入到核心算法中,当服务因自身健康原因从注册中心拉出时,核心算法能够准确剥离拉出soa实例,计算出当前服务的实际有效负载平均值,确保服务能够准确扩缩。
wpa的设计思路一定程度上借鉴了hpa,主要采用crd实现,其设计原理如下:
从图中可以看出,wpa控制器除了从metrics server上获取服务负载指标外,还通过hahas平台获取soa注册指标。控制器通过对两种指标的聚合处理得出扩缩预期副本数,实现准确扩缩。
由于弹性伸缩控制器细节较多,下面主要介绍wpa几个比较核心的概念及其实现。
核心算法
为避免服务频繁扩缩,将hpa的单线扩缩判断指标改为双线判断扩缩指标,可以单独设置向上的扩容触发阈值和向下缩容的触发阈值,如下图:
扩容核心算法:
例如:服务当前有效副本数5个,平均负载为1500m,最大阈值为:1200m,则
upScaleProposal=Ceil(float(5) * 1500 / float(1200))=7(6.25向上取整),
wpa控制器最终会扩容2个实例,扩容后总实例为7个。
缩容核心算法:
当弹性伸缩算法选择均值模式(average)的时候,averaged=n, 当弹性伸缩算法选择绝对值模式(absolute)时,averaged=1,目前wpa均默认为均值模式。
例如:服务当前有效副本数7个,平均负载为300m,最小阈值为:400m,则
downScaleProposal=floor(float64(7) * 300 / float64(400))=5(5.25)
wpa控制器最终会缩容2个实例,缩容后总实例为5个。
两种算法有一点区别,扩容使用ceil方法向上取整,缩容使用floor方法向下取整,两者操作采取最大可能原则,这样可以提高弹性调度的灵敏度和准确性。
噪声处理
噪声主要来自于两部分,一个是kubernetes本身采用recreate方式创建新的实例时,会出现Starting和Stopping两种状态的实例,此时统计的总的实例数会变多,会给计算预期值带来噪音;第二个是新启动的pod还未被metrics server采集到有效指标,此时实例的指标是空值,会对计算预期值带来噪音。
wpa控制器针对第一个问题主要通过在计算指标时对pod状态去噪来解决,将非running状态的指标都进行过滤,降低Starting和Stopping状态指标带来的干扰,核心代码如下:
if pod.DeletionTimestamp != nil || pod.Status.Phase == corev1.PodFailed {
ignoredPods.Insert(pod.Name)
continue
}
if pod.Status.Phase == corev1.PodPending {
unReadyPods.Insert(pod.Name)
continue
}
if condition == nil || pod.Status.StartTime == nil {
unReady = true
} else {
if pod.Status.StartTime.Add(cpuInitializationPeriod).After(time.Now()) {
unReady = condition.Status == corev1.ConditionFalse || metric.Timestamp.Before(condition.LastTransitionTime.Time.Add(metric.Window))
} else {
unReady = condition.Status == corev1.ConditionFalse && pod.Status.StartTime.Add(delayOfInitialReadinessStatus).After(condition.LastTransitionTime.Time)
}
}
if unReady {
unReadyPods.Insert(pod.Name)
continue
}
if ignoredPods != nil && ignoredPods.Len() > 0 {
removeMetricsForPods(metrics, ignoredPods)
}
if unReadyPods != nil && unReadyPods.Len() > 0 {
removeMetricsForPods(metrics, unReadyPods)
}
针对第二个问题则通过过滤metric空值与重新赋值来实现去噪,核心代码如下:
// Pods missing metrics
metric, found := metrics[pod.Name]
if !found {
missingPods.Insert(pod.Name)
continue
}
if len(missPods) > 0 {
if action == v1alpha1.CronScaleDown {
for podName := range missPods {
metrics[podName] = metricsclient.PodMetric{Value: metric.Resource.HighWatermark.MilliValue() + metric.Resource.HighWatermark.MilliValue()*wpa.Spec.Tolerance.MilliValue()/1000}
}
} else {
for podName := range missPods {
metrics[podName] = metricsclient.PodMetric{Value: 0}
}
}
}
冷却周期
冷却周期是指控制器执行扩缩动作的等待时间,即配即生效,主要是防止过快的扩缩对服务稳定性造成影响,核心逻辑如下:
wpa的冷却周期可以在平台上直接配置,如下图:
频率控制
频率控制是指控制器单次执行扩缩的数量限制,计算公式如下:
例如:当前实例数为3,预期副本数6,扩容实例百分比为20%,最大实例数为7,则
本次扩容后总实例数 = min((3 + max( 1, floor(3 * 0.2))), 7) = min(4, 7)=4
加入扩容频率控制后,本次最终扩容实例数为1,扩容后总实例数为4个。
缩容频率控制:
例如:当前副本数为7,预期副本数3,缩容实例百分比为1%,最小实例数为2,则
本次缩容后总实例数 = max((7-max(1,floor(7 * 0.01 ))),2)=max(6,2)=6
加入缩容频率控制后,本次实际缩容实例数为1,缩容后总实例数为6个。
目前弹性扩缩容的频率控制是对用户放开的,用户可以在hke平台上修改如下参数即可。
容忍度
容忍度是对最大阈值和最小阈值设置的缓冲区间,相当于将扩缩容的阈值向外延展,这个参数可以避免阈值临界值的噪音,保证服务稳定性。目前wpa对所有应用都设置的1%,加上容忍度后的阈值计算公式如下:
最大阈值公式:
目前Tolerance默认为1%。
例如:highWaterMark为100m,Tolerance=1%,则添加容忍度后的扩容触发阈值=100*(1+0.01)=101m
最小阈值公式:
目前Tolerance默认为1%。
例如:lowWaterMark为20m,Tolerance=1%,则添加容忍度后的缩容触发阈值=20*(1-0.01)=19.8m
目前弹性伸缩的容忍缓冲是隐式配置的,默认都为1%,也就是容忍实际负载在阈值上下限产生1%的波动噪音。
告警和消息通知
哈啰的弹性伸缩控制器已经接入了监控和告警,并且控制器具备自愈和秒级扩容的能力。对于业务方面,我们定制了两种消息通知和一种告警,消息通知分别是弹性扩缩情况通知和非pro环境的版本不一致通知,告警主要是对pro环境版本不一致告警。
扩缩消息通知是为了让用户感知到控制器对服务的每一次操作,这样可以让用户对服务当前实例情况有一个准确的了解,做到心中有“数”。服务版本不一致则会干扰弹性调度的目标服务版本,导致扩非所需,缩非所想,所以需要及时的告知用户并调整预期服务版本。