目录
- 前言
- 设计
- 采集器
- [问题1: 采集器和谁交互?](#问题1: 采集器和谁交互?)
- [问题2: 采集器怎么处理指标?](#问题2: 采集器怎么处理指标?)
- [问题3: 采集器如何实现高可用?](#问题3: 采集器如何实现高可用?)
- [问题4: 采集器采集哪些指标?](#问题4: 采集器采集哪些指标?)
- 控制器
- [问题1: 控制器的拉取周期怎么定?](#问题1: 控制器的拉取周期怎么定?)
- [问题2: 工作负载资源临时抖动怎么办?](#问题2: 工作负载资源临时抖动怎么办?)
- [问题3: 扩缩容的时机是怎样的?](#问题3: 扩缩容的时机是怎样的?)
- [问题4: 窗口该怎样表示呢?](#问题4: 窗口该怎样表示呢?)
- [问题5: 控制器如何高可用?](#问题5: 控制器如何高可用?)
- [问题6: 扩缩容算法是怎样的?](#问题6: 扩缩容算法是怎样的?)
- [问题7: 控制器要做成什么形式?](#问题7: 控制器要做成什么形式?)
- [问题8: 如果有多个指标怎么算?](#问题8: 如果有多个指标怎么算?)
- 执行器
- [问题1: 是否存在资源反复更新的情况?](#问题1: 是否存在资源反复更新的情况?)
- 采集器
- 架构
前言
在工作中常常用 Deployment/Statefulset 部署工作负载,但是对工作负载的扩缩容却没用过,实属遗憾。
作为弹性扩缩容的重要一环应该是要补上的。
本学习笔记系列旨在梳理 Kubernetes 水平自动伸缩的整体脉络,并且从源码角度入手有的放矢的学习 Kubernetes 水平自动伸缩的实现。
设计
在进入源码学习之前,让我们先自己动手设计一版水平自动伸缩的方案。
首先,为简化需求,这里给定条件,仅根据 cpu 或 memory 指标实现 Deployment 资源的水平自动伸缩。
针对上述需求,我们可以将水平自动伸缩功能拆成三个组件来实现:
- 采集器:负责采集 pod 的 cpu/memory 指标;
- 控制器:负责根据采集器的指标,做伸缩决策;
- 执行器:真正执行水平伸缩操作;
针对这三个组件我们有若干个问题。
采集器
问题1: 采集器和谁交互?
容器的资源指标会被 cAdvisor 采集到,通过 kubelet api 暴露给用户。
采集器可以和 kubelet 交互获取容器的 cpu/memory 指标。
pod 可以运行在多个节点,意味着采集器需要和所有集群节点的 kubelet 交互以获取容器资源。
这么做的好处是简单,直接和 kubelet 交互。缺点是可能会造成网络拥塞。
解决方案可以是并发请求加随机请求,减轻同时并发请求导致网络拥塞/资源消耗等问题。
或者,采集器可以访问 kube-apiserver 获取所有节点上容器的资源指标。这种方式实际上是一种转移,把由采集器做的事转移给 kube-apiserver,kube-apiserver 作为 kubernetes 的门户,应该扁平、轻量,这种操作由 kube-apiserver 来做是不合适的。
因此,采集器需要和集群节点上的 kubelet 交互以获取容器的资源指标。
问题2: 采集器怎么处理指标?
首先,采集器采集到指标应该存储在内存还是持久化存储?
采集器不是 prometheus,它的定位是采集时刻指标,不需要持久化存储。
这里指标存内存即可。
注意,采集器指标的存储和访问是解耦的。采集器定时采集指标,并对外提供服务,用户可通过 api 获取当前时刻的指标。
问题3: 采集器如何实现高可用?
在 问题2 中采集的指标存储在内存中,那么采集器的高可用是如何实现呢?
如果基于 lease leader 来实现一主多从,那么主挂掉到从采集数据并提供服务是有一段时间的。这段时间会导致采集器服务不可用,并不高可用。
解决方案是多主。假设是 2 副本,主和从都正常采集,任何一个不可用,都可以直接切到另外一个,实现了采集器的高可用。
缺点是,kubelet 要承受副本数次 api 请求,这个并发请求数量并不大,可以忽略。
问题4: 采集器采集哪些指标?
这个问题看似是一个没有必要的问题,其实还真不是。
采集器采集所有 pod 的指标还是只采集水平伸缩的 pod 指标,这是有区别的。
如果要采集水平伸缩的 pod 指标,采集器就要和配置同步以确定要采集哪些 pod,这么做的好处是采集指标较少,省内存。缺点是采集器和配置耦合,不够独立。
由于节点上的 pod 是有上限的,并且采集的指标只限于 cpu/memory,这里可以采集所有 pod 的指标。
优点有两块:
- 独立,采集器作为独立组件,无依赖。对升级、扩容、负载均衡都很友好;
- 由于所有 pod 指标都存储在内存中,采集器不仅可以提供查询水平伸缩工作负载指标,也可以查询集群上其它指标服务,可扩展;
控制器
控制器需要和采集器交互获取要水平伸缩的工作负载指标,并且根据指标和配置做伸缩决策,最后触发执行器实际执行伸缩操作。
关于控制器的问题更多。
问题1: 控制器的拉取周期怎么定?
前面说了采集器是周期性采集的,比如每 15s 采集一次资源指标。如果控制器小于 15s,比如每 5s 从采集器拉取指标,那么会导致采集器还未采集,重复拉取指标的情况。
这里建议是控制器的拉取周期大于等于采集器的采集周期。
问题2: 工作负载资源临时抖动怎么办?
有时候工作负载可能临时抖动,并不是真的要水平伸缩,对于这样的情况,控制器该如何决策呢?
控制器可以配一个抖动边界,如果在抖动边界内则不予处理,如果超出边界则按扩缩容处理。
当然,我们可以把这个抖动边界上移到配置中,这样每个工作负载的抖动边界不一样,更加合理。
问题3: 扩缩容的时机是怎样的?
扩缩容的判断条件不一样。扩容应该要快,防止资源不足导致服务不可用。缩容应该要慢,防止工作负载被缩导致服务不可用。
那么,应该要有一个窗口配置。
比如扩容可以将窗口设为 0,意味着一旦超过抖动边界上限,则立即开始扩容。
比如缩容可以将窗口设为 2min,如果 2min 内资源持续减少,则选择 2min 内的最大值做缩容。
为什么要选最大值呢?我们举个例子:
0s 10 初始 replicas
30s 9 < 2min,不动
60s 8 < 2min,不动
90s 7 < 2min,不动
120s 6 < 2min,不动
150s 5 > 2min, 缩到 9
假设初始工作负载副本是 10,在 2min 内期望伸缩副本一直在递减,到 150s 超过递减窗口,需要缩容,这里取 2min 窗口内的最大值 9 做期望副本,工作负载将缩容到 9 个。
这么做的好处是防止缩容跨度太大导致服务不可用。
扩容要选大的,往大了扩。
缩容要选大的,往小了缩。
问题4: 窗口该怎样表示呢?
从采集器拿到的是瞬时值,而窗口需要记录期望副本数,需要把每次采集算的期望副本数及时间戳记录到窗口中。
可以采用环形队列来表示窗口,注意每个水平伸缩的工作负载对应一个环形队列(窗口大小可以不一样)。
问题5: 控制器如何高可用?
这里控制器需要一主多从,原因有二:
- 如果多主的话,主控制请求的时间点不一样,维护的窗口不一样,算出的期望副本数不一样。多个主给执行器发期望副本数,执行器不知道要听谁的。
- 一主多从,如果主不可用,从会立即采集维护自己的窗口,只会影响很短一段时间,并且窗口一般作用于缩容场景,短暂的未缩容并不会影响业务。
问题6: 扩缩容算法是怎样的?
我们有了工作负载的指标,也有工作负载资源的 request,那怎么确定要伸缩多少呢?
如果超过抖动边界就扩缩 1 的话,不够灵活。跟不上业务的需求,这里需要根据使用情况以及副本数做动态调整。
比如,当前工作负载副本数是 2,CPU 的平均使用率是 50%,现在两个工作负载都超过 50%(不考虑抖动边界的情况),需要扩容。如果实际使用量是 100% 和实际使用量是 60%,扩容的期望副本数是不一样的,需要算法动态调整。
问题7: 控制器要做成什么形式?
我们理清了前面几个问题,大致也知道它其实可以做成 kube-controller-manager 的一个控制器,作为一个模块,而不是组件。
这么做的优势是:
- 利用 kube-controller-manager 带的 informer 去拿 pod 信息,模块复用,不需要重复造轮子;
- 省组件,水平伸缩作为 kubernetes 的基础功能,完全不需要在搞个组件出来增加运维/理解的复杂度;
问题8: 如果有多个指标怎么算?
如果水平伸缩工作负载配置了多个指标,比如 cpu/memory 都配置了,期望副本数该怎么算呢?
可以遵守快扩慢缩的原则,扩容选多个指标算出来的期望副本数的最大值,缩容也选期望副本数的最大值。
执行器
执行器是具体执行扩缩容的组件。
控制器更新期望副本数到 Deployment/StatefulSet,由对应的 Deployment/StatefulSet 控制器来执行具体的扩缩容动作。
这里也有问题。
问题1: 是否存在资源反复更新的情况?
假设 ArgoCD 会维护 Git 仓库的 Deployment 资源到集群。如果集群中的 Deployment 期望副本数被控制器改了,那么 ArgoCD 会把新期望副本数改回来以保持和 Git 仓库的期望副本数一致。
这样的问题在于,反复更新同一资源的期望副本数,可能会导致服务不可用。
解决方案可以是在 ArgoCD 中添加忽略标签,并且申明当该资源的期望副本数更新时不做同步操作。
架构
根据上述设计,我们画出架构图如下: