天天看點

實作 Kubernetes 動态LocalVolume挂載本地磁盤實作 Kubernetes 動态LocalVolume挂載本地磁盤

實作 Kubernetes 動态LocalVolume挂載本地磁盤

前言

在 Kubernetes 體系中,存在大量的存儲插件用于實作基于網絡的存儲系統挂載,比如NFS、GFS、Ceph和雲廠商的雲盤裝置。但在某些使用者的環境中,可能無法或沒有必要搭建複雜的網絡存儲系統,需要一個更簡單的存儲方案。另外網絡存儲系統避免不了性能的損耗,然而對于一些分布式資料庫,其在應用層已經實作了資料同步和備援,在存儲層隻需要一個高性能的存儲方案。

在這些情況下如何實作Kubernetes 應用的資料持久化呢?

  • HostPath Volume

對 Kubernetes 有一定使用經驗的夥伴首先會想到HostPath Volume,這是一種可以直接挂載主控端磁盤的Volume實作,将容器中需要持久化的資料挂載到主控端上,它當然可以實作資料持久化。然而會有以下幾個問題:

(1)HostPath Volume與節點無關,意味着在多節點的叢集中,Pod的重新建立不會保障排程到原來的節點,這就意味着資料丢失。于是我們需要搭配設定排程屬性使Pod始終處于某一個節點,這在帶來配置複雜性的同時還破壞了Kubernetes的排程均衡度。

(2)HostPath Volume的資料不易管理,當Volume不需要使用時資料無法自動完成清理進而形成較多的磁盤浪費。

  • Local Persistent Volume

Local Persistent Volume 在 Kubernetes 1.14中完成GA。相對于HostPath Volume,Local Persistent Volume 首先考慮解決排程問題。使用了Local Persistent Volume 的Pod排程器将使其始終運作于同一個節點。使用者不需要在額外設定排程屬性。并且它在第一次排程時遵循其他排程算法,一定層面上保持了均衡度。

遺憾的是 Local Persistent Volume 預設不支援動态配置。在社群方案中有提供一個靜态PV配置器

sig-storage-local-static-provisioner

,其可以達成的效果是管理節點上的磁盤生命周期和PV的建立和回收。雖然可以實作PV的建立,但它的工作模式與正常的Provisioners,它不能根據PVC的需要動态提供PV,需要在節點上預先準備好磁盤和PV資源。

如何在此方案的基礎上進一步簡化,在節點上基于指定的資料目錄,實作動态的LocalVolume挂載呢?

技術方案

需要達成的效果如下:

(1)基于Local Persistent Volume 實作的基礎思路;

(2)實作各節點的資料目錄的管理;

(3)實作動态 PV 配置設定;

StorageClass定義

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rainbondslsc
provisioner: rainbond.io/provisioner-sslc
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer           

其中有一個關鍵性參數

volumeBindingMode

,該參數有兩個取值,分别是

Immediate

WaitForFirstConsumer

Immediate 模式下PVC與PV立即綁定,主要是不等待相關Pod排程完成,不關心其運作節點,直接完成綁定。相反的 WaitForFirstConsumer模式下需要等待Pod排程完成後進行PV綁定。是以PV建立時可以擷取到Pod的運作節點。

我們需要實作的 provisioner 工作在 WaitForFirstConsumer 模式下,在建立PV時擷取到Pod的運作節點,調用該節點的驅動服務建立磁盤路徑進行初始化,進而完成PV的建立。

Provisioner的實作

Provisioner分為兩個部分,一個是控制器部分,負責PV的建立和生命周期,另一部分是節點驅動服務,負責管理節點上的磁盤和資料。

PV控制器部分

控制器部分實作的代碼參考:

Rainbond 本地存儲控制器

控制器部分的主要邏輯是從 Kube-API 監聽 PersistentVolumeClaim 資源的變更,基于spec.storageClassName字段判斷資源是否應該由目前控制器管理。如果是走以下流程:

(1)基于PersistentVolumeClaim擷取到StorageClass資源,例如上面提到的rainbondslsc。

(2)基于StorageClass的provisioner值判定處理流程。

(3)從PersistentVolumeClaim資源中的Annotations配置 volume.kubernetes.io/selected-node 擷取PVC所屬Pod的運作節點。該值是由排程器設定的,這是一個關鍵資訊擷取。

