Kubernetes 跨 StorageClass 遷移 Persistent Volumes 完全指南

KubeSphere發表於2022-06-25

大家好,我是米開朗基楊。

KubeSphere 3.3.0 (不出意外的話~)本週就要 GA 了,作為一名 KubeSphere 腦殘粉,我迫不及待地先安裝 RC 版嚐嚐鮮,一頓操作猛如虎開啟所有元件,裝完之後發現有點尷尬:我用錯了持久化儲存。

我的 K8s 叢集中有兩個儲存類(StorageClass),一個是 OpenEBS 提供的本地儲存,另一個是 QingCloud CSI 提供的分散式儲存,而且預設的 StorageClass 是 OpenEBS 提供的 local-hostpath,所以 KubeSphere 的有狀態元件預設便使用本地儲存來儲存資料。

失誤失誤,我本來是想用分散式儲存作為預設儲存的,但是我忘記將 csi-qingcloud 設定為預設的 StorageClass 了,反正不管怎樣,就這麼稀裡糊塗地搞錯了。雖然重灌可以解決 99% 的問題,但作為一名成熟的 YAML 工程師,重灌是不可能的,必須在不重灌的情況下解決這個問題,才能體現出我的氣質!

事實上不止我一個人遇到過這種情況,很多人都會稀裡糊塗地裝完一整套產品之後發現 StorageClass 用錯了,這時候再想改回去恐怕就沒那麼容易了。這不巧了麼這不是,本文就是來幫助大家解決這個問題的。

思路

我們先來思考一下換 StorageClass 需要做哪幾件事情。首先需要將應用的副本數縮減為 0,然後建立一個新的 PVC,將舊 PV 的資料複製到新 PV,然後讓應用使用新的 PV,並將副本擴充套件到原來的數量,最後再將舊 PV 刪除。在這整個過程中還要防止刪除 PVC 時 Kubernetes 將 PV 也刪除了。

當然,有些 CSI 驅動或者儲存後端可能會有更便利的資料遷移技巧,但是本文提供的是一種更加通用的方案,不管後端是什麼儲存都可以。

KubeSphere 3.3.0 開啟所有元件之後使用的持久卷宣告(PVC)如下:

本文就以 Elasticsearch 為例,演示如何將 Elasticsearch 的儲存從本地儲存替換為分散式儲存。

備份 PVC 和 PV

首先第一步就是備份 PVC 和 PV,萬一後面操作失敗了,還有反悔的餘地。

$ kubectl -n kubesphere-logging-system get pvc
NAME                                     STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS     AGE
data-elasticsearch-logging-data-0        Bound    pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9   20Gi       RWO            local-hostpath   28h
data-elasticsearch-logging-data-1        Bound    pvc-0851350a-270e-4d4d-af8d-081132c1775b   20Gi       RWO            local-hostpath   28h
data-elasticsearch-logging-discovery-0   Bound    pvc-8f32fc97-3d6e-471a-8121-655991d945a8   4Gi        RWO            local-hostpath   28h

$ kubectl -n kubesphere-logging-system get pv pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9 -o yaml > pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9.yaml
$ kubectl -n kubesphere-logging-system get pv pvc-0851350a-270e-4d4d-af8d-081132c1775b -o yaml > pvc-0851350a-270e-4d4d-af8d-081132c1775b.yaml

$ kubectl -n kubesphere-logging-system get pvc data-elasticsearch-logging-data-0 -o yaml > data-elasticsearch-logging-data-0.yaml
$ kubectl -n kubesphere-logging-system get pvc data-elasticsearch-logging-data-1 -o yaml > data-elasticsearch-logging-data-1.yaml

複製資料

不管 PV 的 accessModes 是 ReadWriteOnce 還是 ReadWriteMany,在複製資料之前都要將應用的副本數量縮減為 0,因為 ReadWriteOne 模式同時只允許掛載一個 Pod,新 Pod 無法掛載,而 ReadWriteMany 模式如果不將副本數量縮減為 0,在複製資料時可能會有新的資料寫入。所以無論如何,都要將副本數量縮為 0 。

$ kubectl -n kubesphere-logging-system get sts
NAME                              READY   AGE
elasticsearch-logging-data        2/2     28h
elasticsearch-logging-discovery   1/1     28h

$ kubectl -n kubesphere-logging-system scale sts elasticsearch-logging-data --replicas=0

$ kubectl -n kubesphere-logging-system get sts
NAME                              READY   AGE
elasticsearch-logging-data        0/0     28h
elasticsearch-logging-discovery   1/1     28h

