Cloudpods容器化经验分享
背景介绍
Cloudpods是一个开源的多云混合云管理平台。Cloudpods首先是一个私有云云平台,具备将计算节点使用开源QEMU/KVM虚拟化技术虚拟出虚拟机,实现私有云的功能。其次,Cloudpods能够纳管其他的云平台,包括主流私有云和公有云,实现云管的功能。Cloudpods的目标是帮助用户基于本地基础设置以及已有云基础设置,构建一个统一融合的云上之云,达到降低复杂度,提高管理效率的效果。Cloudpods从3.0开始全面拥抱Kubernetes,基于Kubernetes部署运行云平台的服务组件,采用Kubernetes Operator,基于Kubernetes集群自动化部署服务,实现了云平台的服务的容器化分布式部署。本文总结了Cloudpods在过去3年云平台底层容器化改造的经验。
目前,将Kubernetes作为IAAS平台的底层服务管理平台是一个趋势,例如OpenStack的Kolla项目,VMware的Tanzu,以及基于Kubernetes的虚拟化方案KubeVirt。Cloudpods顺应此趋势,早在2019年下半年开始基于Kubernetes构建Cloudpods的服务组件基础设施。理论上,Cloudpods站在了巨人的肩膀上。有了Kubernetes的加持,我们基于Operator管理CRD(Custom Resource Definition)机制做到了更优雅的服务 自动化部署,符合IaC(Infrastructure as code)实践的服务升级和回滚,服务的自动高可用部署等等。但在实际效果上,我们基于Kubernetes,获得了一些便利,但也遇到了不少未曾预料到的问题。本文介绍自从2019年3.0容器化改造以来,因为引入Kubernetes遇到的问题,我们的一些解决方案,以及将来的规划。
容器化带来了哪些好处
1. 方便对分布于多个节点上的服务的管理
管理员可以在控制节点统一地查看运行在各个节点的服务状态,查看日志,启停和发布回滚服务,甚至exec进入服务容器排查问题。同时我们引入Loki收集所有容器的日志,可以统一地查看各个服务的日志。对分布式集群的运维和排障都变得相对简单。采用Kubernetes之后,直接登录各个节点排障的机会大大降低了。
2. 集群配置变更变得方便和可控
整个集群的状态可以保存为一个OnecloudCluster yaml文件。可以方便地变更集群的配置,包括集群的版本,实现版本的升级和回退,以及集群服务的开启和关闭,镜像版本等关键参数的变更等。更进一步地,可以通过git进行配置yaml的版本控制,做到变更的历史记录审计,并且可以随时恢复到任意指定的配置。
3. 易于适配不同的CPU架构和操作系统
Kubernetes作为一层中间层,从一定程度上屏蔽了底层的差异。采用Kubernetes后,对CPU和操作系统的适配大概分为三部分工作:
1)Kubernetes对CPU和操作系统的适配; 2)不同CPU架构下服务容器镜像的构建; 3)Kubernetes之外的组件的适配,例如平台依赖的rpm包等。
基于Kubernetes自身强大的生态,1)基本都有现成的解决方案,只需要做相应的集成工作。2)只需通过docker buildx工具生成异构CPU架构的镜像。因此,整个适配工作复杂度大大降低了。
4. 部署的便利性增加
引入Kubernetes之后,整个部署流程分为几个阶段:
1)Kubernetes的部署,这个步骤通过基于kubeadm改造的ocadm实现。 2)Cloudpods服务容器的部署,这个步骤通 过ocadm在容器内部署operator,通过operator实现相应configmaps,deployments和daemonsets等资源的创建,进而自动创建服务集群。 3) Kubernetes之外依赖组件的安装部署。这个步骤通过ocboot集成ansible实现。
每个阶段都是基于成熟的开源方案扩展实现,可靠性高。同时,各个组件分工明确,模块化清晰,易于维护和扩展。
5. 可以复用Kubernetes本身自带的强大功能
如coredns可以自定义域名,甚至可以做泛域名解析。Ingress自带反向代理的功能。service+deployment提供的多副本冗余机制。daemonset提供的在新添加节点自动拉起服务的能力。对服务的资源限制(CPU,内存,进程号等)。这些都使得云平台服务功能特性的实现变得更加容易。
容器化遇到了哪些问题,如何解决
下面总结一些遇到的问题。这些问题是我们在采用Kubernetes管理和运行云平台组件中陆续发现的。有些已经彻底解决,但很大一部分还只是部分解决,彻底解决的方案还在持续摸索中。
1. 容器内运行系统级服务
Cloudpods在计算节点运行的服务都是系统级的服务,如计算节点的核心服务hostagent,需具备几个特权:
1)需启动系统的daemon服务进程,如qemu虚拟机进程,vswitchd等系统进程,这些进程由hostagent启动,但需独立于hostagent运行; 2)需访问计算节点的任意目录文件。
在容器化之前,这些服务由systemd管理,以root身份运行。这些特权都自然具备。
容器化后,服务需运行在容器内。虽然可以通过配置给与容器系统级的root权限,但是一些特权操作在容器内依然无法执行。
首先,容器内无法启动系统级daemon服务进程。如果通过容器内的程序启动进程,则该进程只能运行在容器内的PID空间(pid namespace),只能跟随容器的生命周期启停。为了解决这个问题,我们将系统服务的二进制程序安装在计算节点的底层操作系统,并且开发了一个命令执行代理executor-server。该代理安装在底层操作系统,并作为一个系统服务运行。容器内的hostagent通过该代理执行系统级命令,例如启动这些daemon服务,设置内核参数等,从而获得了执行系统级命令的特权。
其次,每个容器具有自己独立的文件系统命名空间(mount namespace)。为了允许容器内服务访问计算节点底层系统的特定路径文件,需要将该路径显式地挂载到容器的文件系统命名空间。例如,虚拟机的配置文件和本地磁盘文件都存储在/opt/cloud/workspace目录下。容器内的hostagent在虚拟机准备和配置阶段需要能够访问这个目录的文件,同时,启动虚拟机后,在底层 操作系统运行的虚拟机qemu进程也需要能够访问对应的文件。并且,由于上述命令执行代理的机制,为了简化和保持向后兼容的目的,需要确保尽量以一致的路径在容器内和容器外访问这些文件。为此,我们将一些特定的系统目录以同样的路径挂载到hostagent的容器内,例如系统设备文件路径/dev,云平台的配置文件路径/etc/yunion,虚拟机系统文件路径/opt/cloud/workspace等。然而,这个机制还无法解决容器内服务访问底层系统任意路径的问题。例如,用户可以将底层系统的任意目录设置为虚拟机磁盘的存储目录,但是该目录其实并未通过容器的spec挂载到hostagent容器内,从而导致hostagent在容器内无法访问该目录。为了解决这个问题,我们对hostagent进行了改造。当hostagent检测到用户添加了新的本地目录作为虚拟磁盘文件的存储路径,会自动地执行底层系统命令,将该路径挂载到底层操作系统的/opt/cloud/workspace目录下。因该目录已经挂载到hostagent容器内,这样hostagent就可以在容器内访问这个目录下的文件。
总之,相比将一个普通应用程序容器化,将系统级的服务程序从systemd托管变为在Kubernetes容器中运行,不是仅仅简单地打一个容器镜像,其实还需要做一系列比较复杂和繁杂的改造工作。
2. 日志持久化
容器化之前,服务日志会记录到journald中,并被持久化到/var/log/messages。按照CentOS的默认策略,保留最近一段时间的日志。遇到问题的时候,可以到对应服务器查找到对应的日志,排查错误原因。但是,不方便的地方是需要登录到服务 运行的节点查看日志。在一个事故涉及的服务分布在多个节点的时候,就需要同时登录多个节点进行日志排查。
容器化之后,可以方便地在一个地方,通过kubectl log命令查看指定容器的日志,不需要登录到服务运行的节点。
然而,如果没有做特殊设置,Kubernetes里的容器的日志都是不持久保存的,并且只保留当前正在运行的容器的最近一段时间的日志。而容器往往非常动态,很容易删除。这就导致遇到问题需要排查已经被删除的容器时候,容易遇到找不到对应的日志的问题。这就使得追溯问题变得比较困难。
我们的解决方案是从3.7开始,会默认在Kubernetes集群里部署Loki套件来收集容器的日志,日志最后存在平台自带的minio的S3 Bucket里面。这样做能够持久化容器的日志。解决上述问题。但是,保存Loki日志有一定的系统负载,并且需要较大容量的存储空间。在集群容量紧张的情况下成为平台的额外负担,可能造成平台的不稳定。
3. 节点Eviction机制
Kubernetes有驱逐机制(Evict)。当节点的资源余量不足时,例如磁盘剩余空间低于阈值或剩余内存低于阈值(默认根分区磁盘空间低于85%,空闲内存低于500M)等,会触发Kubernetes的节点驱逐机制,将该节点设置为不可调度,上面的所有容器都设置为Evict状态,停止运行。
该机制对于无状态应用可以动态地规避有问题的节点,是一个好的特性。然而,在云平台的场景中,甚至对于普遍的有状态服务场景中,Eviction机制导致节点可用性变得非常动态,进而降 低了整体的稳定性。例如,由于用户上传一个大的镜像,导致控制节点根分区利用率超过Eviction的阈值85%,云平台的所有控制服务就会被立即驱除,导致云平台控制平面完全不可用。用户在虚拟机磁盘写入大量数据导致宿主机磁盘空间利用率超过阈值,也会引起计算节点上所有服务被驱逐,进而导致这台计算节点上所有的虚拟机失联,无法控制。可以看到,虽然触发Eviction机制的问题存在造成服务问题的潜在可能,但是这些问题对服务的影响是延后的,逐步生效的。Eviction机制则使得这些潜在风险对服务的影响提前了,并立即发生,起到了放大的作用。
为了避免Eviction机制生效,云平台在计算节点的hostagent启动的时候,会自动检测该节点的Eviction阈值,并设置为计算节点的资源申请上限。云平台在调度主机的时候,会考虑到Eviction的阈值,避免资源分配触发Eviction。这个机制能从一定程度规避Eviction的出现,但云平台只能管理由云平台分配的资源,还是有可能不在云平台管理范围内的存储和内存的分配导致Eviction的情况。因此需要计算节点一定程度的内存和存储的over-provisioning。
目前,Eviction的存在也有一定的积极作用,那就是让节点资源的不足以云平台罢工的方式提出警示。由于云平台的冗余设计,云平台的暂时罢工并不会影响虚拟机的运行,因此影响程度还比较可控。无论如何,以云平台可用性的牺牲来实现资源不足的警示,代价还是有点大。这样的警示可以其他更柔和的方式来实现。随着云平台自身管理资源容量能力的完善,Eviction机制应该可以去除。