Kubernetes 持久化存储

Posted by Vito on January 23, 2024

Volumes

hostPath

  • 将节点上的文件或目录挂载到 Pod 上,即使 Pod 被删除后重启,也可以重新加载到该目录,该目录下的文件不会丢失
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@k8s-master1 test]# vim po-hostpath-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: po-hostpath-test
spec:
  volumes:
  - name: hostpath-volume
    hostPath:
      path: /data # Pod 所在节点的目录
      type: DirectoryOrCreate # 检查类型,在挂载前对挂载目录做什么检查操作,有多种选项,默认为空字符串,不做任何检查
      # 空字符串:默认类型,不做任何检查
      # DirectoryOrCreate:如果给定的 path 不存在,就创建一个权限 755 的空目录
      # Directory:这个目录必须存在
      # FileOrCreate:如果给定的文件不存在,则创建一个权限为 644 空文件
      # File:这个文件必须存在
      # Socket:UNIX 套接字,必须存在
      # CharDevice:字符设备,必须存在
      # BlockDevice:块设备,必须存在
  containers:
  - image: nginx:1.7.9
    name: po-hostpath-test-c
    volumeMounts:
    - name: hostpath-volume # 挂载的 volume
      mountPath: /c-data # 挂载到容器中的目录
1
2
3
4
5
6
7
8
9
10
11
12
13
[root@k8s-master1 test]# kubectl create -f po-hostpath-test.yaml 
pod/po-hostpath-test created

[root@k8s-master1 test]# kubectl get po -o wide
NAME               READY   STATUS    RESTARTS   AGE   IP             NODE                 NOMINATED NODE   READINESS GATES
po-hostpath-test   1/1     Running   0          15s   10.244.8.220   k8s-node2.zhch.lan   <none>           <none>

[root@k8s-node2 ~]# cd /data
[root@k8s-node2 data]# echo 'hostpath......' > a.txt

[root@k8s-master1 test]# kubectl exec -it po-hostpath-test -- bash
root@po-hostpath-test:/# cat /c-data/a.txt
hostpath......

emptyDir

  • EmptyDir 主要用于一个 Pod 中不同的 Container 共享数据使用,由于只是在 Pod 内部使用,因此与其他 volume 比较大的区别是,如果 Pod 被删除了,那么 emptyDir 也会被删除,不具备持久化存储能力
  • 存储介质可以是任意类型,如 SSD、磁盘或网络存储。可以将 emptyDir.medium 设置为 Memory 让 k8s 使用 tmpfs(内存支持文件系统),速度比较快,但是重启 tmpfs 节点时,数据会被清除,且设置的大小会计入到 Container 的内存限制中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@k8s-master1 test]# vim po-emptydir-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: po-emptydir-test
spec:
  volumes:
  - name: cache-volume
    emptyDir: {}
  containers:
  - image: nginx:1.7.9
    name: po-emptydir-test-c1
    volumeMounts:
    - name: cache-volume
      mountPath: /log-cache # 挂载到容器中的目录      
  - image: tomcat:10.1.17-jre21
    name: po-emptydir-test-c2
    volumeMounts:
    - name: cache-volume
      mountPath: /data-cache
1
2
3
4
5
6
7
8
9
10
[root@k8s-master1 test]# kubectl create -f po-emptydir-test.yaml 
pod/po-emptydir-test created

[root@k8s-master1 test]# kubectl exec -it po-emptydir-test -c po-emptydir-test-c1 -- bash
root@po-emptydir-test:/# echo 'empty dir test ......' > /log-cache/cache.txt
root@po-emptydir-test:/# exit 
exit
[root@k8s-master1 test]# kubectl exec -it po-emptydir-test -c po-emptydir-test-c2 -- bash
root@po-emptydir-test:/usr/local/tomcat# cat /data-cache/cache.txt
empty dir test ......

configMap

secret

  • configMap 和 secret 类型的挂载卷详见上一章配置管理的内容

NFS 挂载

  • nfs 卷能将 NFS (网络文件系统) 挂载到你的 Pod 中。
  • 生产环境,推荐给 NFS 共享目录单独挂载一块硬盘或单独的磁盘分区。
  • 生产环境,推荐搭建 NFS 双机热备高可用环境,避免 NFS 单点故障

安装 NFS 服务

  • 正常情况,应该准备另外的 NFS 服务器
  • 这里为了简单,使用 k8s-master1 充当 NFS 服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[root@k8s-master1 ~]# yum install -y nfs-utils

[root@k8s-master1 ~]# systemctl enable --now nfs-server

