TiDB Operator 源碼閱讀 (二) Operator 模式

上一篇文章中我們討論了 TiDB Operator 的應用場景,瞭解了 TiDB Operator 可以在 Kubernetes 集羣中管理 TiDB 的生命週期。可是,TiDB Operator 的代碼是怎樣運行起來的?TiDB 組件的生命週期管理的邏輯又是如何編排的呢?我們將從 Operator 模式的視角,介紹 TiDB Operator 的代碼實現,在這篇文章中我們主要討論 controller-manager 的實現,介紹從代碼入口到組件的生命週期事件被觸發中間的過程。

Operator模式的演化: 從 Controller 模式到 Operator 模式

TiDB Operator 參考了 kube-controller-manager 的設計,瞭解 Kubernetes 的設計有助於瞭解 TiDB Operator 的代碼邏輯。Kubernetes 內的 Resources 都是通過 Controller 實現生命週期管理的,例如 Namespace、Node、Deployment、Statefulset 等等,這些 Controller 的代碼在 kube-controller-manager 中實現並由 kube-controller-manager 啓動後調用。

爲了支持用戶自定義資源的開發需求,Kubernetes 社區基於上面的開發經驗,提出了 Operator 模式。Kubernetes 支持通過 CRD(CustomResourceDefinition)來描述自定義資源,通過 CRD 創建 CR(CustomResource)對象,開發者實現相應 Controller 處理 CR 及關聯資源的變更的需求,通過比對資源最新狀態和期望狀態,逐步完成運維操作,實現最終資源狀態與期望狀態一致。通過定義 CRD 和實現對應 Controller,無需將代碼合併到 Kubernetes 中編譯使用, 即可完成一個資源的生命週期管理。

TiDB Operator 的 Controller Manager

TiDB Operator 使用 tidb-controller-manager 管理各個 CRD 的 Controller。從 cmd/controller-manager/main.go 開始,tidb-controller-manager 首先加載了 kubeconfig,用於連接 kube-apiserver,然後使用一系列 NewController 函數,加載了各個 Controller 的初始化函數。

controllers := []Controller{
    tidbcluster.NewController(deps),
    dmcluster.NewController(deps),
    backup.NewController(deps),
    restore.NewController(deps),
    backupschedule.NewController(deps),
    tidbinitializer.NewController(deps),
    tidbmonitor.NewController(deps),
}

在 Controller 的初始化函數過程中,會初始化一系列 Informer,這些 Informer 主要用來和 kube-apiserver 交互獲取 CRD 和相關資源的變更。以 TiDBCluster 爲例,在初始化函數 NewController 中,會初始化 Informer 對象:

tidbClusterInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: c.enqueueTidbCluster,
        UpdateFunc: func(old, cur interface{}) {
            c.enqueueTidbCluster(cur)
        },
        DeleteFunc: c.enqueueTidbCluster,
    })
statefulsetInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: c.addStatefulSet,
        UpdateFunc: func(old, cur interface{}) {
            c.updateStatefulSet(old, cur)
        },
        DeleteFunc: c.deleteStatefulSet,
    })
 

Informer 中添加了處理添加,更新,刪除事件的 EventHandler,把監聽到的事件涉及到的 CR 的 Key 加入隊列。

初始化完成後啓動 InformerFactory 並等待 cache 同步完成。

informerFactories := []InformerFactory{
            deps.InformerFactory,
            deps.KubeInformerFactory,
            deps.LabelFilterKubeInformerFactory,
        }
        for _, f := range informerFactories {
            f.Start(ctx.Done())
            for v, synced := range f.WaitForCacheSync(wait.NeverStop) {
                if !synced {
                    klog.Fatalf("error syncing informer for %v", v)
                }
            }
        }

隨後 tidb-controller-manager 會調用各個 Controller 的 Run 函數,開始循環執行 Controller 的內部邏輯。

// Start syncLoop for all controllers
for _,controller := range controllers {
    c := controller
    go wait.Forever(func() { c.Run(cliCfg.Workers,ctx.Done()) },cliCfg.WaitDuration)
}

以 TiDBCluster Controller 爲例,Run 函數會啓動 worker 處理工作隊列。

// Run runs the tidbcluster controller.
func (c *Controller) Run(workers int, stopCh <-chan struct{}) {
    defer utilruntime.HandleCrash()
    defer c.queue.ShutDown()
 
    klog.Info("Starting tidbcluster controller")
    defer klog.Info("Shutting down tidbcluster controller")
 
    for i := 0; i < workers; i++ {
        go wait.Until(c.worker, time.Second, stopCh)
    }
 
    <-stopCh
}

