k8s节点资源预留与 pod 驱逐

为什么

K8S 的节点上的资源会被 pod 和系统进程所使用,如果默认什么都不配置,那么节点上的全部资源都是可以分配给pod使用的,系统进程本身没有保障,这样做很危险:

  • 集群雪崩:如果节点上调度了大量pod,且pod没有合理的limit限制,节点资源将被耗尽,sshd、kubelet等进程OOM,节点变成 not ready状态,pod重新继续调度到其他节点,新节点也被打挂,引起集群雪崩。
  • 系统进程异常:就算 pod 设置了limit,但如果机器遇到资源不足,系统进程如 docker 没有资源保障,会频繁 OOM,或者进程 hang 住无响应,虽然能运行,但容器会反复出问题

节点资源主要分为两类:

  • 可压缩资源:如CPU,即使cpu 超配,也可以划分时间片运行,只是运行变慢,进程不会挂。
  • 不可压缩资源:Memory/Storage,内存不同于CPU,系统内存不足时,会触发 OOM杀死进程,按照oom score 来确定先kill谁,oom_score_adj值越高,被kill 的优先级越高。

oom 分数:

所以,OOM 的优先级如下:

BestEffort Pod > Burstable Pod > 其它进程 > Guaranteed Pod > kubelet/docker 等 > sshd 等进程

因此需要对节点的内存等资源进行配置,以保证节点核心进程运行正常。

怎么做

节点资源的配置一般分为 2 种:

  1. 资源预留:为系统进程和 k8s 进程预留资源
  2. pod 驱逐:节点资源到达一定使用量,开始驱逐 pod

  • Node Capacity:Node的所有硬件资源
  • kube-reserved:给kube组件预留的资源:kubelet,kube-proxy以及docker等
  • system-reserved:给system进程预留的资源
  • eviction-threshold:kubelet eviction的阈值设定
  • Allocatable:真正scheduler调度Pod时的参考值(保证Node上所有Pods的request resource不超过Allocatable)

allocatable的值即对应 describe node 时看到的allocatable容量,pod 调度的上限

计算公式:节点上可配置值 = 总量 - 预留值 - 驱逐阈值

Allocatable = Capacity - Reserved(kube+system) - Eviction Threshold

以上配置均在kubelet 中添加,涉及的参数有:

--enforce-node-allocatable=pods,kube-reserved,system-reserved
--kube-reserved-cgroup=/system.slice/kubelet.service
--system-reserved-cgroup=/system.slice
--kube-reserved=cpu=200m,memory=250Mi
--system-reserved=cpu=200m,memory=250Mi
--eviction-hard=memory.available<5%,nodefs.available<10%,imagefs.available<10%
--eviction-soft=memory.available<10%,nodefs.available<15%,imagefs.available<15%
--eviction-soft-grace-period=memory.available=2m,nodefs.available=2m,imagefs.available=2m
--eviction-max-pod-grace-period=30
--eviction-minimum-reclaim=memory.available=0Mi,nodefs.available=500Mi,imagefs.available=500Mi

配置含义

配置的含义如下:

(1)–enforce-node-allocatable


含义:指定kubelet为哪些进程做硬限制,可选的值有: * pods * kube-reserved * system-reserve 这个参数开启并指定pods后kubelet会为所有pod的总cgroup做资源限制(通过cgroup中的kubepods.limit_in_bytes),限制为公式计算出的allocatable的大小。 假如想为系统进程和k8s进程也做cgroup级别的硬限制,还可以在限制列表中再加system-reserved和kube-reserved,同时还要分别加上--kube-reserved-cgroup和--system-reserved-cgroup以指定分别限制在哪个cgroup里。
配置:--enforce-node-allocatable=pods,kube-reserved,system-reserved

(2)设置k8s组件的cgroup

含义:这个参数用来指定k8s系统组件所使用的cgroup。

注意,这里指定的cgroup及其子系统需要预先创建好,kubelet并不会为你自动创建好。

