KUN(Keep UCloud Nimble)是面向 UCloud 内部、基于 Kubernetes 打造的容器服务平台,旨在提升内部研发效率,帮助改善、规范研发流程。在 KUN 平台的建设过程中,内部用户对于一些基础通用的分布式软件如 Redis、Kafka 有强需求,但又不想操心其部署及运维。KUN 团队在分析这些痛点后,决定利用 Kubernetes Operator 的能力,并弥补了开源 Operator 的一些不足,将 Operator 产品化来帮助用户部署和管理这些分布式、带状态的应用。通过 Operator 服务化,KUN 平台扩充了 Kubernetes 交付 Pod、PVC、SVC 的能力,能够快速交付 Redis 等分布式、带状态的系统,提供了一个平台之上的平台。
在这篇文章里,我们主要来聊一下 Operator 对于 Kubernetes 的价值以及我们团队基于 Operator 所做的相关工作。
Operator 是什么,解决了什么问题
为什么需要 Operator
无状态和有状态
2014-2015 年容器和微服务的出现,为软件开发和基础架构带来了巨大的创新和挑战。容器提供了隔离和限制,同时容器的状态是易失的,它对自己外部的状态和数据不关心,专注于单一的服务,比如 Web 应用、日志服务、业务程序、缓存等。这些服务都能作为容器交付和运行,而一旦容器数量形成规模,管理的难度也越来越大。
Kubernetes 作为容器编排框架,可以减轻配置、部署、管理和监控大规模容器应用的负担。事实上早期的 Kubernetes 非常善于管理无状态的应用程序,比如 Kubernetes 提供的 Deployment 控制器。它认为所有的 Pod 都是完全一样的,Pod 间没有顺序和依赖,扩容的时候就根据模板创建一个一样的新的应用,也可以任意删除 Pod。但对于像数据库这样的有状态的应用程序,添加删除实例可能需要不同的节点做不同的配置,与已有的集群进行通信协商等,这些操作通常需要我们人工来干预,这就会增加运维的负担,并且增加出错的可能性,最重要的是它消除了 Kubernetes 的一个主要卖点:自动化。
这是一个大问题,那么如何在 Kubernetes 中管理有状态的应用程序呢?
StatefulSet 的价值和不足
Kubernetes 的 1.5 版本开始出现了 StatefulSet,StatefulSet 提供了一系列资源来处理有状态的容器,比如:volume,稳定的网络标识,从 0 到 N 的顺序索引等。通过为 Pod 编号,再使用 Kubernetes 里的两个标准功能:Headless Service 和 PV/PVC,实现了对 Pod 的拓扑状态和存储状态的维护,从而让用户可以在 Kubernetes 上运行有状态的应用。
然而 Statefullset 只能提供受限的管理,通过 StatefulSet 我们还是需要编写复杂的脚本通过判断节点编号来区别节点的关系和拓扑,需要关心具体的部署工作,并且一旦你的应用没办法通过上述方式进行状态的管理,那就代表了 StatefulSet 已经不能解决它的部署问题了。
既然 StatefulSet 不能完美的胜任管理有状态应用的工作,那还有什么优雅的解决方案呢?答案是 Operator。Operator 在 2016 年由 CoreOS 提出,用来扩充 Kubernetes 管理有状态应用的能力。
Operator 核心原理
解释 Operator 不得不提 Kubernetes 中两个最具价值的理念:“声明式 API” 和 “控制器模式”。“声明式 API” 的核心原理就是当用户向 Kubernetes 提交了一个 API 对象的描述之后,Kubernetes 会负责为你保证整个集群里各项资源的状态,都与你的 API 对象描述的需求相一致。Kubernetes 通过启动一种叫做 “控制器模式” 的无限循环,WATCH 这些 API 对象的变化,不断检查,然后调谐,最后确保整个集群的状态与这个 API 对象的描述一致。
比如 Kubernetes 自带的控制器:Deployment,如果我们想在 Kubernetes 中部署双副本的 Nginx 服务,那么我们就定义一个 repicas 为 2 的 Deployment 对象,Deployment 控制器 WATCH 到我们的对象后,通过控制循环,最终会帮我们在 Kubernetes 启动两个 Pod。
Operator 是同样的道理,以我们的 Redis Operator 为例,为了实现 Operator,我们首先需要将自定义对象的说明注册到 Kubernetes 中,这个对象的说明就叫 CustomResourceDefinition(CRD),它用于描述我们 Operator 控制的应用:redis 集群,这一步是为了让 Kubernetes 能够认识我们应用。然后需要实现自定义控制器去 WATCH 用户提交的 redis 集群实例,这样当用户告诉 Kubernetes 我想要一个 redis 集群实例后,Redis Operator 就能够通过控制循环执行调谐逻辑达到用户定义状态。
所以 Operator 本质上是一个个特殊应用的控制器,其提供了一种在 Kubernetes API 之上构建应用程序并在 Kubernetes 上部署程序的方法,它允许开发者扩展 Kubernetes API,增加新功能,像管理 Kubernetes 原生组件一样管理自定义的资源。如果你想运行一个 Redis 哨兵模式的主从集群或者 TiDB 集群,那么你只需要提交一个声明就可以了,而不需要关心部署这些分布式的应用需要的相关领域的知识,Operator 本身可以做到创建应用、监控应用状态、扩缩容、升级、故障恢复,以及资源清理等,从而将分布式应用的使用门槛降到最低。
Operator 核心价值
在这里我们总结一下 Operator 的价值:
・ Operator 扩展了 Kubernetes 的能力;
・ Operator 将人类的运维知识系统化为代码;
・ Operator 以可扩展、可重复、标准化的方式实现目标;
・ Operator 减轻开发人员的负担。
Operator 服务化目标
聊完 Operator 的能力和价值我们把目光转向 KUN 上的 Operator 平台。前面说过,用户想在 Kubernetes 中快速的运行一些分布式带状态的应用,但是他们本身不想关心部署、运维,既然 Operator 可以灵活和优雅的管理有状态应用,我们的解决方案就是基于 Operator 将 Kubernetes 管理有状态应用的能力方便地暴露给用户。
核心的的目标主要有两方面:
1、针对 Operator 平台
・ 提供一个简单易用的控制台供用户使用,用户只需要点点鼠标就能快速拉起有状态应用。并且能在控制台上实时看到应用部署的进度和事件,查看资源,更新资源等。
・ 通过模板提交声明,参数可配置化,创建应用的参数通用化,将应用名称等通用配置和应用参数(如:redis 的 maxclients、timeout 等参数)解耦。这样带来的好处就是不同的 Operator 可以共用创建页面,而不需要为每种 Operator 定制创建页面,同时 Operator 暴露出更多的应用配置参数时,前端开发也不需关心,由后端通过 API 返回给前端参数,前端渲染参数,用户修改参数后,通过 API 传递到后端,后端将参数与模板渲染成最终的实例声明提交到 Kubernetes 中,节省了前端开发时间。
・ 可以管理通过公共的 Operator 和 Namespace 私有的 Operator 创建的实例。用户可以用我们提供的公用 Operator,也可以把 Operator 部署到自己的 NameSpaces,给自己的项目提供服务,但这两种 Operator 创建的应用实例都可以通过 Operator 控制台管理。
・ 可以无限添加 Operator。
2、针对 Operator 控制器
・ 拉起分布式集群,自动运配置、运维;・ 可以动态更改所控制应用参数;
・ 控制器本身需要无状态,不能依赖外部数据库等;
・ 实时更新状态,维护状态,推送事件;
・ 可以运行在集群范围,也能运行在单 NameSpace,并且可以共存,不能冲突;
针对这些设计目标最终我们的 Operator 控制台如下:
同时我们为 Operator 控制台定制了第一个 Operator:Redis Operator,未来会推出更多的 Operator,接下来我们就来看下 Redis Operator 的实现。
Redis Operator
Redis 集群模式选型
我们知道 Redis 集群模式主要有主从模式、哨兵模式、Redis 官方 Cluster 模式及社区的代理分区模式。
分析以上几种模式,主从模式的 Redis 集群不具备自动容错和恢复功能,主节点和从节点的宕机都会导致读写请求失败,需要等待节点修复才能恢复正常;而 Redis 官方 Cluster 模式及社区的代理分区模式只有在数据量及并发数大的业务中才有使用需求。哨兵模式基于主从模式,但是因为增加了哨兵节点,使得 Redis 集群拥有了主从切换,故障转移的能力,系统可用性更好,而且客户端也只需要通过哨兵节点拿到 Master 和 Slave 地址就能直接使用。因此我们决定为 Kun Operator 平台提供一个快速创建哨兵模式的 Redis 集群的 Redis Operator。
开源 Operator 的不足
目前已经有一些开源的 Redis Operator,通过对这些 Operator 分析下来,我们发现都不能满足我们的需求,这些开源的 Operator:
・ 不能设置 Redis 密码。
・ 不能动态响应更改参数。
・ 没有维护状态,推送事件。
・ 不能在开启了 istio 自动注入的 Namespace 中启动实例。
・ 只能运行在集群或者单 Namespace 模式。
改进工作
当前我们定制开发的 Redis Operator 已经在 Github 上开源
https://github.com/ucloud/red...
。提供:
- 动态响应更改 Redis 配置参数。
- 实时监控集群状态,并且推送事件,更新状态。
- 误删除节点故障恢复。
- 设置密码。
- 打开关闭持久化快捷配置。
- 暴露 Prometheus Metrics。
使用 Redis Operator 我们可以很方便的起一个哨兵模式的集群,集群只有一个 Master 节点,多个 Slave 节点,假如指定 Redis 集群的 size 为 3,那么 Redis Operator 就会帮我们启动一个 Master 节点,两个 Salve 节点,同时启动三个 Sentinel 节点来管理 Redis 集群:
Redis Operator 通过 Statefulset 管理 Redis 节点,通过 Deployment 来管理 Sentinel 节点,这比管理裸 Pod 要容易,节省实现成本。同时创建一个 Service 指向所有的哨兵节点,通过 Service 对客户端提供查询 Master、Slave 节点的服务。最终,Redis Operator 控制循环会调谐集群的状态,设置集群的拓扑,让所有的 Sentinel 监控同一个 Master 节点,监控相同的 Salve 节点,Redis Operator 除了会 WATCH 实例的创建、更新、删除事件,还会定时检测已有的集群的健康状态,实时把集群的状态记录到 spec.status.conditions 中:
status: conditions: - lastTransitionTime: "2019-09-06T11:10:15Z" lastUpdateTime: "2019-09-09T10:50:36Z" message: Cluster ok reason: Cluster available status: "True" type: Healthy - lastTransitionTime: "2019-09-06T11:12:15Z" lastUpdateTime: "2019-09-06T11:12:15Z" message: redis server or sentinel server be removed by user, restart reason: Creating status: "True" type: Creating
为了让用户通过 kubectl 快速查看 redis 集群的状态,我们在 CRD 中定义了如下的 additionalPrinterColumns:
additionalPrinterColumns: - JSONPath: .spec.size description: The number of Redis node in the ensemble name: Size type: integer - JSONPath: .status.conditions[].type description: The status of Redis Cluster name: Status type: string - JSONPath: .metadata.creationTimestamp name: Age type: date
由于 CRD 的 additionalPrinterColumns 对数组类型支持不完善,只能显示数组的第一个元数据,所以需要将 spec.status.conditions 中的状态按时间倒序,最新的状态显示在上方,方便用户查看最新的状态。同时用户也可以通过 kubectl 命令直接查看集群的健康状况:
$ kubectl get redisclusterNAME SIZE STATUS AGEtest 3 Healthy d
cluster-scoped 和 namespace-scoped
我们在 WATCH Redis 集群实例的新建、更新、删除事件时,添加了过滤规则,shoudManage 方法会检测实例是否含有 redis.kun/scope: cluster-scoped 这条 annotation,如果含有这条 annotation 并且 Redis Operator 工作在全局模式下(WATCH 了所有的 Namespace),那么这个实例的所有事件才会被 Operator 所接管。
Pred := predicate.Funcs{UpdateFunc: func(e event.UpdateEvent) bool {// returns false if redisCluster is ignored (not managed) by this operator.if !shoudManage(e.MetaNew) {return false}log.WithValues("namespace", e.MetaNew.GetNamespace(), "name", e.MetaNew.GetName()).Info("Call UpdateFunc")// Ignore updates to CR status in which case metadata.Generation does not changeif e.MetaOld.GetGeneration() != e.MetaNew.GetGeneration() {log.WithValues("namespace", e.MetaNew.GetNamespace(), "name", e.MetaNew.GetName()).Info("Generation change return true")return true}return false},DeleteFunc: func(e event.DeleteEvent) bool {// returns false if redisCluster is ignored (not managed) by this operator.if !shoudManage(e.Meta) {return false}log.WithValues("namespace", e.Meta.GetNamespace(), "name", e.Meta.GetName()).Info("Call DeleteFunc")metrics.ClusterMetrics.DeleteCluster(e.Meta.GetNamespace(), e.Meta.GetName())// Evaluates to false if the object has been confirmed deleted.return !e.DeleteStateUnknown},CreateFunc: func(e event.CreateEvent) bool {// returns false if redisCluster is ignored (not managed) by this operator.if !shoudManage(e.Meta) {return false}log.WithValues("namespace", e.Meta.GetNamespace(), "name", e.Meta.GetName()).Info("Call CreateFunc")return true},}// Watch for changes to primary resource RedisClustererr = c.Watch(&source.Kind{Type: &redisv1beta1.RedisCluster{}}, &handler.EnqueueRequestForObject{}, Pred)if err != nil {return err}
通过识别 annotation,Redis Operator 可以运行在单个 Namespace 下,也可以运行在集群范围,并且单 Namespace 和集群范围的 Operator 不会互相干扰,各司其职。
快速持久化
我们还了解到用户使用 Redis 时,有一些使用场景是直接将 Redis 当做数据库来用,需要持久化配置,而有些只是当做缓存,允许数据丢失。为此我们特意在 Redis 集群的 CRD 中添加了快速持久化配置的开关,默认为启用,这会为用户自动开启和配置 RDB 和 AOF 持久化,同时结合 PVC 可以将用户的数据持久化起来。当节点故障,被误删除时数据也不会丢失,并且 PVC 默认不会跟随 Redis 集群的删除而删除,当用户在相同 Namespace 下启动同名的 Redis 集群时,又可以使用上次的 PVC,从而恢复数据。
podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - podAffinityTerm: labelSelector: matchLabels: app.kubernetes.io/component: redis app.kubernetes.io/managed-by: redis-operator app.kubernetes.io/name: test app.kubernetes.io/part-of: redis-cluster redis.kun/v1beta1: prj-shu_test topologyKey: kubernetes.io/hostname weight: 100
为了让 Redis 拥有更高的可用性,我们为 Redis 节点提供了设置 node affinity, pod anti affinity 的能力,可以灵活的控制 Reids 数据节点跑在不同 Node 或者不同的数据中心,做到跨机房容灾。如上所示,Redis Operator 缺省情况下会为每个 Pod 注入 podAntiAffinity,让每个 redis 服务尽量不会运行在同一个 node 节点。
监控
生产级别的应用离不开监控,Operator 中还内置了 Prometheus Exporter,不光会将 Operator 自身的一些 Metrics 暴露出来,还会将 Operator 创建的每一个 Reids 集群实例的状态通过 Metrics 暴露出来。
HELP redis_operator_controller_cluster_healthy Status of redis clusters managed by the operator.# TYPE redis_operator_controller_cluster_healthy gaugeredis_operator_controller_cluster_healthy{name="config",namespace="xxxx"} 1redis_operator_controller_cluster_healthy{name="flows-redis",namespace="yyyy"} 1# HELP rest_client_requests_total Number of HTTP requests, partitioned by status code, method, and host.# TYPE rest_client_requests_total counterrest_client_requests_total{code="200",host="[2002:xxxx:xxxx:1::1]:443",method="GET"} 665310rest_client_requests_total{code="200",host="[2002:xxxx:xxxx:1::1]:443",method="PATCH"} 82415rest_client_requests_total{code="200",host="[2002:xxxx:xxxx:1::1]:443",method="PUT"} 4.302288e+06rest_client_requests_total{code="201",host="[2002:xxxx:xxxx:1::1]:443",method="POST"} 454rest_client_requests_total{code="404",host="[2002:xxxx:xxxx:1::1]:443",method="GET"} 1rest_client_requests_total{code="404",host="[2002:xxxx:xxxx:1::1]:443",method="PATCH"} 235rest_client_requests_total{code="409",host="[2002:xxxx:xxxx:1::1]:443",method="POST"} 2rest_client_requests_total{code="409",host="[2002:xxxx:xxxx:1::1]:443",method="PUT"} 184# HELP workqueue_adds_total Total number of adds handled by workqueue# TYPE workqueue_adds_total counterworkqueue_adds_total{name="rediscluster-controller"} 614738# HELP workqueue_depth Current depth of workqueue# TYPE workqueue_depth gaugeworkqueue_depth{name="rediscluster-controller"} 0# HELP workqueue_longest_running_processor_microseconds How many microseconds has the longest running processor for workqueue been running.# TYPE workqueue_longest_running_processor_microseconds gaugeworkqueue_longest_running_processor_microseconds{name="rediscluster-controller"} 0
这还不够,我们还为每个 Redis 节点提供了单独暴露 Metrics 的能力,用户可以在启动 redis 集群的时候为每个 redis 节点注入单独的 Exporter,这样每个集群的每个 Redis 数据节点都能被我们单独监控起来,结合 Prometheus 和 Alter Manger 可以很方便将 Operator 以及 Operator 创建的实例监控起来。
结合 Operator 的运维、Statefulset 的能力加上 Sentinel 的能力,等于说为 Redis 集群加了三重保险,可以确保集群的高可用。
UCloud 自研的 Redis Operator 目前已正式开源,详细实现请参考
https://github.com/ucloud/red...
总结
通过 Operator 服务化,KUN 平台可以向用户交付更多复杂的分布式应用,真正做到开箱即用。开发人员可以专心业务实现,而不需要学习关系大量的运维部署调优知识,推进了 Dev、Ops、DevOps 的深度一体化。运维经验、方案和功能通过代码的方式进行固化和传承,减少人为故障的概率,降低了使用有状态应用的门槛,极大了提升了开发人员的效率。
关注 “UCloud 技术”,后台回复 “粉丝” 进粉丝交流群