k8s 持久卷

  |  

全部的 K8S学习笔记总目录,请点击查看。

Kubernetes 中的持久卷(Persistent Volume)是一种存储卷,它的生命周期独立于 Pod,这意味着 Pod 被删除后,持久卷中的数据不会被删除。持久卷可以在多个 Pod 之间共享,但是同一时间只能被一个 Pod 挂载。持久卷的类型可以是 NFS、iSCSI、GlusterFS、Ceph 等。

持久化卷方式

k8s存储的目的就是保证Pod重建后,数据不丢失。简单的数据持久化有以下几种方式:

emptyDir

  • Pod内的容器共享卷的数据
  • 存在于Pod的生命周期,Pod销毁,数据丢失
  • Pod内的容器自动重建后,数据不会丢失
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- image: k8s.gcr.io/test-webserver
name: webserver
volumeMounts:
- mountPath: /cache
name: cache-volume
- image: k8s.gcr.io/test-redis
name: redis
volumeMounts:
# 与webserver容器共享同一个卷,数据会被共享
- mountPath: /data
name: cache-volume
volumes:
- name: cache-volume
emptyDir: {}

hostPath

指定节点上的目录作为卷,这种方式不适合生产环境,因为Pod可能会被调度到其他节点,数据会不一致或者丢失,因此通常配合nodeSelector使用,保证Pod一直调度到指定的节点上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
nodeSelector:
kubernetes.io/hostname: k8s-node1
containers:
- image: k8s.gcr.io/test-webserver
name: test-container
volumeMounts:
- mountPath: /test-pod
name: test-volume
volumes:
- name: test-volume
hostPath:
# 节点上的本地目录
path: /data
# 可选项
type: Directory

nfs存储

通过nfs挂载远程存储

1
2
3
4
5
6
7
8
...
volumes:
- name: redisdata #卷名称
nfs: #使用NFS网络存储卷
server: 192.168.100.1 #NFS服务器地址
path: /data/redis #NFS服务器共享的目录
readOnly: false #是否为只读
...

存储支持

