应用上云的进程在全球范围内,进行得如火如荼。以应用为中心,云原生的相关技术和方案,已经覆盖了非常多的领域。除了应用的运行时以外,最靠近应用,也是应用依赖性最大的范围,就是周边的中间件、数据库、大数据等技术。在云原生持续发展,应用上云逐步成熟的今天,企业和客户也开始考虑自己的下一步云原生能力规划。
本篇就来聊聊中间件领域,在云原生背景下的一些思考、一些思路、一些总结。本篇以 Redis 中间件为切入口,来介绍中间件在云原生中的一些玩法。
01
发展过程
Dockerfile 构建阶段:
从描述可以看出在做的事情,在起初 docker 刚刚进入大家视野的时候,除了用 docker 来构建件镜像,启动容器之外,也有很多社区的参与者,在尝试着使用这个新颖的技术,来结合到自己熟悉的领域,其中就包括中间件。
大致的玩法可以总结为以下几个步骤:
1. 选择基础的操作系统的发行版本;
2. 构建镜像的 entrypoint script,这个步骤是这个阶段最关键,也是最有挑战的一个部分。一般没有运维经验是写不出来健壮的脚本,因为这个镜像的所有能力都在这个脚本里,会有很多脚本的参数、条件判断、集群模式的版本、存储管理、监控管理能力、软件依赖包的安装、中间件本身的基本配置和高级配置;
3. 基于构建好的镜像,设计一下运行和部署的方式,用脚本构建不同角色的机器应该部署怎样的容器,以及对应容器的运行参数是什么,同时还要考虑日志的管理,方便运维排查问题;
4. 最终以部署文档的方式和运维文档,提交给交付人员去交付整体方案。这种尝试和使用容器来运行中间件的方式,是最接近传统的方式。
docker-compose 编排阶段:
经历了第一个阶段,已经相对熟悉了之后,会思考的问题是,有没有一个好的、简单的编排机制,可以统一来编排和交付。这里就出现了,由 docker 官方提供的编排技术 docker-compose。它可以在一台机器上管理编排能力和编排服务中的各个服务的依赖关系,以及基于本机的服务发现能力。
大致的玩法可以总结为以下几个步骤:
1. 准备相关的镜像,这些镜像在第一个阶段已经基本 OK 了;
2. 设计服务之间的依赖关系和启动顺序,保证可以通过统一的命令启动、重启和删除所有的服务,同时在设计这部分的时候,也是需要考虑使用的网络以及使用的存储配置;
3. 构建 docker-compose 文件、部署文档的方式和运维文档,提交给交付人员去交付整体方案。这种方式已经有了云原生编排和统一运维的风格了。
docker swarm:
在有了基本的编排之后,接下来会思考,如何去跨越主机完成集群的下发和服务发现和网络通信能力。在那个年代,可以选择的也就只有类似 docker swarm、mesos 等类似技术。而 swarm 又是 docker 官方推荐的技术,于是尝试用这种方式来构建的方案越来越多。
大致的玩法可以总结为以下几个步骤:
1. 首先要考虑,在不基于主机网络的情况下,容器跨集群的网络应该怎么解决,这里出现了 docker 的 CNM 网络模型,swarm 可以实现跨主机的通信、routing mesh、dns rr 等能力;
2. 需要分析中间件集群在这样的网络方案中,网络的使用方式是什么,特别要考虑是不是使用 dnsrr 的服务发现机制;
3. 基于选择的技术方案,构建编排文件,这里的编排文件其实就是 docker-conpose,因为 docker-compose、swarm 都是 docker 官方的方案,所以互通能力是很正常的;
4. 最终以部署文档的方式和运维文档,提交给交付人员去交付整体方案。这种方式已经是实现了跨主机,在整个 swarm 集群中动态的,统一的管理中间件的部署了,也是看起来更云原生的方式了。
K8s 尝试和验证:
这个时候,就会触发大家的重新思考和技术选型。最终不负所望,K8s 成为了容器运行、容器编排的事实标准。那接下来,中间件的容器化方案,肯定也是要跟着升级。
大致玩法可以总结为以下几个步骤:
1. 首先还是要有可靠的容器镜像方案,随着整个阶段的发展,有很多不错的中间件,都逐步有了官方的镜像,这让中间件容器化更加的有动力去做这个事情;
2. 接下来就是要升级之前的编排方案,依据是之就开始在尝试和积累的阶段。如果上手的阶段,是 K8s YAML 编排阶段,那就是从这里开始设计。如果是 docker-compose 或者 swarm 的阶段,那升级的思路就是使用 K8s YAML 的各种能力,适配之前 docker-compose 和 swarm 的各种能力,如服务发现机制、网络组建能力、镜像启动方式、容器资源使用限制、服务对外访问方式、集群内部组件之间的集群通信网络、存储的方式、基于 sidecar 的监控方式等等;
3. 最终以部署文档的方式和运维文档,提交给交付人员去交付整体方案。这种方式已经基于 K8s 实现了跨主机。在整个 K8s 集群中,相对手动一点的统一管理中间件的部署,完成了基于 K8s 的中间件部署能力。
helm 编排阶段:
helm 是类似 docker-compose 的工作,只是作用的对象不一样,一个是作用于 docker 和 swarm,一个是作用于 K8s。
大致玩法可以总结为以下几个步骤:
1. 根据 YAML 编排阶段,设计和完成的 YAML 文件,使用 helm 的编排技术进行模版的制作;
2. 使用 helm 提供的打包机制,进行打包和发布,这样可以批量完成,在不同环境进行的安装和使用;
3. 如果对模版有特殊的要求,比如有一些平台可以基于 json schema 完成动态表单能力,还会进一步的制作 helm 的模版,让其支持更多的能力;
4. 最终以部署文档的方式和运维文档,提交给交付人员去交付整体方案。
Operator 能力阶段:
最近这一两年,对中间件的更高级的玩法,社区也在不断思考和发展。其中很有代表性的一个技术就是 Operator。那 Operator 是什么?这里简单的总结一下,Operator 是一种基于 K8s 的申明式的能力,是扩展 K8s 生态和能力的方式。Operator 可以理解成是一个框架,或一个 SDK。具体使用这个技术完成什么样的业务能力,由使用者和需求来决定。
举例,基于 Operator 可以完成自己产品的安装和管理、产品能力的开发、服务的统一管理和上下架能力等等。那在有了 Operator 之后,中间件的方案会有怎样的变化呢?首先,最大的特点是程序化、自动化能力可以更加的灵活,可以工程化的设计方案、研发方案、发布方案。每一个中间件都是非常复杂的一套系统,解决特殊领域的问题。既能发挥出中间件复杂业务能力,还能更加自动化、可自治化、可迭代化,可以说 Operator 是一个很好的选择。
前面几个阶段,都是基于手动、模版、编排的能力,完成方案设计和部署能力。在使用 Operator 之后,整体的工作会有很大的区别。首先需要在之前的基础上,熟悉中间件本身的能力,熟悉基本的编排。其次需要精通 Operator 开发的能力,来支持开发出一个健壮稳定的 Operator,这是非常重要的。
大致的玩法可以总结为以下几个步骤:
1. 构建基础镜像,既然有开发能力,那推荐的玩法是可以基于配置文件,或者中间件的 API/SDK 来扩展其能力,所以更加倾向于选择基础镜像,这样可以借助社区的能力;
2. 设计 Operator 实现的目标和范围,也就是具体的需求是怎样的。不同的需求,实现的方案是不同的。社区有一个 Operator 的成熟度模型,不同的Level 的 Operator 要完成的目标是不一样的。
3. 根据需求设计实现的方案;
4. 定义 Operator 的业务模型 CRD;
5. 开发 Operator 的核心 Controller 来完成业务能力;
6. 最终以部署文档的方式和运维文档交付,也可以是一套标准的基于 Operator 开发的中间件产品为交付物。
02
常见玩法
YAML 方式:
这种玩法是熟悉 K8s YAML 的运维人员,很喜欢的一种方式。特点是直观,部署方式固定,而且简单。常见的操作是,能够以传统运维的方式来运维,因为运维人员关注和操作的单元,还是以物理机/虚拟机为运维点。正常使用交付物的方式是,先学习文档,然后规划机器,以及每个机器的角色,然后根据文档进行安装,最后交付出一套一套的中间件集群给到最终客户。这个玩法适合不需要太多自动化、智能化的诉求的方案,快速、简单、交付难度小、交付周期小。
helm 方式:
这种玩法只是在 YAML 的基础上,做了一个封装,以及可以稍微标准化一点的交付方案。这个看习惯。但是和 YAML 不同的地方是,helm 还提供了一套管理机制、打包机制、发布版本机制,能够以制品的方式,将制作好的 chart 包,上架到一些仓库中,例如 harbor 是有 chart 的仓库。还有一些产品和平台,也支持基于 helm charts 的服务上下架能力。
Operator 方式:
这种玩法一般是大规模的,对中间件部署有很高的要求,特别是中间件能力中的可开发能力、可自愈能力、可封装能力、可系统化能力、可云原生能力。随着这部分的需求和方案越来越普及,这种玩法也将逐步成为主流方式。让中间件本身成为云原生的一部分,让企业的这部分基础设施能力可 IT 化、可兑现化。
03
技术术语
哨兵模式:
这是 Redis 的集群模式中的一种,特点是支持三种角色。
第一种是 master 角色,支持读写;
第二种是 slave,支持读,同时需要从 master 复制数据;
第三种是哨兵,从字面意思可以理解,它的作用就是站岗,查看 master 的状态,以及集群中有哪些 slave。当发现 master 不可用了,要选择最可用的 slave 来担任 master,保证 Redis 的数据可写。
集群模式:
这是 Redis 中,处理更大规模数据能力的集群方案。特点是:支持 slot 的方式,分配数据的分布,是一种分片的模式;支持多主多从的能力,不同的主处理不同的 slots;主从之间和哨兵模式的力类似,也会有 slave 复制 master 数据的场景。
他们的不同是,当 master down 了,哨兵模式是由哨兵角色的节点来完成主从切换,而集群模式的主从切换是,由角色是 master 的所有节点参与这个过程,最后由选中的其中一个 master 来完成切换动作。判断 master down 了没有,在哨兵模式中,是由哨兵来完成,而在集群模式中,所有节点都会参与。
对外可达:
有一些方案在实现的时候,需要外部非容器化的服务也可以使用容器化的中间件能力,这就是对外可达的意思。
只是在选择对应的方案的时候,会有不同的选择,有的支持 K8s 的 NodePort Service,有的支持 LoabBalancer Service,有的基于开源 haproxy、nginx、envoy 或者商业化的 4 层负载,还可以使用 eBPF 实现高性能的 4 层负载,如 Cilium 中的 XDP LB 等等。
Operator:
这是一种扩展 K8s 能力,将业务能力云原生化的一种技术框架,可以理解成是由 Controller + CRD 组成的框架,同时 Operator 提供了代码的脚手架,可以快速地生成代码框架。
申明式:
这个是 K8s 的理念,根据定义的配置,不断的调和期望的状态,让被管理的资源对象的状态,逐步得向正确的、期待的方向演进,最终达到正确的状态。这个过程不是一次就结束了,而是不断调和的过程,一旦发现不是期望的状态,就会重复的执行控制逻辑。
04
研发流程
熟悉基本概念和集群架构:
在设计 Operator 之前,首先要熟悉对应中间件的传统的玩法、基本的概念、集群的模式有哪些,以及常见的使用场景和使用方式有哪些。
在这个过程中,更关注的是 Operator 的需求收集,有的客户需要的是哨兵模式的,有的客户需要的是集群模式,有的客户只想在开发环境提供单点的模式的。所以这个过程需要判断出,正确的部署支持的模式。并且根据运维的经验,提出合理的玩法和需要的操作能力,这些都是需求的来源。
设计出高可用方案:
在确定了需求之后,接下来就需要根据具体的需求,选择对应的集群高可用的方案。如:选择怎样的容器网络、怎样的存储方案,是不是一定要完成跨机房的,甚至跨越地区的需求?然后设计对应的高可用的方案,这个方案是后续真正实现 Operator 架构设计时的重要技术方案的输入。
因为不同的方案,需要对应的领域模型和实现的方式都不一样。但是是不是方案就是唯一的?其实不一定,根据需要可以设置参数来支持多个集群方案的能力,或者为不同的集群方案设计不同的 CRD 业务模型,都可以达到适配多方案的能力。在设计高可用的时候,有时会依赖一些外部系统的能力,如基于 K8s 的时候,会涉及到存储方案的选择和设计,以及底层网络的方案的选择,不同的存储方案和不同的网络方案,都会直接影响最终设计的 Operator 的实现方案。
设计容灾方案:
数据备份以及跨机房的部署集群,都是保证数据安全的方法。使用数据备份的机制是最常见的玩法,包含常见的热备和冷备等,根据数据安全以及恢复时间的要求来选择,而且不同的中间件数据备份方案都不太一样。备份毕竟是备份,在出现问题的时候:是否可以在短时间内恢复系统,或者直接切换流量?这个问题,在基于备份的方案中,感觉还是有一定的风险的,不管是不是热备,以及备份的最大的 lag 是多少。那这个时候,对跨中心、零 Down 机 的场景,有时候就需要选择中间件跨机房、跨区域部署。正常情况下,在保证有两个机房是正常的情况下,集群是处于可用状态。一般会采用两地三中心的部署方式,如果要求再高一点的,会采用三地五中心的部署方式。
设计可靠的监控和告警方案:
监控和告警不仅仅是中间件需要的,应用也需要的,只是中间件的监控能力都是相对固定的。因为这些监控能力,都和特定的中间件绑定。在有了监控数据之后,就可以完成告警的设计,这个也是经验活,需要根据常见的一些问题,设计告警规则,逐步验证,最后上线到生产环境。
开发健壮的 Operator:
在中间件的本身需求、常见的运维方案,以及数据安全的需求确定了之后,就可以开始进入研发阶段。在这个阶段中,我们需要根据需求去设计领域模型,然后将领域模型转换成 CRD 的定义。通过 Operator SDK 框架去生成初始代码,其中包含了 CRD 的模型,以及对应的 Controllers。在开发阶段,就需要在可运维性上包含进来,包括日志的可读性、错误的处理、监控的指标设计、告警规则的规划、备份的实现方式等等。
设计集群的验收标准:
在研发阶段结束之后,交付给客户之前,需要对实现的方案进行各种验证,包括可靠性、可恢复性、性能、吞吐量、功能等等,这些是不是都是满足预期的。这个过程需要有一定的中间件的经验和开发 Operator 的经验。其中会包含各种测试的 case 要设计,case 越完整,方案会越稳定,可维护性更高,如包括:down 点测试、压力测试、功能测试、高可用测试、备份/恢复测试、监控测试、告警规则合理性测试等等。
提供完善的运维文档:
不管哪种方式来交付中间件的方案,最后都需要提供完整的运维文档。但是基于 Operator 来交付,运维的文档内容和其它的稍有不同。
本身中间件就是一套复杂的系统,本身有很多运维相关的知识,同时官方的文档也会比较完整。但是基于 Operator 的方案,还需要提供 Operator 部分的运维方式,中间件的使用方式,以及实现思路,说明有没有因为 Operator 方案的设计,而引入特殊的玩法和运维的方式。毕竟是两套系统的整合 (中间件 + Operator 程序),所以需要更加完整的运维交付文档。同时针对容器运行的方式,针对存储和网络等依赖的方案,以及监控和告警的能力,去精细化运维方式。
05
需求分析
5.1 单点模式需求
- 可以部署单点的 Redis 容器
- 版本支持优先 5.0 版本
- 可以调整 Redis 的配置文件
- 可以设置资源大小
- 可以备份
- 可以基于备份恢复
- 可以监控 Redis 的运行情况
- 可以对 Redis 的运行情况进行告警,并对接告警平台
- 支持 Redis 的持久化,使用支持 storageClass 方式的 PVC
- 支持删除 Redis 服务
- 支持对外可访问
- 支持设置密码
- 支持查看 Pod 的实时日志和离线日志
- 支持 Pod 的 exec 能力
- 支持下载 Pod 中的日志文件
- 支持 Pod 以及 Statefulset 资源对象的常见操作
- 支持 Redis 的常见能力的运维操作,如查询 key 等
- 存储空间的扩容能力
5.2 哨兵模式需求
- 可以部署哨兵模式的 Redis 容器集群
- 版本支持优先 5.0 版本
- 可以调整 Redis 的配置文件
- 可以设置资源大小
- 保证部署的排它能力,避免重要的服务跑在一台机器
- 可以备份
- 可以基于备份恢复
- 可以监控 Redis 的运行情况
- 可以对 Redis 的运行情况进行进行告警,并对接告警平台
- 支持 Redis 的持久化,使用支持 storageClass 方式的 PVC
- 支持删除 Redis 服务
- 支持对外可访问,而且保证访问入口的地址不变
- 支持设置密码
- 支持快速扩容
- 支持快速缩容
- 支持查看 Pod 的实时日志和离线日志
- 支持 Pod 的 exec 能力
- 支持下载 Pod 中的日志文件
- 支持 Pod 以及 Statefulset 资源对象的常见操作
- 支持查看集群的拓扑图
- 支持 Redis 的常见能力的运维操作,如查询 key 等
- 存储空间的扩容能力
5.3 集群模式需求
- 可以部署集群模式的 Redis 容器集群
- 版本支持优先 5.0 版本
- 可以调整 Redis 的配置文件
- 可以设置资源大小
- 保证部署的排它能力,避免重要的服务跑在一台机器
- 可以备份
- 可以基于备份恢复
- 可以监控 Redis 的运行情况
- 可以对 Redis 的运行情况进行进行告警,并对接告警平台
- 支持 Redis 的持久化,使用支持 storageClass 方式的 PVC
- 支持删除 Redis 服务
- 支持对外可访问,而且保证访问入口的地址不变
- 支持只对内提供访问地址
- 支持设置密码
- 支持快速扩容,包括支持单个分片集群的规模,以及总体的分片规模
- 支持快速缩容,包括支持单个分片集群的规模,以及总体的分片规模
- 支持在扩容的过程中,自动完全 rebalance 能力,包括自动迁移 slots 和 keys
- 支持查看 Pod 的实时日志和离线日志
- 支持 Pod 的 exec 能力
- 支持下载 Pod 中的日志文件
- 支持 Pod 以及 Statefulset 资源对象的常见操作
- 支持查看集群的拓扑图
- 支持 Redis 的常见能力的运维操作,如查询 key 等
- 存储空间的扩容能力
06
中间件方案
以下介绍了 Redis 组建成集群的两种模式:哨兵和集群,在不同需求场景下的不同的设计方案。
哨兵模式 - 方案一:
在传统的方案中,有一种方案是,可以基于 haproxy 做 master 检查的能力,这里介绍的就是使用此机制的方案。haproxy 的访问地址,一直保持代理的 Backend 节点的角色是 master 角色。
哨兵模式 - 方案二:
使用 K8s 的 service 的 update endpoint 的方式,来设置自己代理的一直是,master 的那个 Pod 的 IP 地址。
哨兵模式 - 方案三:
有一些 JDK 提供了基于访问哨兵的地址,通过 SDK 本身的能力来发现 master 节点。以下方案就是类似的机制的方案。
集群模式 - 方案一:
在集群模式中,客户端访问的方式,支持设置一组地址,由 SDK 来连接和使用集群。以下方案就是提供了类似的方案,同时这些地址是外部可达的,非容器集群的服务也可以访问这个 Redis 集群。
集群模式 - 方案二:
在有一些特殊的安全场景下,客户会要求访问的入口地址,有代理能力。不能因为集群规模的变化而去改变连接地址,而且连接的地址范围越小越好。以下方案就是完成类似能力的方案,客户的访问地址,永远只有一个地址。类似 envoy 支持的代理能力。新版本 6.0 之后也内置支持了 proxy 的能力。
07
架构设计
整体实现可以基于 Operator SDK 技术框架,Operator 的实质就是,用 CRD+Controller 来完成 K8s 特色的申明式的能力,不断的调整和修复,直到达到期望的状态,如果过程中发现偏离理想状态的,会一直调整和修复。这个过程的入口就是 Operator Controller 中的 Reconcile(req ctrl.Request) (ctrl.Result, error) 接口。
Reconcile 的核心思路:
中间件处理的核心逻辑:
08
样例分析
8.1 模型样例
单点模型
哨兵模型
集群模型
备份模型
8.2 使用样例
单点部署/备份/恢复
哨兵部署/备份/恢复
集群部署/备份/恢复
8.3 监控图表
09
跨机房方案的探索
在跨机房的方案设计中,传统的使用方式也有很多成熟的玩法。在跨机房的背景下,除了底层设施的稳定之外,更多关注的还是跨机房的调度能力。但是在云原生的发展过程中,对于跨机房的调度的能力,还在思考和构建中,这里提供一些思路仅供参考,不做详细分析。以下列举大致的几种思路。
传统玩法:大多是设计好部署方式之后,由一个下发点去下发,可以是程序化的下发实现,或者基于自动化运维工具,如类似 ansible 这类工具,操作的目标对象是物理机器或虚拟机。
容器的玩法一:只拿容器当作虚拟机来使用,整体下发方式还是先规划集群、集群中的哪些机器;以及在不同的机上,规划部署的中间件组件的角色,最后通过 K8s 的 cmd 去完成下发。这个玩法直接且更接近传统玩法。
容器的玩法二:这里有一个前提是,K8s 的集群会分为控制集群和计算集群。控制集群也可以理解成管理集群,主要完成控制逻辑,而计算集群是真正运行中间件容器的集群。可以尝试使用社区里最近发展的一些技术。
10
总结
基于 Operator 来设计和实现中间件的能力,更具有可工程化的能力、可编程能力、智能化运维的能力、云原生的能力、监控告警一体化能力。