歡迎光臨
每天分享高質量文章

容器化RDS:PersistentLocalVolumes和VolumeScheduling

容器化RDS系列文章:
資料庫的高可用方案非常依賴底層的儲存架構,這也是集中式儲存作為核心資料庫業務標配的原因之一。現在,越來越多的用戶開始在生產環境中使用基於資料庫層的複製技術來保障資料多副本和資料一致性,該架構使用戶可以使用“Local”儲存構建“Zero Data Lost”的高可用集群,而不依賴“Remote” 的集中式儲存。
“Local”和“Remote”隱含著Kube-Scheduler在調度時需要對Volume的“位置”可見。
本文嘗試從使用本地儲存的場景出發,分享“VolumeScheduling”在代碼中的具體實現和場景局限,以下的總結來自於(能力有限,不盡之處,不吝賜教):
  • https://github.com/kubernetes/community/pull/1054

  • https://github.com/kubernetes/community/pull/1140

  • https://github.com/kubernetes/community/pull/1105

  • Kubernetes 1.9和1.10部分代碼

本地捲

相比“Remote”的捲,本地捲:
  • 更好的利用本地高性能介質(SSD,Flash)提升資料庫服務能力 QPS/TPS(其實這個結論未必成立,後面會有贅述)

  • 更閉環的運維成本,現在越來越多的資料庫支持基於Replicated的技術實現資料多副本和資料一致性(比如MySQL Group Replication / MariaDB Galera Cluster / Percona XtraDB Cluster的),DBA可以處理所有問題,而不在依賴儲存工程師或者SA的支持。

MySQL Group Replication / MariaDB Galera Cluster / Percona XtraDB Cluster 方案詳見:《容器化RDS:計算儲存分離還是本地儲存?
在1.9之後,可以通過Feature Gate “PersistentLocalVolumes”使用本地捲。
apiVersion: v1
kind: PersistentVolume
metadata:
 name: local-pv
spec:
   capacity:
     storage: 10Gi
   accessModes:
   - ReadWriteOnce
   persistentVolumeReclaimPolicy: Delete
   storageClassName: local-storage
   local:
     path: /mnt/disks/ssd1
目前local.path可以是MountPoint或者是BlockDevice。這是我們要使用 MySQL Group Replication / MariaDB Galera Cluster / Percona XtraDB Cluster架構的基礎。
但還不夠,因為Scheduler並不感知捲的“位置”
這裡需要從PVC系結和Pod調度說起。

原有調度機制的問題

當申請匹配workload需求的資源時,可以簡單的把資源分為“計算資源”和“儲存資源”。
以Kubernetes申請Statefulset資源YAML為例:
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
 name: mysql-5.7
spec:
 replicas: 1
 template:
   metadata:
     name: mysql-5.7
   spec:
     containers:
       name: mysql
       resources:
         limits:
           cpu: 5300m
           memory: 5Gi
       volumeMounts:
       - mountPath: /var/lib/mysql
         name: data
 volumeClaimTemplates:
 - metadata:
     name: data
   spec:
     accessModes:
     - ReadWriteOnce
     resources:
       requests:
         storage: 10Gi
YAML中定義了Pod對“計算”和“儲存”資源的要求。隨後Statefulset Controller創建需要的PVC和Pod:
func (spc *realStatefulPodControl) CreateStatefulPod(set *apps.StatefulSet, pod *v1.Pod) error {
   // Create the Pod's PVCs prior to creating the Pod
   if err := spc.createPersistentVolumeClaims(set, pod); err != nil {
       spc.recordPodEvent("create", set, pod, err)
       return err
   }
   // If we created the PVCs attempt to create the Pod
   _, err := spc.client.CoreV1().Pods(set.Namespace).Create(pod)
   // sink already exists errors
   if apierrors.IsAlreadyExists(err) {
       return err
   }
   spc.recordPodEvent("create", set, pod, err)
   return err
}
PVC系結
Pod借用PVC描述需要的儲存資源,PVC是PV的抽象,就像VFS是Linux對具體檔案系統的抽象,所以在PVC創建之後,還需要將PVC跟捲進行系結,也即是PV。PersistentVolume Controller會遍歷現有PV和可以動態創建的StorageClass(譬如NFS、Ceph、EBS),找到滿足條件(訪問權限、容量等)進行系結。
大致流程如下:

Pod調度
Scheduler基於資源要求找到匹配的節點,以過濾和打分的方式選出“匹配度”最高的Node,流程大致如此:

Scheduler通用調度策略詳見:《容器化RDS:調度策略
問題
總結以上流程:
  • PVC系結在Pod調度之前,PersistentVolume Controller不會等待Scheduler調度結果,在Statefulset中PVC先於Pod創建,所以PVC/PV系結可能完成在Pod調度之前。

  • Scheduler不感知捲的“位置”,僅考慮儲存容量、訪問權限、儲存型別、還有第三方CloudProvider上的限制(譬如在AWS、GCE、Aure上使用Disk數量的限制)

