實現 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,該參數有兩個取值,分別是ImmediateWaitForFirstConsumer

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,分配磁盤空間和釋放磁盤空間。在實現上,簡化方案則是直接在指定路徑下創建子路徑和釋放子路徑。較詳細的方案可以像 sig-storage-local-static-provisioner 一樣,實現對節點上存儲設備的管理,包括髮現、初始化、分配、回收等等。

使用方式

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

對應的翻譯爲 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,適合於集羣中間件應用使用。這也是雲原生應用管理平臺 Rainbond 中本地存儲的實現思路。在該項目中有較多的 Kubernetes 高級用法實踐封裝,研究源碼訪問:https://github.com/goodrain/rainbond

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