談到存儲對象的災備,我們可以想象成當你啓動了掛載卷的 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)