UCloud 基于 Kubernetes Operator 的服务化实践

KUN(Keep UCloud Nimble)是面向 UCloud 内部、基于 Kubernetes 打造的容器服务平台,旨在提高内部研发效率,帮助改善、规范研发流程。在 KUN 平台的建设过程当中,内部用户对于一些基础通用的分布式软件如 Redis、Kafka 有强需求,但又不想操心其部署及运维。KUN 团队在分析这些痛点后,决定利用 Kubernetes Operator 的能力,并弥补了开源 Operator 的一些不足,将 Operator 产品化来帮助用户部署和管理这些分布式、带状态的应用。经过 Operator 服务化,KUN 平台扩充了 Kubernetes 交付 Pod、PVC、SVC 的能力,可以快速交付 Redis 等分布式、带状态的系统,提供了一个平台之上的平台。前端

在这篇文章里,咱们主要来聊一下 Operator 对于 Kubernetes 的价值以及咱们团队基于 Operator 所作的相关工做。node

Operator 是什么,解决了什么问题git

为何须要 Operatorgithub

无状态和有状态redis

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 管理有状态应用的能力方便地暴露给用户。

核心的的目标主要有两方面:

一、针对 Operator 平台

・ 提供一个简单易用的控制台供用户使用,用户只须要点点鼠标就能快速拉起有状态应用。而且能在控制台上实时看到应用部署的进度和事件,查看资源,更新资源等。

・ 经过模板提交声明,参数可配置化,建立应用的参数通用化,将应用名称等通用配置和应用参数(如:redis 的 maxclients、timeout 等参数)解耦。这样带来的好处就是不一样的 Operator 能够共用建立页面,而不须要为每种 Operator 定制建立页面,同时 Operator 暴露出更多的应用配置参数时,前端开发也不需关心,由后端经过 API 返回给前端参数,前端渲染参数,用户修改参数后,经过 API 传递到后端,后端将参数与模板渲染成最终的实例声明提交到 Kubernetes 中,节省了前端开发时间。

・ 能够管理经过公共的 Operator 和 Namespace 私有的 Operator 建立的实例。用户能够用咱们提供的公用 Operator,也能够把 Operator 部署到本身的 NameSpaces,给本身的项目提供服务,但这两种 Operator 建立的应用实例均可以经过 Operator 控制台管理。

・ 能够无限添加 Operator。

二、针对 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...

。提供:

  1. 动态响应更改 Redis 配置参数。
  2. 实时监控集群状态,而且推送事件,更新状态。
  3. 误删除节点故障恢复。
  4. 设置密码。
  5. 打开关闭持久化快捷配置。
  6. 暴露 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 技术”,后台回复 “粉丝” 进粉丝交流群

相关文章
相关标签/搜索