CRD Operator要点

头阵子基于kubebuilder实现了一套用于大数据交换平台底层调度的operator。在过程中遇到一些坑,平时学习的时候不太容易注意到的点,简单记录一下。

要点

多个CRD级联删除

我自定义了一个CRD PipelineRun,基于PipelineRun的逻辑会自动创建其子资源deployment。我希望在删除PipelineRun的时候,能够自动删除其对应的所有deployment实例。

这里只需要在PipelineRun创建deployment的时候,指定deployment的ObjectMet.OwnerReferences的值为PipelineRun自己即可。具体见:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func MakeDeploysAndServices(pipeline *v1.Pipeline, run *v1.PipelineRun) ([]appsv1.Deployment, []corev1.Service) {
......
// deployment
deployment := appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: makeDeployName(pipeline.Name, svc.Name),
Namespace: pipeline.Namespace,
Labels: podLabels,
Annotations: CopyMap(pipeline.GetAnnotations()),
OwnerReferences: []metav1.OwnerReference{ // 指定其OwnerReferences为run的属性
*metav1.NewControllerRef(run.GetObjectMeta(), run.GroupVersionKind()),
},
},
Spec: appsv1.DeploymentSpec{
......
},
}
......
return deployments, services
}

订阅删除前的事件

如果在etcd中已经删除了资源后operator才watch到该事件,此时由于资源已经不复存在,很多逻辑操作无法得到足够的参数来执行处理。因此,因此我们需要的是在执行最终删除之前就能够watch到该事件,并执行一些销毁资源的操作。好在k8s api-server已经为我们提供了finalizer机制。

如果某个资源的finalizers不为空,当执行删除之前,会被operator watch到操作。此时,其meta.DeletionTimestamp不为null,对应operator应该在该次事件的handler中删除掉其注册上来的finalizer对象;并执行其他业务逻辑handler。

finalizer定义方式如下:

1
2
3
4
5
6
7
8
9
apiVersion: controller.xxx.cn/v1
kind: Pipeline
metadata:
name: pipeline-test-1
finalizers: # 可以指定多个finalizers数组
- finalizer.xxx.cn
spec:
batchJob:
......

以下是reconcile的一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (r *PipelineReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
......

// delete
if p.ObjectMeta.GetDeletionTimestamp() != nil {
r.CronHandler.release(p)

if result := resources.RemoveFinalizer(p, CustomFinalizer); result == resources.FinalizerRemoved {
if err := r.Update(ctx, p); err != nil {
r.Log.Error(err, "Failed to remove finalizer")
return reconcile.Result{Requeue: true}, nil
}
}

return ctrl.Result{}, nil
}
......
return ctrl.Result{}, nil
}

处理事件风暴

因为k8s operator本来就是一个循环,即: 当资源变化时,operator的reconcile会被调用。如果此时在代码中又update了资源,那么对资源的update操作又会触发下一轮reconcile。如果在reconcile中没做好基于状态来终结循环的逻辑,循环就会无休止的进行,产生事件风暴。

假设你实现的CRD A的状态依赖于其子资源B的状态,在未走到最终状态之前,operator需要不断读取B的状态来同步给A。想想这个要如何实现?

既然k8s operator的机制本身就是一个循环,因此,我们可以利用这种循环来不断读取子资源的状态,并同步给A。这个循环的控制逻辑需要满足:

  1. 在B的状态未到最终状态时,循环必须一直执行下去;
  2. 在B进入不可迁移状态时,A的operator需要同步B的状态,并终结循环。

但是,这里依然存在一个问题!在B处于中间状态的时候,A的operator就一直循环,处于时间风暴中浪费资源吗?其实kubebuilder为我们提供了解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (r *PipelineRunReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
......
//update status
if equality.Semantic.DeepEqual(originState, run.Status) {
logs.Debug("[pipelineRun] status are equal... ", run.Namespace, run.Name)
return reconcile.Result{RequeueAfter: SyncBuildStatusInterval}, nil
}

// 除非有特殊需求,否则劲量在Reconcile层更新状态(少在更深层逻辑中更新状态)
if err := r.Status().Update(ctx, run); err != nil {
logs.Error("failed to update pipelineRun status, err: %s", err.Error())
return reconcile.Result{}, err
}

return ctrl.Result{}, nil
}

上面的reconcile.Result{RequeueAfter: SyncBuildStatusInterval}不会执行到下面的状态更新操作,而是直接返回。operator会将该资源变动的event重新放入队列,然后等到RequeueAfter参数指定的时间间隔之后重新取出来再调用reconcile处理。这样的优点是,到达的效果一样,但不会频繁的写etcd,从而保障k8s集群不受影响。

利弊

另外,kubebuilder自动生成了operator的代码框架,同时生成CRD的yaml文件,这减少了不少工作量,但是它不生成client侧的代码。因此,这也比较坑,需要重新基于CRD类型文件来生成代码。

项目介绍

项目名称: pipeline-operator
项目代码: https://github.com/chenleji/pipeline-operator

0%