Kubernetes的资源管理与垃圾回收机制学习

匠心独运维妙维效公众号
董嗣伯
在 Kubernetes 集群管理中,有一个非常核心的功能,就是为 Pod 选择一个主机运行。调度必须满足一定的条件,其中最基本的是主机上要有足够的资源给 Pod 使用。

引言

Kubernetes从创建之初的核心模块之一就是资源调度,想要在生产环境使用好Kubernetes,就必须对它的资源模型以及资源管理非常了解,同时,为了避免不必要的资源浪费,Kubernetes也提供了基于kubelet的垃圾回收机制,最大限度地保证整个容器内的资源可以被有效利用,并及时清理回收失效的对象资源。

一、Kubernetes的资源和管理

在 Kubernetes 中,有两个基础但是非常重要的概念:Node和Pod。Node通常翻译成节点,是对集群资源的抽象;Pod 是对容器的封装,是应用运行的实体。Node 提供资源,而 Pod 使用资源,这里的资源分为计算资源(CPU、Memory、GPU)、存储资源(Disk、SSD)和网络资源(Network Bandwidth、IP、Ports)等。这些资源提供了应用运行的基础环境,正确理解这些资源以及集群调度如何使用这些资源,对于大规模的 Kubernetes 集群来说至关重要,不仅能保证应用的稳定性,也可以提高资源的利用率。

通常我们在关注一个容器化系统的运行性能时,首先关注的就是它可以获得的计算资源(CPU、内存)的资源情况,一般来说,获得的资源越多、越充分,那么这个系统的工作就会越顺畅,其中CPU 分配的是可使用时间,也就是操作系统管理的时间片,每个进程在一定的时间片里运行自己的任务,而对于内存,系统提供的是可供调配的内存大小。

CPU 的使用时间是可压缩的,换句话说它本身并无状态,资源申请很快,也能快速正常回收;而内存大小是不可压缩的,因为它是有状态的(内存里面保存的数据),申请资源很慢(需要计算和分配内存块的空间),并且回收可能失败(被占用的内存一般不可回收)。

这里把资源分成可压缩和不可压缩,是因为在资源不足的时候,它们的表现很不一样。对于不可压缩资源,如果资源不足,也就无法继续申请资源(内存用完就是用完了),并且会导致 Pod 的运行产生无法预测的错误(应用申请内存失败会导致一系列问题);而对于可压缩资源,比如 CPU 时间片,即使 Pod 使用的 CPU 资源很多,CPU 使用也可以按照权重分配给所有 Pod 使用,虽然每个Pod使用的时间片减少,但不会影响程序的逻辑。

在 Kubernetes 集群管理中,有一个非常核心的功能,就是为 Pod 选择一个主机运行。调度必须满足一定的条件,其中最基本的是主机上要有足够的资源给 Pod 使用。

图1 集群管理示意图

用户在 Pod 中可以配置要使用的资源总量,Kubernetes 根据配置的资源数进行调度和运行。目前主要可以配置的资源是 CPU 和 Memory,对应的配置字段是:

spec.containers[].resource.limits/request.cpu/memory

需要注意的是,用户是对每个容器配置Request值,所有容器的资源请求之和就是 Pod 的资源请求总量。

CPU 一般用核数来标识,一核CPU 对应物理服务器的一个超线程核,也就是操作系统/proc/cpuinfo中列出来的核数。因为对资源进行了池化和虚拟化,所以Kubernetes允许配置非整数个的核数,比如0.5也是合法的,它标识应用可以使用半个CPU核的计算量。CPU的请求有两种方式,一种是刚提到的 0.5、1 这种直接用数字标识CPU核心数;另外一种表示是 500m,它等价于 0.5,也就是说 1Core = 1000m。

内存比较容易理解,是通过字节大小指定的。如果直接一个数字,后面没有任何单位,表示这么多字节的内存。数字后面还可以跟着单位,支持的单位有 E、P、T、G、M、K,前者分别是后者的1000倍大小的关系,此外还支持 Ei、Pi、Ti、Gi、Mi、Ki,其对应的倍数关系是2^10 = 1024。比如要使用100M 内存的话,直接写成100Mi即可。

但是,节点上的的资源,真的可以予取予求吗?

理想情况下,我们希望节点上所有的资源都可以分配给Pod 使用,但实际上节点上除了运行Pods之外,还会运行其他的很多进程:系统相关的进程(比如SSHD、Udev等),以及Kubernetes集群的组件(Kubelet、Docker等)。我们在分配资源的时候,需要给这些进程预留一些资源,剩下的才能给Pod 使用,Kubelet会保证节点上的资源使用率不会真正到100%,因此Pod的实际可使用资源会稍微再少一点。主机上的资源逻辑分配图如图2所示。

