在上一篇文章中我們討論了 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-k8s 或 pingcap/tidb-operator 參與 TiDB Operator 社區交流。