Worker 會調用 processNextWorkItem 函數,彈出隊列的元素,然後調用 sync 函數進行同步:

// worker runs a worker goroutine that invokes processNextWorkItem until the the controller's queue is closed
func (c *Controller) worker() {
    for c.processNextWorkItem() {
    }
}
 
// processNextWorkItem dequeues items, processes them, and marks them done. It enforces that the syncHandler is never
// invoked concurrently with the same key.
func (c *Controller) processNextWorkItem() bool {
    key, quit := c.queue.Get()
    if quit {
        return false
    }
    defer c.queue.Done(key)
    if err := c.sync(key.(string)); err != nil {
        if perrors.Find(err, controller.IsRequeueError) != nil {
            klog.Infof("TidbCluster: %v, still need sync: %v, requeuing", key.(string), err)
        } else {
            utilruntime.HandleError(fmt.Errorf("TidbCluster: %v, sync failed %v, requeuing", key.(string), err))
        }
        c.queue.AddRateLimited(key)
    } else {
        c.queue.Forget(key)
    }
    return true
}

Sync 函數會根據 Key 獲取對應的 CR 對象,例如這裏的 TiDBCluster 對象,然後對這個 TiDBCluster 對象進行同步。

// sync syncs the given tidbcluster.
func (c *Controller) sync(key string) error {
    startTime := time.Now()
    defer func() {
        klog.V(4).Infof("Finished syncing TidbCluster %q (%v)", key, time.Since(startTime))
    }()
 
    ns, name, err := cache.SplitMetaNamespaceKey(key)
    if err != nil {
        return err
    }
    tc, err := c.deps.TiDBClusterLister.TidbClusters(ns).Get(name)
    if errors.IsNotFound(err) {
        klog.Infof("TidbCluster has been deleted %v", key)
        return nil
    }
    if err != nil {
        return err
    }
 
    return c.syncTidbCluster(tc.DeepCopy())
}
 
func (c *Controller) syncTidbCluster(tc *v1alpha1.TidbCluster) error {
    return c.control.UpdateTidbCluster(tc)
}

syncTidbCluster 函數調用 updateTidbCluster 函數,進而調用一系列組件的 Sync 函數實現 TiDB 集羣管理的相關工作。在 pkg/controller/tidbcluster/tidb_cluster_control.go 的 updateTidbCluster 函數實現中,我們可以看到各個組件的 Sync 函數在這裏調用,在相關調用代碼註釋裏描述着每個 Sync 函數執行的生命週期操作事件,可以幫助理解每個組件的 Reconcile 需要完成哪些工作,例如 PD 組件:

// works that should do to making the pd cluster current state match the desired state:
//   - create or update the pd service
//   - create or update the pd headless service
//   - create the pd statefulset
//   - sync pd cluster status from pd to TidbCluster object
//   - upgrade the pd cluster
//   - scale out/in the pd cluster
//   - failover the pd cluster
if err := c.pdMemberManager.Sync(tc); err != nil {
    return err
}

我們將在下篇文章中介紹組件的 Sync 函數完成了哪些工作,TiDBCluster Controller 是怎樣完成各個組件的生命週期管理。

小結

通過這篇文章,我們瞭解到 TiDB Operator 如何從 cmd/controller-manager/main.go 初始化運行和如何實現對應的 Controller 對象,並以 TidbCluster Controller 爲例介紹了 Controller 從初始化到實際工作的過程以及 Controller 內部的工作邏輯。通過上面的代碼運行邏輯的介紹,我們清楚了組件的生命週期控制循環是如何被觸發的,問題已經被縮小到如何細化這個控制循環,添加 TiDB 特殊的運維邏輯,使得 TiDB 能在 Kubernetes 上部署和正常運行,完成其他的生命週期操作。我們將在下一篇文章中討論如何細化這個控制循環,討論組件的控制循環的實現。

我們介紹了社區對於 Operator 模式的探索和演化。對於一些希望使用 Operator 模式開發資源管理系統的小夥伴,Kubernetes 社區中提供了 Kubebuilder 和 Operator Framework 兩個 Controller 腳手架項目。相比於參考 kubernetes/sample-controller 進行開發,Operator 腳手架基於 kubernetes-sigs/controller-runtime 生成 Controller 代碼,減少了許多重複引入的模板化的代碼。開發者只需要專注於完成 CRD 對象的控制循環部分即可,而不需要關心控制循環啓動之前的準備工作。

如果有什麼好的想法,歡迎通過 #sig-k8spingcap/tidb-operator 參與 TiDB Operator 社區交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章