图2 主机上的资源逻辑分配图

用户在创建Pod 的时候,可以指定每个容器的Requests和Limits 两个字段,下面是一个实例:

Requests是容器请求要使用的资源,Kubernetes会保证Pod能使用到这么多的资源。请求的资源是调度的依据,只有当节点上的可用资源大于Pod请求的各种资源时,调度器才会把Pod调度到该节点上(如果CPU资源足够,内存资源不足,调度器也不会选择该节点)。

需要注意的是,调度器只关心节点上可分配的资源,以及节点上所有Pods请求的资源,而不关心节点资源的实际使用情况,换句话说,如果节点上的Pods申请的资源已经把节点上的资源用满,即使它们的使用率非常低,比如说CPU和内存使用率都低于10%,调度器也不会继续调度Pod上去。

Limits是Pod能使用的资源上限,是实际配置到内核cgroups里面的配置数据。对于内存来说,会直接转换成dockerrun 命令行的--memory大小,最终会配置到cgroups对应任务的文件中,文件目录如下:

/sys/fs/cgroup/memory/…/memory.limit_in_bytes

如果Limit没有配置,则表明没有资源的上限,只要节点上有对应的资源,Pod就可以使用。

使用Requests和Limits概念,我们能分配更多的Pod,提升整体的资源使用率。但是这个体系有个非常重要的问题需要考虑,那就是怎么去准确地评估Pod的资源Requests?如果评估过低,会导致应用不稳定;如果过高,则会导致使用率降低。这个就需要开发者和管理员共同讨论和定义,要结合生产实践进行调节。

举个容器化改造后问题排查及优化的例子:某行的移动办公平台进行从传统服务迁移到“微服务+容器化”的改造,开发测试验证未出现问题,转入非功能性能试期间后,随着压力的不断增加,并发量上升,系统开始出现登录请求缓慢、超时等情况,与之前基于开发测试的预估有明显差距,经日志排查后,定位直接原因为接入“移动网关”服务内存使用率过高,检查非功能性能试验环境的移动网关服务资源配置,配置如下:

开发人员与运维人员对非功能性能试验环境及应用程序的日志进行分析后,决定解决问题从两方面进行,一方面开发对应用程序进行优化,减少开源软件产生堆外内存,另外一方面,在非功能性能试验环境对进行资源配置紧急扩容,扩容后配置如下:

经过资源扩容,系统稳定性得到提升,问题出现的概率明显下降,后经应用程序调整后,问题彻底解决。由于此移动网关是通用的消息转发网关服务,根据此次问题的解决结果,后续此服务的默认配置即以此次优化后结果为准。

二、Kubernetes垃圾对象的回收机制

Kubernetes系统在长时间运行后,Kubernetes Node会下载非常多的镜像,其中可能存在很多过期的镜像。同时因为运行大量的容器,容器推出后就变成死亡容器,将数据残留在宿主机上,这样一来,过期镜像和死亡容器都会占用大量的硬盘空间。如果磁盘空间被用光,可能会发生非常糟糕的情况,甚至会导致磁盘的损坏。为此kubelete会进行垃圾清理工作,即定期清理过期镜像和死亡容器。因为kubelet需要通过容器来判断pod的运行状态,如果使用其它方式清除容器有可能影响kubelet的正常工作,所以不推荐使用其它管理工具或手工进行容器和镜像的清理。

那么实现GC的机制是怎么样的呢?各项资源对象(Resource Object)是以怎样的一种方式进行清理的呢?

Kubernetes通过 Garbage Collector组件 和 ownerReference 一起配合实现了“垃圾回收GC”的功能。

通常在K8s 中,有两大类GC:

第一类

级联(Cascading Deletionstrategy)

Kubernetes在不同的 Resource Objects 中维护了一定的“从属关系”,一般会默认在一个Resource Object和它的创建者之间建立一个“从属关系”。Kubernetes利用已经建立的“从属关系”进行资源对象的进行“级联”清理工作。例如,当一个dependent 资源的owner已经被删除或者不存在的时候,从某种角度可以判定,这个dependent的对象已经异常(无人管辖),则需要进行清理。而“Cascading Deletion”则是被通过Garbage Collector(GC)组件实现。

在级联删除中,又有两种模式:前台(foreground)和后台(background)。

前台级联删除(Foreground Cascading Deletion):在这种删除策略中,所有者对象的删除将会持续到其所有从属对象都被删除为止。当所有者被删除时,会进入“正在删除”(deletionin progress)状态,此时:

对象仍然可以通过REST API 查询到(可通过kubectl 或kuboard 查询到)

对象的deletion Timestamp 字段被设置

对象的metadata.finalizers 包含值foreground Deletion

