TiDB Operator 源碼閱讀 (三) 編排組件控制循環

上篇文章中,我們介紹了 TiDB Operator 的 Controller Manager 的設計和實現,瞭解了各個 Controller 如何接受和處理變更。在這篇文章中,我們將討論組件的 Controller 的實現。TiDBCluster Controller 負責了 TiDB 主要組件的生命週期管理,我們將以此爲例, 介紹組件控制循環的編排設計。我們將會了解到完成 TiDB 集羣的生命週期管理過程中,各種控制循環事件經過了怎樣的編排,這些事件中又完成了哪些資源管理操作。在閱讀時,大家瞭解這些工作的大致過程和定義即可,我們將在下一篇文章中具體介紹各個組件如何套用下面的範式。

組件控制循環的調用

在上一篇文章的代碼介紹中,我們提到了 TiDBCluster controllerupdateTidbCluster 函數,位於 pkg/controller/tidbcluster/tidb_cluster_control.go,它是 TiDB 組件生命週期管理的入口,調用了一系列生命週期管理函數。略去註釋,我們可以發現 updateTidbCluster 函數依次調用了以下函數:

  1. c.reclaimPolicyManager.Sync(tc)

  2. c.orphanPodsCleaner.Clean(tc)

  3. c.discoveryManager.Reconcile(tc)

  4. c.ticdcMemberManager.Sync(tc)

  5. c.tiflashMemberManager.Sync(tc)

  6. c.pdMemberManager.Sync(tc)

  7. c.tikvMemberManager.Sync(tc)

  8. c.pumpMemberManager.Sync(tc)

  9. c.tidbMemberManager.Sync(tc)

  10. c.metaManager.Sync(tc)

  11. c.pvcCleaner.Clean(tc)

  12. c.pvcResizer.Resize(tc)

  13. c.tidbClusterStatusManager.Sync(tc)

這些函數可以分爲兩類,一是 TiDB 組件的視角組織的控制循環實現,例如 PD,TiDB,TiKV,TiFlash,TiCDC,Pump,Discovery,另外一類是負責管理 TiDB 組件所使用的 Kubernetes 資源的管理以及其他組件外圍的生命週期管理操作,例如 PV 的 ReclaimPolicy 的維護,OrphanPod 的清理,Kubernetes 資源的 Meta 信息維護,PVC 的清理和擴容,TiDBCluster 對象的狀態管理等。

TiDB 組件的生命週期管理過程

TiDB 的主要組件控制循環的代碼分佈在 pkg/manager/member 目錄下以 _member_manager.go 結尾的文件下,比如 pd_member_manager.go,這些文件又引用了諸如 _scaler.go_upgrader.go 的文件,這些文件包含了擴縮容和升級相關功能的實現。從各個組件的 _member_manager.go 相關文件,我們可以提煉出以下通用實現:

// Sync Service
if err := m.syncServiceForTidbCluster(tc); err != nil {
    return err
}
 
// Sync Headless Service
if err := m.syncHeadlessServiceForTidbCluster(tc); err != nil {
    return err
}
 
// Sync StatefulSet
return syncStatefulSetForTidbCluster(tc)
 
func syncStatefulSetForTidbCluster(tc *v1alpha1.TidbCluster) error {
    if err := m.syncTidbClusterStatus(tc, oldSet); err != nil {
        klog.Errorf("failed to sync TidbCluster: [%s/%s]'s status, error: %v", ns, tcName, err)
    }
 
    if tc.Spec.Paused {
        klog.V(4).Infof("tidb cluster %s/%s is paused, skip syncing for statefulset", tc.GetNamespace(), tc.GetName())
        return nil
    }
 
    cm, err := m.syncConfigMap(tc, oldSet)
 
    newSet, err := getnewSetForTidbCluster(tc, cm)
 
    if err := m.scaler.Scale(tc, oldSet, newSet); err != nil {
        return err
    }
 
    if err := m.failover.Failover(tc); err != nil {
        return err
    }
 
    if err := m.upgrader.Upgrade(tc, oldSet, newSet); err != nil {
        return err
    }
 
    return UpdateStatefulSet(m.deps.StatefulSetControl, tc, newSet, oldSet)
}

這段代碼主要完成了同步 Service 和 同步 Statefulset 的工作,同步 Service 主要是爲組件創建或同步 Service 資源,同步 Statefulset 具體包含了一下工作:

  1. 同步組件的 Status;

  2. 檢查 TiDBCluster 是否停止暫停了同步;

  3. 同步 ConfigMap;

  4. 根據 TidbCluster 配置生成新的 Statefulset,並且對新 Statefulset 進行滾動更新,擴縮容,Failover 相關邏輯的處理;

  5. 創建或者更新 Statefulset;

組件的控制循環是對上面幾項工作循環執行,使得組件保持最新狀態。下面將介紹 TiDB Operator 中這幾項工作具體需要完成哪些方面的工作。

