前言
在 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,分配磁盤空間和釋放磁盤空間。在實現上,簡化方案則是直接在指定路徑下創建子路徑和釋放子路徑。較詳細的方案可以像 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
storageClssName: rainbondslsc
volumeMode: Filesystem