volume支持的种类众多(参考 https://kubernetes.io/docs/concepts/storage/volumes/#types-of-volumes )每种对应不同的存储后端实现。

为了屏蔽后端存储的细节,同时使得Pod在使用存储的时候更加简洁和规范,k8s引入了两个新的资源类型,PV和PVC。

PV

PersistentVolume(持久化卷),是对底层的存储的一种抽象,它和具体的底层的共享存储技术的实现方式有关,比如 Ceph、GlusterFS、NFS 等,都是通过插件机制完成与共享存储的对接。如使用PV对接NFS存储:

pv的声明式清单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: /data/k8s
server: 192.168.100.1
  • capacity,存储能力, 目前只支持存储空间的设置, 就是我们这里的 storage=1Gi,不过未来可能会加入 IOPS、吞吐量等指标的配置。
  • persistentVolumeReclaimPolicy,pv的回收策略, 目前只有 NFS 和 HostPath 两种类型支持回收策略
    • Retain(保留)- 保留数据,需要管理员手工清理数据
    • Recycle(回收)- 清除 PV 中的数据,效果相当于执行 rm -rf /thevolume/*
    • Delete(删除)- 与 PV 相连的后端存储完成 volume 的删除操作,当然这常见于云服务商的存储服务,比如 ASW EBS。
  • accessModes,访问模式, 是用来对 PV 进行访问模式的设置,用于描述用户应用对存储资源的访问权限,访问权限包括下面几种方式:
    • ReadWriteOnce(RWO):读写权限,但是只能被单个节点挂载
    • ReadOnlyMany(ROX):只读权限,可以被多个节点挂载
    • ReadWriteMany(RWX):读写权限,可以被多个节点挂载

pv-access-mode

PVC

因为PV是直接对接底层存储的,就像集群中的Node可以为Pod提供计算资源(CPU和内存)一样,PV可以为Pod提供存储资源。因此PV不是namespaced的资源,属于集群层面可用的资源。Pod如果想使用该PV,需要通过创建PVC挂载到Pod中。

PVC全写是PersistentVolumeClaim(持久化卷声明),PVC 是用户存储的一种声明,创建完成后,可以和PV实现一对一绑定。对于真正使用存储的用户不需要关心底层的存储实现细节,只需要直接使用 PVC 即可。

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-nfs
namespace: default
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi

然后Pod中通过如下方式去使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
spec:
containers:
- name: nginx
image: nginx:alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: web
volumeMounts: #挂载容器中的目录到 pvc nfs 中的目录
- name: www # 指定挂载的卷名称
mountPath: /usr/share/nginx/html # 挂载到容器中的目录
volumes:
- name: www # 指定卷名称
persistentVolumeClaim: #指定pvc
claimName: pvc-nfs
...

PV是集群层面的资源,PVC是命名空间层面的资源,因此PVC定义的时候需要指定命名空间,而且PVC只能给同一个命名空间下的Pod使用。

PV与PVC管理NFS存储卷实践

环境准备

服务端:192.168.100.1

1
2
3
4
5
6
7
8
9
10
11
12
$ yum -y install nfs-utils rpcbind

# 共享目录
$ mkdir -p /data/k8s && chmod 755 /data/k8s

$ echo '/data/k8s *(insecure,rw,sync,no_root_squash,fsid=0)'>>/etc/exports

$ systemctl enable rpcbind && systemctl start rpcbind
$ systemctl enable nfs-server && systemctl start nfs-server

# 查看共享目录,确认nfs服务正常
$ showmount -e 127.0.0.1

客户端:k8s集群slave节点

1
2
3
$ yum -y install nfs-utils
$ mkdir /nfsdata
$ mount -t nfs 192.168.100.1:/ /nfsdata

创建PV

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 在nfs-server机器中创建
$ mkdir /data/k8s/nginx

# 把/data/k8s/nginx 目录作为数据卷给k8s集群中的Pod使用
$ cat pv-nfs.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: /nginx
server: 192.168.100.1

$ kubectl create -f pv-nfs.yaml

$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS
nfs-pv 1Gi RWO Retain Available

一个 PV 的生命周期中,可能会处于4中不同的阶段:

  • Available(可用):表示可用状态,还未被任何 PVC 绑定
  • Bound(已绑定):表示 PV 已经被 PVC 绑定
  • Released(已释放):PVC 被删除,但是资源还未被集群重新声明
  • Failed(失败): 表示该 PV 的自动回收失败

创建PVC

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
$ cat pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-nfs
namespace: default
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi

$ kubectl create -f pvc.yaml
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
pvc-nfs Bound nfs-pv 1Gi RWO 3s
$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM
nfs-pv 1Gi RWO Retain Bound default/pvc-nfs

#访问模式,storage大小(pvc大小需要小于pv大小),以及 PV 和 PVC 的 storageClassName 字段必须一样,这样才能够进行绑定。

# PersistentVolumeController会不断地循环去查看每一个 PVC,是不是已经处于 Bound(已绑定)状态。如果不是,那它就会遍历所有的、可用的 PV,并尝试将其与未绑定的 PVC 进行绑定,这样,Kubernetes 就可以保证用户提交的每一个 PVC,只要有合适的 PV 出现,它就能够很快进入绑定状态。而所谓将一个 PV 与 PVC 进行“绑定”,其实就是将这个 PV 对象的名字,填在了 PVC 对象的 spec.volumeName 字段上。

# 查看nfs数据目录
$ ls /nfsdata

创建Pod挂载PVC

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
$ cat deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-pvc
spec:
replicas: 1
selector: #指定Pod的选择器
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumes:
- name: www
persistentVolumeClaim:
claimName: pvc-nfs


$ kubectl create -f deployment.yaml

# 查看容器/usr/share/nginx/html目录

# 删除pvc

StorageClass动态挂载

上述的PV和PVC都是静态的,需要手动创建,且PV与PVC一一对应,手动创建很繁琐。因此,通过 storageClass + provisioner 的方式来实现通过PVC自动创建并绑定PV。

storage-class

一些关于nfs的StorageClass可以查看github仓库 (sig-storage-lib-external-provisioner)[https://github.com/kubernetes-sigs/sig-storage-lib-external-provisioner]

准备yaml文件

在github仓库中下载配置文件nfs-subdir-external-provisioner/deploy/deployment.yaml,然后进行修改。

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: nfs-client-provisioner
labels:
app: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: nfs-client-provisioner
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
# 修改镜像地址,使用阿里云镜像
image: registry.cn-beijing.aliyuncs.com/mydlq/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
- name: NFS_SERVER
# 修改为nfs-server的ip地址
value: 192.168.100.1
- name: NFS_PATH
# 修改为nfs-server的共享目录
value: /
volumes:
- name: nfs-client-root
nfs:
# 修改为nfs-server的ip地址
server: 192.168.100.1
# 修改为nfs-server的共享目录
path: /

再下载nfs-subdir-external-provisioner/deploy/rbac.yaml,然后进行修改。

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
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
---
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"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
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
# replace with namespace where provisioner is deployed
namespace: default
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
# replace with namespace where provisioner is deployed
namespace: default
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: default
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io

最后,下载nfs-subdir-external-provisioner/deploy/class.yaml文件,然后进行修改。

1
2
3
4
5
6
7
8
9
10
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
# 添加一个注解,设置为default StorageClass
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
archiveOnDelete: "false"

创建资源

1
2
3
4
5
6
7
8
9
# 上面文件使用的ns是default,没有指定其他ns,因此这一步不执行
# $ kubectl create namespace nfs-provisioner
$ kubectl create -f rbac.yaml
$ kubectl create -f deployment.yaml
$ kubectl create -f class.yaml

# 等待pod启动成功
$ kubectl -n nfs-provisioner get pod
nfs-client-provisioner-6cfb58c597-rmc45 1/1 Running 0 27s

验证效果

验证使用storageclass自动创建并绑定pv

创建一个 pvc.yaml

1
2
3
4
5
6
7
8
9
10
11
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: registry-claim
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
storageClassName: nfs-client

创建资源

1
$ kubectl create -f pvc.yaml

查看pvc和pv是否绑定成功

1
2
3
4
5
6
7
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
registry-claim Bound pvc-eb692323-553e-4350-8749-d972ff785567 1Gi RWX nfs-client 8s

$ kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-eb692323-553e-4350-8749-d972ff785567 1Gi RWX Delete Bound default/registry-claim nfs-client 8s

查看nfs-server上的数据目录

1
2
3
$ ls -l /data/k8s/nginx
total 0
drwxrwxrwx 2 root root 6 Nov 23 10:28 default-test-claim-pvc-eb692323-553e-4350-8749-d972ff785567

总结

我们所说的容器的持久化,实际上应该理解为宿主机中volume的持久化,因为Pod是支持销毁重建的,所以只能通过宿主机volume持久化,然后挂载到Pod内部来实现Pod的数据持久化。

宿主机上的volume持久化,因为要支持数据漂移,所以通常是数据存储在分布式存储中,宿主机本地挂载远程存储(NFS,Ceph,OSS),这样即使Pod漂移也不影响数据。

k8s的pod的挂载盘通常的格式为:

1
/var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>
文章目录
  1. 1. 持久化卷方式
    1. 1.1. emptyDir
    2. 1.2. hostPath
    3. 1.3. nfs存储
  2. 2. 存储支持
  3. 3. PV
  4. 4. PVC
  5. 5. PV与PVC管理NFS存储卷实践
    1. 5.1. 环境准备
    2. 5.2. 创建PV
    3. 5.3. 创建PVC
  6. 6. 创建Pod挂载PVC
  7. 7. StorageClass动态挂载
    1. 7.1. 准备yaml文件
    2. 7.2. 创建资源
    3. 7.3. 验证效果
  8. 8. 总结