配置:--kube-reserved-cgroup=/system.slice/kubelet.service

(3)设置系统守护进程的cgroup

含义:这个参数用来指定系统守护进程所使用的cgroup。

注意,这里指定的cgroup及其子系统需要预先创建好,kubelet并不会为你自动创建好。
配置:--system-reserved-cgroup=/system.slice

(4)配置 k8s组件预留资源的大小,CPU、Mem

指定为k8s系统组件(kubelet、kube-proxy、dockerd等)预留的资源量,

如:--kube-reserved=cpu=1,memory=2Gi,ephemeral-storage=1Gi。

这里的kube-reserved只为非pod形式启动的kube组件预留资源,假如组件要是以static pod(kubeadm)形式启动的,那并不在这个kube-reserved管理并限制的cgroup中,而是在kubepod这个cgroup中。

(ephemeral storage需要kubelet开启feature-gates,预留的是临时存储空间(log,EmptyDir),生产环境建议先不使用)

ephemeral-storage是kubernetes1.8开始引入的一个资源限制的对象,kubernetes 1.10版本中kubelet默认已经打开的了,到目前1.11还是beta阶段,主要是用于对本地临时存储使用空间大小的限制,如对pod的empty dir、/var/lib/kubelet、日志、容器可读写层的使用大小的限制。

(5)配置 系统守护进程预留资源的大小(预留的值需要根据机器上容器的密度做一个合理的值)

含义:为系统守护进程(sshd, udev等)预留的资源量,

如:--system-reserved=cpu=500m,memory=1Gi,ephemeral-storage=1Gi。

注意,除了考虑为系统进程预留的量之外,还应该为kernel和用户登录会话预留一些内存。

配置:--system-reserved=cpu=200m,memory=250Mi

(6)配置 驱逐pod的硬阈值


含义:设置进行pod驱逐的阈值,这个参数只支持内存和磁盘。 通过--eviction-hard标志预留一些内存后,当节点上的可用内存降至保留值以下时, kubelet 将会对pod进行驱逐。
配置:--eviction-hard=memory.available<5%,nodefs.available<10%,imagefs.available<10%

(7)配置 驱逐pod的软阈值

--eviction-soft=memory.available<10%,nodefs.available<15%,imagefs.available<15%

(8)定义达到软阈值之后,持续时间超过多久才进行驱逐

--eviction-soft-grace-period=memory.available=2m,nodefs.available=2m,imagefs.available=2m

(9)驱逐pod前最大等待时间=min(pod.Spec.TerminationGracePeriodSeconds, eviction-max-pod-grace-period),单位为秒

--eviction-max-pod-grace-period=30

(10)至少回收的资源量

--eviction-minimum-reclaim=memory.available=0Mi,nodefs.available=500Mi,imagefs.available=500Mi

以上配置均为百分比,举例:

以2核4GB内存40GB磁盘空间的配置为例,Allocatable是1.6 CPU,3.3Gi 内存,25Gi磁盘。当pod的总内存消耗大于3.3Gi或者磁盘消耗大于25Gi时,会根据相应策略驱逐pod。

硬驱逐与软驱逐

硬驱逐

kubelet 利用metric的值作为决策依据来触发驱逐行为,下面内容来自于 Kubelet summary API。

一旦超出阈值,就会触发 kubelet 进行资源回收的动作(区别于软驱逐,有宽限期),指标如下:

  • nodefs: 机器文件系统
  • imagesfs: Kubelet 能够利用 cAdvisor 自动发现这些文件系统,镜像存储空间

例如如果一个 Node 有 10Gi 内存,我们希望在可用内存不足 1Gi 时进行驱逐,就可以选取下面的一种方式来定义驱逐阈值:

  • memory.available<10%
  • memory.available<1Gi

可以配置百分比或者实际值,但是操作符只能使用小于号,即<

软驱逐

