随着 Kubernetes 使用的越来越广泛,k8s管理的native的对象资源有时并不能满足用户的需求,为了提高可扩展性,自 v1.7 以来,Kubernetes 引入了 CRD 机制(CustomResourceDefinition),简单的来说它允许用户定义自己的对象资源注册到集群中,并且通过自定义的controller来管理这些资源的生命周期,这样就可以像操作pod,deployment一样来方便的管理运维一些复杂的资源对象。伴随着云原生这股技术浪潮,lindorm-operator充分利用k8s底座的特点与优势,在云环境(包括公共云、私有云和混合云)极大提升了Lindorm数据库的生产和运维效率。
本文以lindorm-operator项目为例,深入介绍了如何在 Kubernetes 中基于 Operator 模式实现自定义资源对象和控制器。
Lindorm是面向物联网、互联网、车联网等设计和优化的云原生多模超融合数据库,支持宽表、时序、文本、对象、流、空间等多种数据的统一访问和融合处理。
Lindorm的整个部署架构大致可以分为三个阶段,On 物理机--> On ECS --> On K8S ,早期输出时大部分是on 物理机的部署形态,在2020年公有云商业化时开始以On ECS形态输出,虽然实现服务全面上云,但是整个架构上并没有全面云原生化,到了2021年左右在混合云输出形态构建上开始全面拥抱云原生,所有组件都On k8s部署,生产层只依赖Operator,实现了真正意义上云原生化。虽然发展时间不长,但是管控架构基本上走的比较靠前,充分吸取云原生演进道路上的红利,让Lindorm生产效率越来越高。其他两种形态本文就不做重点介绍了,主要介绍On k8s模式的设计细节和实践。
Operator 可以看成是 CRD 和 Controller 的一种组合特例,Operator 是一个特定的应用程序的控制器,通过扩展 Kubernetes API 资源以代表 Kubernetes 用户创建、配置和管理复杂应用程序的实例,通常包含资源模型定义和控制器,通过 Operator 通常是为了实现某种特定软件(通常是有状态服务)的自动化运维。
controller基本工作原理:
上图图展示了 controller 的工作原理,实际上是一个事件监听变更再变更的循环流程,以一个pod变更为例对照该图说明下go-client的核心组件和我们的controller是如何配合工作的:
虽然client-go已经封装了 controller-runtime 和 controller-tools,用于快速构建 Operator,但是从头开始去构建一个 CRD 控制器并不容易,需要对 Kubernetes 的 API 有深入了解,还需要自己实现和api-server的一些交互细节。为了解决这个问题,社区就推出了对应的简单易用的 Operator 框架,比较主流的是 kubebuilder 和 Operator Framework,这两个框架的使用基本上差别不大,我们可以根据自己习惯选择一个即可。
Lindorm之前的部署形态是On ecs的,管控的架构基于pengine,通过任务流编排来实现实例的生命周期管理,这套架构需要管控侧需要感知很多调度细节。而Lindorm和Operator结合能把资源的调度,节点的弹性伸缩能力,安全隔离等特性更好的下沉到K8S集群,充分利用云原生技术红利实现一套Lindorm CR定义和一个Operator组件能在各种复杂环境轻松交付lindorm实例。为达成这个目标,从相应系统的设计,开发,构建,部署,交付,监控及运维等整个应用生命周期各环节都需要被重塑。从Lindorm集群本身的管理上看,核心要求主要有以下几个:
虽然K8S为我们提供了基础的workload、存储,网络的调度和管理能力,但是如何将这些能力充分利用起来是Operator面临的挑战,LindormOperator是集群内实例生命周期管理和运维的入口,Lindorm集群的生产和运维就是Operator通过合理的编排调度K8S的原生资源来实现的,如下图所示operator会持续监听K8S原生资源和Lindorm CR的状态变化,并做出响应的动作,通过不断的reconcile使Lindorm集群最终达到我们想要的状态,所以operator是Lindorm生产层的关键组件,关于workload,存储相关技术方案的选型和思考直接影响着Lindorm的性能能否在K8S上充分发挥,下面会围绕这些问题和挑战重点介绍lindorm-operator的一些主要设计逻辑和技术选型的一些思考。
Lindorm的部署架构经历了物理机 -> 虚拟化(On ecs) -> 云原生化(On k8s)的逐步演进,目前在公有云的售卖形态是On ECS,公有云基于存储池的On K8s云原生架构也已将上线,整体架构如下图所示:
其中,LindormCluster 是由 CRD(CustomResourceDefinition)定义的自定义资源,用于描述用户期望的 Lindorm 集群状态, 集群的编排和调度逻辑则由下列组件负责:
实例资源编排一定是基于部署拓扑的,为了保证内核组件架构的一致性,On K8S架构下内核组件的部署拓扑和公有云目前售卖形态基本保持一致,具体如下图所示:
实例创建和弹性变配都是通过修改Lindorm CR实现,只要通过kube api可以很方便的修改Lindorm资源,从管控视角和运维视角就有一致的体验,可以避免很多重复建设的工作,下图就是Lindorm实例变更时各个组件的交互逻辑。
以实例生产和变更流程为例,说明内部的流程:
Lindorm作为一款数据库产品在迁移到k8s的过程中也遇到了不少问题,因为k8s的调度设计对无状态的服务是有天然的友好性,但对于Lindorm这种重存储的产品来说会面临诸多挑战,下面分别从workload,存储和网络三个方面来介绍lindorm在k8s上实践的一些思考。
在谈workload选型前我们可以先思考下数据库这种重存储、有状态的服务对部署的基本要求有哪些,在我看来 最基本的特性主要有4个:
(1) 稳定的网络: Pod需要具备稳定的网络标识,因为这个数据可能会被持久化;
(2) 稳定的存储: 存储不随pod的状态变化而有所改变;
(3) 安全可控的升级:灰度可控的升级和重启;
(4)运维可控:具备指定pod升级的能力,有状态节点运维会涉及主从节点切换,需要按需升级
K8S内置了 Pod、Deployment、StatefulSet 等负载类型(Workload)。 Pod是k8s最小的调度、部署单位,由一组容器组成,它们共享网络、数据卷等资源。通过 Pod 成功将业务进程容器化了,然而 Pod 本身并不具备高可用、自动扩缩容、滚动更新等特性,因此为了解决以上挑战,Kubernetes 提供了更高级的 Workload,主要有Deployment和Statefuleset,前者主要针对无状态服务场景,生成的pod名称是变化的,无稳定的网络标识,而statefulset为每个 Pod 提供唯一的名称、固定的网络身份标识、持久化数据存储、有序的滚动更新发布机制。基于 StatefulSet 可以比较方便的将 etcd、zookeeper 等组件较单一的有状态服务进行容器化部署。
使用Statefulset好处是可以复用部分已有的能力,实现横向扩展的能力。但是业务诉求是多样化的,以Lindorm为例,Statefulset虽然能满足各个节点基本的部署和扩展需求,但是对于运维场景的特殊需求就无法满足,比如Pod不重建、支持原地更新,或者按需升级等等。
为了满足业务上述的原地更新、指定Pod更新等高级特性需求,阿里的开源项目 Openkruise 就包含一系列 Kubernetes 增强型的控制器组件,包括 CloneSet、Advanced StatefulSet、SideCarSet等,CloneSet 是个专注解决无状态服务痛点的 Workload,支持原地更新、指定 Pod 删除、滚动更新过程中支持Partition, Advanced StatefulSet 顾名思义,是个加强版的 StatefulSet, 同时支持原地更新、暂停和最大不可用数。这些特性给运维带来了极大的便利,Lindorm在有状态节点的编排上也是用了Openkruise
存储是Lindorm数据库系统非常重要的一部分,存储要求稳定、可用、性能、可靠。在用户看来存储就是一块盘或者一个目录,用户不关心盘或者目录如何实现,用户要求非常“简单”,就是稳定,性能好。为了能够提供稳定可靠的存储产品,各个厂家推出了各种各样的存储技术和概念。无论是开源的存储项目还是商业的存储产品,评估方法具有普适性,主要从以下几个方面来考虑:
Lindorm在k8s上是容器化部署,可选的存储方案从大的方面可以分为本地存储和网络存储:
本地存储:如HostPath,emptyDir等,这种存储的特点是数据保存在集群的节点上,不能跟着pod漂移,节 点宕机数据不可用,但是优点是读写性能高;
网络存储:Ceph、Glusterfs、NFS,OSS等类型,这些存储卷的特点是数据不在集群的某个节点上,而是在远端的存储服务上,使用存储卷时需要将存储服务挂载到本地使用,性能上较本地存储差。
Lindorm作为一款高性能的多模数据库,网络存储会大大降低读写性能,而且网络存储的带宽也极易形成性能瓶颈,所以在这个大方向上Lindorm首选还是本地存储。但是选用本地存储就会丧失K8S对于存储pv管理的很多便利性,所以为了保证在使用本地盘的同时有能做到调度的便利,Lindorm在对比以下多种技术方案后使用了Pod独占本地盘的模式:
HostPath模式:需要管理具体的物理盘,包括感知具体盘符,格式化,目录创建等,从目录创建到挂载是一个静态过程,也就是只能"写死“,扩展性太差,一般很少使用这种方案
Local PV:local pv的关键核心技术点是容量隔离(lvm、xfs quota)、IO隔离、动态provision等问题
1 基于volumeGroup: VG屏蔽了多个物理磁盘,pod使用时只需考虑空间大小的问题,帮我们屏蔽了底层硬盘带来的复杂性。但是这种方案也导致一份完整的数据被分布到多个磁盘上,任何一个磁盘上的数据都是不完整,也无法进行还原,一块磁盘的抖动影响面会成倍放大。
2 pod独占盘:每个pod绑定的pv独占一块盘的存储,这种模式可以避免上面基于volumeGroup造成的数据分散,pv数据与盘强相关,有利于故障隔离。比较简单的实现方式就是,为每一块盘创建一个独立的pv,pod声明对指定的pv的绑定,就能实现pod的单盘的独占。在K8S的实现角度可以通过静态PV或者动态pv实现这种绑定,但是动态PV需要依赖k8s csi插件的支持。
静态PV:
动态PV:
回到Lindorm的场景,我们的数据节点基本要求是高性能和高可靠,并且在存储池的部署场景下,存储节点是多租形态,对于故障隔离的要求尤其高,因此我们选择了本地盘独占的存储方案,在调度上,ASI上已经实现了基于此种场景的CSI插件,可以满足此种场景;针对其他不满足条件的标准ACK,我们也可以使用静态pv实现,只是需要提前创建好相关pv。
网络架构是Kubernetes中较为复杂、让很多用户头疼的方面之一。Kubernetes网络模型本身对某些特定的网络功能有一定要求,但在实现方面也具有一定的灵活性。因此,业界已有不少不同的网络方案来满足特定的环境和要求。在基于 CNI 实现的各种 Kubernetes 的网络解决方案中(例如Flannel,Calico等),按数据包的收发模式实现可分为 underlay 和 overlay 两类。前者是直接基于底层网络,实现互联互通,拥有良好的性能,后者是基于隧道转发,它是在底层网络的基础上,加上隧道技术,构建一个虚拟的网络,因此存在一定的性能损失。对于不同网络插件实现的技术细节本文就不扩展来讲了。
Lindorm在考量网络分案选型时充分结合了Lindorm自身的业务特点和部署形态来考虑,从技术上来看,当前流行的几种CNI的网络插件等都能满足lindorm的基本诉求,但是这种Overlay的网络会有一定的性能损失,因此在综合性能和调度便利性上Lindorm在不同的部署场景希望选择的不同的网络方案。在基于Lindorm存储池形态下,因为存储池的Node是属于我们强管控的资源,其网络资源都是提前规划好的,并且存储池Pod在部署形态上也充分做了打散,数据节点独占Node,因此为了最大限度利用节点的网络性能,存储池没有用基于CNI的方案,直接使用HostNet,Operator能够在节点调度上保证不会存在端口冲突的场景。而引擎层由于基于弹性eci部署,使用了underlay网络terway。
Lindorm的部分容错能力已经内置到内核中,例如故障自动转移,备份与恢复都由内核实现,在日常运维中更多是重启部分节点或者调整集群参数来修复线上问题,目前支持的运维能力主要有基础运维和配置变更两类。
LindormOperator还没有配套的白屏化的运维控制台(能力正在建设中),主要的运维操作都通过黑屏方式操作;对于一些标准化的运维动作已经实现了一个巡检CronJob,可以对集群的状态做巡检并做出对应的运维操作,例如集群的容量水位保护就通过这种方式实现,水位超过90%会触发写保护,保护引擎不会被写挂,后续还会在此基础上支持更多的能力。
自动运维主要流程如下:
1.集群中增加cronjob角色,由lindorm-operator创建,定期拉起pod运行巡检任务;
2.cornjob检测异常下发lindorm-operator的customResource;
3.增强sre能力,sre通通过拉取sre operator执行特定的运维动作;
在日常运维过程中大部分动作是修改集群的参数解决问题,基于此Lindorm-operator中实现一套动态配置更新的机制,常规做法是登录到容器中修改配置然后重启容器,但是当节点比较多时效率就会很低。实现思路如下图所示,LindormOperation的的CRD定义了具体的操作类型和操作的相关参数,并创建对应的资源,Lindorm的所有POD都会内置一个SER Container,这个Container实际上也是一个“小operator” ,会监听当前所在pod的operation事件,然后进行处理,配置的修改就是通过SRE Container修改和MainContainer共享的Volume下的配置文件实现,对于需要重启才能生效的配置变更,Sre还会触发MainContainer的重启。
对于Operator的可观测主要包含两部分,一个是集群reconcile事件的trace,一部分是性能指标的监控
Trace可观测性:
我们对每一个组件的Controller的Trace日志都定义了一个独立的event,能根据trace检索每一次reconcile事件的步骤细节,可以帮助研发快速定位问题。
Metric可观测性:
Operator在启动的时候就开启了监控数据采集端口,支持promethues的servie monitor拉取operator的性能数据,主要包括基础的性能指标和Controller Runtime指标,并可依据此监控配置promethues报警,在集群出现事件异常时可以提前感知。
Lindorm Operator是一个完全无状态的服务,通过Deployment部署到k8s集群中,具有readness和liveness检测,在服务异常时可以重新被拉起,同时所有的reconcile逻辑都做了幂等性保证,这样即使Operator异常宕机在恢复后也能重新处理事件。
在Kubernetes中,通常kube-schduler和kube-controller-manager都是多副本进行部署的来保证高可用,而真正在工作的实例其实只有一个。这里就利用到 leaderelection 的选主机制,保证leader是处于工作状态,并且在leader挂掉之后,从其他节点选取新的leader保证组件正常工作。Lindorm operator通过Kubebuiler构建,支持配置化的leadelection
基本原理其实就是利用通过Kubernetes中 configmap , endpoints 或者 lease 资源实现一个分布式锁,抢(acqure)到锁的节点成为leader,并且定期更新(renew)。其他进程也在不断的尝试进行抢占,抢占不到则继续等待下次循环。当leader节点挂掉之后,租约到期,其他节点就成为新的leader。通过这个特性就可以在Operator多副本部署时自动实现主备切换,大大提升可用性。
Lindorm作为一款多模数据库其组件数量和编排复杂度在数据库产品中是比较少见的,之前的On ecs的架构整体编排逻辑非常重,而通过拥抱云原生技术,通过Operator将大部分编排细节大大简化了,通过Statefulset拉起核心组件,用ConfigMap管理配置,通过Service暴露服务,全面拥抱k8s技术生态让Lindorm的交付体系更加的标准化,可移植;能同时满足混合云和公有云场景输出。
本文为阿里云原创内容,未经允许不得转载。