當應用對捲的“位置”有要求,比如使用本地捲,可能出現Pod被Scheduler調度到NodeB,但PersistentVolume Controller系結了在NodeD上的本地捲,以致PV和Pod不在一個節點,如下圖所示:

不僅僅是本地捲,如果對儲存“位置”(譬如:Rack、Zone)有要求,都會有類似問題。

好比,Pod作為下屬,它實現自身價值的核心資源來自於兩個上級Scheduler和PersistentVolume Controller,但是這兩個上級從來不溝通,甚至出現矛盾的地方。作為下屬,要解決這個問題,無非如下幾種選擇:
  1. 嘗試讓兩個老闆溝通

  2. 站隊,挑一個老闆,只聽其中一個的指揮

  3. 辭職

Kubernetes做出了“正常人”的選擇:站隊。
如果Pod使用的Volume對“位置”有要求(又叫Topology-Aware Volume),通過延時系結(DelayBinding)使PersistentVolume Controller不再參與,PVC系結的工作全部由Scheduler完成。
在通過代碼瞭解特性“VolumeScheduling”的具體實現時,還可以先思考如下幾個問題:
  • 如何標記Topology-Aware Volume

  • 如何讓PersistentVolume Controller不再參與,同時不影響原有流程

Feature:VolumeScheduling

Kubernetes在捲管理中通過策略VolumeScheduling重構Scheduler以支持Topology-Aware Volume,步驟大致如下:
預分配使用本地捲的PV。
通過NodeAffinity方式標記Topology-Aware Volume和“位置”信息:
"volume.alpha.kubernetes.io/node-affinity": '{
           "requiredDuringSchedulingIgnoredDuringExecution": {
               "nodeSelectorTerms": [
                   { "matchExpressions": [
                       { "key": "kubernetes.io/hostname",
                         "operator": "In",
                         "values": ["Node1"]
                       }
                   ]}
                ]}
             }'
創建StorageClass,通過StorageClass間接標記PVC的系結需要延後(系結延時)。
標記該PVC需要延後到Node選擇出來之後再系結:
  • 創建StorageClass “X”(無需Provisioner),並設置StorageClass.VolumeBindingMode = VolumeBindingWaitForFirstConsumer

  • PVC.StorageClass設置為X

