正常云厂商 kubernetes 集群的节点池(也叫节点组或 NodeGroup)都是由另一款较老的产品 弹性伸缩 来实现的,当然啦,不同云厂商有不同叫法,这儿就不细说了。也正是由于这种不那么紧密的关系使得集群弹性伸缩对于异常情况的处理显得有些欠火候,用户也是叫苦连天。

cluster-autoscaler 中有一个超时参数在很多地方都有用到 --max-node-provision-time,默认为 15min,下面会大量用到。

扩容逻辑在另一篇文章中有讲,所以本文会略过,主要讲一下当 pending 的 pod 已经触发扩容后失败的情况。

创建节点失败

正常来说,经过一系列的逻辑处理,有了扩容方案后,CA 会调用底层弹性伸缩的接口调整节点的的期望实例数触发扩容,同时通过 ScaleUpRequest 缓存该事件,在以后的每次的循环中查看该事件的完成情况,如果扩容完成了就会清理掉该事情。

CA 判断底层有没有将节点扩出来的标准是看通过 (cloudprovider.NodeGroup).Nodes() 查到的节点数量是不是小于 (cloudprovider.NodeGroup).TargetSize(),如果小于,那就说明还在扩。

但事情往往总会发生点意外:

  1. 如果 CA 调用底层弹性伸缩的接口失败了该怎么办?
  2. 如果底层一直建不出机器怎么办?

针对情况 1,CA 会立即将对应的节点池 backoff 掉,并报告事件 FailedToScaleUpGroup;情况 2,CA 的等待会有一个超时时间(--max-node-provision-time指定),时间一到也会将其 backoff 掉并报告事件 ScaleUpTimedOut

然而,针对情况 2,仅仅是 backoff 掉没有扩出来的节点池还是不够的,因为弹性伸缩的期望实例数并不等于当前实例数,这在 CA 看来就是有扩容中的节点从而不会触发新扩容,而且后面确实有可能被弹性伸缩再扩出来(比如在弹性伸缩在超过--max-node-provision-time指定的时间的重试中又给扩出来了)。此时就引出了 CA 的修正逻辑,当 CA 观察到伸缩组已经没有在扩容了,并且长时间(--max-node-provision-time指定)期望实例数小于当前实例数且数量一直不曾改变时,就会将期望实例数调整与当前实例数保持一致。

情况 1 多见于 IaaS 层故障,并不多见,且我们也无能为力。但是针对于情况 2,扩容要 15min--max-node-provision-time默认)超时才算作失败,且在超时 15min 后还得观察 15min 修正后才会触发新的扩容,里外里足足 30 分钟1,这哪个客户受得了,一般扩容都是十万火急的场景。

也因为这个原因,CA 社区又给提供了第三种失败,即在每一次循环中查询失败的节点,如果看到有失败的节点就 backoff 节点池,并报告事件 ScaleUpFailed,紧接着立即删除失败的节点。这就使得 CA 能在最短的时间内响应扩容失败并立即触发其它节点池扩容。

既然这个特性这么好,是不是各厂商都用起来了呢?其实并没有,大多厂商的弹性伸缩并不会保存创建失败节点。这里列一下已经支持的厂商:

  • gce,原本就在 MIG 中保留了失败的节点,所以先天支持,也是目前支持的最好的
  • aws,大概一年前支持的,通过 mock 创建中的节点来实现的
  • tke原生节点,原生节点失败的 machine 会被保留下来,所以也是先天支持

不过,总结来说,创建失败一定跟 弹性伸缩 有关。

节点池的 backoff 机制

  • --initial-node-group-backoff-duration,初始 backoff 时间
  • --max-node-group-backoff-duration,最大 backoff 时间
  • --node-group-backoff-reset-timeout,backoff 重置时间

假设节点池一直扩不出来 5m 10m 20m 30m 30m … 每次 backoff 时间不超过 3h,3h 后又会按 5m 开始

异常分析

这里假设只能通过超时来发现扩容失败,同时也从侧面讲一下循环中查询扩容失败的必要性。

  • 场景 1:集群 pod 触发节点池 A 扩容 2 个节点,15min 过后节点没扩出来,节点池 A 被 backoff, 5min 后节点池 A 退出 backoff 状态,后续一直没有再触发节点池 A 产生新的扩容,15min 后触发修正逻辑,将节点池 A 的期望实例数减 2,由于在之前节点池 A 就已经退出了 backoff 状态,所以这次扩容还有可能扩到节点池 A,如果再次扩节点池 A,过 15min 后,再次被 backoff,此次 10min 退出 backoff 状态,15min 后第二次修正节点池 A,同理,第三次扩容还有可能扩到节点池 A,又过 15min 后,继续 backoff,这次 20min 才能退出,由于还处理 backoff 状态中,所以再过 15min 第三次修正后,可以确保不会触发节点池 A 扩容。至此,当节点池 A 持续不可用时,可能需要 15x6=90min(实际不止这么多,因为还有--scan-interval指定的循环间隔需要被算入) 才能切换到其它节点池扩容。 Alt text
  • 场景 2:在场景 1 中,如果在扩容的 15min 内又有新的 pod pending,并且还是触发节点池 A 的扩容,那么超时时间会被重置,那么无法扩出的时间可能比 90min 还会更长。
  • 场景 3:当前节点池是一个异常状态,没有节点在集群中,并且当前实例数小于期望数,CA 由于重启或者其它原因并没有该节点池的 ScaleUpRequest 缓存,这种情况下,CA 将无法获取到异常节点池的修正信息,从而不会触发修正,而又由于节点池的期望实例没有修正会被 CA 认为是有节点池节点正常加入中,而不会触发新的扩容。这种情况下要人工介入修正节点数才能解决。

如果云厂商提供了在查询扩容失败的解决路径,那么以上三种情况都可解决。

注册节点失败

如果有节点被创建出来了,长时间(--max-node-provision-time指定)没有注册,也会被删除并上报事件 DeleteUnregistered。这种一般是由于集群的节点添加流程出现了问题。

不满足前置条件

集群健康

如果 CA 的日志中出现 Cluster is not ready for autoscaling,那说明集群当前状态异常,这样 CA 唯一能进行的写操作是移除长时间未注册的节点(这有可能使集群状态恢复正常),而不会再触发扩缩容,满足下述条件时,集群会被认为异常:

  1. 集群中未就绪节点数2达到--ok-total-unready-count指定值(默认是3),其占比达到max-total-unready-percentage指定值,默认是45

节点池健康

当节点池被认为不健康或已经 backoff 时,不会触发该节点池扩缩,即当出现下述情况之一时:

  1. 当集群中没有该节点池节点,但节点池的期望实例数又不为 0 时,此时 CA 日志中会出现Failed to find readiness information
  2. 当前节点池中已就绪实例数比该节点池期可接受的最小实例数3还小时,且其差值大于--ok-total-unready-count,并且在节点池中的占比超过max-total-unready-percentage

参数限制

达到下面这些参数限制时,不会触发扩缩容:

  1. --max-nodes-total,限制集群最大节点数
  2. --cores-total,限制集群最大核心数
  3. --memory-total,限制集群最大内存数
  4. --gpu-total,限制集群最大 gpu 数

  1. 实际应该是超过 90 分钟,具体待测试,在异常分析场景 2 中有介绍 ↩︎

  2. 未就绪节点的定义方式在系列文章中有提到,可以自行查阅 ↩︎

  3. 当节点池正在扩容时,这个值为期望实例数-正在扩容节点数;当其正在缩容时,这个值为期望实例数 ↩︎