在近日的 ArchSummit 全球架构师峰会 2021 上海站上,继网易副总裁、杭研院执行院长、互联网技术委员会主席、网易数帆总经理汪源发表主题演讲《打造开放的云原生操作系统和系统软件架构》之后,网易技术委员会委员、网易数帆基础架构总监张晓龙向与会者进一步讲述了网易数帆在云原生中间件上的思考、实现与经验。本文为演讲内容实录。
今天给大家分享我们面向生产环境的中间件容器化实践,主要包括四个部分的内容:
第一部分从基础中间件面临的运维挑战出发,介绍网易解决这些挑战的技术演进路径,以及为什么要去做中间件容器化。
第二部分介绍中间件容器化的需求以及网易数帆整体平台架构。
第三部分针对中间件容器化过程中的一些共性问题,给出我们的思考,以及最佳实践。
最后是中间件容器化工作的总结和未来的计划。
在容器技术出来之前,基础中间件技术如 MySQL、Redis、Kafka 等早已开源,并成为服务端架构设计的标准组件,一个典型的互联网应用,数据库、缓存、消息队列三大中间件是必不可少的。
架构师应用这些中间件去架构一个个应用平台非常简单,但运维人员遇到了较大的问题,包括如下 5 个方面:
中间件本身是比较复杂的分布式系统,运维需要理解这些分布式系统的工作原理,编写出适合它们的运维脚本,复杂性非常高;
运维效率比较低下,50 个以下 MySQL 实例用手工运维可能没有问题,但 500、1000 个数据库实例,或者如网易云音乐的数千个 Redis 实例,如果还用手工脚本来运维,效率必然很低;
稳定性不足,这是由于运维人员总是用手工脚本来运维,在线上抄命令,不小心抄错命令可能中间件就宕了;
传统的中间件是部署在物理机上面的,而物理机制没办法提供很强的资源弹性;
所有比较资深的中间件运维都基本上在互联网上大厂,因为这些运维非常复杂,一般企业很难招到一个非常专业的运维,我们认为解决这个挑战的最佳实践,是将中间件运维能力云服务化。
将这些中间件做成云服务有几个优势。第一是运维简单易上手,第二能够高效地实现大批量实例的自动化运维,第三有很强的 SLA 保障,因为不需要敲太多手工的一个命令。第四是能借助 IaaS 弹性资源能力快速扩容。最后因为整个运维变得简单,不再需要大量的专业人员就可以帮业务运维好中间件。
其实公有云厂商也看到了这个趋势,国内三大主流公有云都把开源的基础中间件做成了云服务。我想这主要有两个原因:首先,IaaS 资源层面竞争趋于同质化,把 PaaS 中间件做成云服务可以消耗更多的资源,把用户绑定得更深;其次,中间件作为云上的增值服务,毛利率远高于云主机、云硬盘,所以很多公有云用户不喜欢 RDS,自己买云主机搭 MySQL。
为了解决中间件运维复杂性的挑战,网易在六七年前就研发了一个云基础中间件平台。这个平台有一些技术特点,首先是基于 IaaS 提供资源弹性,也就是说中间件运行的计算资源是云主机,存储资源是云盘,网络资源可能就是在租户的 VPC 里面。
第二它采用了 IaaS 的租户隔离策略,如果一个租户想要中间件实例,平台就用他的云主机、云硬盘自动化地帮他搭起来,可以做到不同租户之间很好的隔离。
我们当时研发了 6 款基础中间件云服务,业务团队研发产品需要中间件,它只需要接入这些云服务就可以了,不需要重新做一遍。我们主要做的是左边的控制管理部分,比如实例高可用、部署安装、实例管理等。当时我们也取得了一些成效,大大提升了运维团队对中间件的运维能力。
随着时间的推移,第一代基础中间件暴露出了三大缺陷,难以解决。第一大缺陷是极限性能不足。因为它使用 KVM 虚拟机作为计算资源,比在物理上运行有非常大的性能折损,没办法满足业务高负载/高压力下对中间件性能和稳定性的苛刻要求。
第二是实现资源成本太高,因为它是基于 OpenStack 来提供资源编排能力,另外 KVM 虚拟化技术强隔离的特性使得内存资源没办法在多个中间件实例之间共享,这两个因素使得跑在虚拟机上的中间件实例部署密度非常低,哪怕有租户的中间件负载不高,他也不可能把内存释放,因为 KVM 是强隔离的。
第三点它的交付非常不灵活,它就跟网易的 IaaS 绑定,没办法支持我们未来把它商业化,输出到网易以外的企业,这个企业的基础设施可能是在公有云上,也可能是在自己的 IDC 机房。
近几年,Docker、Kubernetes 等容器技术诞生并飞快发展,无状态应用的容器化已经成熟,我们认为容器作为一个新的已经广泛落地的基础设施的技术,完美地对应了第一代基础中间件的缺陷能力—弱隔离有有助于资源共享;轻量化的虚拟化能够消除性能损耗,满足业务在高负载场景;基于镜像进行标准化的封装,有利于高效交付;还有强大灵活的调度能力;最关键的一点,它是整个云原生技术栈的一个基石。
Kubernetes 编排技术,最关键的是它跟基础设施是松耦合的,使得我们能够将应用搬到任何一个地方,因为它就是面向混合云设计的。另外它是面向大规模生产环境的设计,继承了 Google 的大规模生产环境的经验,所以用容器技术解决中间件服务化的问题是有希望的。
网易内部基于 Kubernetes 构建了一套云原生操作系统,它向下能够适配各类的基础设施资源,向上能够作为各种应用负载的统一提供商--这也是 Kubernetes 的目标之一。中间件正是整个云原生操作系统所要支撑的一类业务。从这个角度来看,中间件容器化也是顺理成章的。
中间件容器化要解决它的运维问题,尤其下面几个需求必须要考虑的。
第一,生命周期的管理,我们需要容器化中间件平台能够帮助运维完成对于中间件实例级别的各种运维操作,网易数帆会基于 Kubernetes Operator 这一套框架来实现。
第二点是高可用的部署,中间件,特别是在追求更高的可用性的情况下,往往要做多机房的部署,一个中间件集群里面的所有实例,要按照什么样的比例分布在不同的机房,标准的 Kubernetes 调度器没办法做到,我们需要扩展 Kubernetes 的调度器来实现这样的编排。
同时,还要完善监控告警的指标,这个指标就对应云原生的 Prometheus 的可观测性体系。
性能是第一代中间件的一个痛点,我们要确保容器化中间件基本达到物理机部署的性能才能支撑核心应用,这需要有针对性地优化各类中间件实例的性能。
还有一点是产品化,因为我们希望中间件容器化不仅能够在网易使用,还能够商业化输出,所以我们参考公有云上 RDS、Redis 的产品形态,需要有同等的产品能力,能够在任意的基础设施上低成本、灵活交付,我们必须采用松耦合和高复用的架构设计。
网易数帆选择了 Kubernetes Operator 的机制。从深层次理解,Kubernetes 构建了一个分布式系统部署运维所需的“原语”,它内置的对象如 Pod、Node、Deployment、StatefulSet 等,都是为了实现一个典型的无状态分布式系统提出来的。这些内置的对象相互配合,使得无状态应用的部署和运维非常高效。
但是 Kubernetes 内置的这些对象没办法直接解决中间部署运营的问题。第一点,中间件是有状态的,它的状态是存储,可能网络 IP。第二,中间件实例与无状态应用的实例不同,后者的副本相互之间没有关系,而中间件实例和实例之间、副本和副本之间是有关系的,是要相互访问的,中间件之间形成一个复杂的拓扑关系,比如在做故障恢复时,Redis 两个副本之间是有主从关系的。
社区在两年多之前也开始实现中间件或者说有状态的应用,提出了一套 Operator 开发框架。如果我们把 Kubernetes 理解成为一个操作系统,那么 Operator 就是在这个操作系统上开发原生应用的一套开发框架,支持更高效、更自动化、更可扩展的开发方式。
Operator 有 4 个特点,第一它是需要开发出来,是遵循的声明式的编程理念,有对象的定义,还有控制器部署。Operator 其实是一个控制器,遵循着观察、分析、行动的决策链闭环。如果用户定义了 4 个资源,Operator 就分析这 4 个资源当前的状态和目标状态有哪些不一致。
图中可以看到当前的状态有 1 个 Pod,他现在是 0.0.1 的版本,我们定义的状态要求 0.02,还少了一个 Pod,如果发现了不一致,它会有一些 Action,再扩一个 Pod,把它升级到 0.0.2。我们实现 Operator,其实就是去写这些 Action 应该怎么做。这实际上是封装了特定领域的运维知识跟经验,能够被设计用来管理复杂的状态应用。
Operator 开发框架的主体包括三部分,第一部分 operator-sdk,研发的一个脚手架;第二部分是 operator-lifecycle-manager,一个生命周期管理的组件;第三部分是 operatorhub.io,既然任何人都可以为开发一个应用,一个它可以部署安装运维的应用,他就应该可以把这个应用放到一个应用市场,operatorhub.io 就是这样的一个应用市场。
不同的机构去开发 Operator,在运维看来是有一定的成熟级别的,应用部署都能够自动化运维,这是对应运维最希望的一个级别。最基本的第一个级别就是基本安装 Operator,该怎么去做到把原来安装部署脚本,用 Operator 这种工程模式实现。
这是网易数帆实现的一个基于 Kubernetes Operator 的中间件平台架构,包括控制面和数据面。左边控制面面向运维管理的能力,包括一些跟中间件业务无关的但是大家都需要的通用组件,如审计、认证权限、控制台等。
中间就是中间件 Operator,在这里我们用 Operator 的机制研发了 Redis、Kafka、MySQL 等中间件。
我们实现了中间件的生命周期管理,这些 Operator 本身也是运行在 Kubernetes 的上面,而且它是一种无状态应用,以 Deployment 方式可以运行在上面,因为它的状态都是存在 etcd 里面的。
再下面是 Kubernetes 的管控面,Master 节点需要的一些组件。
最下面是日志、监控、报警的组件,我们自研的一个日志管理平台实现从采集信息去动态更新它的配置,以及把日志收集上来。
右边是中间件的数据面,我画了三个 Node,我们把一个中间件的集群用 StatefulSet 来实现,每一个实例跑在一个 Pod 上,每个 Pod 可能会声明它的对持久卷的用途,Pod 跟 Node 之间是有拓扑关系的,它需要相互进行数据和拓扑同步,用于状态变更以及故障恢复。每个节点上都会运行 Kubernetes 的两个组件,Kublet,kube-proxy,还有一个采集器,用于日志监控。
我们还实现了 Pod 的挂盘功能,不管是本地盘还是远程盘,通过 StorageClass 的方式去实现,这也是 Kubernetes 的标准。
接下来探讨中间件容器化过程中的一些共性问题的解决办法。中间件最大的特点在于它是有状态的,Kubernetes 只负责计算的编排,中间件的状态存储有两种可能,一种是远程存储,一种是本地存储。
我们认为远程存储是最佳实践。如果你在私有云环境上有一套类似于开源 Ceph 的远程分布式存储,应该毫不犹豫地使用它来存。如果说 Ceph 性能不足,你可以找其他更好的分布式存储来去直接用。如果你在公有云上,那你应该毫不犹豫地用云盘来作为中间件的存储。
很多情况下,本地存储是不得已而为之的一个选择,因为没有太靠谱的分布式存储,有可能这个分布式存储性能不行,和用本地盘跑起来相差很远,也有可能分布式系统后端可靠性不行,会丢数据。
为此,我们实现了本地存储的接入。我们做本地存储需求有两个,一是要求当 Pod 去申请 PVC 的时候做好动态管理配置,本地盘在创建、删除时,要去做对应的操作。同时在 Pod 调度时,要实现它与本地盘强绑定,既然 Pod 开始创建的时候,有本地盘在某一个 Node 上,你必须保证 Pod 经过故障恢复或者重调度之后还是跑在那个 Node 上,以确保中间件数据不丢失。
在技术实现上,我们对于节点上的本地磁盘引入了一个 LVM 去动态的管理,也采用了 Kubernetes Local PV,后者的不足在于需要运维提前在节点上创建 PV,这个是不可取的。所以我们做了两件事,一是调度器扩展,实现本地存储的资源准备,在创建 Pod 时声明所需本地盘的大小,它就能够动态给创建挂载到这个 Pod 里面去,不需要运维提前手动准备。
如图中一个 Pod 的调度过程,用户创建了一个 Pod,它声明了一个 PVC,我们加了一个本地存储调度器扩展,先做一个预调度,算一下每个节点上的本地盘的存储容量够不够,如果够就把 Node 的信息也放到 PVC 里面,接下来通知这个 Node 上一个本地存储资源准备器,让资源准备器收到请求的时候去调用 LVM 把存储资源给创建出来,并把对应的 PV 创建出来。在资源准备器上把 PV 和 PVC 绑定,然后通知调度器可以把 Pod 调度到这个节点上,因为声明的本地存储已经准备好。接下来用 Kubernetes 把那个节点所在的本地盘挂载到 Pod 里面去,完成一个整体的调度。
关于中间件容器化的网络,有两个场景的实现。第一个场景,我们设计的中间件运行在不同的基础设施上,对应不同的网络配置,如果是物理网络,可以用 Calico、Flannel 这样的网络方案,直接用它的 CNI;如果是公有云,就对接公有云上的 VPC 网络,好处是每一家公有云都为 Kubernetes 提供了一个标准 CNI,使得运行在云主机上的 Kubernetes 可以去接入他们的网络。
第二个场景,我们需要优化网络性能。我们引入了一个容器的 SR-IOV 方案,好处是能够做到优于物理机的低时延。它采用的是网卡直通技术实现,能够降低 50%的时延,可以满足一些对时延要求很高的超高性能任务需求,但 PPS 提升不了。直通少了网络传输的虚拟化开销,但是缺点也比较明显,这个方案只能用在物理网络,因为它完全依赖于硬件网卡,无法用在公有云上实现网络加速。
在物理网络环境上要去处理网卡异构问题,包括说是我们可能用英特尔网卡,可能有 Mellanox 的网卡,需要对 VF(SR-IOV 的一个概念)进行精细管理。我们把 VF 当成一个扩展的调度资源,通过标准的 Kubernetes Device Plugin 来发现和注册节点的 VF 资源, 结合 label 和 taint 标记,原生的调度器就可以进行资源管理和分配。
轻舟中间件的集群是用 StatefulSet 抽象的,每个实例都是 StatefulSet 的一个 Pod,StatefulSet 只能做到 Pod 的名字不变,它发生不同更新的时候,或者挂了再恢复的时候,都保持 Pod 的名字不变,但是它没办法保持 Pod 的 IP 不变。然而,在传统的中间件运维眼里,基于物理机部署的 IP 是不变的,机器重启之后也还是原来的 IP,所以他们的一些运维习惯,都是喜欢用 IP 而不是域名。
为了让容器化中间件能够更快地推广落地,以及兼顾已有的应用,我们做了保持 StatefulSet 的 IP 不变的功能,通过引入一个全局的容器地址池组件接管对 Pod IP 的分配来实现。创建 StatefulSet 的时候,把分配给它的 IP 记录好,哪怕 Pod 更新的时候被删掉,IP 还给保持住不释放,等它重新建起来之后,如果名字跟原来那个是一样的,就把这个 IP 重新分配给他。
工程化,我们研发容器化中间件,相对于第一代基于虚拟化的中间件,因为重用了 Kubernetes 内置的一些概念以及它在运维、控制上的一些机制,使得我们去研发相同的基础中间件,研发代价能够大幅度减少,这个体现在代码比第一代基础中间件要减少很多,当然这个代码减少也是有代价的——开发人员必须非常了解 Kubernetes Operator 这套开发框架,必须得深刻地理解 Kubernetes 声明式编程的概念,他才能写出来。
在质量保障方面,我们做了两个事情,第一个就是混沌测试,就是故障测试,基于开源的 ChaosBlade 去模拟 Kubernetes 资源故障对中间件服务的影响,另外我们也借助 Kubernetes e2e 测试框架来确保运维人员能够模拟各种中间件实例的生命周期操作是否正常。
还有一点,要做中间件实例生命周期管理,需要做监控、告警,很多情况下它的 UI 都是有共同之处,UI 的使用模式都是一样的,这是我们设计的一个前端页面渲染,渲染引擎使得用动态表单机制能够很快地开发控制台,后端通过配置一下就可以实现控制台业务的开发能力,这样使得研发代价更小。
性能优化,我们采取了一些策略,使得容器化中间件的性能基本接近于它运行在物理机上的水平。我们在 CPU 开了性能模式,降低唤醒延迟。在内存方面,我们关闭 SWAP 及透明大页,调优同步内存脏页回写阈值,这些都是参数级的调优。
I/O 方面使能内核 blk-mq,增大预读缓存。还有一个比较重要的就是网卡中断,我们将物理方法中断跟容器的 veth 虚拟网卡中断处理跟 CPU 给隔离了,确保系统性能不发生抖动。
NUMA 也是我们优化的一点,这在高负载上面体现得比较明显。我们使得容器部署感知 NUMA 拓扑,将 Pod 尽量的分配在本地的 NUMA,尽量不要让一个 Pod 跨 NUMA,以免带来比较大的 CPU 缓存的开销。
第一代中间件的一个缺陷是不能够去往外交付。去年我们做了容器化中间件这个产品,名字叫轻舟中间件,具备基础中间件的标准能力。在接入层我们也增加了一些能力,因为我们基于 Kubernetes 来做的,运维人员甚至可以通过 Kubectl、YAML 文件就可以运维中间件。中间件服务层,我们实现了 7 个基础中间件服务,这些中间件基本上具备了前面提到的核心运维能力。
整体上中间件基于 Operator,能够跑在任意 Kubernetes 集群之上,底层的资源无所谓,公有云的虚拟机可以作为 Kubernetes 的 Node,云盘可以作为 Kubernetes 的存储。另外,我们也允许社区基于 Operator 开发的一些中间件在我们的平台上跑。
技术是为业务服务的,中间件最大的痛点是运维,要把它做到托管的云服务去解决,而容器技术的优势使得中间件容器化成为实现中间件云服务的最佳实践。在实现上需要 Operator,需要有更加云原生的模式来把容器化中间件给研发出来,当然对开发人员的要求也很高的。
未来的计划有两点,第一,我们现在的容器化中间件平台可以跑在任意 Kubernetes 上面,但是我们还是要做到跑在 Kubernetes 发行版上,如 OpenShift、Rancher 等,希望容器化中间件这些 Operator 也能跑在上面,但是需要做一些兼容。第二,我们整体是想建设云原生操作系统,中间件是其中的一个负载,我为什么不把中间件的负载和无状态应用负载实现混部?这样可以给公司带来更高的一个资源利用率,可以降低成本。
谢谢大家!