建立一個新的 PVC 叫 new-data-elasticsearch-logging-data-0,容量和 data-elasticsearch-logging-data-0 一樣,並將 storageClassName 指定為新的 StorageClass。

建立一個 Deployment,將新 PV 和舊 PV 都掛載進去,然後再將舊 PV 的資料拷貝到新 PV。

在『工作負載』介面點選『建立』,將下面的 YAML 貼上進去即可。

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: kubesphere-logging-system
  labels:
    app: datacopy
  name: datacopy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: datacopy
  template:
    metadata:
      labels:
        app: datacopy
    spec:
      containers:
        - name: datacopy
          image: ubuntu
          command:
            - 'sleep'
          args:
            - infinity
          volumeMounts:
            - name: old-pv
              readOnly: false
              mountPath: /mnt/old
            - name: new-pv
              readOnly: false
              mountPath: /mnt/new
      volumes:
        - name: old-pv
          persistentVolumeClaim:
            claimName: data-elasticsearch-logging-data-0
        - name: new-pv
          persistentVolumeClaim:
            claimName: new-data-elasticsearch-logging-data-0

這個 Deployment 將新 PV 和舊 PV 都掛載進去了,稍後我們會將舊 PV 的資料拷貝到新 PV。

Pod 啟動成功後,點選容器的終端圖示進入容器的終端。

在容器中先驗證舊 PV 的掛載點是否包含應用資料,新 PV 的掛載點是否是空的,之後再執行命令 (cd /mnt/old; tar -cf - .) | (cd /mnt/new; tar -xpf -),以確保所有資料的所有權和許可權被繼承。

執行完成後,驗證新 PV 的掛載點是否包含舊 PV 的資料,以及所有權和許可權是否被正確繼承。

到這裡複製資料的任務就完成了,現在我們需要將 datacopy 的副本數量縮為 0。

遷移 PVC

遷移儲存的理想狀態是使用舊的 PVC,並將其指向新的 PV,這樣工作負載的 YAML 配置清單就不需要做任何改變。但 PVC 和 PV 之間的繫結關係是不可更改的,要想讓它們解綁,必須先刪除舊的 PVC,再建立同名的 PVC,並將舊的 PV 與它繫結。

需要注意的是,預設情況下 PV 的回收策略是 Delete,一旦刪除 PVC,與之繫結的 PV 和 PV 裡的資料都會被刪除。這是我們不希望看到的,所以我們需要修改回收策略,以便刪除 PVC 時 PV 能夠保留下來。

事實上可以通過 StorageClass 來設定全域性的回收策略(reclaimPolicy),如果不設定,預設就是 Delete。可以通過命令 kubectl describe pv <pv-name> 來檢視 PV 的回收策略(reclaimPolicy):

$ kubectl describe pv pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9
Name:              pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9
Labels:            openebs.io/cas-type=local-hostpath
Annotations:       pv.kubernetes.io/provisioned-by: openebs.io/local
Finalizers:        [kubernetes.io/pv-protection]
StorageClass:      local-hostpath
Status:            Bound
Claim:             kubesphere-logging-system/data-elasticsearch-logging-data-0
Reclaim Policy:    Delete
...

$ kubectl describe pv pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e
Name:              pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e
Labels:            <none>
Annotations:       pv.kubernetes.io/provisioned-by: disk.csi.qingcloud.com
Finalizers:        [kubernetes.io/pv-protection external-attacher/disk-csi-qingcloud-com]
StorageClass:      csi-qingcloud
Status:            Bound
Claim:             kubesphere-logging-system/new-data-elasticsearch-logging-data-0
Reclaim Policy:    Delete
...

我們可以通過 patch 命令將新舊 PV 的回收策略設定為 Retain

$ kubectl patch pv pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9 -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
persistentvolume/pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9 patched

$ kubectl patch pv pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
persistentvolume/pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9 patched
⚠️注意:該命令對 PV 的穩定性和可用性沒有任何影響,可以隨時執行。

現在可以將新舊 PVC 全部刪除,PV 不會受到任何影響。

$ kubectl -n kubesphere-logging-system delete pvc data-elasticsearch-logging-data-0 new-data-elasticsearch-logging-data-0
persistentvolumeclaim "data-elasticsearch-logging-data-0" deleted
persistentvolumeclaim "new-data-elasticsearch-logging-data-0" deleted

