K8s 集羣中存儲對象災備的落地實踐

談到存儲對象的災備,我們可以想象成當你啓動了掛載卷的 Pod 的時候,突然集羣機器宕機的場景,我們應該如何應對存儲對象的容錯能力呢?應用的高可用固然最好,但是災備方案一直都是最後一道門檻,在很多極限情況下,容錯的備份是你安心提供服務的保障。

在虛擬機時代,我們通過控制應用平均分配到各個虛擬機中和定期計劃執行的數據備份,讓業務可靠性不斷地提高。現在升級到 Kubernetes 時代,所有業務都被 Kubernetes 託管,集羣可以迅速調度並自維護應用的容器狀態,隨時可以擴縮資源來應對突發情況。

聽筆者這麼說,感覺好像並不需要對存儲有多大的擔心,只要掛載的是網絡存儲,即使應用集羣壞了,數據還在麼,好像也沒有多大的事情,那麼學這個存儲對象的災備又有什麼意義呢?

筆者想說事情遠沒有想象中那麼簡單,我們需要帶入接近業務的場景中,再來通過破壞集羣狀態,看看讀存儲對象是否有破壞性。

因爲我們從虛擬機時代升級到 Kubernetes 時代,我們的目的是利用動態擴縮的資源來減少業務中斷的時間,讓應用可以隨需擴縮,隨需自愈。所以在 Kubernetes 時代,我們要的並不是數據丟不丟的問題,而是能不能有快速保障讓業務恢復時間越來越短,甚至讓用戶沒有感知。這個可能實現嗎?

筆者認爲 Kubernetes 通過不斷豐富的資源對象已經快接近實現這個目標了。所以筆者這裏帶着大家一起梳理一遍各種存儲對象的災備在 Kubernetes 落地的實踐經驗,以備不時之需。

NFS 存儲對象的災備落地經驗
首先我們應該理解 PV/PVC 創建 NFS 網絡卷的配置方法,注意 mountOptions 參數的使用姿勢。如下例子參考:

### nfs-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Recycle
  storageClassName: nfs
  mountOptions:
    - hard
    - nfsvers=4.1
  nfs:
    path: /opt/k8s-pods/data   # 指定 nfs 的掛載點
    server: 192.168.1.40  # 指定 nfs 服務地址
---
### nfs-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
spec:
  storageClassName: nfs
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Gi

在這個例子中,PersistentVolume 是 NFS 類型的,因此需要輔助程序 /sbin/mount.nfs 來支持掛載 NFS 文件系統。

[kadmin@k8s-master ~]$ kubectl get pvc nfs-pvc
NAME      STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nfs-pvc   Bound    nfs-pv   10Gi       RWX            nfs            3m54s
[kadmin@k8s-master ~]$
[kadmin@k8s-master ~]$ kubectl get pv nfs-pv
NAME     CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM             STORAGECLASS   REASON   AGE
nfs-pv   10Gi       RWX            Recycle          Bound    default/nfs-pvc   nfs                     18m

執行一個 Pod 掛載 NFS 卷:

### nfs-pv-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pv-pod
spec:
  volumes:
    - name: nginx-pv-storage
      persistentVolumeClaim:
        claimName: nfs-pvc
  containers:
    - name: nginx
      image: nginx
      ports:
        - containerPort: 80
          name: "nginx-server"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: nginx-pv-storage

複製

[kadmin@k8s-master ~]$ kubectl create -f nfs-pv-pod.yaml
pod/nginx-pv-pod created
[kadmin@k8s-master ~]$
[kadmin@k8s-master ~]$ kubectl get pod nginx-pv-pod -o wide
NAME           READY   STATUS    RESTARTS   AGE   IP              NODE           NOMINATED NODE   READINESS GATES
nginx-pv-pod   1/1     Running   0          66s   172.16.140.28   k8s-worker-2   <none>           <none>

[kadmin@k8s-master ~]$ curl http://172.16.140.28
Hello, NFS Storage NGINX

當你在一個 Pod 裏面掛載了 NFS 卷之後,就需要考慮如何把數據備份出來。velero 作爲雲原生的備份恢復工具出現了,它可以幫助我們備份持久化數據對象。velero 案例如下:

velero backup create backupName --include-cluster-resources=true --ordered-resources 'pods=ns1/pod1,ns1/pod2;persistentvolumes=pv4,pv8' --include-namespaces=ns1

注意 velero 默認沒法備份卷,所以它集成了開源組件 restic 支持了存儲卷的支持。因爲目前還處於試驗階段,注意請不要在生產環境中使用。

Ceph 數據備份及恢復
Rook 是管理 Ceph 集羣的雲原生管理系統,在早前的課程中我已經和大家實踐過使用 Rook 創建 Ceph 集羣的方法。現在假設 Ceph 集羣癱瘓了應該如何修復它。是的,我們需要手工修復它。步驟如下:

第一步,停止 Ceph operator 把 Ceph 集羣的控制器關掉,不讓它能自動負載自己的程序。

