Kubernetes资源管理
为了避免集群中的 Pod 负载加大时节点资源不足,导致某些用户进程被“杀掉”,Kubernetes 需要有一套完备的资源配额限制及对应的 Pod 服务等级机制,解决思路如下:
(1)可以全面限制一个应用及其中的 Pod 所能占用的资源配额。具体包括三种方式:
<1>定义每个 Pod 上资源配额相关的参数,比如 CPU/Memory Request/Limit;
<2>自动为每个没有定义资源配额的 Pod 添加资源配额模板(LimitRange);
<3>从总量上限制一个租户(应用)所能使用的资源配额(ResourceQuota)。
(2)允许集群的资源被超额分配,以提高集群的资源利用率,同时允许用户根据业务的优先级,为不同的 Pod 定义相应的服务保障等级(QoS)。我们可以将 Qos 理解为“活命优先级”,当系统资源不足时,低等级的 Pod 会被操作系统自动清理,以确保高等级的 Pod 稳定运行。
一、计算资源管理
1. Requests 和 Limits 参数简介
Requests 和 Limits 参数只能被设置到容器上,用于表示当前容器的 CPU 或 内存的使用范围,在 Kubernetes 中,可用 Requests 或 Limits 参数设置某个 Pod 中所有容器的 Requests 或 Limits 的总和,对于没有设置 Requests 或 Limits 的容器,该项的值被当作 0 或者按照集群配置的默认值来计算。
CPU 在容器技术中属于可压缩资源,因此对 CPU 的 Limits 配置一般不会因为偶然超标使用而导致容器被操作系统“杀掉”,但对于内存这种不可压缩的资源来说,若容器试图请求超过其 Limits 参数设置的资源,则会被操作系统“杀掉”。
2. 基于 Requests 和 Limits 的 Pod 调度机制
Kubernetes 的 kube-scheduler 通过计算 Pod 中所有容器的 Requests 总和来决定对 Pod 的调度。
不管是 CPU 还是内存,Kubemetes 调度器和 kubelet 都会确保节点上所有 Pod 的 Requests 总和不会超过在该节点上可分配给容器使用的资源容量上限(即使实际使用的资源很少)。
3. Requests 和 Limits 的背后机制
Kubelet 在启动 Pod 的某个容器时,会将容器的 Requests 和 Limits 值转化为相应的容器启动参数传递给容器执行器(Docker 或 rkt)。
(1)spec.container[].resources.requests.cpu
这个参数值会被转化为 core 数(比如配置的 100m 会转化为 0.1),然后乘以 1024,再将这个结果作为 --cpu-shares 参数的值传递给 docker run 命令。在 docker run 命令中,--cpu-share 参数是一个相对权重值,主要用于设置资源分配比例。
(2)spec.container[].resources.limits.cpu
这个参数值会被转化为 millicore 数(比如配置的 1 被转化为 1000,而配置的 100m 被转化为 100) ,将此值乘以 100000,再除以 1000,然后将结果值作为 --cpu-quota 参数的值传递给 docker run 命令。 docke run 令中的另一个参数 --cpu-period 默认被设置为 100000,表示 Docker 重新计量和分配 CPU 的使用时间间隔为 100000µs ( 100 ms ) 。两个参数一起配合完成对容器 CPU 的使用限制。
(3)spec.container[].resources.requests.memory
这个参数值只提供给 Kubernetes 调度器作为调度和管理的依据,不会作为任何参数传递给 Docker。
(4)spec.container[].resources.limits.memory
这个参数值会被转化为单位为 Bytes 整数,值作为 --memory 参数传递给 docker run 命令。
4. 计算资源使用情况监控
Pod 资源用量会作为 Pod 状态信息一同上报给 Master。如果在集群中配置了 Heapster 来监控集群的性能数据,那么还可以从 Heapster 查看 Pod 的资源用量信息。
二、资源配置范围管理(LimitRange)
在默认情况下,Kubernetes 不会对 Pod 加上 CPU 和内存限制,这意味着 Kubernetes 系统中的任何 Pod 都可以使用其所在节点所有可用的 CPU 和内存。通过配置 Pod 的计算资源 Requests 和 Limits,可以限制 Pod 的资源使用,但配置每一个 Pod 的 Requests 和 Limits 过于烦琐,需要对集群内 Requests 和 Limits 的配置做一个全局限制,这时候 LimitRange 就派上用场了。
LimitRange 各项配置的意义和特点:
三、资源服务质量管理(Resource QoS)
1. Requests 和 Limits 申请资源的可靠性
Requests 是 Kubernetes 调度时能为容器提供的完全、可保障的资源量(最低保璋),而 Limits 是系统允许容器运行时可能使用的资源量的上限(最高上限)。
Kubemetes 中 Pod 的 Requests 和 Limits 资源配置有如下特点:
(1)如果 Pod 配置的 Requests 值等于 Limits 值,那么该 Pod 可以获得的资源是完全可靠的。
(2)如果 Pod 的 Requests 值小于 Limits 值,那么该 Pod 获得的资源可分为两部分:
<1>完全可靠的资源,资源量的大小等于 Requests 值;
<2>不可靠的资源,资源量最大等于 Limits 与 Requests 的差额,这份不可靠的资源能够申请到多少,取决于当时主机上容器可用资源的余量。
通过这种机制, Kubernetes 可以实现节点资源的超售(Over Subscription),如果节点上的容器的资源使用峰值能错开,那么所有容器都可以正常运行。
2. 根据 Requests 和 Limits 定义服务质量等级(QoS Classes)
Kubernetes 的资源配置定义了 Pod 的三种 QoS 级别:
(1)Guaranteed
如果 Pod 中的所有容器对所有资源类型都定义了 Limits 和 Requests,并且所有容器的 Limits 值都和 Requests 值相等(且都不为 0),那么该 Pod 的 QoS 级别就是 Guaranteed。
(2)BestEffort
如果 Pod 所有容器都未定义资源配置(Requests 和 Limits 都未定义),那么该 Pod 的 QoS 级别就是 BestEffort 。
(3)Burstable
当一个 Pod 不为 Guaranteed 级别,也不为 BestEffort 级别时,Pod 的 QoS 级别就是 Burstable。Burstable 级别的 Pod 涉及两种情况:
<1>Pod 中的一部分容器在一种或多种资源类型的资源配置中定义了 Requests 值和 Limits 值(都不为 0),且 Requests 值小于 Limits 值;
<2>Pod 中的一部分容器未定义资源配置(Requests 和 Limits 都未定义)。
QoS 优先级由高到低为:Guaranteed、Burstable、BestEffort。
Kubernetes QoS 的工作特点:
在 Pod 的 CPU Requests 无法得到满足(比如节点的系统级任务占用过多的 CPU 导致无法分配足够的 CPU 给容器使用)时,容器得到的 CPU 会被压缩限流。
由于内存是不可压缩的资源,所以针对内存资源紧缺的情况,会按照以下逻辑处理:
(1)BestEffort Pod 的优先级最低,在这类 Pod 中运行的进程会在系统内存紧缺时被第一优先“杀掉"。当然,从另一个角度来看,BestEffort Pod 由于没有设置资源 Limits,所以在资源充足时,它们可以充分使用所有闲置资源。
(2)Burstable Pod 的优先级居中,这类 Pod 在初始时会被分配较少的可靠资源,但可以按需申请更多的资源。当然,如果整个系统内存紧缺,又没有 BestEffort 容器可以被杀掉以释放资源,那么这类 Pod 中的进程可能被“杀掉"。
(3)Guaranteed Pod 的优先级最高,而且一般情况下这类 Pod 只要不超过其资源 Limits 的限制就不会被“杀掉"。当然,如果整个系统内存紧缺,又没有其他更低优先级的容器可以被“杀掉“以释放资源,那么这类 Pod 中的进程也可能会被“杀掉"。
四、资源配额管理(Resource Quotas)
通过 ResourceQuota 对象,我们可以定义资源配额,这个资源配额可以为每个命名空间都提供一个总体的资源使用限制:它可以限制命名空间中某种类型的对象的总数量上限,也可以设置命名空间中 Pod 可以使用的计算资源、存储资源的总上限。
五、ResourceQuota 和 LimitRange 实践
1. 创建命名空间
创建了一个名为 wjt-ns 的命名空间。
2. 创建 ResourceQuota
创建了一个名为 wjt-resource-quota 的 ResourceQuota。
配额系统会自动防止在该命名空间中同时拥有超过 4 个非“终止态”的 Pod。此外,由于该项资源配额限制了 CPU 和内存的 Requests 和 Limits 总量,因此会强制要求该命名空间中的所有容器都显式定义 CPU 和 内存的 Limits、Requests (可使用默认值, Requests 默认等于 Limits)。
这时候创建不显式定义 CPU 和 内存的 Limits、Requests 的 Pod 会直接报错:
3. 创建 LimitRange
创建了一个名为 wjt-limit-range 的 LimitRange。
4. 创建 Pod
创建一个不指定资源限制的 Pod,系统会自动为该 Pod 设置默认的资源限制。
5. 再次查看命名空间与 ResourceQuota
可以看到,每个 Pod 在创建时都会消耗指定的资源量,而这些使用最都会被 Kubernetes 准确跟踪、 监控和管理。
另外,ResourceQuota 还可以指定作用域,例如:
只有配置了 Requests 的 Pod 才会受其限制。
六、节点的 CPU 管理策略
kubelet 默认使用 CFS Quota 技术基于 Pod 的 CPU Limit 对 Node 上 CPU 资源的使用进行限制和管理(CFS,Completely Fair Scheduler,即完全公平调度算法)。
当在 Node 上运行了很多 CPU 密集型 Pod 时,容器进程可能会被调度到不同的 CPU 核上进行运算,从而发生 CPU 上下文的切换。许多应用对这种 CPU 的切换不敏感,但也有一些应用的性能会明显受到 CPU 缓存亲和性及调度延迟的影响,针对这类应用,Kubernetes 提供了一个可选的 CPU 管理策略,来确定节点上 CPU 资源调度的优先级,为 Pod 运行达到更好的性能提供支待 。
CPU 管理策略通过 Node 上的 kubelet 启动参数 --cpu-manager-policy 进行指定,目前支持两种策略:
(1)None 策略
None 策略使用默认的 CPU 亲和性方案,即操作系统默认的 CPU 调度策略。对于 QoS 级别为 Guaranteed 的 Pod,会强制使用 CFS Quota 机制对 CPU 资源进行限制。
(2)Static 策略
Static 策略针对具有特定 CPU 资源需求的 Pod。对于 QoS 级别为 Guaranteed 的 Pod,如果其 Container 设置的 CPU Request 为大于等于 1 的整数, Kubernete 就能允许容器绑定节点上的一个或多个 CPU 核独占运行。这种独占是使用 cpuset cgroup 控制器来实现的。
该策略管理一个共享 CPU 资源池,该资源池最初包含节点上的所有 CPU 资源 。可用的独占性 CPU 资源数最等于节点的 CPU 总量减去通过 --kube-reserved 和 --system-reserved 参数设置保留给系统的 CPU 资源数量。
共享池是 QoS 级别为 BestEffort 和 Burstable 的 Pod 运行所需的 CPU 集合。 QoS 级别为 Guaranteed 的 Pod 中的容器,如果声明了非整数值的 CPU Request,则也将运行在共享池上,只有声明了整数 CPU Request 的容器才会被分配独占的 CPU 资源。
QoS 级别为 Guaranteed 的 Pod 被调度到节点上时,如果容器的 CPU 资源需求设置符合静态分配的要求,则所需的 CPU 核会被从共享池中取出并放到容器的 cpuset 中,供容器独占使用。容器 cpuset 中的 CPU 核数与 Pod 定义中指定的整数个 CPU limit 相等,无须再使用 CFS Quota 机制分配 CPU 资源。这种静态分配机制增强了 CPU 亲和 性,减少了 CPU 上下文切换的次数。
七、资源紧缺时的 Pod 驱逐机制
在资源严重不足的情况下,操作系统会触发 OOM Killer 的终极审批。为了避免出现这种严重后果,Kubemetes 设计和实现了一套自动化的 Pod 驱逐机制,该机制会自动从资源紧张的节点上驱逐一定数量的 Pod ,以保证在该节点上有充足的资源。
1. 资源监控与驱逐行为
(1)资源监控
每个节点上的 kubelet 都会通过 cAdvisor 提供的资源使用指标来监控自身节点的资源使用量,并根据这些指标的变化做出相应的驱逐决定和操作。
(2)驱逐行为
驱逐时机:
<1>节点磁盘资源不足时会触发 Pod 的驱逐行为
Kubernetes 包括两种文件系统: nodefs 和 imagefs。nodefs 时 kubelet 用于存储卷系统、服务程序日志等的文件系统;imagefs 是容器运行时使用的可选文件系统,用于存储容器镜像和容器可写层数据。cAdvisor 提供了这两种文件系统的相关统计指标,分别如下:
available:表示该文件系统中可用的磁盘空间。
inodesFree:表示该文件系统中可用的 inode 数量(索引节点数量)。
默认情况下,kubelet 检测到下面的任意条件满足时,就会触发 Pod 的驱逐行为。
如果 nodefs 达到驱逐阈值, kubelet 就会删除所有已失效的 Pod 及其容器实例对应的磁盘文件。相应地,如果 imagefs 达到驱逐阔值,则 kubelet 会删除所有未使用的容器镜像。
<2>节点内存不足时会触发 Pod 的驱逐行为
memory.available 代表当前节点的可用内存,默认情况下 memory.available<100Mi 时会触发 Pod 的驱逐行为。驱逐 Pod 的过程:kubelet 从 cAdvisor 中定期获取相关的资源使用量指标数据,通过配置的阈值筛选出满足驱逐条件的 Pod;kubelet 对这些 Pod 进行排序,每次都选择一个 Pod 进行驱逐。
驱逐阈值:
驱逐阈值可以通过软阈值和硬阈值两种方式进行设置。
<1>驱逐软阈值
驱逐软阙值由一个驱逐阈值和一个管理员设定的宽限期共同定义。当系统资源消耗达到软阈值时,在这一状况的持续时间达到宽限期之前,kubelet 不会触发驱逐动作。
<2>驱逐硬阈值
硬阙值没有宽限期,如果达到了硬阈值,则 kubelet 会立即“杀掉 “Pod 并进行资源回收。
2. 资源回收
如果达到了驱逐阔值,并且也过了宽限期,kubelet 就会回收超出限量的资源,直到驱逐信号量回到阙值以内。
(1)回收 Node 级别的资源
kubelet 在驱逐用户 Pod 之前,会尝试回收 Node 级别的资源。在观测到磁盘压力时,基于服务器是否为容器运行时定义了独立的 imagefs,会有不同的资源回收过程:
<1>有 lmagefs 时
如果 nodefs 文件系统达到了驱逐阙值,kubelet 会删掉已停掉的 Pod 和容器来清理空间。
如果 imagefs 文件系统达到了驱逐阙值,则 kubelet 会删掉所有无用的镜像来清理空间。
<2>没有 lmagefs 时
如果 nodefs 文件系统达到了驱逐阈值,则 kubelet 会这样清理空间:首先删除已停掉 Pod、容器;然后删除所有无用的镜像。
(2)驱逐用户的 Pod
kubelet 如果无法在节点上回收足够的资源,就会开始驱逐用户的 Pod。
kubelet 会按照下面的标准对 Pod 的驱逐行为进行判断:
<1>Pod 要求的服务质量。
<2>Pod 对紧缺资源的消耗量(相对于资源请求 Request)。
接下来, kubelet 会按照下面的顺序驱逐 Pod:
<1>BestEffort:紧缺资源消耗最多的 Pod 最先被驱逐。
<2>Burstable:根据相对请求来判断,紧缺资源消耗最多的 Pod 最先被驱逐,如果没 Pod 超出它们的请求,则策略会瞄准紧缺资源消耗量最大的 Pod 。
<3>Guaranteed:根据相对请求来判断,紧缺资源消耗最多的 Pod 最先被驱逐,如果没有 Pod 超出它们的请求,则策略会瞄准紧缺资源消耗量最大的 Pod。
(3)最少资源回收量
在某些场景下,驱逐 Pod 可能只回收了很少的资源,这就导致了 kubelet 反复触发驱逐阈值。另外,磁盘这样的资源是需要消耗时间的。
要缓和这种状况 kubelet 可以对每种资源都定义 minimum-reclaim。kubelet 一旦监测到了资源压力,就会试着回收不少于 minimum-reclaim 的资源数量,使得资源消耗量回到期望的范围。
3. 节点资源紧缺情况下的系统行为
(1)调度器的行为
在节点资源紧缺的情况下,节点会向 Master 报告这一状况。在 Master 上运行的调度以此为信号,不再继续向该节点调度新的 Pod。
(2)Node 的 OOM 行为
如果节点在 kubelet 能够回收内存之前遭遇了系统的 OOM (内存不足),节点则依赖 oom_killer 的设置进行响应。
(3)对 DaemonSet 类型的 Pod 驱逐的考虑
通过 DaemonSet 创建的 Pod 具有在节点上自动重启的特性,所以不要在 DaemonSet 中创建 BestEffort 类型的 Pod。
4. 存在问题
kubelet 目前通过 cAdvisor 定时获取内存使用状况的统计情况,无法及时观测到内存压力,可能会触发 OOMKiller,可以通过降低驱逐阈值来改善这个问题。同时也可能因为状态收集存在时间差导致 kubelet 错误地驱逐了更多的 Pod。
参考:
《Kubernetes 权威指南第 5 版》