一旦对象被设置为“正在删除”的状态,垃圾回收器将删除其从属对象。当垃圾回收器已经删除了所有的“blocking”从属对象之后(ownerReference.blockOwnerDeletion=true的对象),将删除所有者对象。

后台级联删除(Background Cascading Deletion):这种删除策略会简单很多,它会立即删除所有者的对象,并由垃圾回收器在后台删除其从属对象。这种方式比前台级联删除快的多,因为不用等待时间来删除从属对象。

第二类

孤儿(Orphan)

这种情况下,对所有者的进行删除只会将其从集群中删除,并使所有对象处于“孤儿”状态。在孤儿删除策略(orphandeletion strategy)中,会直接删除所有者对象,并将从属对象中的ownerReference元数据设置为默认值。之后垃圾回收器会确定孤儿对象并将其删除。

Garbage Collector 组件的工作通常由三部分实现,具体如图3所示。

图3 GarbageCollector 组件工作的实现

1

Scanner:它负责收集目前系统中已存在的Resource,并且周期性的将这些资源对象放入一个队列中,等待处理(检测是否要对某一个Resource Object 进行 GC操作)。

2

GarbageProcessor:Garbage Processor 由两部分组成。

a、Dirty Queue:Scanner会将周期性扫描到的Resource Object 放入这个队列中等待处理

b、Worker:worker负责从Dirty Queue队列中取出元素进行处理

检查Object 的metaData部分,查看ownerReference字段是否为空。如果为空,则本次处理结束,如果不为空,继续检测ownerReference字段内标识的Owner Resource Object是否存在,如果存在:则本次处理结束;如果不存在:删除这个Object。

3

Propagator:Propagator 由三个部分构成。

a、Event Queue:负责存储Kubernetes中资源对象的事件(Eg:ADD,UPDATE,DELETE)

b、DAG(有向无环图):负责存储Kubernetes中所有资源对象的“owner-dependent”关系

c、Worker:从EventQueue中,取出资源对象的事件,根据事件的类型会采取以下两种操作:

1)ADD/UPDATE:将该事件对应的资源对象加入DAG,且如果该对象有owner 且owner 不在DAG 中,将它同时加入Garbage Processor 的Dirty Queue 中;

2)DELETE:将该事件对应的资源对象从DAG中删除,并且将其“管辖”的对象(只向下寻找一级,如删除Deployment,那么只操作ReplicaSet )加入Garbage Processor 的Dirty Queue 中。

其实,在有了Scanner 和Garbage Processor 之后,Garbage Collector 就已经能够实现“垃圾回收”的功能了。但是有一个明显的问题:Scanner的扫描频率设置多少好呢?时间间隔太长了,Kubernetes内部就会积累过多的“废弃资源”;间隔太短了,尤其是在集群内部资源对象较多的时候,频繁地拉取信息对API-Server 也是一个不小的压力。

Kubernetes作为一个分布式的服务编排系统,其内部执行任何一项逻辑或者行为,都依赖一种机制:“事件驱动”。说的简单点,Kubernetes中一些看起来“自动”的行为,其实都是由我们所说的“Event”驱动的。任意一个Resource Object发生变动的时候(新建、更新、删除),都会触发一个Kubernetes的事件(Event),而这个事件在Kubernetes的内部是公开的,也就是说,我们可以在任意一个地方监听这些事件。

总的来说,无论是“事件的监听机制”还是“周期性访问API-Server 批量获取Resource Object信息”,其目的都是为了能够掌握Resource Object的最新信息。两者是各有优势的:

(1)批量拉取:一次性拉取所有的Resource Object,获取信息全面;

(2)实时监听Resource 的Event:实时性强,且对 API—SERVER不会造成太大的压力。

综上所述,在实现Garbage Collector的过程中,Kubernetes向其添加了一个“增强型”的组件:Propagator。在有了Propagator 的加入之后,我们完全可以仅在GC 开始运行的时候,让Scanner 扫描一下系统中所有的Object,然后将这些信息传递给Propagator 和Dirty Queue。只要DAG 一建立起来之后,那么Scanner其实就没有再工作的必要了。“事件驱动”的机制提供了一种增量的方式让GC 来监控k8s 集群内部的资源对象变化情况。

总结

docker+Kubernetes确实是有效缓解分布式架构升级改造后虚拟机设备激增代理的运维压力的一剂良方。充分熟悉Kubernetes的各种使用技巧,优化对容器资源的有效管理和配置,是进一步提升系统运行性能,支撑系统稳定运行的基础,随着容器化改造的不断深入,早期的那种套用传统虚拟机架构下的运维管理方式必然将被更有弹性更灵活的方式取代,请大家开始主动学习和掌握Kubernetes,以应对未来的要求和挑战吧。

THEEND

最新评论(评论仅代表用户观点)

更多
暂无评论