作者:苏厚镇 青云科技数据库钻研工程师
从事 RadonDB ClickHouse 相干工作,热衷于钻研数据库内核。
通过《ClickHouse on K8s 部署篇》,比照了 RadonDB ClickHouse 集群在 Kubernetes 中部署的几种计划,表明应用 Operator 进行部署和治理是最方便快捷的。
那么到底什么才是 Operator,Operator 又是如何与 Kubernetes 进行协同工作的,Operator 的代码逻辑又是怎么的?本篇将基于 Operator 基本概念和源代码解析,深度解析 ClickHouse Operator 运行原理。
什么是 Operator?
在 Kubernetes 官网文档 [[1]](https://kubernetes.io/docs/co…) 中,对 Operator 的定义如下:
Operators are software extensions to Kubernetes that make use of custom resources to manage applications and their components. Operators follow Kubernetes principles, notably the control loop.
简略来说:Operator = 定制资源 + 控制器。
那么定制资源和控制器又是什么呢?
定制资源
在 Kubernetes 官网文档 [[1]](https://kubernetes.io/docs/co…) 中,对定制资源的定义如下:
Custom resources are extensions of the Kubernetes API.
It represents a customization of a particular Kubernetes installation.
Kubernetes 提供了一系列的资源,包含 Statefulset、Service、Configmap 等。然而这些资源并不能齐全满足应用需要,例如在 K8s 中部署 ClickHouse 利用时,需定制一个 ClickHouse 利用资源。
Kubernetes 提供了两种形式向集群中增加定制资源:
- CRD:无需编程。K8s 从 1.7 版本减少了 CRD 来扩大 API,通过 CRD 能够向 API 中减少新资源类型,无需批改 K8s 源码或创立自定义的 API server,该性能大大提高了 Kubernetes 的扩大能力。
- API 聚合:须要编程。但反对对 API 行为进行更多的管制,例如数据如何存储以及在不同 API 版本间如何转换等。
ClickHouse Operator 在定制资源方面,选用了 CRD 形式增加定制资源。
然而应用 CRD 定制资源后,仅仅是让 Kubernetes 可能辨认定制资源的身份。创立定制资源实例后,Kubernetes 只会将创立的实例存储到数据库中,并不会触发任何业务逻辑。在 ClickHouse 数据库保留定制资源实例是没有意义的,如果须要进行业务逻辑管制,就须要创立 控制器。
控制器
Controller 的作用就是监听指定对象的新增、删除、批改等变动,并针对这些变动做出相应的响应,对于 Controller 的具体设计,能够参考 Harry (Lei) Zhang 老师在 twitter 上的分享,根本架构图如下:
从图中可看出,定制资源实例的变动会通过 Informer 存入 WorkQueue,之后 Controller 会生产 WorkQueue,并对其中的数据做出业务响应。
Operator 其实就是图中除了 API Server 和 etcd 的残余局部。因为 Client、Informer 和 WorkQueue 是高度类似的,所以有很多我的项目能够自动化生成 Controller 之外的业务逻辑(如 Client、Informer、Lister),因而用户只须要专一于 Controller 中的业务逻辑即可。
ClickHouse Operator 代码解析
代码构造
.
├── cmd # metrics_exporter 和 operator 的入口函数
│ ├── metrics_exporter
│ └── operator
├── config # ClickHouse 和 ClickHouse Operator 的配置文件,会通过 ConfigMap 挂载到 pod
├── deploy # 各类组件的部署脚本和部署 yaml 文件
│ ├── dev # clickhouse operator 开发版本装置部署
│ ├── grafana # grafana 监控面板装置部署
│ ├── operator # clickhouse operator 装置部署
│ ├── operator-web-installer
│ ├── prometheus # prometheus 监控部署
│ └── zookeeper # zookeeper 装置部署
├── dev # 各类脚本,如镜像生成、利用构建、利用启动等
├── dockerfile # 镜像 dockerfile
├── docs # 文档
├── grafana-dashboard
├── hack
├── pkg # 代码逻辑
│ ├── announcer # 告诉器
│ ├── apis # api, 定制资源类型
│ │ ├── clickhouse.radondb.com
│ │ └── metrics
│ ├── chop # clickhouse operator 类型
│ ├── client # 主动生成,作用参考下面的图
│ │ ├── clientset
│ │ ├── informers
│ │ └── listers
│ ├── controller # controller 逻辑,次要关怀局部
│ ├── model # controller 调用 model
│ ├── util # util
│ └── version # version 信息
└── tests # 主动测试代码
代码逻辑
以下代码均为简化版,仅取外围逻辑局部。
Controller 中次要的工作逻辑存在于 Worker 中。
Run
Run 是 Worker 中整个工作逻辑入口。Run 是一个无休止的工作循环,冀望在一个线程中运行。
func (w *worker) run() {
...
for {
// 生产 workqueue,该办法会阻塞,直到它能够返回一个我的项目
item, shutdown := w.queue.Get()
// 解决工作
if err := w.processItem(item); err != nil {utilruntime.HandleError(err)
}
// 后置解决,从 workqueue 中删除我的项目
w.queue.Forget(item)
w.queue.Done(item)
}
...
}
processItem
processItem 解决 item,依据 item 的类型决定须要调用的解决逻辑。
func (w *worker) processItem(item interface{}) error {
...
switch item.(type) {
...
case *ReconcileChi:
reconcile, _ := item.(*ReconcileChi)
switch reconcile.cmd {
case reconcileAdd: // 解决定制资源的新增
return w.updateCHI(nil, reconcile.new)
case reconcileUpdate: // 解决定制资源的批改
return w.updateCHI(reconcile.old, reconcile.new)
case reconcileDelete: // 解决定制资源的删除
return w.deleteCHI(reconcile.old)
}
utilruntime.HandleError(fmt.Errorf("unexpected reconcile - %#v", reconcile))
return nil
...
}
...
}
updateCHI
以最罕用的 updateCHI 逻辑为例,看一下其解决逻辑。
// updateCHI 创立或者更新 CHI
func (w *worker) updateCHI(old, new *chop.ClickHouseInstallation) error {
...
// 判断是否须要执行解决
update := (old != nil) && (new != nil)
if update && (old.ObjectMeta.ResourceVersion == new.ObjectMeta.ResourceVersion) {w.a.V(3).M(new).F().Info("ResourceVersion did not change: %s", new.ObjectMeta.ResourceVersion)
return nil
}
// 判断 new chi 是否正在被删除
if new.ObjectMeta.DeletionTimestamp.IsZero() {w.ensureFinalizer(new) // 如果没有,则增加 finalizer 避免 CHI 被删除
} else {return w.finalizeCHI(new) // 如果删除,则无奈继续执行操作,返回
}
// 归一化,不便前面应用
old = w.normalize(old)
new = w.normalize(new)
actionPlan := NewActionPlan(old, new) // 比照 old 和 new,生成 action plan
// 进行一系列的标记,不便 reconcile 进行解决,如 add、update 等,代码省略
// 执行 reconcile(须要深刻了解)if err := w.reconcile(new); err != nil {w.a.WithEvent(new, eventActionReconcile, eventReasonReconcileFailed).
WithStatusError(new).
M(new).A().
Error("FAILED update: %v", err)
return nil
}
// 后置解决
// 移除须要 delete 的我的项目
actionPlan.WalkRemoved(func(cluster *chop.ChiCluster) {_ = w.deleteCluster(cluster)
},
func(shard *chop.ChiShard) {_ = w.deleteShard(shard)
},
func(host *chop.ChiHost) {_ = w.deleteHost(host)
},
)
// 将新的 CHI 增加到监控中
if !new.IsStopped() {w.c.updateWatch(new.Namespace, new.Name, chopmodel.CreatePodFQDNsOfCHI(new))
}
...
}
reconcile
updateCHI 中最重要的办法即 reconcile,该办法依据增加的标记做理论的解决。
func (w *worker) reconcile(chi *chop.ClickHouseInstallation) error {w.a.V(2).M(chi).S().P()
defer w.a.V(2).M(chi).E().P()
w.creator = chopmodel.NewCreator(w.c.chop, chi) // cretea creator
return chi.WalkTillError(
// 前置解决
// 1. 解决 CHI svc,即 svc/clickhouse-{CHIName}
// 2. 解决 CHI configmap,即 configmap/chi-{CHIName}-common-{configd/usersd}
w.reconcileCHIAuxObjectsPreliminary,
// 解决集群
// 1. 解决 Cluster svc,即 svc/cluster-{CHIName}-{ClusterName},不过貌似没有?w.reconcileCluster,
// 解决分片
// 1. 解决 Shard svc,即 svc/shard-{CHIName}-{ClusterName}-{ShardName},不过貌似没有?w.reconcileShard,
// 解决正本
// 0. 将正本从集群中解除
// 1. 解决 Host Configmap,即 chi-{CHIName}-deploy-confd-{ClusterName}-{ShardName}-{HostName}
// 2. 解决 Host StatefulSet,即 chi-{CHIName}-{ClusterName}-{ShardName}-{HostName}
// 3. 解决 Host PV,即 chi-{CHIName}-{ClusterName}-{ShardName}-{HostName}
// 4. 解决 Host svc,即 chi-{CHIName}-{ClusterName}-{ShardName}-{HostName}
// 5. 解除 Host 的 add 状态
// 6. 判断 Host 是否失常运行
// 7. 将正本增加到集群中,如果 Host 出错,则回滚
w.reconcileHost,
// 后置解决
// 1. 更新 CHI configmap,即 configmap/chi-{CHIName}-common-configd
w.reconcileCHIAuxObjectsFinal,
)
}
总结
至此,便揭开了 Operator 的神秘面纱。如果对 Operator 有更多趣味,欢送到 Github 代码库查看更多细节。
[1]. Kubernetes 官网文档 : https://kubernetes.io/docs/co…
[2]. RadonDB ClickHouse Kubernetes : https://github.com/radondb/ra…
举荐浏览
- 设计 | ClickHouse 分布式表实现数据同步
- 容器化 | ClickHouse on K8s 部署篇
- 容器化 | ClickHouse on K8s 根底篇
- HTAP | MySQL 到 ClickHouse 的高速公路