2.4 节点资源管理
计算节点除CPU、内存和存储等硬件相关资源外,还有操作系统资源,例如进程上限、磁盘I/O 等。在Kubernetes 集群中,同一节点上会运行多个不同应用的容器进程。不可避免地,这些进程会共享节点资源,并可能发生资源竞争。合理的节点资源管理能提高节点资源利用率,避免相邻进程彼此干扰,保证系统服务正常运行。
Kubernetes 计算节点资源管理方案已渐趋成熟:具体体现在状态汇报、资源预留、防止节点资源耗尽的防御机制驱逐及容器和系统资源的配置。
2.4.1 状态上报
kubelet 是部署在每个Kubernetes 节点上、负责Pod 生命周期及节点状态上报的组件。它周期性地向 API Server 进行汇报,并更新节点的相关健康和资源使用信息,以供Kubernetes 的控制平面模块对节点和节点上的Pod 进行管理和决策。上报信息如下:
● 节点基础信息,包括IP 地址、操作系统、内核、运行时、kubelet、kube-proxy 版本信息。部分信息直接从节点获取,而部分信息需要调用云提供商的API 获取。
● 节点资源信息包括CPU、内存、Hugepage、临时存储、GPU 等注册设备,以及这些资源中可以分配给容器使用的部分。
● 调度器在为Pod 选择节点时会将机器的状态信息作为依据。表2-6 展示了节点状态及其代表的意义。比如Ready 状态反映了节点是否就绪,True 表示节点健康;False 表示节点不健康;Unknown 表示节点控制器在最近40s 内没有收到节点的消息。调度器在调度Pod 时会过滤掉所有Ready 状态为非True 的节点。
表2-6 节点的状态及其意义
以下三个参数可以控制kubelet 更新节点状态频率:
● NodeStatusUpdateFrequency。
● NodeStatusReportFrequency。
● NodeLeaseDurationSeconds。
早期版本只有NodeStatusUpdateFrequency,默认配置下所有节点每隔10s 上报一次状态,而上报的信息包含状态信息和资源信息,因此需要传输的数据包较大。随着集群规模的增长,状态的频繁更新对控制平面组件造成较大压力:与节点相关的控制器会不断接收节点变更通知,从而增加控制器开销;极端场景中,它甚至会使etcd 迅速到达其存储上限;节点IP 地址等要上报的信息需要从云提供商的API 获取,因此频繁的调用对底层云平台也造成较大压力。
自1.12 版本起,Kubernetes 引入了NodeLease 特性:将上报信息划分为更新表2-6 中罗列的节点状态和Lease 对象。Kubernetes 为每个节点创建一个轻量级的Lease 对象,该对象只包含最基本的节点信息。它的频繁变更对系统造成的压力,会比直接更新节点对象小很多。kubelet 在节点状态发生变更或者默认一分钟的NodeStatusReportFrequency 时钟周期到达时,更新节点的状态信息,同时以默认10s 的NodeStatusUpdateFrequency 周期更新Lease 对象。在默认40s 的NodeLeaseDurationSeconds 周期内,若Lease 对象没有被更新,则对应节点可以被判定为不健康。
kube-scheduler 在调度Pod 时会根据节点状态来决定是否可以将新的Pod 调度到该节点上,以免让本来处于不健康状态的节点的情况进一步恶化。
2.4.2 资源预留
计算节点除用户容器外,还存在很多支撑系统运行的基础服务,譬如 systemd、journald、sshd、dockerd、Containerd、kubelet 等。如果这些服务的运行受到影响,系统将变得不稳定,进而影响用户的容器进程。为了使服务进程能够正常运行,要确保它们在任何时候都可以获取足够的系统资源,所以我们要为这些系统进程预留资源。
kubelet 可以通过众多启动参数为系统预留 CPU 、 内存、 PID 等资源, 比如SystemReserved、KubeReserved 等。如下代码所示,在节点对象状态中可以看到当前节点的CPU、memory、emphermal-storage 等资源信息,其中每一项资源分为系统的可分配资源(Allocatable)和节点的容量(Capacity)资源。
容量资源(Capacity)是指 kubelet 获取的计算节点当前的资源信息。CPU 是从/proc/cpuinfo 文件中获取的节点CPU 核数;memory 是从/proc/memoryinfo 中获取的节点内存大小;ephemeral-storage 是指节点根分区的大小。资源可分配额(Allocatable)是用户Pod可用的资源,是资源容量减去分配给系统的资源的剩余部分,两者的关系如表2-7 所示。
表2-7 节点的资源容量和可分配资源
节点的预留资源由kubelet 设置到对应的容器或者系统进程的Cgroup 中,以确保系统服务的健康运行。
2.4.3 驱逐管理
kubelet 会在系统资源不够时中止一些容器进程,以空出系统资源,保证节点的稳定性。但由kubelet 发起的驱逐只停止Pod 的所有容器进程,并不会直接删除Pod。在驱逐完成后,Pod 的status.phase 会被标记为Failed,status.reason 会被设置为Evicted,status.message则会记录被驱逐的原因。
2.4.3.1 资源可用额监控
kubelet 依赖内嵌的开源软件 cAdvisor,周期性检查节点资源是否短缺。当前版本(v1.18)的驱逐策略是基于磁盘和内存资源用量进行的,因为两者属于不可压缩的资源,当此类资源使用耗尽时将无法再申请。而CPU 是可压缩资源,根据不同进程分配时间配额和权重,CPU 可被多个进程竞相使用。表2-8 显示了当前资源监控收集的可用额指标。
表2-8 资源监控指标
2.4.3.2 驱逐策略
kubelet 获得节点的可用额信息后,会结合节点的容量信息来判断当前节点运行的Pod是否满足驱逐条件。驱逐条件可以是绝对值或百分比,当监控资源的可使用额少于设定的数值或百分比时,kubelet 就会发起驱逐操作。例如,驱逐条件是memory.available<10%或memory.available<5Gi,则kubelet 会在系统内存少于内存总量的10%或少于5GiB 的情况下,对Pod 发起驱逐操作。
根据当前资源的使用情况,驱逐方式可分为软驱逐和硬驱逐,两者的区别如表 2-9所示。
表2-9 驱逐分类
kubelet 参数EvictionMinimumReclaim 可以设置每次回收的资源的最小值,以防止小资源的多次回收。
接下来,介绍一下基于内存压力的驱逐和基于磁盘压力的驱逐。
1.基于内存压力的驱逐
memory.avaiable 表示当前系统的可用内存情况。 kubelet 默认设置了memory.avaiable<100Mi 的硬驱逐条件,当kubelet 检测到当前节点可用内存资源紧张并满足驱逐条件时,会将节点的MemoryPressure 状态设置为True,调度器会阻止BestEffort Pod调度到内存承压的节点。
kubelet 启动对内存不足的驱逐操作时,会依照如下的顺序选取目标Pod:
(1)判断Pod 所有容器的内存使用量总和是否超出了请求的内存量,超出请求资源的Pod 会成为备选目标。
(2)查询Pod 的调度优先级,低优先级的Pod 被优先驱逐。
(3)计算Pod 所有容器的内存使用量和Pod 请求的内存量的差值,差值越小,越容易被驱逐。
2.基于磁盘压力的驱逐
nodefs.available、nodefs.inodesFree、imagefs.available 及imagefs.inodesFree 从多维度展现了系统分区和容器运行时分区的磁盘使用情况。节点的分区形式多种多样,为保证系统的安全,可以为kubelet 工作目录(包括Pod 的emptyDir 卷、容器日志目录、容器运行时目录(用户镜像和容器可写层))划分单独的分区。这种分区可以让不同用途的空间相互隔离,以保证系统分区的安全性。但若完全按照该方式进行分区,会让kubelet 磁盘管理变得极其复杂。因此,除系统分区和容器运行时分区外,kubelet 并未对其他分区进行管理。此处给出的建议是,将kubelet 工作目录和容器日志放在系统分区中,容器运行时分区是可选的,可以合并到系统分区中。当kubelet 检测到当前节点上的nodefs.available、nodefs.inodesFree、imagefs.available 或imagefs.inodesFree 中的任何一项满足驱逐条件时,它会将节点的DiskPressure 状态设置为True,调度器不会再调度任何Pod 到该节点上。
当磁盘资源使用率高时,kubelet 就会在驱逐正在运行的Pod 之前,尝试通过删除一些已经退出的容器和当前未使用的镜像资源来释放磁盘空间和inode。根据分区的不同,采取的方式也不一样:
(1)有容器运行时分区:如果系统分区(nodefs)达到驱逐阈值,那么kubelet 删除已经退出的容器;如果运行时分区(imagefs)达到驱逐阈值,那么kubelet 删除所有未使用的镜像。
(2)无容器运行时分区:kubelet 同时删除未运行的容器和未使用的镜像。
回收已经退出的容器和未使用的镜像后,如果节点依然满足驱逐条件,kubelet 就会开始驱逐正在运行的Pod,进一步释放磁盘空间。选择目标Pod 的顺序如下:
(1)判断Pod 的磁盘使用量是否超过请求的大小,超出请求资源的Pod 会成为备选目标。
(2)查询Pod 的调度优先级,低优先级的Pod 优先驱逐。
(3)根据磁盘使用超过请求的数量进行排序,差值越小,越容易被驱逐。
由于数据在不同的分区上,所以kubelet 针对不同驱逐信号采取的驱逐策略也不一样:
(1)有容器运行时分区。如果是系统分区触发了驱逐,那么kubelet 将计算Pod 的磁盘使用量中所有容器的日志和本地卷数据;如果是运行时分区触发了驱逐,那么kubelet计算Pod 的磁盘使用量中所有容器的可写层。
(2)无容器运行时分区。对于系统分区触发的驱逐,kubelet 将计算Pod 的磁盘使用量中容器的日志、本地卷和容器的可写层。
2.4.4 容器和系统资源配置
在1.3.3 节中我们介绍过,依据Pod Spec 中对资源请求定义的不同,Pod 可划分为不同的QoS 等级:Guaranteed、Burstable 和BestEffort。kubelet 对不同QoS 等级的Pod 会做不同的处理。
2.4.4.1 CPU 资源
CPU 资源申请包含cpu.request 和cpu.limit。内核对CPU 资源的使用以时间片为单位,因此Pod 对CPU 资源的申请可以以Millicore 为最小单位,一个CPU Core 等于1000 Millicore。
Kubernetes 调度Pod 时,会判断当前节点正在运行的Pod 的CPU Request 的总和,再加上当前调度Pod 的CPU request,计算其是否超过节点的CPU 的可分配资源。如果超出,则该节点应被过滤掉。换言之,调度器会判断当前节点的剩余CPU 资源是否满足Pod 的CPU Request。
kubectl describe node 命令可显示当前节点的CPU 和内存使用率。命令返回结果展示了每个Pod 的资源请求、申请资源占整个节点的比例,以及节点的总资源等信息,如下所示:
kubelet 会读取Pod 的cpu.request 和cpu.limit,为Pod 对应的CPU CGroup 进行资源配置。相关参数之间的关系如表2-10 所示。
表2-10 CPU 的CGroup 的相关参数设置
容器内部可以通过查看/sys/fs/cgroup/cpu/cpu.shares 和/sys/fs/cgroup/cpu/cpu.cfs_ quota_us 来获取当前容器CGroup 的CPU 信息。
节点资源充裕时,容器可以使用不超过限额的CPU 资源。而节点资源紧张时,Linux内核进程调度器通过cpu.shares 确保该容器不会占用其他容器或者进程申请的CPU 资源,同时保证该容器能够获取相应的CPU 时间。Kubernetes 通过request 和limit 的组合,极大地提升了节点资源利用率。
需要额外关注的是:
● 当kube-scheduler 调度带有多个init 容器的Pod 时,只计算cpu.request 最多的init容器,而不是计算所有的init 容器总和。由于多个init 容器按顺序执行,并且执行完成立即退出,所以申请最多的资源init 容器中的所需资源,即可满足所有init容器需求。当所有的init 容器都执行完成并退出后,业务容器将被创建和执行,此时Pod 进入Running 状态。kube-scheduler 在计算该节点被占用的资源时,init容器的资源依然会被纳入计算。因为init 容器在特定情况下可能会被再次执行,比如由于更换镜像而引起Sandbox 重建时。
● 如果Pod 定义中的nodeName 直接指定了目标节点,那么nodeName 被创建后,会直接被节点上的kubelet 监听到,无须通过kube-scheudler 进行调度。kubelet 在启动该Pod 的容器之前,会启动准入机制计算当前节点空闲的CPU 资源是否能够满足Pod 需求,如果不满足则停止启动,并将Pod 标记为OutOfCPU 状态。即使随后节点释放了足够的CPU 资源,Pod 也不会再被启动。
CGroup 在节点上是按层级树结构组织的。不同QoS 等级的Pod 及其容器,CGroup的管理和配置也不相同。如图2-18 所示,当CGroup 驱动基于systemd,并且使用Docker作为运行时时,kubelet 对节点上的容器进行如下的CGroup 层级创建:
● 为节点上运行的所有Pod 创建父CGroup 的kubepods.slice。
● 为 Guarnteed Pod 创建名为 kubepods-pod<pid uid>.slice 的 CGroup, 并置于kubepod.slice 下。同时为Pod 中的每个容器(包括Sandbox)创建一个名为docker- <docker id>.scope 的CGroup,置于Pod CGroup 下。
● 为节点上所有Bustable 类型的Pod 创建一个名为kubepods-burstable.slice 的父CGroup。该CGroup 的下一级是每个Pod 的CGroup,再往下是容器CGroup,它的命名方式和Guarnteed 类型的Pod 一致。
● 对于BestEffort 类型的Pod,kubelet 的处理方式与Bustable 类型一致,也会对该类型的所有Pod 统一创建一个名为kubepods-besteffort.slice 的父CGroup,且其下属的Pod 和容器CGroup 的创建方式与Bustable 的类型一致。
图2-18 CPU CGroup 层级图
对于不同层级的CGroup,相关参数的设置有所不同,表2-11 展示了容器和Pod 的CGroup 配置细节。所有pause 容器的cpu.shares 都设置为2。另外,init 容器在执行完成退出后,容器的CGroup 也会被删除。
表2-11 容器和Pod 的CGroup 配置
在CPU 空闲时,对QoS 层级的CGroup 和Pod 的总CGroup 的参数设置如下:cpu.cfs_quota_us 都设置为-1,cpu.shares 设置为相应的请求值,目的是在CPU 资源空闲的时候,不同类型的Pod 都可以最大化利用CPU 的资源。然而,在CPU 资源紧张的时候,不同类型的Pod 可以按照比例分配资源,同时不影响系统服务,如表2-12 所示。
表2-12 QoS 层级和Pod 的总CGroup 参数配置
kubepod.slice 隶属于节点根CGroup,CGroup 在系统层面的组织结构如图2-19 所示。在系统根CGroup 下,cpu.shares 设置为1024,cpu.cfs_quota_us 设置为-1。众多系统进程如ksoftirqd、migration、kworker 等都在根CGroup 下,而systemd-journald、docker、udevd等服务则位于system.slice 下。kubelet 和dockershim 隶属于systemd。
图2-19 CGroup 在系统层面的组织结构
以上这些CGroup 的CPU 资源都有相同的设置,即cpu.shares=1024,cpu.cfs_quota_ us=-1。
2.4.4.2 内存资源
同CPU 资源一样,Pod Spec 通过定义requests.memory 和limits.memory 的值来为Pod申请内存。Kubernetes 调度Pod 时会判断节点的剩余内存是否满足Pod 的内存请求量,以确定是否可以将Pod 调度到该节点。同样地,kube-scheudler 只计算内存requests,而不看内存limits。
Pod 调度完成后,对应节点的kubelet 会将容器申请的内存资源设置到该容器对应的memory CGroup 中。参数转换关系如表2-13 所示。
表2-13 内存CGroup 参数设置
为什么在进行调度的时候计算容器的内存是requests,而设置CGroup 时用的内存却是limits?requests 在节点上有什么意义呢?
在节点内存资源宽松时,容器的内存使用量可以在不超过memory.limits 的前提下使用。而在内存资源紧张的情况下,kubelet 会周期性地按照既定优先级对容器进行驱逐。在kubelet 对容器进行驱逐之前,系统的OOM Killer 可能会采取OOM 的方式来中止某些容器的进程,进行必要的内存回收操作。而系统根据进程的oom_score 来进行优先级排序,选择待终止的进程,且进程的oom_score 越高,越容易被终止。进程的oom_score 是根据当前进程使用的内存占节点总内存的比例值乘以10,再加上oom_score_adj 综合得到的,而容器进程的oom_score_adj 正是kubelet 根据memory.request 进行设置的。在容器中的/proc/[PID]目录下可以看到当前进程的oom_score_adj 和oom_score,如表2-14 所示。
表2-14 oom_score_adj 的设置
图2-20 展示了节点内存的CGroup 的层级树。内存CGroup 的层级树和CPU 层级树的管理是一致的,都是根据Pod 的QoS 类型来创建相应的CGroup。不同层级的CGroup,参数设置也有所不同。
图2-20 节点内存的CGroup 的层级树
下面分别介绍一下容器和Pod 的CGroup、QoS 层级的CGroup 和Pod 总的CGroup。
1.容器和Pod 的CGroup
容器和Pod 的CGroup 内存参数设置如表2-15 所示。
表2-15 容器和Pod 的CGroup 内存参数设置
2.QoS 层级的CGroup 和Pod 总的CGroup
QoS 层级的CGroup 根据是否设置QosReserved 进行不同的参数设置。设置QosReserved的目的是限制相同QoS 的Pod 能够使用的内存最大值,防止kubelet 驱逐或者OOM Killer处理不够及时,导致高优先级的Pod 受到影响。具体的参数设置如表2-16 所示。
表2-16 QoS 层级和Pod 的总CGroup 内存参数配置
将数字9223372036854771712 转换为16 进制是0x7FFFFFFFFFFFF000,它是能被4096 (内存页面的大小)整除的最大有符号数。如果memory.limit_in_bytes 设置为这个值,就说明对内存无限制。
内存根CGroup 的层级结构和CPU 的根CGroup 一致:在默认设置下,根CGroup 除包含所有Pod 的CGroup kubepods.slice 外,其他CGroup 的memory.limit_in_bytes 都被设置为9223372036854771712。
2.4.4.3 磁盘资源
kubelet 通过cAdvisor 支持系统分区和运行时分区的发现管理,针对计算节点的磁盘分区建议如下:
● 节点系统根分区包含操作系统、容器标准输出日志(默认路径为/var/log/pods/)、各系统模块log(默认路径为/var/log/)、kubelet 工作目录(/var/lib/kubelet)等。Pod 声明的emptyDir 卷数据也存储在kubelet 的工作目录下。
● 容器运行时分区(imagefs)不单独分配,而是共享根分区,用来存储容器的镜像、用户可写层的数据等。
● 其他磁盘分区可以通过hostPath 或本地卷供Pod 挂载。
ephemeral-storage 用于对本地临时存储空间进行管理,目前只支持对根分区的管理,因此节点总的ephemeral-storage 容量就是根分区大小。建议不要对ephemeral-storage 划分运行时分区(imagefs),以便利用ephemeral-storage 特性进行资源管理。
容器临时存储(ephemeral-storage)包含日志和可写层数据,可以通过定义Pod Spec中的limits.ephemeral-storage 和requests.ephemeral-storage 来申请。Pod 调度完成后,计算节点对临时存储的限制不是基于CGroup 的,而是由kubelet 定时获取容器的日志和容器可写层的磁盘使用情况,如果超过限制,则会对Pod 进行驱逐。
用户的应用行为模式是无法预知的,因此可能会由于应用行为不当导致磁盘满,这是集群运维过程中最常见的节点问题之一。磁盘满不仅会导致新容器进程无法启动,还可能导致当前运行的容器进程异常。因此,管理节点内所有占用磁盘空间的行为对确保集群的可用性来说异常重要,主要体现在以下几方面。
1.日志管理
日志管理包括对系统服务日志的管理,也包括对容器日志的管理。
节点上需要通过运行logrotate 的定时任务对系统服务日志进行rotate 清理,以防止系统服务日志占用大量的磁盘空间。Logrotate 的执行周期不能过长,以防日志短时间内大量增长。同时配置日志的rotate 条件,在日志不占用太多空间的情况下,保证有足够的日志可供查看。
容器的日志管理方式因运行时的不同而有所区别:
● 如果选择Docker 作为运行时,那么除了基于系统logrotate 管理日志,还可以依赖Docker 自带的日志管理功能来设置容器日志的数量和每个日志文件的大小。Docker 写入数据之前会对日志大小进行检查和rotate 操作,确保日志文件不会超过配置的数量和大小。
● 如果选择Containerd 作为运行时,那么日志的管理是通过kubelet 定期(默认为10s)执行du 命令,来检查容器日志的数量和文件的大小的。每个容器日志的大小和可以保留的文件个数, 可以通过 kubelet 的配置参数 container-log-max-size 和container-log-max-files 进行调整。
2.hostPath 卷管理
Kubernetes 允许用户通过hostPath 卷将计算节点的文件目录挂载给容器,但目录挂载完成后,系统无法限制用户往该卷写入的数据的大小。这就导致删除容器时,如果不主动进行数据清理,数据就会遗留在节点上,占用磁盘空间。因此,用户能使用的节点路径需要被限制,即Kubernetes 可基于PodSecurityPolicy 限制hostPath 可以映射的主机文件路径。当某一文件路径开放给用户后,用户如何使用hostPath 卷就不受系统管理员的管控了。
因此,是否开放节点路径是一个需要慎重考虑的问题。
3.emptyDir 卷管理
emptyDir 卷可以理解为与Pod 生命周期绑定的、可以限制用量的主机磁盘挂载方式。1.15 版本之前,kubelet 定期执行du 命令获取容器emptyDir 卷的使用量,但是这种方式的实时性不够,且耗时过长,导致磁盘可能在计算过程中就被写满,进而影响系统服务。1.15版本之后,kubelet 使用文件系统自带的Quota 特性设置,并监控emptyDir 卷的大小,kubelet目前只支持XFS 和ext4 的文件系统。emptyDir 卷的使用情况可以实时反馈给kubelet,以防出现磁盘写满的情况。基于文件系统的Quota 特性监控emptyDir 卷的使用情况,相比du 更精准。du 无法监测文件打开后尚未关闭就被删除的情况,但是Quota 能够跟踪到已被删除而文件描述符尚未关闭的文件。
4.容器的可写层管理
容器的用户行为无法预知,如用户可能会将大量数据写入容器的可写层(比如/tmp 目录)。容器的可写层与容器的生命周期一致,数据会随着容器的重启而丢失。如果节点因为磁盘分区管理等原因,发生未开启临时存储管理限制而容器可写层却已达到上限的情况,就只能退而求其次,利用运行时的存储驱动进行限制:DeviceMapper 天然支持对每个容器的可写层限制,而overlayfs2 则需要依赖xfs_quota 特性的支持。
5.Docker 卷管理
在构建容器镜像时,可以在Dockerfile 中通过VOLUME 指令声明一个存储卷。运行时创建该容器时,会在主机上的运行时目录下创建一个子目录,并将其挂载至用户容器指定的目录。容器进程写入目标目录的数据,本质上是被写到主机的根盘或者给运行时分配的磁盘上,而不属于容器的可写层,目前Kubernetes 尚未将其纳入管控范围。当前有众多的开源容器镜像采用此方式定义存储空间,但当这样的镜像被部署到集群中时,会给系统带来潜在的隐患。如果确实需要使用该镜像,又希望存储空间能够被Kubernetes 管理,那么可以在Pod Spec 中定义一个卷,使其在容器内的路径与Dockerfile 中的路径一致。这样容器使用的存储就是定义的卷,而不是系统创建的默认卷。
容器会共享节点的根分区和运行时分区。如果容器进程在可写层或emptyDir 卷进行大量读写操作,就会导致磁盘I/O 过高,从而影响其他容器进程甚至系统进程。另外,对于网络存储卷和本地存储卷也有诸多并发问题需要解决:减少不同容器对同一个磁盘设备的I/O 竞争,以提升性能;降低并发读写,以减少后端的存储压力;减少大量传输数据对网络带宽的影响,等等。为满足这些需求,我们要对容器进程在共享分区的操作进行I/O限速。
Docker 和Containerd 运行时都基于CGroup v1。对于块设备,只支持对Dirtect I/O 限速,而对于Buffer I/O 还不具备有效的支持。因此,针对设备限速的问题,目前还没有完美的解决方案,对于有特殊I/O 需求的容器,建议使用独立的磁盘空间。
针对磁盘的资源管理,磁盘空间使用率和磁盘I/O 是非常必要且有用的节点监控指标。在节点根分区和运行时分区磁盘使用率到达一定的门限,或者一段时间内I/O 使用率一直比较高的情况下,可以先触发告警,再进行针对性的排查,寻找问题出现的原因,并采取相关措施进行后续防范。
2.4.4.4 网络资源
Kubernetes 可以通过添加kubernetes.io/ingress-bandwidth 的Annotation 来设置容器网络Ingress 的带宽,也可以通过添加kubernetes.io/egress-bandwidth 的Annotation 来设置容器网络Egress 的带宽。下面代码是添加了Annotation 的Pod Spec 的信息:
kubelet 会把相关的限制信息通过运行时传递给CNI 网络插件,再由CNI 网络插件通过Linux Traffic Control 限制带宽。同一计算节点的所有容器和主机共享网络带宽、相互竞争。如果网络负载较高的多个容器被部署到同一节点,则会导致系统的CPU 负载较高、网络延时增大、抖动增加等问题。因此,针对延时敏感的业务,建议在业务部署和调度层面进行隔离。另外,可在数据中心交换机和节点上配置全链路QoS,以对高优先级的网络数据作优先处理。
2.4.4.5 进程数
Linux 支持有限的进程数,当CPU 核数小于32 时,默认最大进程数(pid_max)为32768。如果CPU 核数大于等于32,则默认最大进程数为CPU 的核数×1024。如果不控制容器的进程数量,那么一个容器可能会不断创建子进程,导致整个节点的PID 数量不足。为此,Kubernetes 通过引入SupportPodPidsLimit 来限制Pod 能创建的最多PID 数。
Kubelet 默认不限制Pod 可以创建的子进程数量,但可以通过启动参数podPidsLimit开启限制,还可以由reserved 参数为系统进程预留进程数。系统管理员也可以增大进程数,以降低问题出现的概率。但若该值设置得过大,则意味着单一节点运行的进程过多,这将带来更高的负载和更低的系统稳定性。
Kubernetes 管理PID 资源的方式与CPU、内存不同:用户不能申请容器进程数。Kubelet通过系统调用周期性地获取当前系统的PID 的使用量,并读取/proc/sys/kernel/pid_max,获取系统支持的PID 上限。如果当前的可用进程数少于设定阈值,那么kubelet 会将节点对象的PIDPressure 标记为 True 。 kube-scheduler 在进行调度时, 会从备选节点中对处于NodeUnderPIDPressure 状态的节点进行过滤。
除了进程数上限,Linux 还允许定义线程数上限(thread_max),以限制系统的可用线程数量。该值由如图2-21 所示的公式计算,其中mempages 是系统当前的内存页数,等于总内存除以页大小;THREAD_SIZE 是线程栈大小,可以通过ulimit 命令查看,通常为8192;PAGE_SIZE 是页的大小,通常为4K。thread_max 通常较大,当前Kubernetes 并没有考虑将其作为考量值。
图2-21 线程数上限公式
当系统进程数不足时,应用程序尝试创建子进程,此时可能会出现设备空间不足的错误信息,一般我们会将其归咎于磁盘空间不足。然而,其主要原因是,在内核实现过程中如果无法创建新进程,那么某些代码分支会返回NOSPACEERR,从而导致产生这样的错误。
为防止进程泄露,操作系统会对每个用户设置最大PID 数。通过ulimit 命令能够看到具体的限制值, 也可以通过相应的配置文件进行查看。 以 CentOS 7 为例,/etc/security/limits.d/20-nproc.conf 中可查看的最大PID 数如下:
除root 用户外,其他用户默认进程数上限都是4096。在未开启UID Namespace 时,不同容器中的相同的UID 对主机而言都属于同一个UID。在计算进程数时,UID 相同的容器进程会被累加到一起。因此,应适当将这个数值增大,以降低用户的进程总数被系统限制的概率。