同步 Service

一般 Service 的 Reconcile 在組件 Reconcile 開始部分,它負責創建和同步組件所使用的 Service,例如 cluster1-pdcluster1-pd-peer。在控制循環函數中,會調用 getNewServiceForTidbCluster 函數,通過 Tidbcluster CR 中記錄的信息創建一個新的 Service 的模板,如果 Service 不存在,直接創建 Service,否則,通過比對新老 Service Spec 是否一致,來決定是否要更新 Service 對象。

TiDB 組件使用的 Service 中,包括了 Service 和 Headless Serivce,爲組件提供了被訪問的能力。當組件不需要知道是哪個實例正在與它通信,並且可以接受負載均衡方式的訪問,則可以使用 Service 服務,例如 TiKV,TiDB 等組件訪問 PD 時,就可以使用 Service 地址。當組件需要區分是那個 Pod 在提供服務時,則需要用 Pod DNS 進行通信,例如 TiKV 在啓動時,會將自己的 Pod DNS 作爲 Advertise Address 對外暴露,其他 Pod 可以通過這個 Pod DNS 訪問到自己。

同步 Status

完成 Service 的同步後,組件接入了集羣的網絡,可以在集羣內訪問和被訪問。控制循環會進入 syncStatefulSetForTidbCluster,開始對 Statefulset 進行 Reconcile,首先是使用 syncTidbClusterStatus 對組件的 Status 信息進行同步,後續的擴縮容、Failover、升級等操作會依賴 Status 中的信息進行決策。

同步 Status 是 TiDB Operator 比較關鍵的操作,它需要同步 Kubernetes 各個組件的信息和 TiDB 的集羣內部信息,例如在 Kubernetes 方面,在這一操作中會同步集羣的副本數量,更新狀態,鏡像版本等信息,檢查 Statefulset 是否處於更新狀態。在 TiDB 集羣信息方面,TiDB Operator 還需要將 TiDB 集羣內部的信息從 PD 中同步下來。例如 PD 的成員信息,TiKV 的存儲信息,TiDB 的成員信息等,TiDB 集羣的健康檢查的操作便是在更新 Status 這一操作內完成。

檢查 TiDBCluster 是否暫停同步

更新完狀態後,會通過 tc.Spec.Paused 判斷集羣是否處於暫停同步狀態,如果暫停同步,則會跳過下面更新 Statefulset 的操作。

同步 ConfigMap

在同步完 Status 之後,syncConfigMap 函數會更新 ConfigMap,ConfigMap 一般包括了組件的配置文件和啓動腳本。配置文件是通過 YAML 中 Spec 的 Config 項提取而來,目前支持 TOML 透傳和 YAML 轉換,並且推薦 TOML 格式。啓動腳本則包含了獲取組件所需的啓動參數,然後用獲取好的啓動參數啓動組件進程。在 TiDB Operator 中,當組件啓動時需要向 TiDB Operator 獲取啓動參數時,TiDB Operator 側的信息處理會放到 discovery 組件完成。如 PD 獲取參數用於決定初始化還是加入某個節點,就會使用 wget 訪問 discovery 拿到自己需要的參數。這種在啓動腳本中獲取參數的方法,避免了更新 Statefulset 過程中引起的非預期滾動更新,對線上業務造成影響。

生成新 Statefulset

getNewPDSetForTidbCluster 函數會得到一個新的 Statefulset 的模板,它包含了對剛纔生成的 Service,ConfigMap 的引用,並且根據最新的 Status 和 Spec 生成其他項,例如 env,container,volume 等,這個新的 Statefulset 還需要送到下面 3 個流程進行滾動更新,擴縮容,Failover 方面的加工,最後將這個新生成的 Statefulset 會被送到 UpdateStatefulSet 函數處理,判斷其是否需要實際更新到組件對應的 Statefulset。

新 Statefulset 的加工(一): 滾動更新

m.upgrader.Upgrade 函數負責滾動更新相關操作,主要更新 Statefulset 中 UpgradeStrategy.TypeUpgradeStrategy.Partition,滾動更新是藉助 Statefulset 的 RollingUpdate 策略實現的。組件 Reconcile 會設置 Statefulset 的升級策略爲滾動更新,即組件 Statefulset 的 UpgradeStrategy.Type 設置爲 RollingUpdate 。在 Kubernetes 的 Statefulset 使用中,可以通過配置 UpgradeStrategy.Partition 控制滾動更新的進度,即 Statefulset 只會更新序號大於或等於 partition 的值,並且未被更新的 Pod。通過這一機制就可以實現每個 Pod 在正常對外服務後才繼續滾動更新。在非升級狀態或者升級的啓動階段,組件的 Reconcile 會將 Statefulset 的 UpgradeStrategy.Partition 設置爲 Statefulset 中最大的 Pod 序號,防止有 Pod 更新。在開始更新後,當一個 Pod 更新,並且重啓後對外提供服務,例如 TiKV 的 Store 狀態變爲 Up,TiDB 的 Member 狀態爲 healthy,滿足這樣的條件的 Pod 才被視爲升級成功,然後調小 Partition 的值進行下一 Pod 的更新。