在建立最終的 PVC 之前,我們必須要確保新建立的 PVC 能夠被繫結到新的 PV 上。通過以下命令可以看到新 PV 目前處於釋放狀態,不能被新 PVC 繫結:

$ kubectl describe pv pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e
Name:              pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e
Labels:            <none>
Annotations:       pv.kubernetes.io/provisioned-by: disk.csi.qingcloud.com
Finalizers:        [kubernetes.io/pv-protection external-attacher/disk-csi-qingcloud-com]
StorageClass:      csi-qingcloud
Status:            Released
Claim:             kubesphere-logging-system/new-data-elasticsearch-logging-data-0
Reclaim Policy:    Retain
Access Modes:      RWO
VolumeMode:        Filesystem
Capacity:          20Gi
...

這是因為 PV 在 spec.claimRef 中仍然引用了已經被刪除的 PVC:

$ kubectl get pv pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e -o yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  ...
  name: pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e
  ...
spec:
  accessModes:
  - ReadWriteOnce
  capacity:
    storage: 20Gi
  claimRef:
    apiVersion: v1
    kind: PersistentVolumeClaim
    name: new-data-elasticsearch-logging-data-0
    namespace: kubesphere-logging-system
    resourceVersion: "657019"
    uid: f4e96f69-b3be-4afe-bb52-1e8e728ca55e
  ...
  persistentVolumeReclaimPolicy: Retain
  storageClassName: csi-qingcloud
  volumeMode: Filesystem

為了解決這個問題,可以直接通過命令 kubectl edit pv <pv-name> 編輯 PV,將 claimRef 的內容全部刪除。然後再檢視 PV 已經處於可用狀態(Available):

$ kubectl describe pv pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e
Name:              pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e
Labels:            <none>
Annotations:       pv.kubernetes.io/provisioned-by: disk.csi.qingcloud.com
Finalizers:        [kubernetes.io/pv-protection external-attacher/disk-csi-qingcloud-com]
StorageClass:      csi-qingcloud
Status:            Available
Claim:
Reclaim Policy:    Retain
Access Modes:      RWO
VolumeMode:        Filesystem
Capacity:          20Gi

最終我們需要建立與舊 PVC 同名的新 PVC,而且要儘可能保證與舊 PVC 的引數相同:

  • 新 PVC 的名字和舊 PVC 的名字相同;
  • spec.volumeName 指向新 PV;
  • 新 PVC 的 metadata.annotationsmetadata.labels 和舊 PVC 儲存相同,因為這些值可能會影響到應用部署(比如 Helm chart 等)。

最終 PVC 內容如下:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    app: elasticsearch
    component: data
    release: elasticsearch-logging
    role: data
  name: data-elasticsearch-logging-data-0
  namespace: kubesphere-logging-system
spec:
  storageClassName: csi-qingcloud
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi
  volumeMode: Filesystem
  volumeName: pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e

在『儲存卷宣告』頁面點選『建立』:

選擇『編輯 YAML』,將上面的 YAML 內容複製貼上進去,然後點選『建立』:

最終可以看到新的 PVC 和 PV 全部都是 Bound 狀態:

$ kubectl -n kubesphere-logging-system get pvc data-elasticsearch-logging-data-0
NAME                                STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS    AGE
data-elasticsearch-logging-data-0   Bound    pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e   20Gi       RWO            csi-qingcloud   64s

$ kubectl get pv pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                                                         STORAGECLASS    REASON   AGE
pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e   20Gi       RWO            Retain           Bound    kubesphere-logging-system/data-elasticsearch-logging-data-0   csi-qingcloud            11h

再來一遍

到目前為止,我們只遷移了 data-elasticsearch-logging-data-0 的資料,對於 data-elasticsearch-logging-data-1,按照上面的步驟再重複一遍就行了,記得將 datacopy 中的 PVC 改為 data-elasticsearch-logging-data-1new-data-elasticsearch-logging-data-0,其他地方的配置內容也要修改為新的。

恢復工作負載

現在所有的儲存都遷移完成,PVC 名稱保持不變,PV 使用的是新的儲存。

$ kubectl get pv -A|grep elasticsearch-logging-data
pvc-0851350a-270e-4d4d-af8d-081132c1775b   20Gi       RWO            Retain           Released   kubesphere-logging-system/data-elasticsearch-logging-data-1        local-hostpath            40h
pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9   20Gi       RWO            Retain           Released   kubesphere-logging-system/data-elasticsearch-logging-data-0        local-hostpath            40h
pvc-d0acd2e7-ee1d-47cf-8506-69147fe25563   20Gi       RWO            Retain           Bound      kubesphere-logging-system/data-elasticsearch-logging-data-1        csi-qingcloud             9m53s
pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e   20Gi       RWO            Retain           Bound      kubesphere-logging-system/data-elasticsearch-logging-data-0        csi-qingcloud             11h

