原文鏈接:https://zdyxry.github.io/2019/09/13/Kubernetes-%E5%AE%9E%E6%88%98-Operator-Finalizers/
Finalizers
Finalizers 允許 Operator 控制器實現異步的 pre-delete hook。比如你給 API 類型中的每個對象都創建了對應的外部資源,你希望在 k8s 刪除對應資源時同時刪除關聯的外部資源,那么可以通過 Finalizers 來實現。
Finalizers 是由字符串組成的列表,當 Finalizers 字段存在時,相關資源不允許被強制刪除。存在 Finalizers 字段的的資源對象接收的第一個刪除請求設置 metadata.deletionTimestamp
字段的值, 但不刪除具體資源,在該字段設置后, finalizer
列表中的對象只能被刪除,不能做其他操作。
當 metadata.deletionTimestamp
字段非空時,controller watch 對象並執行對應 finalizers 的動作,當所有動作執行完后,需要清空 finalizers ,之后 k8s 會刪除真正想要刪除的資源。
Operator finalizers 使用
介紹了 Finalizers 概念,那么我們來看看在 Operator 中如何使用,在 Operator Controller 中,最重要的邏輯就是 Reconcile 方法,finalizers 也是在 Reconcile 中實現的。要注意的是,設置了 Finalizers 會導致 k8s 的 delete 動作轉為設置 metadata.deletionTimestamp
字段,如果你通過 kubectl get
命令看到資源存在這個字段,則表示資源正在刪除(deleting)。
有以下幾點需要理解:
- 如果資源對象未被刪除且未設置 finalizers,則添加 finalizer並更新 k8s 資源對象;
- 如果正在刪除資源對象並且 finalizers 仍然存在於 finalizers 列表中,則執行 pre-delete hook並刪除 finalizers ,更新資源對象;
- 由於以上兩點,需要確保 pre-delete hook是冪等的。
kuberbuilder 示例
func (r *CronJobReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() log := r.Log.WithValues("cronjob", req.NamespacedName) var cronJob batch.CronJob if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil { log.Error(err, "unable to fetch CronJob") return ctrl.Result{}, ignoreNotFound(err) } // 聲明 finalizer 字段,類型為字符串 myFinalizerName := "storage.finalizers.tutorial.kubebuilder.io" // 通過檢查 DeletionTimestamp 字段是否為0 判斷資源是否被刪除 if cronJob.ObjectMeta.DeletionTimestamp.IsZero() { // 如果為0 ,則資源未被刪除,我們需要檢測是否存在 finalizer,如果不存在,則添加,並更新到資源對象中 if !containsString(cronJob.ObjectMeta.Finalizers, myFinalizerName) { cronJob.ObjectMeta.Finalizers = append(cronJob.ObjectMeta.Finalizers, myFinalizerName) if err := r.Update(context.Background(), cronJob); err != nil { return ctrl.Result{}, err } } } else { // 如果不為 0 ,則對象處於刪除中 if containsString(cronJob.ObjectMeta.Finalizers, myFinalizerName) { // 如果存在 finalizer 且與上述聲明的 finalizer 匹配,那么執行對應 hook 邏輯 if err := r.deleteExternalResources(cronJob); err != nil { // 如果刪除失敗,則直接返回對應 err,controller 會自動執行重試邏輯 return ctrl.Result{}, err } // 如果對應 hook 執行成功,那么清空 finalizers, k8s 刪除對應資源 cronJob.ObjectMeta.Finalizers = removeString(cronJob.ObjectMeta.Finalizers, myFinalizerName) if err := r.Update(context.Background(), cronJob); err != nil { return ctrl.Result{}, err } } return ctrl.Result{}, err } } func (r *Reconciler) deleteExternalResources(cronJob *batch.CronJob) error { // // 刪除 crobJob關聯的外部資源邏輯 // // 需要確保實現是冪等的 } func containsString(slice []string, s string) bool { for _, item := range slice { if item == s { return true } } return false } func removeString(slice []string, s string) (result []string) { for _, item := range slice { if item == s { continue } result = append(result, item) } return }
cluster-api-provider-vsphere 實現
看完了示例,我們來招一個具體項目看看,cluster-api-provider-vsphere 是 cluster-api 相關項目,用於提供 vsphere 相關資源創建的 Operator,采用 kubebuilder 來實現的。
vspheremachine_controller.go 中實現了 Reconcile 方法:
// Reconcile ensures the back-end state reflects the Kubernetes resource state intent. func (r *VSphereMachineReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, reterr error) { ... // Always close the context when exiting this function so we can persist any VSphereMachine changes. defer func() { if err := machineContext.Patch(); err != nil && reterr == nil { reterr = err } }() // Handle deleted machines if !vsphereMachine.ObjectMeta.DeletionTimestamp.IsZero() { return r.reconcileDelete(machineContext) } // Handle non-deleted machines return r.reconcileNormal(machineContext) }
在 Reconcile
中檢測了 DeletionTimestamp
是否為0 ,如果不為0 ,則表示資源處於正在刪除中,那么來看下 reconcileDelete
實現:
func (r *VSphereMachineReconciler) reconcileDelete(ctx *context.MachineContext) (reconcile.Result, error) { ctx.Logger.Info("Handling deleted VSphereMachine") var vmService services.VirtualMachineService = &govmomi.VMService{} // 執行刪除虛擬機邏輯 vm, err := vmService.DestroyVM(ctx) if err != nil { // 如果刪除失敗,則直接返回錯誤,controller 會自動重試 return reconcile.Result{}, errors.Wrapf(err, "failed to destroy VM") } // 重新調度刪除虛擬機邏輯,直到虛擬機狀態處於 notfound 狀態 if vm.State != infrav1.VirtualMachineStateNotFound { ctx.Logger.V(6).Info("requeuing operation until vm state is reconciled", "expected-vm-state", infrav1.VirtualMachineStateNotFound, "actual-vm-state", vm.State) return reconcile.Result{RequeueAfter: config.DefaultRequeue}, nil } // pre-delete hook執行成功,也就是上面的刪除虛擬機邏輯執行成功,則清空 Finalizers ctx.VSphereMachine.Finalizers = clusterutilv1.Filter(ctx.VSphereMachine.Finalizers, infrav1.MachineFinalizer) return reconcile.Result{}, nil }
可以看到整體邏輯與示例的使用是一致的,主要通過這種方式來達到 pre-delete hook 的效果。
k8s-initializer-finalizer-practice
在搜索相關資料的時候,看到有人在 SO 上問了如何使用的問題,其中有個回答中附上了一個練習項目,項目很小,很適合了解 Finalizers 概念。
相關邏輯如下:
}else{ customdeployment:=obj.(*crdv1alpha1.CustomDeployment).DeepCopy() fmt.Println("Event..............................") if customdeployment.DeletionTimestamp != nil{ // check if it has finalizer if customdeployment.GetFinalizers()!=nil{ finalizers:=customdeployment.GetFinalizers() // check if first finalizer match with deletepod.crd.emruz.com if finalizers[0]=="deletepods.crd.emruz.com"{ // _,err:=myutil.PatchCustomDeployment(c.clientset,customdeployment, func(deployment *crdv1alpha1.CustomDeployment) *crdv1alpha1.CustomDeployment { // delete pods under this deployment err:=myutil.DeletePods(c.kubeclient,c.podLabel) if err!=nil{ fmt.Println("Failed to remove all pods. Reason: ",err) return nil } // pods sucessfully removed. remove the finalizer customdeployment.ObjectMeta=myutil.RemoveFinalizer(customdeployment.ObjectMeta) return customdeployment }) if err!=nil{ return err } } }