kubectl -n rook-ceph scale deployment rook-ceph-operator --replicas=0

第二步,這個 Ceph 的 monmap 保持跟蹤 Ceph 節點的容錯數量。我們先通過更新保持健康監控節點的實例正常運行。此處爲 rook-ceph-mon-b,不健康的實例爲 rook-ceph-mon-a 和 rook-ceph-mon-c。備份 rook-ceph-mon-b 的 Deployment 對象:

kubectl -n rook-ceph get deployment rook-ceph-mon-b -o yaml > rook-ceph-mon-b-deployment.yaml

修改監控實例的命令:

kubectl -n rook-ceph patch deployment rook-ceph-mon-b -p '{"spec": {"template": {"spec": {"containers": [{"name": "mon", "command": ["sleep", "infinity"], "args": []}]}}}}'

進入健康的監控實例中:

kubectl -n rook-ceph exec -it <mon-pod> bash

# set a few simple variables
cluster_namespace=rook-ceph
good_mon_id=b
monmap_path=/tmp/monmap

# extract the monmap to a file, by pasting the ceph mon command
# from the good mon deployment and adding the
# `--extract-monmap=${monmap_path}` flag
ceph-mon \
    --fsid=41a537f2-f282-428e-989f-a9e07be32e47 \
    --keyring=/etc/ceph/keyring-store/keyring \
    --log-to-stderr=true \
    --err-to-stderr=true \
    --mon-cluster-log-to-stderr=true \
    --log-stderr-prefix=debug \
    --default-log-to-file=false \
    --default-mon-cluster-log-to-file=false \
    --mon-host=$ROOK_CEPH_MON_HOST \
    --mon-initial-members=$ROOK_CEPH_MON_INITIAL_MEMBERS \
    --id=b \
    --setuser=ceph \
    --setgroup=ceph \
    --foreground \
    --public-addr=10.100.13.242 \
    --setuser-match-path=/var/lib/ceph/mon/ceph-b/store.db \
    --public-bind-addr=$ROOK_POD_IP \
    --extract-monmap=${monmap_path}

# review the contents of the monmap
monmaptool --print /tmp/monmap

# remove the bad mon(s) from the monmap
monmaptool ${monmap_path} --rm <bad_mon>

# in this example we remove mon0 and mon2:
monmaptool ${monmap_path} --rm a
monmaptool ${monmap_path} --rm c

# inject the modified monmap into the good mon, by pasting
# the ceph mon command and adding the
# `--inject-monmap=${monmap_path}` flag, like this
ceph-mon \
    --fsid=41a537f2-f282-428e-989f-a9e07be32e47 \
    --keyring=/etc/ceph/keyring-store/keyring \
    --log-to-stderr=true \
    --err-to-stderr=true \
    --mon-cluster-log-to-stderr=true \
    --log-stderr-prefix=debug \
    --default-log-to-file=false \
    --default-mon-cluster-log-to-file=false \
    --mon-host=$ROOK_CEPH_MON_HOST \
    --mon-initial-members=$ROOK_CEPH_MON_INITIAL_MEMBERS \
    --id=b \
    --setuser=ceph \
    --setgroup=ceph \
    --foreground \
    --public-addr=10.100.13.242 \
    --setuser-match-path=/var/lib/ceph/mon/ceph-b/store.db \
    --public-bind-addr=$ROOK_POD_IP \
    --inject-monmap=${monmap_path}

編輯 rook configmap 文件:

kubectl -n rook-ceph edit configmap rook-ceph-mon-endpoints

在 data 字段那裏去掉過期的 a 和 b:

data: a=10.100.35.200:6789;b=10.100.13.242:6789;c=10.100.35.12:6789
變成:

data: b=10.100.13.242:6789

更新 secret 配置:

mon_host=$(kubectl -n rook-ceph get svc rook-ceph-mon-b -o jsonpath='{.spec.clusterIP}')
kubectl -n rook-ceph patch secret rook-ceph-config -p '{"stringData": {"mon_host": "[v2:'"${mon_host}"':3300,v1:'"${mon_host}"':6789]", "mon_initial_members": "'"${good_mon_id}"'"}}'

重啓監控實例:

kubectl replace --force -f rook-ceph-mon-b-deployment.yaml

重啓 operator:

# create the operator. it is safe to ignore the errors that a number of resources already exist.
kubectl -n rook-ceph scale deployment rook-ceph-operator --replicas=1

Jenkins 掛載 PVC 應用的數據恢復
假設 Jenkins 數據損壞,想修復 Jenkins 的數據目錄,可以採用把 PVC 掛載帶臨時鏡像並配合 kubectl cp 實現,步驟如下。

獲得當前 Jenkins 容器的運行權限:

$ kubectl --namespace=cje-cluster-example get pods cjoc-0 -o jsonpath='{.spec.securityContext}'
map[fsGroup:1000]

關閉容器:

$ kubectl --namespace=cje-cluster-example scale statefulset/cjoc --replicas=0
statefulset.apps "cjoc" scaled

