Notes on Kubernetes Operator Development
/最近在和李老师、朱师兄一起开发Rhino这个项目,项目地址在这里。Rhino是为HPC开发者提供的一套框架,方便HPC开发者将自己开发的MPI应用部署到 k8s 集群中。目前我主要开发的是Operator项目,该项目是基于kubebuilder 这个脚手架的,在这篇文章中记录一些讨论的会议纪要,和自己的一些体会。
关于Operator开发的背景知识,可以参考这一篇文章:还在手写 Operator?是时候使用 Kubebuilder 了。
1. 声明式API与Operator模式原理
当我们要创建资源(比如使用kubectl apply yaml文件,或者调用k8s提供的API创建资源)时,k8s返回资源创建成功,这个创建成功是什么意思呢?
我们要创建资源时,请求都会先发送到API server;API server收到请求之后,如果系统中没有这个资源,那么就会将资源创建出来:向数据库etcd中写一条记录,记录当前资源的期望状态,之后etcd会向API server发送确认,表示这条记录已经成功记录下来,此时API server会向客户端返回资源创建成功。
也就是说,当API server返回资源创建成功之后,并不是真正创建成功了,而是表明这条记录写到了etcd数据库里面。
2. RhinoJob 工作原理
关于RhinoJob的工作原理,更详细的可以看这篇文章。
RhinoJob的任务是在k8s上将MPI运算任务启动起来。首先Reconciler会启动一个Launcher Job,来协调和管理MPI工作节点;当Launcher Job启动成功后,Reconciler会根据提交任务的并行度(Parallelism
字段)来创建Workers Job,运算结束后,Launcher Job 和 Workers Job状态会发生改变,此时再次触发Reconciler,将对应的Job,Pod等删除,并据此修改RhinoJob的状态。
3. 技术细节实现
3.1 TTL
目前Rhino Operator中的CRD(Custom Resource Definition)为RhinoJob
,其中包含TTL
字段,表示在TTL
秒后,之前创建的CR应该被销毁,但是这是如何实现的呢?
来看一下处理TTL的代码:
1 | if *rhinojob.Spec.TTL > 0 { |
每次触发Reconcile逻辑,都会对TLL进行处理,计算CR剩余存活时间。如果ttl_left > 0
,那么返回ctrl.Result{RequeueAfter: ttl_left}
这个结构体;否则调用k8s API删除资源。
上面的ctrl.Result
的结构体是用来干什么的呢?先来看一下Reconciler的工作逻辑:
Reconciler 处理的事件按照队列排列,也就是图中的workQueue
,Reconciler处理完一个事件之后,再从队列中取出下一个事件,进行处理。
回到TTL的处理逻辑上来,ctrl.Result
结构体中RequeueAfter: ttl_left
字段的意思就是,在ttl_left
时间后,将事件放到WorkQueue
中,让Reconciler处理。这样我们就能实现在TTL事件后,将CR删除,不会出现程序一直跑一直跑,CR不会被删除的情况。
同样的,由于Reconciler是用队列来处理事件的,并发只有1,所以在Reconciler中调用time.Sleep
是非常糟糕的设计,这会导致后面所有的事件都会被阻塞在这个队列中,无法被Reconciler处理。
3.2 Scheme
Scheme实现的是Go struct 和 Custom Resource的对应,和Web后端开发中的model非常相似,其实就是数据库中表的结构。使用kubectl apply的yaml文件的结构,就可以转换成对应的结构体,实现上面所说的对应。
3.3 对CR的监视
我们定义的CR的状态发生改变时,会触发Reconciler逻辑,但这个是如何实现的呢?难不成有一个东西一直在watch?
我们使用kubebuilder搭建的Custom Controller会与k8s的API Server建立HTTP长连接,当有与我们定义的CR相关的资源状态变化时,API Server会向Controller发送一个Response,此后触发一系列逻辑,最终将事件加入到WorkQueue,之后触发Reconciler。
3.4 launcher和worker资源状态变化为何会触发Reconciler
为什么要设计成这样,让launcher job和workers job(都是k8s job)状态变化也会触发Reconciler?Link
1 | if errGetLauncherJob != nil || errGetWorkersJob != nil { |
从代码可以看出,RhinoJob的状态取决于launcher job和workers job的状态,它们的每一次状态变化,都有可能触发RhinoJob的状态变化。
那么这是如何实现的呢?Link
1 | ctrl.SetControllerReference(rj, job, r.Scheme) |
1 | func (r *RhinoJobReconciler) SetupWithManager(mgr ctrl.Manager) error { |
只需要在创建这些job时,调用k8s runtime提供的API,并在实现SetupWithManager
方法时,调用Owns
方法即可。
3.5 Reconciler 返回 err 后发生了什么
当Reconciler执行出错时,会返回err。如果返回nil
,表示Reconcile逻辑执行完毕;返回非nil
error时,会让事件重新在队列里面排队。
3.6 什么情况下Reconciler可以忽略执行过程中的 err
当Reconciler中调用Get方法获取正在运行的RhinoJob CR时,如果是IsNotFound
类型的错误的话,我们不对该错误进行处理,因为这种错误通常是由资源被删除(主动删除;超时触发Reconciler被删除)导致的,是正常情况,我们不需要对其进行处理。
1 | if errors.IsNotFound(err) { |