作者:鄧栓
來源:細說雲端計算
作為一款定位在 Cloud-native 的資料庫,現如今 TiDB 在雲整合上已取得了階段性的進展。日前 Cloud TiDB 產品在 UCloud 平臺正式開啟公測,TiDB 彈性伸縮的特性在 Cloud 提供的基礎設施支援下發揮的淋漓盡致。在感受雲資料庫魅力的同時,讓我們來一探究竟,看一下 TiDB 與 Cloud 背後的技術祕密。
TiDB 的架構
首先還是要先從 TiDB 的架構說起,TiDB 和傳統的單機關係型資料庫有什麼不同?相信長期以來一直關注 TiDB 的同學都比較瞭解了,但這裡還是科普一下。TiDB 作為一個開源的分散式資料庫產品,具有多副本強一致性的同時能夠根據業務需求非常方便的進行彈性伸縮,並且擴縮容期間對上層業務無感知。TiDB 的主體架構包含三個模組,對應 GitHub 上面 PingCAP 組織下的三個開源專案,TiDB / TiKV / PD:
- TiDB 主要是負責 SQL 的解析器和優化器,它相當於計算執行層,同時也負責客戶端接入和互動;
- TiKV 是一套分散式的 Key-Value 儲存引擎,它承擔整個資料庫的儲存層,資料的水平擴充套件和多副本高可用特性都是在這一層實現;
- PD 相當於分散式資料庫的大腦,一方面負責收集和維護資料在各個 TiKV 節點的分佈情況,另一方面 PD 承擔排程器的角色,根據資料分佈狀況以及各個儲存節點的負載來採取合適的排程策略,維持整個系統的平衡與穩定。
上面的這三個模組,每個角色都是一個多節點組成的叢集,所以最終 TiDB 的架構看起來是這樣的。
由此可見,分散式系統本身的複雜性導致手工部署和運維的成本是比較高的,並且容易出錯。傳統的自動化部署運維工具如 Puppet / Chef / SaltStack / Ansible 等,由於缺乏狀態管理,在節點出現問題時不能及時自動完成故障轉移,需要運維人員人工干預。有些則需要寫大量的 DSL 甚至與 Shell 指令碼一起混合使用,可移植性較差,維護成本比較高。
TiDB 與 Kubernetes 的整合歷程
在雲時代,容器成為應用分發部署的基本單位,而谷歌基於內部使用數十年的容器編排系統 Borg 經驗推出的開源容器編排系統 Kubernetes 成為當前容器編排技術的主流。作為 Cloud Native Database,TiDB 選擇擁抱容器技術,並與 Kubernetes 進行深度整合,使其可以非常方便地基於 Kubernetes 完成資料庫的自動化管理。
Kubernetes 專案可以說是為 Cloud 而生,利用雲平臺的 IaaS 層提供的 API 可以很方便的和雲進行整合。這樣我們要做的事情就很明確了,只要讓 TiDB 與 Kubernetes 結合的更好,進而就實現了和各個雲平臺的整合, 使得 TiDB 在雲上的快速部署和高效運維成為現實。
Kubernetes 最早是作為一個純粹的容器編排系統而誕生的,使用者部署好 Kubernetes 叢集之後,直接使用其內建的各種功能部署應用服務。由於這個 PaaS 平臺使用起來非常便利,吸引了很多使用者,不同使用者也提出了各種不同的需求,有些特性需求 Kubernetes 直接在其核心程式碼裡面實現了,但是有些特性並不適合合併到主幹分支,為滿足這類需求,Kubernetes 開放出一些 API 供使用者自己擴充套件,實現自己的需求。當前 Kubernetes 已經發展到 v1.8,其內部的 API 變得越來越開放,使其更像是一個跑在雲上的作業系統。使用者可以把它當作一套雲的 SDK 或 Framework 來使用,而且可以很方便地開發元件來擴充套件滿足自己的業務需求。對有狀態服務的支援就是一個很有代表性的例子。
Kubernetes 專案最早期只支援無狀態服務 (Stateless Service) 來管理的,無狀態服務通過 ReplicationController 定義多個副本,由 Kubernetes 排程器來決定在不同節點上啟動多個 Pod,實現負載均衡和故障轉移。對於無狀態服務,多個副本對應的 Pod 是等價的,所以在節點出現故障時,在新節點上啟動一個 Pod 與失效的 Pod 是等價的,不會涉及狀態遷移問題,因而管理非常簡單。但是對於有狀態服務 (Stateful Service),由於需要將資料持久化到磁碟,使得不同 Pod 之間不能再認為成等價,也就不能再像無狀態服務那樣隨意進行排程遷移。Kubernetes v1.3 版本提出 PetSet 的概念用來管理有狀態服務並於 v1.5 將其更名為 StatefulSet。StatefulSet 明確定義一組 Pod 中每個的身份,啟動和升級都按特定順序來操作。另外使用持久化卷儲存 (PersistentVolume) 來作為儲存資料的載體,當節點失效 Pod 需要遷移時,對應的 PV 也會重新掛載,而 PV 的底層依託於分散式檔案系統,所以 Pod 仍然能訪問到之前的資料。同時 Pod 在發生遷移時,其網路身份例如 IP 地址是會發生變化的,很多分散式系統不能接受這種情況。所以 StatefulSet 在遷移 Pod 時可以通過繫結域名的方式來保證 Pod 在叢集中網路身份不發生變化。
然而現實中一些分散式系統更為複雜,StatefulSet 也顯得捉襟見肘。舉例來說,某些分散式系統的節點在加入叢集或下線時還需要做些額外的註冊和清理操作,或者滾動升級要考量版本相容性等。基於這個原因 CoreOS 公司提出了 Operator 概念,並實現了 etcd-operator 和 prometheus-operator 來管理 Etcd 和 Prometheus 這樣的複雜分散式系統。使用者可以開發自己的 Operator,在 Kubernetes 之上實現自定義的 Controller,將有狀態服務的領域特定的運維知識編碼進去,從而實現對特定分散式系統的管理。同時 Operator 本身也是跑在 Kubernetes 中的一組 Pod(deployment),對 Kubernetes 系統並無侵入性。
TiDB 系列元件及其作用
針對 TiDB 這種複雜的分散式服務,我們開發了 tidb-operator 等一系列元件,來管理 TiDB 叢集例項在 Kubernetes 平臺上的建立、銷燬、擴縮容、滾動升級和故障轉移等運維操作。同時在上層封裝一個 tidb-cloud-manager 元件,提供 RESTful 介面,實現與雲平臺的控制檯打通。這樣也就實現了一個 DBaaS (資料庫即服務)架構的基本形態。
由於 TiDB 對磁碟 I/O 有比較高的要求,通過 PV 掛載網路盤效能上會有明顯的效能損耗。另外 TiKV 本身維護了資料多副本,這點和分散式檔案系統的多副本是有重複的。所以我們要給 Pod 上掛載本地磁碟,並且在 Kubernetes 上面把 Local PV 管理起來,作為一種特定的資源來維護。Kubernetes 長期以來官方一直沒有提供 Local PV 支援,本地儲存只支援 hostPath 和 emptyDir 兩種方式。其中 hostPath 的生命週期是脫離 Kubernetes 管理的,使用 hostPath 的 Pod 銷燬後,裡面的資料是不會被自動清理,下次再掛載 Pod 就會造成髒資料。而 emptyDir 更像一個臨時磁碟,在 Pod 重建時會被清理重置,不能成為持久化 PV 來使用。為此我們開發了一個 tidb-volume-manager 元件,用於管理 Kubernetes 叢集中每臺物理主機上的本地磁碟,並且將其暴露成一種特殊的 PV 資源。結合 Operator 在部署 TiDB 節點時會參考 Local PV 資源的情況來選擇特定的節點來部署,分配一個空的 Local PV 和 Pod 繫結。而當 Pod 銷燬時候會根據具體情況來決定是否結束 Local PV 的生命週期,釋放掉的 Local PV 再經歷一個 gc 週期後,被 tidb-volume-manager 回收,清理其盤上資料等待再次被分配使用。
將這些元件整合起來,就形成了上圖描述了 Cloud TiDB 的總體架構,在 Kubenetes 管理的叢集之上通過 tidb-operator 等元件來針對性的調配和使用叢集資源,從而實現 TiDB 叢集例項的生命週期管理。通過這種方式,來實現 TiDB 分散式資料庫和雲平臺的整合。接下來,我們再針對 Cloud TiDB 的關鍵特性和實現細節分別進行解讀。
自動化運維
資料庫產品上雲的一個先決條件是能實現自動化的運維管理,否則在雲上靠手工運維幾乎是不現實的。我們首先用 Kubernetes 將雲平臺的主機資源管理起來,組成一個大的資源池。然後再通過 tidb-opeartor 及 tidb-cloud-manager 等元件來自動化完成 TiDB 例項的一鍵部署、擴容縮容、線上滾動升級、自動故障轉移等運維操作。
首先拿叢集建立來說。前面提到過,TiDB 包含三大核心元件:TiDB / TiKV / PD,每個服務又都是一個多節點的分散式結構。服務和服務之間的啟動順序也存在依賴關係。此外,PD 節點的建立和加入叢集方式和 etcd 類似,是需要先建立一個單節點的 initial 叢集,後面加入的節點需要用特殊的 join 方式,啟動命令上都有差別。有一些操作完成後還需要呼叫 API 進行通知。Kubernetes 自身提供的 StatefulSet 是很難應付這種複雜的部署,所以需要 tidb-operator 中實現特定的 Controller 來完成這樣一系列的操作。並且結合 Kubernetese 強大的排程功能,合理的規劃和分配整個叢集資源,儘量讓新部署的 TiDB 例項節點在叢集中均勻分佈,最終通過 LB 暴露給對應的租戶使用。
線上升級也是類似。由於 TiKV / PD 的 Pod 掛載的是本地儲存,並不能像雲平臺提供的塊儲存或網路檔案系統那樣可以隨意掛載。如果 TiKV / PD 遷移到其它節點,相當於資料目錄也被清空,所以必須保證 TiKV / PD 的 Pod 在升級完成後仍然能夠排程在原地,這也是要由 tidb-operator 的 Controller 來保證。TiDB 的資料副本之間由 Raft 演算法來保證一致性,因此當叢集中某一個節點暫時斷開可以不影響整個服務的。所以在叢集升級的過程中,必須嚴格按照服務的依賴關係,再依次對 Pod 進行升級。
當節點出現故障時,同樣是由於掛載本地資料盤的原因,也不能像 StatefulSet 那樣直接把 Pod 遷移走。當 TiDB Operator 檢測到節點失效,首先要等一定的時間確認節點不會再恢復了,開始遷移恢復的操作。首先排程選擇一個新節點啟動一個 Pod, 然後通知 TiDB 將失效的節點放棄掉,並將新啟的 Pod 加入叢集。後面會由 TiDB 的 PD 模組來完成資料副本數的恢復,以及資料往新節點上進行搬移,從而重新維持叢集內資料平衡。
以上只是列舉了 TiDB 幾種典型的運維操作流程,實際生產上運維還有很多 case 需要考慮,這些都以程式的方式實現在 tidb-operator 裡面。藉助 Kubernetes 和 tidb-operator 來代替人工,高效的完成 TiDB 資料庫在雲平臺上的複雜運維管理。
動態擴縮容
彈性水平伸縮是 TiDB 資料庫最主要的特性之一。在大資料時代,人們對資料儲存的需求在快速膨脹。有時候使用者很難預估自己的業務規模的增長速度,如果採用傳統的儲存方案,可能很快發現儲存容量達到了瓶頸,然後不得不停機來做遷移和完成擴容。如果使用 Cloud TiDB 的方案,這個過程就非常簡單,只需要在 Cloud 控制檯上修改一下 TiDB 的節點數量,很快就能完成擴容操作,期間還不會影響業務的正常服務。
那麼在 Cloud 後臺,同樣藉助 Kubernetes 和 tidb-operator 的能力來完成 TiDB 增減節點操作。Kubernetes 本身的運作是基於一種 Reconcile 的機制。簡單來說當使用者提交一個新的請求,比如期望叢集裡面跑 5 個 TiKV 節點,而目前正在跑的只有 3 個,那麼 Reconcile 機制就會發現這個差異,首先由 Kubernetes 的排程器根據叢集整體資源情況,並結合 TiDB 節點分配的親和性原則和資源隔離原則來分配節點。另外很重要一點就是選擇有空閒 Local PV 的機器來建立 Pod 並進行掛載。最終通過 tidb-operator 將 2 個節點加入 TiDB 叢集。
對於縮容的過程也是類似。假如資料庫儲存的總資料量變少,需要減少節點以節省成本。首先使用者通過雲控制檯向後端提交請求,在一個 Reconciling 週期內發現差異,tidb-operator 的 Controller 開始通知 TiDB 叢集執行節點下線的操作。安全下線可能是個比較長的過程,因為期間需要由 PD 模組將下線節點的資料搬移到其他節點,期間叢集都可以正常服務。當下線完成,這些 TiKV 變成 tombstone 狀態。而 tidb-operator 也會通知 Kubernetes 銷燬這些 Pod,並且由 tidb-volume-manager 來回收 Local PV。
資源隔離
資源隔離也是雲上使用者關心的一個問題。尤其是資料庫這類應用,不同租戶的資料庫例項,甚至一個租戶的多套資料庫例項,都跑在一套大的 Kubernetes 管理的叢集上,相互間會不會有資源的爭搶問題,某個例項執行高負載的計算任務時,CPU、記憶體、I/O 等會不會對同臺機器上部署的其他例項產生影響。其實容器本身就是資源隔離的一個解決方案,容器的底層是 Linux 核心提供的 cgroups 技術,用於限制容器內的 CPU、記憶體以及 IO 等資源的使用,並通過 namespace 技術實現隔離。而 Kubernetes 作為容器編排系統,能夠根據叢集中各個節點的資源狀況,選擇最優的策略來排程容器。同時 tidb-operator 會根據 TiDB 自身的特性和約束,來綜合決策 TiDB 節點的排程分配。舉例來說,當一個 Kubernetes 叢集橫跨多個可用區,使用者申請建立一個 TiDB 叢集,那麼首先根據高可用性原則,將儲存節點儘量分配到不同的可用區,並給 TiKV 打上 label。那麼同一個可用區內也儘量不把多個 TiKV 部署到相同的物理節點上,以保證叢集資源最大化利用。此外,每個 Local PV 也是一塊獨立的磁碟,每個 TiKV 的 Pod 分別掛載不同的盤,所以 I/O 上也是完全隔離的。Kubernetes 還可以配置 Pod 之間的親和性(affinity)和反親和性(anti-affinity),例如 TiKV 和 TiDB 之間我們可以通過親和性使其排程到網路延時較小的節點之上,提高網路傳輸效率,TiKV 之間藉助反親和性,使其分散部署到不同的主機、機架和可用區上,降低因硬體或機房故障造成的丟資料的風險。
上面解釋了容器層面的隔離,可以看作是物理層面的隔離。那麼資料層面的隔離,TiDB 的排程體系也是有所考慮的。比如一個大的 TiDB 叢集,節點分佈在很多臺主機,跨越多個機架、可用區。那麼使用者可以定義 Namespace,這是一個邏輯概念,不同業務的資料庫和表放置在不同的 Namespace。再通過配置 Namespace 和 TiKV 節點以及區域的對應關係,由 PD 模組來進行排程,從而實現不同業務的資料在物理上的隔離。
高可用性
TiDB 作為一個分散式資料庫本身就具有高可用性,每個核心元件都可以獨立的擴縮容,任意一個模組在部署多份副本時如果有一個掛掉,整體仍然可以正常對外提供服務,這是由 Raft 協議保證的。但是如果對資料庫節點的排程不加任何限制,包含一份資料的多個副本的節點可能會被排程到同一臺主機。這時如果主機發生故障,就會同時失去多個副本,一個 Raft 分組內在失去多數派節點就會使整個叢集處於不可用的狀態。因此 tidb-operator 在排程 TiKV 節點時需要避免出現這種情況。
另外 TiDB 支援基於 label 的資料排程的,給不同的 TiKV 例項加上描述物理資訊的 label,例如地域(Region)、可用區(AZ)、機架(Rack)、主機(Host),這樣 PD 在對資料進行排程時就會參考這些資訊更加智慧的制定排程策略,盡最大可能保證資料的可用性。例如 PD 會基於 label 資訊儘量把相同資料的副本分散排程到不同的主機、機架、可用區、地域上,這樣在物理節點掛掉或機架掉電或機房出故障時,其它地方仍然有該資料足夠的副本數。藉助 tidb-operator 中 controller-manager 元件我們可以自動給 TiKV 例項加上物理拓撲位置標籤,充分發揮 PD 對資料的智慧排程能力,實現資料層面的高可用性。
同時我們還可以實現例項級別的高可用性,通過 Kubernetes 強大的排程規則和我們擴充套件的排程器,我們按優先順序會盡量選擇讓 TiKV 部署到不同的主機、機架和可用區上,把因主機、機架、機房出問題造成的影響降到最低,使資料具有最大的高可用性。
另外執行在 Kubernetes 之上我們能實時監測到 TiDB 各元件的執行情況,當出現問題時,我們也能第一時間讓 tidb-operator 對叢集進行自動修復 (self-healing)。具體表現為 TiDB / TiKV / PD 例項出現故障時,執行安全的下線操作。同時增加新的例項,來保證叢集的規模和之前一致。
總結
TiDB 作為一款 Cloud Native Database,通過 tidb-operator 的方式充分發揮 Kubernetes 平臺的強大能力,實現雲上自動化管理,極大降低人力運維成本。使用者可以根據業務需要進行動態擴容縮容,多租戶隔離特性讓不同租戶的例項可以共享計算和儲存資源,互不干擾,同時最大程度充分使用雲上資源。Raft 演算法和 tidb-operator 自動修復能力以及兩層排程機制保證了 Cloud TiDB 的高可用性。UCloud 和 PingCAP 公司深度合作,推出 Cloud TiDB 產品現已開啟公測,歡迎大家來體驗雲時代的新一代資料庫。