软阈值需要和一个宽限期参数协同工作。当系统资源消耗达到软阈值时,这一状况的持续时间超过了宽限期之前,Kubelet 不会触发任何动作。如果没有定义宽限期,Kubelet 会拒绝启动。

另外还可以定义一个 Pod 结束的宽限期。如果定义了这一宽限期,那么 Kubelet 会使用 pod.Spec.TerminationGracePeriodSeconds 和最大宽限期这两个值之间较小的那个(进行宽限),如果没有指定的话,kubelet 会不留宽限立即杀死 Pod。

软阈值的定义包括以下几个参数:

  • eviction-soft:驱逐阈值,例如 memory.available<1.5Gi,如果满足这一条件的持续时间超过宽限期,就会触发对 Pod 的驱逐动作。

  • eviction-soft-grace-period:驱逐宽限期,例如 memory.available=1m30s,用于定义达到软阈值之后,持续时间超过多久才进行驱逐。

  • eviction-max-pod-grace-period:达到软阈值之后,到驱逐一个 Pod 之前的最大宽限时间(单位是秒)

判断周期

Housekeeping interval 参数定义一个时间间隔,Kubelet 每隔这一段就会对驱逐阈值进行评估。

  • housekeeping-interval:容器检查的时间间隔。

节点表现

如果触发了硬阈值,或者符合软阈值的时间持续了与其对应的宽限期,Kubelet 就会认为当前节点压力太大,下面的节点状态定义描述了这种对应关系。

Kubelet 会持续报告节点状态的更新过程,这一频率由参数 —node-status-update-frequency 指定,缺省情况下取值为 10s。

如果一个节点的状况在软阈值的上下波动,但是又不会超过他的宽限期,将会导致该节点的状态持续的在是否之间徘徊,最终会影响降低调度的决策过程。

要防止这种状况,下面的标志可以用来通知 Kubelet,在脱离pressure之前,必须等待。

eviction-pressure-transition-period 定义了在脱离pressure状态之前要等待的时间

Kubelet 在把pressure状态设置为 False 之前,会确认在周期之内,该节点没有达到阈值


如果达到了驱逐阈值,并且超出了宽限期,那么 Kubelet 会开始回收超出限量的资源,直到回到阈值以内。

Kubelet 在驱逐用户 Pod 之前,会尝试回收节点级别的资源。如果服务器为容器定义了独立的 imagefs,他的回收过程会有所不同。

有 Imagefs

如果 nodefs 文件系统到达了驱逐阈值,kubelet 会按照下面的顺序来清理空间:

  • 1.删除死掉的 Pod/容器

如果 imagefs 文件系统到达了驱逐阈值,kubelet 会按照下面的顺序来清理空间:

  • 1.删掉所有无用镜像

没有 Imagefs

如果 nodefs 文件系统到达了驱逐阈值,kubelet 会按照下面的顺序来清理空间。

  1. 删除死掉的 Pod/容器
  2. 删掉所有无用镜像

pod驱逐策略

Kubelet 会按照下面的标准对 Pod 的驱逐行为进行评判:

  • 根据服务质量:即BestEffort、Burstable、Guaranteed
  • 根据 Pod 调度请求的被耗尽资源的消耗量

接下来,Pod 按照下面的顺序进行驱逐(QOS):

  1. BestEffort:消耗最多紧缺资源的 Pod 最先驱逐。
  2. Burstable:请求(request)最多紧缺资源的 Pod 被驱逐,如果没有 Pod 超出他们的请求,会驱逐资源消耗量最大的 Pod。
  3. Guaranteed:请求(request)最多紧缺资源的 Pod 被驱逐,如果没有 Pod 超出他们的请求,会驱逐资源消耗量最大的 Pod。

参考 POD的QOS:服务质量等级

Guaranteed Pod 不会因为其他 Pod 的资源被驱逐。如果系统进程(例如 kubelet、docker、journald 等)消耗了超出 system-reserved 或者 kube-reserved 的资源,而且这一节点上只运行了 Guaranteed Pod,那么为了保证节点的稳定性并降低异常请求对其他 Guaranteed Pod 的影响,必须选择一个 Guaranteed Pod 进行驱逐。