# 查看 nfs 版本
[root@k8s-master1 ~]# cat /proc/fs/nfsd/versions
+3 +4 +4.1 +4.2

# 创建共享目录
[root@k8s-master1 ~]# mkdir -p /root/nfs_data
[root@k8s-master1 ~]# cd /root/nfs_data
[root@k8s-master1 nfs_data]# mkdir rw
[root@k8s-master1 nfs_data]# mkdir ro

# 设置共享目录
[root@k8s-master1 nfs_data]# vim /etc/exports
/root/nfs_data/rw 192.168.99.0/24(rw,sync,no_subtree_check,no_root_squash)
/root/nfs_data/ro 192.168.99.0/24(ro,sync,no_subtree_check,no_root_squash)

# 重启 NFS
[root@k8s-master1 nfs_data]# systemctl restart nfs-server
[root@k8s-master1 nfs_data]# exportfs -v
/root/nfs_data/rw
		192.168.99.0/24(sync,wdelay,hide,no_subtree_check,sec=sys,rw,secure,no_root_squash,no_all_squash)
/root/nfs_data/ro
		192.168.99.0/24(sync,wdelay,hide,no_subtree_check,sec=sys,ro,secure,no_root_squash,no_all_squash)

# 测试(在另一台服务器上挂载 NFS 共享目录)
[root@Test ~]# yum install -y nfs-utils
[root@Test ~]# systemctl start nfs-server

[root@Test ~]# mkdir -p /mnt/nfs_test/rw
[root@Test ~]# mount -t nfs k8s-master1.zhch.lan:/root/nfs_data/rw /mnt/nfs_test/rw
[root@Test ~]# cd /mnt/nfs_test/rw
[root@Test rw]# echo 'nfs test...' > test.txt
[root@k8s-master1 nfs_data]# cat /root/nfs_data/rw/test.txt 
nfs test...

[root@Test ~]# ls /mnt/nfs_test/rw
test.txt
[root@Test ~]# umount /mnt/nfs_test/rw
[root@Test ~]# ls /mnt/nfs_test/rw
[root@Test ~]# 

使用 NFS 挂载卷

1
2
3
# 在所有 Kubernetes Node 节点安装 NFS (仅安装,无需启动)
[root@k8s-node1 ~]# yum install -y nfs-utils
[root@k8s-node2 ~]# yum install -y nfs-utils
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[root@k8s-master1 test]# vim po-nfs-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: po-nfs-test
spec:
  volumes:
  - name: nfs-volume
    nfs:
      server: k8s-master1.zhch.lan # 网络存储服务 NFS 的地址
      path: /root/nfs_data/rw # 网络存储目录,可选择 NFS 共享目录或其子目录,目录必须存在
      readOnly: false # 是否只读
  containers:
  - image: nginx:1.7.9
    name: po-nfs-test-c
    volumeMounts:
    - name: nfs-volume
      mountPath: /my-nfs-data

[root@k8s-master1 test]# kubectl create -f po-nfs-test.yaml
pod/po-nfs-test created

[root@k8s-master1 test]# kubectl exec -it po-nfs-test -- bash
root@po-nfs-test:/# 
root@po-nfs-test:/# ls /my-nfs-data
test.txt
root@po-nfs-test:/# cat /my-nfs-data/test.txt 
nfs test...

