1. 程式人生 > >容器化RDS:PersistentLocalVolumes和VolumeScheduling_Kubernetes中文社群

容器化RDS:PersistentLocalVolumes和VolumeScheduling_Kubernetes中文社群

|  導語

資料庫的高可用方案非常依賴底層的儲存架構,這也是集中式儲存作為核心資料庫業務標配的原因之一。現在,越來越多的使用者開始在生產環境中使用基於資料庫層的複製技術來保障資料多副本和資料一致性,該架構使使用者可以使用“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年關係型資料庫工作經驗,目前致力於將雲原生技術引入到關係型資料庫服務中。