if ctrl.kubeVersion.AtLeast(utilversion.MustParseSemantic("v1.11.0")) {
        // Get SelectedNode
        if nodeName, ok := claim.Annotations[annSelectedNode]; ok {
            selectedNode, err = ctrl.client.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) // TODO (verult) cache Nodes
            if err != nil {
                err = fmt.Errorf("failed to get target node: %v", err)
                ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, "ProvisioningFailed", err.Error())
                return err
            }
        }

        // Get AllowedTopologies
        allowedTopologies, err = ctrl.fetchAllowedTopologies(claimClass)
        if err != nil {
            err = fmt.Errorf("failed to get AllowedTopologies from StorageClass: %v", err)
            ctrl.eventRecorder.Event(claim, v1.EventTypeWarning, "ProvisioningFailed", err.Error())
            return err
        }
    }           

(4)調用節點服務建立對應存儲目錄或獨立磁盤。

path, err := p.createPath(options)
    if err != nil {
        if err == dao.ErrVolumeNotFound {
            return nil, err
        }
        return nil, fmt.Errorf("create local volume from node %s failure %s", options.SelectedNode.Name, err.Error())
    }
    if path == "" {
        return nil, fmt.Errorf("create local volume failure,local path is not create")
    }           

(5) 建立對應的PV資源。

pv := &v1.PersistentVolume{
        ObjectMeta: metav1.ObjectMeta{
            Name:   options.PVName,
            Labels: options.PVC.Labels,
        },
        Spec: v1.PersistentVolumeSpec{
            PersistentVolumeReclaimPolicy: options.PersistentVolumeReclaimPolicy,
            AccessModes:                   options.PVC.Spec.AccessModes,
            Capacity: v1.ResourceList{
                v1.ResourceName(v1.ResourceStorage): options.PVC.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)],
            },
            PersistentVolumeSource: v1.PersistentVolumeSource{
                HostPath: &v1.HostPathVolumeSource{
                    Path: path,
                },
            },
            MountOptions: options.MountOptions,
            NodeAffinity: &v1.VolumeNodeAffinity{
                Required: &v1.NodeSelector{
                    NodeSelectorTerms: []v1.NodeSelectorTerm{
                        {
                            MatchExpressions: []v1.NodeSelectorRequirement{
                                {
                                    Key:      "kubernetes.io/hostname",
                                    Operator: v1.NodeSelectorOpIn,
                                    Values:   []string{options.SelectedNode.Labels["kubernetes.io/hostname"]},
                                },
                            },
                        },
                    },
                },
            },
        },
    }           

其中關鍵性參數是設定PV的NodeAffinity參數,使其綁定在標明的節點。然後使用 HostPath 類型的PersistentVolumeSource指定挂載的路徑。

當PV資源删除時,根據PV綁定的節點進行磁盤資源的釋放:

nodeIP := func() string {
                    for _, me := range pv.Spec.NodeAffinity.Required.NodeSelectorTerms[0].MatchExpressions {
                        if me.Key != "kubernetes.io/hostname" {
                            continue
                        }
                        return me.Values[0]
                    }
                    return ""
                }()

                if nodeIP == "" {
                    logrus.Errorf("storage class: rainbondslsc; name: %s; node ip not found", pv.Name)
                    return
                }

                if err := deletePath(nodeIP, path); err != nil {
                    logrus.Errorf("delete path: %v", err)
                    return
                }           

節點驅動服務

節點驅動服務主要提供兩個API,配置設定磁盤空間和釋放磁盤空間。在實作上,簡化方案則是直接在指定路徑下建立子路徑和釋放子路徑。較詳細的方案可以像

一樣,實作對節點上儲存設備的管理,包括發現、初始化、配置設定、回收等等。

使用方式

Rainbond

中,使用者僅需指定挂載路徑和選擇本地存儲即可。

實作 Kubernetes 動态LocalVolume挂載本地磁盤實作 Kubernetes 動态LocalVolume挂載本地磁盤

對應的翻譯為 Kubernetes 資源後PVC配置如下:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    app_id: 6f67c68fc3ee493ea7d1705a17c0744b
    creater_id: "1614686043101141901"
    creator: Rainbond
    name: gr39f329
    service_alias: gr39f329
    service_id: deb5552806914dbc93646c7df839f329
    tenant_id: 3be96e95700a480c9b37c6ef5daf3566
    tenant_name: 2c9v614j
    version: "20210302192942"
    volume_name: log
  name: manual3432-gr39f329-0
  namespace: 3be96e95700a480c9b37c6ef5daf3566
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 500Gi
  storageClassName: rainbondslsc
  volumeMode: Filesystem           

總結

基于上訴的方案,我們可以自定義實作一個基礎的動态LocalVolume,适合于叢集中間件應用使用。這也是雲原生應用管理平台

中本地存儲的實作思路。在該項目中有較多的 Kubernetes 進階用法實踐封裝,研究源碼通路:

https://github.com/goodrain/rainbond

繼續閱讀