PV 与 PVC

  • 持久卷(PersistentVolume,PV)是集群中由管理员配置的一段网络存储。
  • 持久卷申领(PersistentVolumeClaim,PVC)表达的是用户对存储的请求。
  • StorageClass:充当 PV 的模板,自动为 PVC 创建 PV
    • StorageClass 不受 namespace 作用域限制
  • PV 生命周期:
    • Available(可用): 表示可用状态,还未被任何 PVC 绑定
    • Bound(已绑定): 表示 PV 已经被 PVC 绑定
    • Released(已释放): 表示 PVC 被删除,但是资源还未被集群重新声明
    • Failed(失败): 表示该 PV 的自动回收失败
  • PV 回收策略:
    • Retain (保留):保留数据,需要管理员手工清理数据
    • Recycle(回收):清除 PV 中的数据,效果相当于执行 rm -rf /thevolume/*
    • Delete (删除):与 PV 相连的后端存储完成 volume 的删除操作,这常见于云服务商的存储服务
  • PV 访问模式:
    • ReadWriteOnce(RWO):读写权限,但是只能被单个节点挂载
    • ReadWriteMany(RWX):读写权限,可以被多个节点挂载
    • ReadOnlyMany(ROX): 只读权限,可以被多个节点挂载
    • ReadWriteOncePod:( Kubernetes v1.27 [beta] )卷可以被单个 Pod 以读写方式挂载。

    需要注意的是,底层不同的存储类型支持的访问模式可能不同。

    由于访问控制是 node 层,实际使用中,很容易遇到一个特殊的情况,即 2 个 Pods 对 RWO 的 PV 的读写,有时可行,有时不行。这是因为,若恰好 2 个 Pods 调度到同一个节点上,则这 2 个 Pods 就可以对数据同时访问。

  • PersistentVolume.spec.storageClassName
    • https://kubernetes.io/zh-cn/docs/concepts/storage/persistent-volumes/#class
    • storageClassName 是该持久卷所属的 StorageClass 的名称。 空串表示该卷不属于任何 StorageClass。
    • 每个 PV 可以属于某个类(Class),通过将其 storageClassName 属性设置为某个 StorageClass 的名称来指定。
    • 特定 class 的 PV 卷只能绑定到请求该 class 存储卷的 PVC 申领。
    • 未设置 storageClassName 的 PV 卷没有 class 设定,只能绑定到那些没有指定特定 storageClass 的 PVC 申领。
  • PersistentVolume.spec.mountOptions
  • PersistentVolume.spec.volumeMode : Filesystem 、Block
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[root@k8s-master1 test]# vim pv0001.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv0001
spec:
  capacity: # 容量
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: any-scn # 可以是不存在的 StorageClass ,PV 的 storageClassName 的作用是与 pvc 的 storageClassName 进行匹配
  mountOptions: # 挂载选项
    - hard
    - nfsvers=4.2
  nfs:
    server: k8s-master1.zhch.lan
    path: /root/nfs_data/rw/pv0001

[root@k8s-master1 test]# kubectl create -f pv0001.yaml 
persistentvolume/pv0001 created
[root@k8s-master1 test]# kubectl get pv
NAME     CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
pv0001   5Gi        RWX            Retain           Available           any-scn                 10s
  • PersistentVolumeClaim.spec.selector
    • 目前, 若 PVC 设置了非空 selector ,则无法让集群为其动态制备 PV 卷。
    • 目前 selector.matchExpressions 支持的操作符有 In、NotIn、Exists 和 DoesNotExist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[root@k8s-master1 test]# vim pvc0001.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc0001
spec:
  accessModes:
    - ReadWriteMany # 此申领所要求的 PV 需支持的 accessModes
  volumeMode: Filesystem # 此申领所要求的 PV 的 volumeMode
  resources:
    requests:
      storage: 3Gi  # 此申领所要求的 PV 的最小容量
  storageClassName: any-scn # 此申领所要求的 PV 的 StorageClass 名称,如果不存在满足要求的 PV, 则由该 StorageClass 动态创建 PV
  # 如果 storageClassName: "" ,则表示此 PVC 没有 StorageClass ,永远不会去动态创建 PV
  # 如果没有设置 storageClassName,则此 PVC 的 StorageClass 是集群中的 DefaultStorageClass (注意集群中不一定存在 DefaultStorageClass)
  #selector:
  #  matchLabels:
  #    release: "stable" # 要求 pv 具有名为`release`值为`stable`的标签
  #  matchExpressions:
  #    - {key: environment, operator: In, values: [dev]}  # 要求 pv 具有标签`environment`且其值满足表达式: In [dev]

[root@k8s-master1 test]# kubectl create -f pvc0001.yaml
persistentvolumeclaim/pvc0001 created
[root@k8s-master1 test]# kubectl get pvc
NAME      STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
pvc0001   Bound    pv0001   5Gi        RWX            any-scn        5s
[root@k8s-master1 test]# kubectl get pv
NAME     CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM             STORAGECLASS   REASON   AGE
pv0001   5Gi        RWX            Retain           Bound    default/pvc0001   any-scn                 71s
  • PVC 关联 Pod
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
[root@k8s-master1 test]# vim po-pvc-test.yaml
apiVersion: v1
kind: Pod
metadata:
  name: po-pvc-test
spec:
  volumes:
  - name: pvc0001-volume
    persistentVolumeClaim:
      claimName: pvc0001
  containers:
  - image: nginx:1.7.9
    name: po-pvc-test-c
    volumeMounts:
    - name: pvc0001-volume
      mountPath: /usr/share/nginx/html

[root@k8s-master1 test]# kubectl create -f po-pvc-test.yaml 
pod/po-pvc-test created

[root@k8s-master1 test]# kubectl get po -o wide
NAME          READY   STATUS    RESTARTS   AGE   IP             NODE                 NOMINATED NODE   READINESS GATES
po-pvc-test   1/1     Running   0          29s   10.244.8.231   k8s-node2.zhch.lan   <none>           <none>

[root@k8s-master1 test]# curl 10.244.8.231
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.7.9</center>
</body>
</html>

[root@k8s-master1 test]# echo 'pvc test...' > /root/nfs_data/rw/pv0001/index.html

[root@k8s-master1 test]# curl 10.244.8.231
pvc test...

创建 NFS StorageClass ,并设置为 Default Stroage Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
[root@k8s-master1 kubernetes]# vim nfs-storage.yaml
---
apiVersion: v1
kind: Namespace
metadata:
  name: nfs-storage
---
apiVersion: v1
kind: ServiceAccount # 创建 ServiceAccount ,主要用来管理 NFS provisioner 在k8s集群中运行的权限
metadata:
  name: nfs-client-provisioner
  namespace: nfs-storage
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["persistentvolumes"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "update"]
  - apiGroups: ["storage.k8s.io"]
    resources: ["storageclasses"]
    verbs: ["get", "list", "watch"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "update", "patch"]
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: nfs-storage
roleRef:
  kind: ClusterRole
  name: nfs-client-provisioner-runner
  apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
  namespace: nfs-storage
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: leader-locking-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: nfs-storage
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: managed-nfs-storage
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # 存储分配器的名字,自定义
parameters:
  archiveOnDelete: "false" # 删除 pv 的时候, pv 的内容是偶要备份
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  labels:
    app: nfs-client-provisioner
  namespace: nfs-storage
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nfs-client-provisioner
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: nfs-client-provisioner
    spec:
      serviceAccountName: nfs-client-provisioner # ServiceAccount
      containers:
        - name: nfs-client-provisioner
          # image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2 # nfs存储分配器镜像
          image: lank8s.cn/sig-storage/nfs-subdir-external-provisioner:v4.0.2
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: k8s-sigs.io/nfs-subdir-external-provisioner  # 与 StorageClass 中自定义的存储分配器的名字保持一致
            - name: NFS_SERVER
              value: k8s-master1.zhch.lan  # 与 template.spec.volumes 的 nfs.server 保持一致
            - name: NFS_PATH  
              value: /root/nfs_data/rw/default # 与 template.spec.volumes 的 nfs.path 保持一致
      volumes:
        - name: nfs-client-root
          nfs:
            server: k8s-master1.zhch.lan
            path: /root/nfs_data/rw/default
1
2
3
4
5
6
7
8
9
10
11
12
13
[root@k8s-master1 kubernetes]# kubectl apply -f nfs-storage.yaml 
namespace/nfs-storage created
serviceaccount/nfs-client-provisioner created
clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/run-nfs-client-provisioner created
role.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
rolebinding.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
storageclass.storage.k8s.io/managed-nfs-storage created
deployment.apps/nfs-client-provisioner created

[root@k8s-master1 kubernetes]# kubectl get sc
NAME                            PROVISIONER                                   RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
managed-nfs-storage (default)   k8s-sigs.io/nfs-subdir-external-provisioner   Delete          Immediate           false                  28s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 测试
[root@k8s-master1 test]# vim test-claim.yaml 
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Mi

[root@k8s-master1 test]# kubectl create -f test-claim.yaml 
persistentvolumeclaim/test-claim created

[root@k8s-master1 test]# kubectl get pvc
NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
test-claim   Bound    pvc-a96c1c0c-a65a-4e47-997a-6ca99fc65790   1Mi        RWX            managed-nfs-storage   4s

[root@k8s-master1 test]# kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                STORAGECLASS          REASON   AGE
pvc-a96c1c0c-a65a-4e47-997a-6ca99fc65790   1Mi        RWX            Delete           Bound    default/test-claim   managed-nfs-storage            6s

排错过程记录

  • 测试时发现 PVC 一直处理 Pending 状态
    • describe pvc : waiting for a volume to be created, either by external provisioner “k8s-sigs.io/nfs-subdir-external-provisioner” or manually created by system administrator
  • 检查 pod
    • kubectl get po -n nfs-storage
    • kubectl logs nfs-client-provisioner-77f6478f79-f2rh8 -n nfs-storage
      • error retrieving resource lock nfs-storage/k8s-sigs.io-nfs-subdir-external-provisioner: endpoints “k8s-sigs.io-nfs-subdir-external-provisioner” is forbidden: User “system:serviceaccount:nfs-storage:nfs-client-provisioner” cannot get resource “endpoints” in API group “” in the namespace “nfs-storage”
  • 至此,找到问题原因

附录