查看 PVC:

$ kubectl --namespace=cje-cluster-example get pvc
NAME                  STATUS    VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
jenkins-home-cjoc-0   Bound     pvc-6b27e963-b770-11e8-bcbf-42010a8400c1   20Gi       RWO            standard       46d
jenkins-home-mm1-0    Bound     pvc-b2b7e305-ba66-11e8-bcbf-42010a8400c1   50Gi       RWO            standard       42d
jenkins-home-mm2-0    Bound     pvc-6561b8da-c0c8-11e8-bcbf-42010a8400c1   50Gi       RWO            standard       34d

掛載 PVC 到臨時鏡像中方便恢復數據:

$ cat <<EOF | kubectl --namespace=cje-cluster-example create -f -
kind: Pod
apiVersion: v1
metadata:
  name: rescue-pod
spec:
  securityContext:
    runAsUser: 1000
    fsGroup: 1000
  volumes:
    - name: rescue-storage
      persistentVolumeClaim:
       claimName: jenkins-home-cjoc-0
  containers:
    - name: rescue-container
      image: nginx
      command: ["/bin/sh"]
      args: ["-c", "while true; do echo hello; sleep 10;done"]
      volumeMounts:
        - mountPath: "/tmp/jenkins-home"
          name: rescue-storage
EOF
pod "rescue-pod" created

複製備份數據到臨時鏡像:

kubectl cp oc-jenkins-home.backup.tar.gz rescue-pod:/tmp/

解壓數據到 PVC 掛載卷:

kubectl exec --namespace=cje-cluster-example rescue-pod -it -- tar -xzf /tmp/oc-jenkins-home.backup.tar.gz -C /tmp/jenkins-home

刪除臨時鏡像 Pod:

kubectl --namespace=cje-cluster-example delete pod rescue-pod

恢復 Jenkins 容器:

kubectl --namespace=cje-cluster-example scale statefulset/cjoc --replicas=1

Kubernetes 集羣的備份
Kubernetes 集羣是分佈式集羣,我們備份集羣的元數據的目的一般有兩個主要目的:

能快速恢復控制節點而不是計算節點 能恢復應用容器 從集羣備份的難度來講,我們要清楚理解集羣控制節點上有哪些關鍵數據是需要備份的:自簽名證書、etcd 數據、kubeconfig。

拿單個控制幾點服務器上的備份步驟來看:

# Backup certificates
sudo cp -r /etc/kubernetes/pki backup/
# Make etcd snapshot
sudo docker run --rm -v $(pwd)/backup:/backup \
    --network host \
    -v /etc/kubernetes/pki/etcd:/etc/kubernetes/pki/etcd \
    --env ETCDCTL_API=3 \
    k8s.gcr.io/etcd:3.4.3-0 \
    etcdctl --endpoints=https://127.0.0.1:2379 \
    --cacert=/etc/kubernetes/pki/etcd/ca.crt \
    --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
    --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \
    snapshot save /backup/etcd-snapshot-latest.db

# Backup kubeadm-config
sudo cp /etc/kubeadm/kubeadm-config.yaml backup/

數據恢復一個控制節點的操作如下:

# Restore certificates
sudo cp -r backup/pki /etc/kubernetes/

# Restore etcd backup
sudo mkdir -p /var/lib/etcd
sudo docker run --rm \
    -v $(pwd)/backup:/backup \
    -v /var/lib/etcd:/var/lib/etcd \
    --env ETCDCTL_API=3 \
    k8s.gcr.io/etcd:3.4.3-0 \
    /bin/sh -c "etcdctl snapshot restore '/backup/etcd-snapshot-latest.db' ; \
    mv /default.etcd/member/ /var/lib/etcd/"

# Restore kubeadm-config
sudo mkdir /etc/kubeadm
sudo cp backup/kubeadm-config.yaml /etc/kubeadm/

# Initialize the master with backup
sudo kubeadm init --ignore-preflight-errors=DirAvailable--var-lib-etcd \
    --config /etc/kubeadm/kubeadm-config.yaml

通過以上案例知道 Kubernetes 集羣中 etcd 數據的備份和恢復,學會善用和 kubectl cp 的配合使用。

總結
依賴 Kubernetes 原生的數據複製能力 kubectl cp 和 cronjob,我們可以應對大部分的數據備份和恢復工作。當需要處理分佈式系統的備份和恢復的時候,大部分情況並不是去備份數據,而是嘗試從有效節點中去除故障節點,讓集羣能自愈。這是分佈式系統的特點,它可以自愈。但是分佈式系統的弱點也在於自愈是有條件的,如果故障節點超過可用節點數 Quorum,再智能也是無用的。所以備份仍然是最後一道防線。一定要做定期的並且冗餘的數據備份。

參考鏈接
https://github.com/rook/rook/blob/master/Documentation/ceph-disaster-recovery.md

https://zh.wikipedia.org/wiki/Quorum_(%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F)

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