依照原有流程創建PVC和Pod,但對於需要延時系結的PVC,PersistentVolume Controller不再參與。
通過PVC.StorageClass,PersistentVolume Controller得知PVC是否需要延時系結。
return *class.VolumeBindingMode == storage.VolumeBindingWaitForFirstConsumer
如需延時系結,do nothing。
if claim.Spec.VolumeName == "" {
       // User did not care which PV they get.
       delayBinding, err := ctrl.shouldDelayBinding(claim)
                       ….
                       switch {
           case delayBinding:
                                   do nothing
  • 執行原有Predicates函式

  • 執行添加Predicate函式CheckVolumeBinding校驗候選Node是否滿足PV物理拓撲(主要邏輯由FindPodVolumes提供):

    已系結PVC:對應PV.NodeAffinity需匹配候選Node,否則該節點需要pass

    未系結PVC:該PVC是否需要延時系結,如需要,遍歷未系結PV,其NodeAffinity是否匹配候選Node,如滿足,記錄PVC和PV的映射關係到快取bindingInfo中,留待節點最終選出來之後進行最終的系結。

    以上都不滿足時 : PVC.StorageClass是否可以動態創建 Topology-Aware Volume(又叫 Topology-aware dynamic provisioning)

  • 執行原有Priorities函式

  • 執行添加Priority函式PrioritizeVolumes。Volume容量匹配越高越好,避免本地儲存資源浪費。

  • Scheduler選出Node

  • 由Scheduler進行API update,完成最終的PVC/PV系結(異步操作,時間具有不確定性,可能失敗)

  • 從快取bindingInfo中獲取候選Node上PVC和PV的系結關係,並通過API完成實際的系結

  • 如果需要StorageClass動態創建,被選出Node將被賦值給StorageClass.topologyKey,作為StorageClass創建Volume的拓撲約束,該功能的實現還在討論中。

  • 系結被調度Pod和Node

Scheduler的流程調整如下(依據Kubernetes 1.9代碼):

從代碼層面,對Controller Manager和Scheduler都有改造,如下圖所示(依據Kubernetes 1.9代碼):

舉個例子
先運行一個例子。
預分配的方式創建使用本地儲存的PV。
為了使用本地儲存需要啟動FeatureGate:PersistentLocalVolumes支持本地儲存,1.9是alpha版本,1.10是beta版,預設開啟。
apiVersion: v1
kind: PersistentVolume
metadata:
 name: local-pv
 annotations:
       "volume.alpha.kubernetes.io/node-affinity": '{
           "requiredDuringSchedulingIgnoredDuringExecution": {
               "nodeSelectorTerms": [
                   { "matchExpressions": [
                       { "key": "kubernetes.io/hostname",
                         "operator": "In",
                         "values": ["k8s-node1-product"]
                       }
                   ]}
                ]}
             }'
spec:
   capacity:
     storage: 10Gi
   accessModes:
   - ReadWriteOnce
   persistentVolumeReclaimPolicy: Delete
   storageClassName: local-storage
   local:
     path: /mnt/disks/ssd1
創建Storage Class。
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
 name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
創建使用本地儲存的Statefulset(僅列出關鍵信息)。
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
 name: mysql-5.7
spec:
 replicas: 1
 template:
   metadata:
     name: mysql-5.7
   spec:
     containers:
       name: mysql
       resources:
         limits:
           cpu: 5300m
           memory: 5Gi
       volumeMounts:
       - mountPath: /var/lib/mysql
         name: data
 volumeClaimTemplates:
 - metadata:
     annotations:
       volume.beta.kubernetes.io/storage-class: local-storage
     name: data
   spec:
     accessModes:
     - ReadWriteOnce
     resources:
       requests:
         storage: 10Gi
該Statefulset的Pod將會調度到k8s-node1-product,並使用本地儲存“local-pv”。

“PersistentLocalVolumes”和“VolumeScheduling”的局限

具體部署時。
使用局限需要考慮:
  • 資源利用率降低。一旦本地儲存使用完,即使CPU、Memory剩餘再多,該節點也無法提供服務;

  • 需要做好本地儲存規劃,譬如每個節點Volume的數量、容量等,就像原來使用儲存時需要把LUN規劃好一樣,在一個大規模運行的環境,存在落地難度。

高可用風險需要考慮:
當Pod調度到某個節點後,將會跟該節點產生親和,一旦Node發生故障,Pod不能調度到其他節點,只能等待該節點恢復,你能做的就是等待“Node恢復”,如果部署3節點MySQL集群,再掛一個Node,集群將無法提供服務,你能做的還是“等待Node恢復”。這麼設計也是合理的,社區認為該Node為Stateful節點,Pod被調度到其他可用Node會導致資料丟失。當然,你的老闆肯定不會聽這套解釋。
而且還要思考,初衷更好的利用本地高性能介質(SSD,Flash)提升資料庫服務能力QPS/TPS
真的成立嗎?
資料庫是IO延時敏感型應用,同時它也極度依賴系統的平衡性,基於Replicated架構的資料庫集群對集群網絡的要求會很高,一旦網路成為瓶頸影響到資料的sync replication,都會極大的影響資料庫集群服務能力。目前,Kubernetes的網絡解決方案還無法提供高吞吐,低延時的網絡能力。
當然,我們可以做點什麼解決“等待Node恢復”的問題。
以MySQL Group Replication / MariaDB Galera Cluster / Percona XtraDB Cluster架構為例,完全可以在這個基礎上,結合Kubernetes現有的Control-Plane做進一步優化。
  • Node不可用後,等待閾值超時,以確定Node無法恢復

  • 如確認Node不可恢復,刪除PVC,通過解除PVC和PV系結的方式,解除Pod和Node的系結

  • Scheduler將Pod調度到其他可用Node,PVC重新系結到可用Node的PV。

  • Operator查找MySQL最新備份,拷貝到新的PV

  • MySQL集群通過增量同步方式恢復實體資料

  • 增量同步變為實時同步,MySQL集群恢復

最後,借用Google工程師Kelsey Hightower的一句話:
“We very receptive this Kubernetes can’t be everything to everyone.”
Kubernetes並不是“鴻茅藥酒”包治百病,瞭解邊界是使用的開始,泛泛而談Cloud-Native無法獲得任何收益,在特定的場景和領域,很多問題還需要我們自己解決。最後說下個人理解,如無特別必要,儘量不選用Local Volume。
作者:熊中哲,沃趣科技產品及研發負責人。曾就職於阿裡巴巴和百度,超過10年關係型資料庫工作經驗,目前致力於將雲原生技術引入到關係型資料庫服務中。
Kubernetes入門與進階實戰培訓

本次培訓內容包括:Docker基礎、容器技術、Docker鏡像、資料共享與持久化、Docker三駕馬車、Docker實踐、Kubernetes基礎、Pod基礎與進階、常用物件操作、服務發現、Helm、Kubernetes核心組件原理分析、Kubernetes服務質量保證、調度詳解與應用場景、網絡、基於Kubernetes的CI/CD、基於Kubernetes的配置管理等,點擊瞭解具體培訓內容

5月11日正式上課,點擊閱讀原文鏈接即可報名。
赞(0)

分享創造快樂