實作 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 資源後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