本地磁盘是一个 BestEffort 资源。如有必要,kubelet 会在 DiskPressure 的情况下,kubelet 会按照 QoS 进行评估。如果 Kubelet 判定缺乏 inode 资源,就会通过驱逐最低 QoS 的 Pod 的方式来回收 inodes。如果 kubelet 判定缺乏磁盘空间,就会通过在相同 QoS 的 Pods 中,选择消耗最多磁盘空间的 Pod 进行驱逐。


有 Imagefs

  • 如果 nodefs 触发了驱逐,Kubelet 会用 nodefs 的使用对 Pod 进行排序 – Pod 中所有容器的本地卷和日志。

  • 如果 imagefs 触发了驱逐,Kubelet 会根据 Pod 中所有容器的消耗的可写入层进行排序。


没有 Imagefs

  • 如果 nodefs 触发了驱逐,Kubelet 会对各个 Pod 的所有容器的总体磁盘消耗进行排序 —— 本地卷 + 日志 + 写入层。

  • 在某些场景下,驱逐 Pod 可能只回收了很少的资源。这就导致了 kubelet 反复触发驱逐阈值。另外回收资源例如磁盘资源,是需要消耗时间的。

  • 要缓和这种状况,Kubelet 能够对每种资源定义 minimum-reclaim。kubelet 一旦发现了资源压力,就会试着回收至少 minimum-reclaim 的资源,使得资源消耗量回到期望范围。

例如下面的配置:

--eviction-hard=memory.available<500Mi,nodefs.available<1Gi,imagefs.available<100Gi

--eviction-minimum-reclaim="memory.available=0Mi,nodefs.available=500Mi,imagefs.available=2Gi"

  • 如果 memory.available 被触发,Kubelet 会启动回收,让 memory.available 至少有 500Mi。

  • 如果是 nodefs.available,Kubelet 就要想法子让 nodefs.available 回到至少 1.5Gi。

  • 而对于 imagefs.available, kubelet 就要回收到最少 102Gi。

缺省情况下,所有资源的 eviction-minimum-reclaim 为 0。

在节点资源紧缺的情况下,调度器将不再继续向此节点部署新的 Pod

节点 OOM 时

如果节点在 Kubelet 能够回收内存之前,遭遇到了系统的 OOM (内存不足),节点就依赖 oom_killer 进行响应了。

kubelet 根据 Pod 的 QoS 为每个容器设置了一个 oom_score_adj 值。

如果 kubelet 无法在系统 OOM 之前回收足够的内存,oom_killer 就会根据根据内存使用比率来计算 oom_score,得出结果和 oom_score_adj 相加,最后得分最高的 Pod 会被首先驱逐。

跟 Pod 驱逐不同,如果一个 Pod 的容器被 OOM 杀掉,他是可能被 kubelet 根据 RestartPolicy 重启的。

Daemonset 的处理

因为 DaemonSet 中的 Pod 会立即重建到同一个节点,所以 Kubelet 不应驱逐 DaemonSet 中的 Pod。

但是目前 Kubelet 无法分辨一个 Pod 是否由 DaemonSet 创建。如果Kubelet 能够识别这一点,那么就可以先从驱逐候选列表中过滤掉 DaemonSet 的 Pod。

一般来说,强烈建议 DaemonSet 不要创建 BestEffort Pod,而是使用 Guaranteed Pod,来避免进入驱逐候选列表。

已知问题

Kubelet 无法及时监测到内存压力

Kubelet 目前从 cAdvisor 定时获取内存使用状况统计。如果内存使用在这个时间段内发生了快速增长,Kubelet 就无法观察到 MemoryPressure,可能会触发 OOMKiller。我们正在尝试将这一过程集成到 memcg 通知 API 中,来降低这一延迟,而不是让内核首先发现这一情况。