新 Statefulset 的加工(二): 擴縮容

m.scaler.Scale 函數負責擴縮容相關操作,主要是更新 Statefulset 中組件的 Replicas。Scale 遵循逐個擴縮容的原則,每次擴縮容的跨度爲 1。Scale 函數會將 TiDBCluster 中指定的組件 Replicas 數,如 tc.Spec.PD.Replicas,與現有比較,先判斷是擴容需求還是縮容需求,然後完成一個步長的擴縮容的操作,再進入下一次組件 Reconcile,通過多次 Reconcile 完成所有的擴縮容需求。在擴縮容的過程中,會涉及到 PD 轉移 Leader,TiKV 刪除 Store 等使用 PD API 的操作,組件 Reconcile 過程中會使用 PD API 完成上述操作,並且判斷操作是否成功,再逐步進行下一次擴縮容。

新 Statefulset 的加工(三): Failover

m.failover.Failover 函數負責容災相關的操作,包括髮現和記錄災難狀態,恢復災難狀態等,在部署 TiDB Operator 時配置打開 AutoFailover 後,當發現有 Failure 的組件時記錄相關信息到 FailureStores 或者 FailureMembers 這樣的狀態存儲的鍵值,並啓動一個新的組件 Pod 用於承擔這個 Pod 的工作負載。當原 Pod 恢復工作後,通過修改 Statefulset 的 Replicas 數量,將用於容災時分擔工作負載的 Pod 進行縮容操作。但是在 TiKV/TiFlash 的容災邏輯中,自動縮容容災過程中的 Pod 不是默認操作,需要設置 spec.tikv.recoverFailover: true 纔會對新啓動的 Pod 縮容。

使用新 Statefulset 進行更新

在同步 Statefulset 的最後階段,已經完成了新 Statefulset 的生成,這時候會進入 UpdateStatefulSet 函數,這一函數中主要比對新的 Statefulset 和現有 StatefulSet 差異,如果不一致,則進行 Statefulset 的實際更新。另外,這一函數還需要檢查是否有沒有被管理的 Statefulset,這部分主要是舊版本使用 Helm Chart 部署的 TiDB,需要將這些 Statefulset 納入 TiDB Operator 的管理,給他們添加依賴標記。

完成上述操作後,TiDBCluster CR 的 Status 更新到最新,相關 Service,ConfigMap 被創建,生成了新的 Statefulset,並且滿足了滾動更新,擴縮容,Failover 的工作。組件的 Reconcile 週而復始,監控着組件的生命週期狀態,響應生命週期狀態改變和用戶輸入改變,使得集羣在符合預期的狀態下正常運行。

其他生命週期維護工作

除了 TiDB 主要組件的視角之外,還有一些運維操作被編排到了下面的函數實現中,他們分別負責了以下工作:

  1. Discovery,用於 PD 啓動參數的配置和 TiDB Dashboard Proxy,Discovery 的存在,可以提供一些動態信息供組件索取,避免了修改 ConfigMap 導致 Pod 滾動更新。

  2. Reclaim PolicyManager,用於同步 tc.Spec.PVReclaimPolicy 的配置,默認配置下會將PV 的 Reclaim Policy 設置爲 Retain,降低數據丟失的風險。

  3. OrphanPodCleaner,用於在 PVC 創建失敗的時候清除 Pod,讓 Statefulset Controller 重試 Pod 和對應 PVC 的創建。

  4. PVC Cleaner 用於 PVC 相關資源清理,清理被標記可以刪除的 PVC。

  5. PVC Resizer 用於 PVC 的擴容,在雲上使用時可以通過修改 TidbCluster 中的 Storage 相關配置修改 PVC 的大小。

  6. Meta Manager 用於同步 StoreIDLabel,MemberIDLabel,NamespaceLabel 等信息到 Pod,PVC,PV 的 label 上。

  7. TiDBCluster Status Manager 用於同步 TidbMonitor 和 TiDB Dashboard 相關信息。

小結

這篇文章介紹了 TiDBCluster 組件的控制循環邏輯的設計,試圖讓大家瞭解,當 TiDBCluster Controller 循環觸發各個組件的 Reconcile 函數時,組件 Reconcile 函數是按照怎樣的流程巡檢組件的相關資源,用戶預期的狀態,是如何通過 Reconcile 函數,變成實際運行的組件。TiDB Operator 中的控制循環都大致符合以上的設計邏輯,在後面的文章中,我們會具體介紹每個組件是如何套用上面的設計邏輯,實現組件的生命週期管理。

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

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