k8s学习总结3-存储

PV PVC StorageClass

容器化一个应用最麻烦的地方,莫过于对其状态的管理,而最常见的状态就是存储状态。K8s提出了PV和PVC这样的概念,来方便开发人员对存储状态进行管理

PV

PV文件描述的是一个持久化存储卷,例如一个Ceph文件系统,一个云盘等等,主要信息就是声明了访问方式以及存储容量的大小,主要由运维人员来维护,开发人员无需关心存储的具体细节。

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
nfs:
server: 10.244.1.4
path: "/"

PVC

PVC描述的是Pod所希望持久化存储的属性,例如所需磁盘的大小,可读写的权限等

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

这里PVC真正能够使用起来,必须要同一个PV进行绑定,这里包括两部分检查:

  1. PV的存储空间大于等于PVC所声明的
  2. PV与PVC的storageClassName必须相同

当PV与PVC进行绑定之后,我们就可以在yaml里使用这个存储了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: v1
kind: Pod
metadata:
labels:
role: web-frontend
spec:
containers:
- name: web
image: nginx
ports:
- name: web
containerPort: 80
volumeMounts:
- name: nfs
mountPath: "/usr/share/nginx/html"
volumes:
- name: nfs
persistentVolumeClaim:
claimName: nfs

PV与PVC

PV与PVC是如何做到持久化呢?我们知道Docker的volume机制就是将宿主机上的一个目录与容器里的目录绑定挂在到一起。而对于K8s这种分布式的系统来说,数据的持久化一定不能落在本地盘上,因为这样不具备分布式的特性,会引起单点的故障,所以hostPath和enptyDir是不行的。所以大多数情况下,持久化volume的实现依赖于一个远程存储服务,如NFS。

K8S所需要做的就是将这个远程存储服务与容器的本地目录进行绑定,这个过程分为两步:

  1. Attach:连接到远程的存储服务
  2. Mount: 将磁盘设备格式化并挂载到宿主机目录

经过这两个阶段的处理,我们就得到了一个持久化的volume宿主机目录,然后通过-v 就可以为Pod里的容器挂在这个持久化volume了,这就是K8s处理PV的过程。

可以看到PV与PVC的关系就像JAVA中的接口与实现类,这样做的好出就是实现了解耦,与面向对象的思想一致。但是这样的方式也引入了一些困难:因为PV一般都是运维人员进行编写的,如果开发声明了一个PVC但是无法绑定PV,那么Pod就会创建失败,当K8s集群大到一定规模时,这种方式一定会成为一种灾难。

StorageClass

由于PV与PVC带来的这种问题,我们很自然的就希望能够提供一个自动创建PV的机制,这就是dynamic provisioning,相比于人工管理PV的方式就叫做static provisioning。

dynamic provisioning机制的核心就是StorageClass对象,即创建一个PV的模板。一般来说一个StorageClass会定义两部分内容:

  1. PV的属性,如存储类型,大小
  2. 创建这种PV所需要的插件,如ceph,nfs

有了这个模板,K8S就能根据用户提交的PVC,找到一个对应的storageClass,然后调用storageClass所声明的插件,创建出PV。

1
2
3
4
5
6
7
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: block-service
provisioner: kubernetes.io/gce-pd
parameters:
type: pd-ssd

而作为开发者,我们就只需要在yaml中指定需要使用的storageClass就可以了

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: claim1
spec:
accessModes:
- ReadWriteOnce
storageClassName: block-service
resources:
requests:
storage: 30Gi

本地持久化

虽然K8S内置了20中持久化实现方式,但是并没有提供本地的持久化存储方式。但是依然有很多用户希望能够直接使用宿主机上的本地磁盘目录,而不依赖远程的存储服务。这样做好出也很明显,Volume直接使用本地磁盘,IO性能会好很多。所以在1.10版本之后,K8S依靠PV/PVC实现了这个特性,即Local Persistent Volume。

首先本地持久卷并不适用于所有应用,并且相对于其他PV,一旦这些节点宕机,那么数据就会丢失,这就要求使用Local Persistent Volume的节点必须具有备份和回复能力。

难点1

Local Persistent Volume并不等于hostPath+nodeAffinity。实际上并不应该把宿主机上的一个目录当做PV来使用,因为本地目录的存储完全不可控,随时都有可能被写满,其次缺少最基础IO隔离机制。所以一个Local Persistent Volume应该等于一块额外挂载到宿主机的磁盘,也就是一个PV一块盘。

难点2

调度器如何保证Pod始终能被正确的调度到他所请求的Local Persistent Volume所在的节点。对于local PV来说,每个节点挂载情况可能完全不同,有的节点甚至没有挂载,那么K8s就需要维护这种关系,才能调度Pod,也就是在调度的时候考虑volume分布。

使用

首先需要手动在node上挂载磁盘,例如/mnt/disks

接着定义PV

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: PersistentVolume
metadata:
name: example-pv
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Delete
storageClassName: local-storage
local:
path: /mnt/disks/vol1
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- node-1

可以看到这个PV定义了lcoal字段并且指定了路径.如果Pod要使用这个PV
那么就必须运行在这个node-1节点上,所以指定了nodeAffinity。

接着定义一个StorageClass

1
2
3
4
5
6
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer

这里需要注意local pv目前不支持动态创建,所以需要指定为no-provisioner。所以创建PV的过程是不可以省略的。

volumeBindingMode=WaitForFirstConsumer属性也非常重要,这是一种延迟绑定的机制,这种绑定会在调度的时候才去绑定,否则就会引起Pod调度的失败。

接着我们就可以编写一个普通的PVC来使用这个local pv了,这就类似于面向对象的设计,我们只需要修改接口的实现类,就可以动态修改类的表现。