如果用户不是希望获得终极使用率,而是作为一个过量使用的衡量方式,对付这一个问题的较为可靠的方式就是设置驱逐阈值为 75% 容量。这样就提高了避开 OOM 的能力,提高了驱逐的标准,有助于集群状态的平衡。

Kubelet 可能驱逐超出需要的更多 Pod

这也是因为状态搜集的时间差导致的。未来会加入功能,让根容器的统计频率和其他容器分别开来(https://github.com/google/cadvisor/issues/1247)。

Kubelet 如何在 inode 耗尽的时候评价 Pod 的驱逐

目前不可能知道一个容器消耗了多少 inode。如果 Kubelet 觉察到了 inode 耗尽,他会利用 QoS 对 Pod 进行驱逐评估。在 cadvisor 中有一个 issue,来跟踪容器的 inode 消耗,这样我们就能利用 inode 进行评估了。例如如果我们知道一个容器创建了大量的 0 字节文件,就会优先驱逐这一 Pod

最佳实践

资源预留

1、资源预留需要设置,pod 的 limit 也要设置。
2、cpu是可压缩资源,内存、磁盘资源是不可压缩资源。内存一定要预留,CPU可以根据实际情况来调整
3、预留多少合适:根据集群规模设置阶梯,如下(GKE建议):

Allocatable = Capacity – Reserved – Eviction Threshold

对于内存资源:

  • 内存少于1GB,则设置255 MiB
  • 内存大于4G,设置前4GB内存的25%
  • 接下来4GB内存的20%(最多8GB)
  • 接下来8GB内存的10%(最多16GB)
  • 接下来112GB内存的6%(最高128GB)
  • 超过128GB的任何内存的2%
  • 在1.12.0之前的版本中,内存小于1GB的节点不需要保留内存

对于 CPU 资源:

  • 第一个核的6%
  • 下一个核的1%(最多2个核)
  • 接下来2个核的0.5%(最多4个核)
  • 4个核以上的都是总数的0.25%

对于磁盘资源(不是正式特性,仅供参考):

效果:查看节点的可分配资源:

kubectl describe node [NODE_NAME] | grep Allocatable -B 4 -A 3

驱逐配置

--eviction-hard=memory.available<5%,nodefs.available<10%,imagefs.available<10%

--eviction-soft=memory.available<10%,nodefs.available<15%,imagefs.available<15%

--eviction-soft-grace-period=memory.available=2m,nodefs.available=2m,imagefs.available=2m

--eviction-max-pod-grace-period=30

--eviction-minimum-reclaim=memory.available=0Mi,nodefs.available=500Mi,imagefs.available=500Mi

关于 pod 驱逐

这里补充一些关于 pod evicted 的内容,解释之前的一些误解,内容来自谈谈 K8S 的 pod eviction

从发起模块的角度,pod eviction 可以分为两类:

  • Kube-controller-manager: 周期性检查所有节点状态,当节点处于 NotReady 状态超过一段时间后,驱逐该节点上所有 pod。
  • Kubelet: 周期性检查本节点资源,当资源不足时,按照优先级驱逐部分 pod。

Kube-controller-manger 发起的驱逐

Kube-controller-manager 周期性检查节点状态,每当节点状态为 NotReady,并且超出 podEvictionTimeout 时间后,就把该节点上的 pod 全部驱逐到其它节点,其中具体驱逐速度还受驱逐速度参数,集群大小等的影响。最常用的 2 个参数如下:

  • –pod-eviction-timeout:NotReady 状态节点超过该时间后,执行驱逐,默认 5 min。
  • –node-eviction-rate:驱逐速度,默认为 0.1 pod/秒
  • 当某个 zone 故障节点的数目超过一定阈值时,采用二级驱逐速度进行驱逐。

  • –large-cluster-size-threshold:判断集群是否为大集群,默认为 50,即 50 个节点以上的集群为大集群。

  • –unhealthy-zone-threshold:故障节点数比例,默认为 55%
  • –secondary-node-eviction-rate:当大集群的故障节点超过 55% 时,采用二级驱逐速率,默认为 0.01 pod/秒。当小集群故障节点超过 55% 时,驱逐速率为 0 pod/秒。
func (nc *NodeController) monitorNodeStatus() error {
        ......
        if currentReadyCondition != nil {
            // Check eviction timeout against decisionTimestamp
            if observedReadyCondition.Status == api.ConditionFalse &&
                decisionTimestamp.After(nc.nodeStatusMap[node.Name].readyTransitionTimestamp.Add(nc.podEvictionTimeout)) {
                if nc.evictPods(node) {
                    glog.V(2).Infof("Evicting pods on node %s: %v is later than %v + %v", node.Name, decisionTimestamp, nc.nodeStatusMap[node.Name].readyTransitionTimestamp, nc.podEvictionTimeout)
                }
            }
            if observedReadyCondition.Status == api.ConditionUnknown &&
                decisionTimestamp.After(nc.nodeStatusMap[node.Name].probeTimestamp.Add(nc.podEvictionTimeout)) {
                if nc.evictPods(node) {
                    glog.V(2).Infof("Evicting pods on node %s: %v is later than %v + %v", node.Name, decisionTimestamp, nc.nodeStatusMap[node.Name].readyTransitionTimestamp, nc.podEvictionTimeout-gracePeriod)
                }
            }
   ......
}

Kubelet 发起的驱逐

Kubelet 周期性检查本节点的内存和磁盘资源,当可用资源低于阈值时,则按照优先级驱逐 pod,具体检查的资源如下:

  • memory.available
  • nodefs.available
  • nodefs.inodesFree
  • imagefs.available
  • imagefs.inodesFree

以内存资源为例,当内存资源低于阈值时,驱逐的优先级大体为 BestEffort > Burstable > Guaranteed,具体的顺序可能因实际使用量有所调整。当发生驱逐时,kubelet 支持 soft 和 hard 两种模式,soft 模式表示缓期一段时间后驱逐,hard 模式表示立刻驱逐。

驱逐效果

对于 kubelet 发起的驱逐,往往是资源不足导致,它优先驱逐 BestEffort 类型的容器,这些容器多为离线批处理类业务,对可靠性要求低。驱逐后释放资源,减缓节点压力,弃卒保帅,保护了该节点的其它容器。无论是从其设计出发,还是实际使用情况,该特性非常 nice。

对于由 kube-controller-manager 发起的驱逐,效果需要商榷。正常情况下,计算节点周期上报心跳给 master,如果心跳超时,则认为计算节点 NotReady,当 NotReady 状态达到一定时间后,kube-controller-manager 发起驱逐。然而造成心跳超时的场景非常多,例如:

  • 原生 bug:kubelet 进程彻底阻塞
  • 误操作:误把 kubelet 停止
  • 基础设施异常:如交换机故障演练,NTP 异常,DNS 异常
  • 节点故障:硬件损坏,掉电等

从实际情况看,真正因计算节点故障造成心跳超时概率很低,反而由原生 bug,基础设施异常造成心跳超时的概率更大,造成不必要的驱逐。

理想的情况下,驱逐对无状态且设计良好的业务方影响很小。但是并非所有的业务方都是无状态的,也并非所有的业务方都针对 Kubernetes 优化其业务逻辑。例如,对于有状态的业务,如果没有共享存储,异地重建后的 pod 完全丢失原有数据;即使数据不丢失,对于 Mysql 类的应用,如果出现双写,重则破坏数据。对于关心 IP 层的业务,异地重建后的 pod IP 往往会变化,虽然部分业务方可以利用 service 和 dns 来解决问题,但是引入了额外的模块和复杂性。

Reference

Written by

说点什么

欢迎讨论

avatar

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

  Subscribe  
提醒