kubernetes 水平自动伸缩学习笔记(一)


前言

在工作中常常用 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 中添加忽略标签,并且申明当该资源的期望副本数更新时不做同步操作。

架构

根据上述设计,我们画出架构图如下:

image


posted @ 2026-11-14 00:43  胡云Troy  阅读(6)  评论(0)    收藏  举报