Kubernetes Pod驱逐策略
假设资源完全够用,而且应用也都是在使用规定范围内的资源,Kubernetes 会在理想状态下运行。
但现实不会如此简单,在管理集群的时候我们常常会遇到资源不足的情况,在这种情况下我们要保证整个集群可用,并且尽可能减少应用的损失。保证集群可用比较容易理解,首先要保证系统层面的核心进程正常,其次要保证 kubernetes 本身组件进程不出问题;但是如果量化应用的损失呢?首先能想到的是如果要杀死 pod,要尽量减少总数。另外一个就和 pod 的优先级相关了,那就是尽量杀死不那么重要的应用,让重要的应用不受影响。
Pod 的驱逐是在 kubelet 中实现的,因为 kubelet 能动态地感知到节点上资源使用率实时的变化情况。其核心的逻辑是:kubelet 实时监控节点上各种资源的使用情况,一旦发现某个不可压缩资源出现要耗尽的情况,就会主动终止节点上的 pod,让节点能够正常运行。被终止的 pod 所有容器会停止,状态会被设置为 failed。
驱逐触发条件
那么哪些资源不足会导致 kubelet 执行驱逐程序呢?目前主要有三种情况:实际内存不足、节点文件系统的可用空间(文件系统剩余大小和 inode 数量)不足、以及镜像文件系统的可用空间(包括文件系统剩余大小和 inode 数量)不足。
下面这图是具体的触发条件:
有了数据的来源,另外一个问题是触发的时机,也就是到什么程度需要触发驱逐程序?kubernetes 运行用户自己配置,并且支持两种模式:按照百分比和按照绝对数量。比如对于一个 32G 内存的节点当可用内存少于 10% 时启动驱逐程序,可以配置 memory.available<10%
或者 memory.available<3.2Gi
。
NOTE:默认情况下,kubelet 的驱逐规则是 memory.available<100Mi
,对于生产环境这个配置是不可接受的,所以一定要根据实际情况进行修改。
软驱逐(soft eviction)和硬驱逐(hard eviction)
因为驱逐 pod 是具有毁坏性的行为,因此必须要谨慎。有时候内存使用率增高只是暂时性的,有可能 20s 内就能恢复,这时候启动驱逐程序意义不大,而且可能会导致应用的不稳定,我们要考虑到这种情况应该如何处理;另外需要注意的是,如果内存使用率过高,比如高于 95%(或者 90%,取决于主机内存大小和应用对稳定性的要求),那么我们不应该再多做评估和考虑,而是赶紧启动驱逐程序,因为这种情况再花费时间去判断可能会导致内存继续增长,系统完全崩溃。
为了解决这个问题,kubernetes 引入了 soft eviction 和 hard eviction 的概念。
软驱逐可以在资源紧缺情况并没有哪些严重的时候触发,比如内存使用率为 85%,软驱逐还需要配置一个时间指定软驱逐条件持续多久才触发,也就是说 kubelet 在发现资源使用率达到设定的阈值之后,并不会立即触发驱逐程序,而是继续观察一段时间,如果资源使用率高于阈值的情况持续一定时间,才开始驱逐。并且驱逐 pod 的时候,会遵循 grace period ,等待 pod 处理完清理逻辑。和软驱逐相关的启动参数是:
--eviction-soft
:软驱逐触发条件,比如memory.available<1Gi
--eviction-sfot-grace-period
:触发条件持续多久才开始驱逐,比如memory.available=2m30s
--eviction-max-pod-grace-period
:kill pod 时等待 grace period 的时间让 pod 做一些清理工作,如果到时间还没有结束就做 kill
前面两个参数必须同时配置,软驱逐才能正常工作;后一个参数会和 pod 本身配置的 grace period 比较,选择较小的一个生效。
硬驱逐更加直接干脆,kubelet 发现节点达到配置的硬驱逐阈值后,立即开始驱逐程序,并且不会遵循 grace period,也就是说立即强制杀死 pod。对应的配置参数只有一个 --evictio-hard
,可以选择上面表格中的任意条件搭配。
设置这两种驱逐程序是为了平衡节点稳定性和对 pod 的影响,软驱逐照顾到了 pod 的优雅退出,减少驱逐对 pod 的影响;而硬驱逐则照顾到节点的稳定性,防止资源的快速消耗导致节点不可用。
软驱逐和硬驱逐可以单独配置,不过还是推荐两者都进行配置,一起使用。
驱逐哪些 pods?
上面我们已经整体介绍了 kubelet 驱逐 pod 的逻辑和过程,那这里就牵涉到一个具体的问题:要驱逐哪些 pod?驱逐的重要原则是尽量减少对应用程序的影响。
如果是存储资源不足,kubelet 会根据情况清理状态为 Dead 的 pod 和它的所有容器,以及清理所有没有使用的镜像。如果上述清理并没有让节点回归正常,kubelet 就开始清理 pod。
一个节点上会运行多个 pod,驱逐所有的 pods 显然是不必要的,因此要做出一个抉择:在节点上运行的所有 pod 中选择一部分来驱逐。虽然这些 pod 乍看起来没有区别,但是它们的地位是不一样的,正如乔治·奥威尔在《动物庄园》的那句话:
所有动物生而平等,但有些动物比其他动物更平等。
Pod 也是不平等的,有些 pod 要比其他 pod 更重要。只管来说,系统组件的 pod 要比普通的 pod 更重要,另外运行数据库的 pod 自然要比运行一个无状态应用的 pod 更重要。kubernetes 又是怎么决定 pod 的优先级的呢?这个问题的答案就藏在我们之前已经介绍过的内容里:pod requests 和 limits、优先级(priority),以及 pod 实际的资源使用。
简单来说,kubelet 会根据以下内容对 pod 进行排序:pod 是否使用了超过请求的紧张资源、pod 的优先级、然后是使用的紧缺资源和请求的紧张资源之间的比例。具体来说,kubelet 会按照如下的顺序驱逐 pod:
使用的紧张资源超过请求数量的
BestEffort
和Burstable
pod,这些 pod 内部又会按照优先级和使用比例进行排序紧张资源使用量低于 requests 的
Burstable
和Guaranteed
的 pod 后面才会驱逐,只有当系统组件(kubelet、docker、journald 等)内存不够,并且没有上面 QoS 比较低的 pod 时才会做。执行的时候还会根据 priority 排序,优先选择优先级低的 pod
防止波动
这里的波动有两种情况,我们先说说第一种。驱逐条件出发后,如果 kubelet 驱逐一部分 pod,让资源使用率低于阈值就停止,那么很可能过一段时间资源使用率又会达到阈值,从而再次出发驱逐,如此循环往复……为了处理这种问题,我们可以使用 --eviction-minimum-reclaim
解决,这个参数配置每次驱逐至少清理出来多少资源才会停止。
另外一个波动情况是这样的:Pod 被驱逐之后并不会从此消失不见,常见的情况是 kubernetes 会自动生成一个新的 pod 来取代,并经过调度选择一个节点继续运行。如果不做额外处理,有理由相信 pod 选择原来节点的可能性比较大(因为调度逻辑没变,而它上次调度选择的就是该节点),之所以说可能而不是绝对会再次选择该节点,是因为集群 pod 的运行和分布和上次调度时极有可能发生了变化。
无论如何,如果被驱逐的 pod 再次调度到原来的节点,很可能会再次触发驱逐程序,然后 pod 再次被调度到当前节点,循环往复…… 这种事情当然是我们不愿意看到的,虽然看似复杂,但这个问题解决起来非常简单:驱逐发生后,kubelet 更新节点状态,调度器感知到这一情况,暂时不往该节点调度 pod 即可。--eviction-pressure-transition-period
参数可以指定 kubelet 多久才上报节点的状态,因为默认的上报状态周期比较短,频繁更改节点状态会导致驱逐波动。
做一个总结,下面是一个使用了上面多种参数的驱逐配置实例(你应该能看懂它们是什么意思了):
–eviction-soft=memory.available<80%,nodefs.available<2Gi \
–eviction-soft-grace-period=memory.available=1m30s,nodefs.available=1m30s \
–eviction-max-pod-grace-period=120 \
–eviction-hard=memory.available<500Mi,nodefs.available<1Gi \
–eviction-pressure-transition-period=30s \
--eviction-minimum-reclaim="memory.available=0Mi,nodefs.available=500Mi,imagefs.available=2Gi"
碎片整理和重调度
Kubernetes 的调度器在为 pod 选择运行节点的时候,只会考虑到调度那个时间点集群的状态,经过一系列的算法选择一个当时最合适的节点。但是集群的状态是不断变化的,用户创建的 pod 也是动态的,随着时间变化,原来调度到某个节点上的 pod 现在看来可能有更好的节点可以选择。比如考虑到下面这些情况:
调度 pod 的条件已经不再满足,比如节点的 taints 和 labels 发生了变化
新节点加入了集群。如果默认配置了把 pod 打散,那么应该有一些 pod 最好运行在新节点上
节点的使用率不均匀。调度后,有些节点的分配率和使用率比较高,另外一些比较低
节点上有资源碎片。有些节点调度之后还剩余部分资源,但是又低于任何 pod 的请求资源;或者 memory 资源已经用完,但是 CPU 还有挺多没有使用
想要解决上述的这些问题,都需要把 pod 重新进行调度(把 pod 从当前节点移动到另外一个节点)。但是默认情况下,一旦 pod 被调度到节点上,除非给杀死否则不会移动到另外一个节点的。
为此 kubernetes 社区孵化了一个称为 descheduler
的项目,专门用来做重调度。重调度的逻辑很简单:找到上面几种情况中已经不是最优的 pod,把它们驱逐掉(eviction)。
目前,descheduler 不会决定驱逐的 pod 应该调度到哪台机器,而是假定默认的调度器会做出正确的调度抉择。也就是说,之所以 pod 目前不合适,不是因为调度器的算法有问题,而是因为集群的情况发生了变化。如果让调度器重新选择,调度器现在会把 pod 放到合适的节点上。这种做法让 descheduler 逻辑比较简单,而且避免了调度逻辑出现在两个组件中。
Descheduler 执行的逻辑是可以配置的,目前有几种场景:
RemoveDuplicates
:RS、deployment 中的 pod 不能同时出现在一台机器上LowNodeUtilization
:找到资源使用率比较低的 node,然后驱逐其他资源使用率比较高节点上的 pod,期望调度器能够重新调度让资源更均衡RemovePodsViolatingInterPodAntiAffinity
:找到已经违反 Pod Anti Affinity 规则的 pods 进行驱逐,可能是因为反亲和是后面加上去的RemovePodsViolatingNodeAffinity
:找到违反 Node Affinity 规则的 pods 进行驱逐,可能是因为 node 后面修改了 label
当然,为了保证应用的稳定性,descheduler 并不会随意地驱逐 pod,还是会尊重 pod 运行的规则,包括 pod 的优先级(不会驱逐 Critical pod,并且按照优先级顺序进行驱逐)和 PDB(如果违反了 PDB,则不会进行驱逐),并且不会驱逐没有 deployment、rs、jobs 的 pod 不会驱逐,daemonset pod 不会驱逐,有 local storage 的 pod 也不会驱逐。
Descheduler 不是一个常驻的任务,每次执行完之后会退出,因此推荐使用 CronJob 来运行。
总的来说,descheduler 是对原生调度器的补充,用来解决原生调度器的调度决策随着时间会变得失效,或者不够优化的缺陷。
- END -
往期推荐:
Kubernetes + Jenkins + Helm + Springboot 实践
🔥 1万位K8S爱好者都在这里,您值得关注!
点亮,为你2020年锦上添花