$ kubectl -n kubesphere-logging-system get pvc|grep elasticsearch-logging-data
data-elasticsearch-logging-data-0        Bound    pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e   20Gi       RWO            csi-qingcloud    27m
data-elasticsearch-logging-data-1        Bound    pvc-d0acd2e7-ee1d-47cf-8506-69147fe25563   20Gi       RWO            csi-qingcloud    3m49s

將工作負載的副本恢復到之前的數量:

$ kubectl -n kubesphere-logging-system scale sts elasticsearch-logging-data --replicas=2
statefulset.apps/elasticsearch-logging-data scaled

$ kubectl -n kubesphere-logging-system get pod -l app=elasticsearch,component=data
NAME                           READY   STATUS    RESTARTS   AGE
elasticsearch-logging-data-0   1/1     Running   0          4m12s
elasticsearch-logging-data-1   1/1     Running   0          3m42s

完美!

最後還有一點收尾工作,我們需要將所有新 PV 的回收策略重新設定為 Delete

$ kubectl patch pv pvc-d0acd2e7-ee1d-47cf-8506-69147fe25563 -p '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}'
persistentvolume/pvc-d0acd2e7-ee1d-47cf-8506-69147fe25563 patched

$ kubectl patch pv pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e -p '{"spec":{"persistentVolumeReclaimPolicy":"Delete"}}'
persistentvolume/pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e patched

$ kubectl get pv -A|grep elasticsearch-logging-data
pvc-0851350a-270e-4d4d-af8d-081132c1775b   20Gi       RWO            Retain           Released   kubesphere-logging-system/data-elasticsearch-logging-data-1        local-hostpath            40h
pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9   20Gi       RWO            Retain           Released   kubesphere-logging-system/data-elasticsearch-logging-data-0        local-hostpath            40h
pvc-d0acd2e7-ee1d-47cf-8506-69147fe25563   20Gi       RWO            Delete           Bound      kubesphere-logging-system/data-elasticsearch-logging-data-1        csi-qingcloud             15m
pvc-f4e96f69-b3be-4afe-bb52-1e8e728ca55e   20Gi       RWO            Delete           Bound      kubesphere-logging-system/data-elasticsearch-logging-data-0        csi-qingcloud             11h

最後的最後,就可以將舊 PV 全部刪除了:

$ kubectl delete pv pvc-0851350a-270e-4d4d-af8d-081132c1775b
persistentvolume "pvc-0851350a-270e-4d4d-af8d-081132c1775b" deleted

$ kubectl delete pv pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9
persistentvolume "pvc-9aed3d1b-09a6-4fe3-8adc-9195a2bbb2b9" deleted

更簡單的方案

上面的方案雖然完美解決了問題,但步驟比較繁瑣,有沒有更簡潔的方法呢?

可以試試青雲推出的雲原生備份容災 SaaS 服務,無需部署、維護本地備份基礎架構,即可輕鬆完成多雲異構環境下資料的自由遷移,從而實現多地、按需的資料保護與應用的高可用。而且價格比較親民,對白嫖黨友好,提供了 100GB 的免費儲存,遷移幾個 PV 完全夠用了。

使用起來非常簡單,先註冊賬戶,然後匯入 Kubernetes 叢集。如果選擇通過代理連線 Kubernetes 叢集,需要執行紅色方框內命令在 Kubernetes 叢集中安裝代理。

然後新建託管倉庫。

接下來直接建立備份計劃,選擇直接複製

備份成功之後,將叢集中的 PVC 和 PV 刪除,並將工作負載的副本數縮減為 0。最後建立恢復計劃,注意將源儲存型別名稱為 local-hostpath 的目標儲存型別名稱設定為你想遷移的儲存,這樣恢復後的 PV 使用的就是新的 StorageClass。

完了。

總結

本文介紹瞭如何將 Kubernetes 叢集中現有 PV 的資料遷移到新的 PV,並建立同名的 PVC 來指向新的 PV,這樣就完成了應用的資料遷移而不需要對應用的配置清單做任何更改。最後還介紹瞭如何通過雲原生備份容災 SaaS 服務來簡化遷移過程。

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章