PV、PVC、StorageClass、Provisioner、StatefulSet
参考:Kubernetes 入门实战课 | 极客时间 (opens new window) 第 24-26 讲
接下来的高级篇,将进一步探索 Kubernetes 更深层次的知识点和更高级的应用技巧。
# 1. PersistentVolume:怎么解决数据持久化的难题?
之前我们在介绍 ConfigMap 和 Secret 就遇到了 Kubernetes 的 Volume 存储卷的概念,在那时我们使用它的时候,是使用字段 volumes
和 volumeMounts
,相当于是给 Pod 挂载了一个“虚拟盘”,把配置信息以文件的形式注入进 Pod 供进程使用。
不过,那个时候的 Volume 只能存放较少的数据,离真正的“虚拟盘”还差得很远。今天我们就一起来了解 Volume 的高级用法,看看 Kubernetes 管理存储资源的API对象 PersistentVolume、PersistentVolumeClaim、StorageClass,然后使用本地磁盘来创建实际可用的存储卷。
# 1.1 什么是 PersistentVolume
我们之前搭建了 WordPress 网站,但存在一个严重的问题:Pod 没有持久化的功能,导致 MariaDB 无法永久存储数据。原因在于 Pod 一旦销毁,临时存储的数据也就丢失了。为保证即使 Pod 销毁后数据依然存在,我们需要给 Pod 用上真正的“虚拟盘”。这该怎么办呢?
其实,Kubernetes 的 Volume 对数据存储已经给出了一个很好的抽象,它只是定义了有这么一个“存储卷”,而这个“存储卷”是什么类型、有多大容量、怎么存储,我们都可以自由发挥。Pod 不需要关心那些专业、复杂的细节,只要设置好 volumeMounts
,就可以把 Volume 加载进容器里使用。
所以,Kubernetes 就顺着Volume的概念,延伸出了 PersistentVolume 对象,它专门用来表示持久存储设备,但隐藏了存储的底层实现,我们只需要知道它能安全可靠地保管数据就可以了(由于 PersistentVolume 这个词很长,一般都把它简称为 PV)。
那么,集群里的 PV 都从哪里来呢?作为存储的抽象,PV 实际上就是一些存储设备、文件系统,比如 Ceph、GlusterFS、NFS,甚至是本地磁盘,管理它们已经超出了 Kubernetes 的能力范围,所以,一般会由系统管理员单独维护,然后再在 Kubernetes 里创建对应的 PV。
要注意的是,PV 属于集群的系统资源,是和 Node 平级的一种对象,Pod 对它没有管理权,只有使用权。
# 1.2 什么是 PersistentVolumeClaim/StorageClass
现在有了 PV,我们是不是可以直接在Pod里挂载使用了呢?
还不行。因为不同存储设备的差异实在是太大了:有的速度快,有的速度慢;有的可以共享读写,有的只能独占读写;有的容量小,只有几百MB,有的容量大到TB、PB级别……
这么多种存储设备,只用一个PV对象来管理还是有点太勉强了,不符合“单一职责”的原则,让Pod直接去选择PV也很不灵活。于是Kubernetes就又增加了两个新对象,PersistentVolumeClaim 和 StorageClass,用的还是“中间层”的思想,把存储卷的分配管理过程再次细化。
我们看这两个新对象。
PersistentVolumeClaim,简称 PVC,从名字上看比较好理解,就是用来向 Kubernetes 申请存储资源的。PVC 是给 Pod 使用的对象,它相当于是 Pod 的代理,代表 Pod 向系统申请 PV。一旦资源申请成功,Kubernetes 就会把 PV 和 PVC 关联在一起,这个动作叫做“绑定”(bind)。
但是,系统里的存储资源非常多,如果要 PVC 去直接遍历查找合适的PV也很麻烦,所以就要用到 StorageClass。
StorageClass 的作用有点像 IngressClass,它抽象了特定类型的存储系统(比如 Ceph、NFS),在 PVC 和 PV 之间充当“协调人”的角色,帮助 PVC 找到合适的 PV。也就是说它可以简化 Pod 挂载“虚拟盘”的过程,让 Pod 看不到 PV 的实现细节。
用生活中的例子来类比一下:假设你在公司里想要10张纸打印资料,于是你给前台打电话讲清楚了需求。
- “打电话”这个动作,就相当于PVC,向Kubernetes申请存储资源。
- 前台里有各种牌子的办公用纸,大小、规格也不一样,这就相当于StorageClass。
- 前台根据你的需要,挑选了一个品牌,再从库存里拿出一包A4纸,可能不止10张,但也能够满足要求,就在登记表上新添了一条记录,写上你在某天申领了办公用品。这个过程就是PVC到PV的绑定。
- 而最后到你手里的A4纸包,就是PV存储对象。
接下来我们结合 YAML 描述来慢慢体会这些概念。
# 1.3 用 YAML 描述 PV
Kubernetes 里有很多种类型的 PV,我们先看看最容易的本机存储“HostPath”,它和 Docker 里挂载本地目录的 -v
参数非常类似,可以用它来初步认识一下 PV 的用法。
因为Pod会在集群的任意节点上运行,所以首先,我们要作为系统管理员在每个节点上创建一个目录,它将会作为本地存储卷挂载到Pod里。
为了省事,我就在 /tmp
里建立名字是 host-10m-pv
的目录,表示一个只有 10MB 容量的存储设备。有了存储,我们就可以使用 YAML 来描述这个 PV 对象了。
不过很遗憾,你不能用 kubectl create
直接创建PV对象,只能用 kubectl api-resources
、 kubectl explain
查看 PV 的字段说明,手动编写 PV 的 YAML 描述文件。
下面我给出一个 YAML 示例,你可以把它作为样板,编辑出自己的 PV:
apiVersion: v1
kind: PersistentVolume
metadata:
name: host-10m-pv
spec:
storageClassName: host-test
accessModes:
- ReadWriteOnce
capacity:
storage: 10Mi
hostPath:
path: /tmp/host-10m-pv/
2
3
4
5
6
7
8
9
10
11
12
13
这里重点看 spec 部分,这部分每个字段都很重要,描述了存储的详细信息。
- storageClassName:就是对存储类型的抽象 StorageClass,这个 PV 是我们手动管理的,名字可以任意起,这里我写的是
host-test
,你也可以把它改成manual
、hand-work
之类的词汇。 - accessModes:定义了存储设备的访问模式,简单来说就是虚拟盘的读写权限,和 Linux 的文件访问模式差不多,目前 Kubernetes 里有 3 种:
- ReadWriteOnce:存储卷可读可写,但只能被一个节点上的 Pod 挂载。
- ReadOnlyMany:存储卷只读不可写,可以被任意节点上的 Pod 多次挂载。
- ReadWriteMany:存储卷可读可写,也可以被任意节点上的 Pod 多次挂载。
- capacity:表示存储设备的容量,这里设置为 10MB。
- 再次提醒,Kubernetes 里定义存储容量使用的是国际标准,我们日常习惯使用的KB/MB/GB的基数是1024,要写成Ki/Mi/Gi,一定要小心不要写错了,否则单位不一致实际容量就会对不上。
- hostPath:指定了存储卷的本地路径,也就是我们在节点上创建的目录。
你要注意,3 种 accessMode 限制的对象是节点而不是 Pod,因为存储是系统级别的概念,不属于 Pod 里的进程。显然,本地目录只能是在本机使用,所以这个 PV 使用了 ReadWriteOnce
。
在上述 YAML 中,用这些字段把 PV 的类型、访问模式、容量、存储位置都描述清楚,一个存储设备就创建好了。
# 1.4 用 YAML 描述 PVC
有了 PV,就表示集群里有了这么一个持久化存储可以供 Pod 使用,我们需要再定义 PVC 对象,向 Kubernetes 申请存储。
下面这份 YAML 就是一个 PVC,要求使用一个 5MB 的存储设备,访问模式是 ReadWriteOnce
:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: host-5m-pvc
spec:
storageClassName: host-test
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Mi
2
3
4
5
6
7
8
9
10
11
12
PVC 的内容与 PV 很像,但它不表示实际的存储,而是一个“申请”或者“声明”,spec 里的字段描述的是对存储的“期望状态”。所以 PVC 里的 storageClassName
、 accessModes
和 PV是一样的,但不会有字段 capacity
,而是要用 resources.request
表示希望要有多大的容量。
这样,Kubernetes 就会根据PVC里的描述,去找能够匹配 StorageClass 和容量的 PV,然后把 PV 和 PVC bind 在一起,实现存储的分配,和前面打电话要 A4 纸的过程差不多。
PV 与 PVC 的关系就如 Pod 与 Node 的关系一样。
# 1.5 在 k8s 里使用 PV
现在我们已经准备好了 PV 和 PVC,就可以让 Pod 实现持久化存储了。
首先需要使用 kubectl apply
创建 PV 对象:
kubectl apply -f host-path-pv.yml
然后用 kubectl get pv
查看状态:
从截图里我们可以看到,这个 PV 的容量是 10MB,访问模式是 RWO(ReadWriteOnce),StorageClass 是我们自己定义的 host-test
,状态显示的是 Available
,也就是处于可用状态,可以随时分配给 Pod 使用。
接下来我们创建 PVC,申请存储资源:
kubectl apply -f host-path-pvc.yml
kubectl get pvc
2
你会看到PVC会一直处于 Pending
状态,这意味着Kubernetes在系统里没有找到符合要求的存储,无法分配资源,只能等有满足要求的PV才能完成绑定。
# 1.6 为 Pod 挂载 PV
PV 和 PVC 绑定好了,有了持久化存储,现在我们就可以为 Pod 挂载存储卷。
大致做法就是:现在 spec.volumes
定义存储卷,然后在 containers.volumeMounts
挂载进容器。
不过因为我们用的是 PVC,所以要在 volumes
里用字段 persistentVolumeClaim
指定 PVC 的名字。
下面就是 Pod 的 YAML 描述文件,把存储卷挂载到了 Nginx 容器的 /tmp
目录:
apiVersion: v1
kind: Pod
metadata:
name: host-pvc-pod
spec:
volumes:
- name: host-pvc-vol
persistentVolumeClaim:
claimName: host-5m-pvc
containers:
- name: ngx-pvc-pod
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: host-pvc-vol
mountPath: /tmp
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
这个过程中,Pod、PVC 和 PV 的关系如下图(省略了字段 accessModes):
现在我们创建这个 Pod,查看它的状态:
kubectl apply -f host-path-pod.yml
kubectl get pod -o wide
2
它被 Kubernetes 调到了worker节点上,那么 PV 是否确实挂载成功了呢?让我们用 kubectl exec
进入容器,执行一些命令看看:
容器的 /tmp
目录里生成了一个 a.txt
的文件,根据PV的定义,它就应该落在worker节点的磁盘上,所以我们就登录worker节点检查一下:
你会看到确实在worker节点的本地目录有一个 a.txt
的文件,再对一下时间,就可以确认是刚才在Pod里生成的文件。
注意是 worker 节点,不一定是你执行 kubectl apply 的节点。
因为 Pod 产生的数据已经通过 PV 存在了磁盘上,所以如果 Pod 删除后再重新创建,挂载存储卷时会依然使用这个目录,数据保持不变,也就实现了持久化存储。
不过还有一点小问题,因为这个PV是HostPath类型,只在本节点存储,如果Pod重建时被调度到了其他节点上,那么即使加载了本地目录,也不会是之前的存储位置,持久化功能也就失效了。
所以,HostPath 类型的PV一般用来做测试,或者是用于 DaemonSet 这样与节点关系比较密切的应用,我们下节课再讲实现真正任意的数据持久化。
# 1.7 小结
这一章学习了 Kubernetes 里应对持久化存储的解决方案,一共有三个 API 对象:
- PersistentVolume简称为 PV,是 Kubernetes 对存储设备的抽象,由系统管理员维护,需要描述清楚存储设备的类型、访问模式、容量等信息。
- PersistentVolumeClaim 简称为 PVC,代表 Pod 向系统申请存储资源,它声明对存储的要求,Kubernetes 会查找最合适的PV然后绑定。
- StorageClass抽象特定类型的存储系统,归类分组 PV 对象,用来简化 PV/PVC 的绑定过程。
HostPath 是最简单的一种 PV,数据存储在节点本地,速度快但不能跟随 Pod 迁移。
课外小贴士:
- Kubernetes 有一种特殊形式的存储卷,叫 emptyDir,它的生命周期与 Pod 相同,比容器长,但不是持久存储,可以用做暂存或者缓存。
- 如果存储系统符合 CSI 标准,那么 accessModes 里还可以使用“ReadWriteOncePod”属性,只允许单个 Pod 读写,控制的粒度更精细。
- KB/MB/GB 与 KiB/MiB/GiB 的用法混乱由来已久好像最早是由 Windows 误用引起的,而 Mac 一直使用的是 1000 作为基数的 MB/GB,而各种磁盘的标称容量也用的是 MB/GB。
# 2. PersistentVolume + NFS:怎么使用网络共享存储?
PV 实现了为 Pod 挂载一块“虚拟盘”,让 Pod 在其中任意读写数据。不过上一节我们用的是 HostPath,这样的存储卷只能在本机使用,而 Pod 经常在集群内“漂移”,所以这种方式不是特别实用。
如果想让存储卷真正能被 Pod 任意挂载,我们需要变更存储为网络存储,这样 Pod 无论在哪运行,只要知道 IP 或 domain,就可以通过网络通信访问存储设备。
网络存储一直是一个热门领域,有很多知名产品,比如 AWS、Azure、Ceph 等,Kubernetes 还专门定义了 CSI(Container Storage Interface)规范,不过这些存储类型的安装、使用都比较复杂,在我们的实验环境里部署难度比较高。
我们今天主要选用简单的 NFS(Network File System),依此为例讲解如何在 Kubernetes 中使用网络存储。
# 2.1 安装 NFS 服务器
作为一个经典的网络存储系统,NFS有着近40年的发展历史,基本上已经成为了各种UNIX系统的标准配置,Linux自然也提供对它的支持。
NFS 采用的是Client/Server架构,需要选定一台主机作为Server,安装NFS服务端;其他要使用存储的主机作为Client,安装NFS客户端工具。
# 2.2 安装 NFS 客户端
// 跳过
# 2.3 如何使用 NFS 存储卷
现在我们已经为Kubernetes配置好了NFS存储系统,就可以使用它来创建新的PV存储对象了。
先来手工分配一个存储卷,需要指定 storageClassName
是 nfs
,而 accessModes
可以设置成 ReadWriteMany
,这是由NFS的特性决定的,它 支持多个节点同时访问一个共享目录。
因为这个存储卷是NFS系统,所以我们还需要在YAML里添加 nfs
字段,指定NFS服务器的IP地址和共享目录名。
这里我在NFS服务器的 /tmp/nfs
目录里又创建了一个新的目录 1g-pv
,表示分配了1GB的可用存储空间,相应的,PV里的 capacity
也要设置成同样的数值,也就是 1Gi
。
把这些字段都整理好后,我们就得到了一个使用NFS网络存储的YAML描述文件:
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-1g-pv
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
capacity:
storage: 1Gi
nfs:
path: /tmp/nfs/1g-pv
server: 192.168.10.208
2
3
4
5
6
7
8
9
10
11
12
13
14
15
现在就可以用命令 kubectl apply
来创建PV对象,再用 kubectl get pv
查看它的状态:
kubectl apply -f nfs-static-pv.yml
kubectl get pv
2
再次提醒你注意, spec.nfs
里的IP地址一定要正确,路径一定要存在(事先创建好),否则Kubernetes按照PV的描述会无法挂载NFS共享目录,PV就会处于“pending”状态无法使用。
有了PV,我们就可以定义申请存储的PVC对象了,它的内容和PV差不多,但不涉及NFS存储的细节,只需要用 resources.request
来表示希望要有多大的容量,这里我写成1GB,和PV的容量相同:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-static-pvc
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
2
3
4
5
6
7
8
9
10
11
12
13
14
创建PVC对象之后,Kubernetes就会根据PVC的描述,找到最合适的PV,把它们“绑定”在一起,也就是存储分配成功:
我们再创建一个Pod,把PVC挂载成它的一个 volume,具体的做法和之前的一样,用 persistentVolumeClaim
指定PVC的名字就可以了:
apiVersion: v1
kind: Pod
metadata:
name: nfs-static-pod
spec:
volumes:
- name: nfs-pvc-vol
persistentVolumeClaim:
claimName: nfs-static-pvc
containers:
- name: nfs-pvc-test
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: nfs-pvc-vol
mountPath: /tmp
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Pod、PVC、PV和NFS存储的关系可以用下图来形象地表示,你可以对比一下HostPath PV的用法,看看有什么不同:
因为我们在PV/PVC里指定了 storageClassName
是 nfs
,节点上也安装了NFS客户端,所以Kubernetes就会自动执行NFS挂载动作,把NFS的共享目录 /tmp/nfs/1g-pv
挂载到Pod里的 /tmp
,完全不需要我们去手动管理。
最后还是测试一下,用 kubectl apply
创建Pod之后,我们用 kubectl exec
进入Pod,再试着操作NFS共享目录:
退出Pod,再看一下NFS服务器的 /tmp/nfs/1g-pv
目录,你就会发现Pod里创建的文件确实写入了共享目录:
而且更好的是,因为NFS是一个网络服务,不会受Pod调度位置的影响,所以只要网络通畅,这个PV对象就会一直可用,数据也就实现了真正的持久化存储。
# 2.4 如何部署 NFS Provisioner
在之前的使用过程中,PV 需要人工管理,但在一个大集群中,每天都可能有成百上千的应用需要 PV 存储,人力难以手动维护各种存储设备,空间大小也可能精确控制,从而导致分配存储的工作大量积压。
那能不能让计算机来代替人类分配存储卷呢?这在 k8s 里就是动态存储卷的概念:它可以用StorageClass绑定一个Provisioner对象,而这个Provisioner就是一个能够自动管理存储、创建PV的应用,代替了原来系统管理员的手工劳动。
它可以用StorageClass绑定一个Provisioner对象,而这个Provisioner就是一个能够自动管理存储、创建PV的应用,代替了原来系统管理员的手工劳动。
目前,Kubernetes里每类存储设备都有相应的Provisioner对象,对于NFS来说,它的Provisioner就是“NFS subdir external provisioner”,你可以在GitHub上找到这个项目 https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner (opens new window)。
FS Provisioner也是以Pod的形式运行在Kubernetes里的, 在GitHub的 deploy
目录里是部署它所需的YAML文件,一共有三个,分别是rbac.yaml、class.yaml和deployment.yaml。
不过这三个文件只是示例,想在我们的集群里真正运行起来还要修改其中的两个文件。
第一个要修改的是rbac.yaml,它使用的是默认的 default
名字空间,应该把它改成其他的名字空间,避免与普通应用混在一起,你可以用“查找替换”的方式把它统一改成 kube-system
。
第二个要修改的是deployment.yaml,它要修改的地方比较多。首先要把名字空间改成和rbac.yaml一样,比如是 kube-system
,然后重点要修改 volumes
和 env
里的IP地址和共享目录名,必须和集群里的NFS服务器配置一样。
按照我们当前的环境设置,就应该把IP地址改成 192.168.10.208
,目录名改成 /tmp/nfs
:
spec:
template:
spec:
serviceAccountName: nfs-client-provisioner
containers:
...
env:
- name: PROVISIONER_NAME
value: k8s-sigs.io/nfs-subdir-external-provisioner
- name: NFS_SERVER
value: 192.168.10.208 #改IP地址
- name: NFS_PATH
value: /tmp/nfs #改共享目录名
volumes:
- name: nfs-client-root
nfs:
server: 192.168.10.208 #改IP地址
Path: /tmp/nfs #改共享目录名
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
还有一件麻烦事,deployment.yaml的镜像仓库用的是gcr.io,拉取很困难,而国内的镜像网站上偏偏还没有它,为了让实验能够顺利进行,我不得不“曲线救国”,把它的镜像转存到了Docker Hub上。所以你还需要把镜像的名字由原来的“k8s.gcr.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2”改成“chronolaw/nfs-subdir-external-provisioner:v4.0.2”,其实也就是变动一下镜像的用户名而已。
把这两个YAML修改好之后,我们就可以在Kubernetes里创建NFS Provisioner了:
kubectl apply -f rbac.yaml
kubectl apply -f class.yaml
kubectl apply -f deployment.yaml
2
3
使用命令 kubectl get
,再加上名字空间限定 -n kube-system
,就可以看到NFS Provisioner在Kubernetes里运行起来了。
# 2.5 如何使用 NFS 动态存储卷
比起静态存储卷,动态存储卷的用法简单了很多。因为有了Provisioner,我们就不再需要手工定义PV对象了,只需要在PVC里指定StorageClass对象,它再关联到Provisioner。
我们来看一下NFS默认的StorageClass定义:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
archiveOnDelete: "false"
2
3
4
5
6
7
8
YAML里的关键字段是 provisioner
,它指定了应该使用哪个Provisioner。另一个字段 parameters
是调节Provisioner运行的参数,需要参考文档来确定具体值,在这里的 archiveOnDelete: "false"
就是自动回收存储空间。
理解了StorageClass的YAML之后,你也可以不使用默认的StorageClass,而是根据自己的需求,任意定制具有不同存储特性的StorageClass,比如添加字段 onDelete: "retain"
暂时保留分配的存储,之后再手动删除:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client-retained
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
parameters:
onDelete: "retain"
2
3
4
5
6
7
8
接下来我们定义一个PVC,向系统申请10MB的存储空间,使用的StorageClass是默认的 nfs-client
:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-dyn-10m-pvc
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Mi
2
3
4
5
6
7
8
9
10
11
12
13
写好了PVC,我们还是在Pod里用 volumes
和 volumeMounts
挂载,然后Kubernetes就会自动找到NFS Provisioner,在NFS的共享目录上创建出合适的PV对象:
apiVersion: v1
kind: Pod
metadata:
name: nfs-dyn-pod
spec:
volumes:
- name: nfs-dyn-10m-vol
persistentVolumeClaim:
claimName: nfs-dyn-10m-pvc
containers:
- name: nfs-dyn-test
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: nfs-dyn-10m-vol
mountPath: /tmp
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
使用 kubectl apply
创建好PVC和Pod,让我们来查看一下集群里的PV状态:
从截图你可以看到,虽然我们没有直接定义PV对象,但由于有NFS Provisioner,它就自动创建一个PV,大小刚好是在PVC里申请的10MB。
如果你这个时候再去NFS服务器上查看共享目录,也会发现多出了一个目录,名字与这个自动创建的PV一样,但加上了名字空间和PVC的前缀:
这里把 Pod、PVC、StorageClass 和 Provisioner 的关系画成了一张图:
# 2.6 小结
好了,今天的这节课里我们继续学习PV/PVC,引入了网络存储系统,以NFS为例研究了静态存储卷和动态存储卷的用法,其中的核心对象是 StorageClass 和 Provisioner。
小结一下这一章的要点:
- 在Kubernetes集群里,网络存储系统更适合数据持久化,NFS是最容易使用的一种网络存储系统,要事先安装好服务端和客户端。
- 可以编写PV手工定义NFS静态存储卷,要指定NFS服务器的IP地址和共享目录名。
- 使用NFS动态存储卷必须要部署相应的Provisioner,在YAML里正确配置NFS服务器。
- 动态存储卷不需要手工定义PV,而是要定义StorageClass,由关联的Provisioner自动创建PV完成绑定。
课外小贴士:
- NFS Provisioner 的名字其实是由 YAML 里的环境变量“PROVISIONER NAME”指定的,如果觉得原来的名字太长也可以改名,但关联的 StorageClass 也必须同步修改。
- StorageClass 里的 OnDelete、archiveOnDelete 源自 PV “存储回收策略”,指定 PV 被销毁时数据是"保留(Retain)"还是"删除(Delete)"。
# 3. StatefulSet:怎么管理有状态的应用?
之前学习的 Deployment 和 DaemonSet 只能管理无状态应用,不能管理有状态应用。这一章看一下 k8s 为什么要设计一个新对象 StatefulSet 来管理有状态应用。
# 3.1 什么是有状态应用
像 nginx 这种就是无状态应用,而 Redis、MySQL 就是有状态应用,这些有状态的应用不允许因重启而丢失了状态,比如 Redis 和 MySQL 的状态就是内存与磁盘中的数据,这也是应用的核心价值所在。
也许通过 Deployment + PV 可以实现有状态的应用,但 Kubernetes 的眼光更全面,它认为“状态”不仅仅是数据持久化,在集群化、分布式的场景里,还有多实例的依赖关系、启动顺序和网络标识等问题需要解决,而这些问题恰恰是Deployment力所不及的。
比如如果只使用 Deployment,多个实例之间是无关的,启动的顺序不固定,Pod的名字、IP地址、域名也都是完全随机的,这正是“无状态应用”的特点。
但对于有状态的应用,多个实例之间可能存在依赖关系,比如master/slave、active/passive,需要依次启动才能保证应用正常运行,外界的客户端也可能要使用固定的网络标识来访问实例,而且这些信息还必须要保证在Pod重启后不变。
所以,Kubernetes就在Deployment的基础之上定义了一个新的API对象,名字也很好理解,就叫 StatefulSet,专门用来管理有状态的应用。
# 3.2 用 YAML 描述 StatefulSet
StatefulSet 简称 sts,其对象描述与 Deployment 差不多。如下是一个 Redis 的 StatefulSet:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-sts
spec:
serviceName: redis-svc
replicas: 2
selector:
matchLabels:
app: redis-sts
template:
metadata:
labels:
app: redis-sts
spec:
containers:
- image: redis:5-alpine
name: redis
ports:
- containerPort: 6379
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
我们会发现,YAML 文件里除了 kind
必须是 StatefulSet
,在 spec
里还多出了一个 serviceName
字段,其余的部分和 Deployment 是一模一样的,比如 replicas
、 selector
、 template
等等。
这两个不同之处其实就是StatefulSet与Deployment的关键区别。想要真正理解这一点,我们得结合StatefulSet在Kubernetes里的使用方法来分析。
# 3.3 在 Kubernetes 中使用 StatefulSet
让我们用 kubectl apply
创建 StatefulSet 对象,用 kubectl get
先看看它是什么样的:
kubectl apply -f redis-sts.yml
kubectl get sts
kubectl get pod
2
3
从截图里,你应该能够看到,StatefulSet所管理的Pod不再是随机的名字了,而是有了顺序编号,从0开始分别被命名为 redis-sts-0
、 redis-sts-1
,Kubernetes也会按照这个顺序依次创建(0号比1号的AGE要长一点),这就解决了有状态应用的第一个问题:启动顺序。
有了启动的先后顺序,应用该怎么知道自己的身份,进而确定互相之间的依赖关系呢?
Kubernetes 给出的方法是:使用hostname,也就是每个Pod里的主机名,让我们再用 kubectl exec
登录Pod内部看看:
kubectl exec -it redis-sts-0 -- sh
在Pod里查看环境变量 $HOSTNAME
或者是执行命令 hostname
,都可以得到这个Pod的名字 redis-sts-0
。有了这个唯一的名字,应用就可以自行决定依赖关系了,比如在这个Redis例子里,就可以让先启动的0号Pod是主实例,后启动的1号Pod是从实例。
解决了启动顺序和依赖关系,还剩下第三个问题:网络标识,这就需要用到Service对象。下面我们写一个 Service 的 yaml,它的 metadata.name
必须是和 StatefulSet 中的 serviceName
相同,selector
里的标签也必须和 StatefulSet 里的一致:
apiVersion: v1
kind: Service
metadata:
name: redis-svc
spec:
selector:
app: redis-sts
ports:
- port: 6379
protocol: TCP
targetPort: 6379
2
3
4
5
6
7
8
9
10
11
12
13
写好 Service 之后,还是用 kubectl apply
创建这个对象:
可以看到这个 Service 并没有什么特殊的地方,也是用标签选择器找到 StatefulSet 管理的两个 Pod,然后找到它们的 IP 地址。
不过,StatefulSet 的奥秘就在它的域名上。之前说过,每个 Service 自己会有一个格式为 "对象名.名字空间" 的域名,每个 Pod 也会有一个域名,形式为 "IP地址.名字空间"。但因为IP地址不稳定,所以Pod的域名并不实用,一般我们会使用稳定的Service域名。
当我们把Service对象应用于StatefulSet的时候,情况就不一样了。Service 发现这些 Pod 不是一般的应用,而是有状态应用,需要有稳定的网络标识,所以就会为 Pod 再多创建出一个新的域名,格式是“Pod名.服务名.名字空间.svc.cluster.local”。当然,这个域名也可以简写成“Pod名.服务名”。
我们还是用 kubectl exec
进入Pod内部,用ping命令来验证一下:
kubectl exec -it redis-sts-0 -- sh
显然,在StatefulSet里的这两个Pod都有了各自的域名,也就是稳定的网络标识。那么接下来,外部的客户端只要知道了StatefulSet对象,就可以用固定的编号去访问某个具体的实例了,虽然Pod的IP地址可能会变,但这个有编号的域名由Service对象维护,是稳定不变的。
到这里,通过StatefulSet和Service的联合使用,Kubernetes就解决了“有状态应用”的依赖关系、启动顺序和网络标识这三个问题,剩下的多实例之间内部沟通协调等事情就需要应用自己去想办法处理了。
关于Service,有一点值得再多提一下。
Service原本的目的是负载均衡,应该由它在Pod前面来转发流量,但是对StatefulSet来说,这项功能反而是不必要的,因为Pod已经有了稳定的域名,外界访问服务就不应该再通过Service这一层了。所以,从安全和节约系统资源的角度考虑,我们可以在Service里添加一个字段 clusterIP: None
,告诉Kubernetes不必再为这个对象分配IP地址。
下图展示了 StatefulSet 与 Service 对象的关系:
# 3.4 StatefulSet 的数据持久化
现在StatefulSet已经有了固定的名字、启动顺序和网络标识,只要再给它加上数据持久化功能,我们就可以实现对“有状态应用”的管理了。
这里就需要用到 PV 和 NFS 的知识,我们可以很容易地定义StorageClass,然后编写PVC,再给Pod挂载Volume。
不过,为了强调持久化存储与StatefulSet的一对一绑定关系,Kubernetes为StatefulSet专门定义了一个字段“volumeClaimTemplates”,直接把PVC定义嵌入StatefulSet的YAML文件里。这样能保证创建StatefulSet的同时,就会为每个Pod自动创建PVC,让StatefulSet的可用性更高。
“volumeClaimTemplates”这个字段好像有点难以理解,你可以把它和Pod的
template
、Job的jobTemplate
对比起来学习,它其实也是一个“套娃”的对象组合结构,里面就是应用了StorageClass的普通PVC而已。
让我们把刚才的 Redis StatefulSet 对象稍微改造一下,加上持久化存储功能:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-pv-sts
spec:
serviceName: redis-pv-svc
volumeClaimTemplates:
- metadata:
name: redis-100m-pvc
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Mi
replicas: 2
selector:
matchLabels:
app: redis-pv-sts
template:
metadata:
labels:
app: redis-pv-sts
spec:
containers:
- image: redis:5-alpine
name: redis
ports:
- containerPort: 6379
volumeMounts:
- name: redis-100m-pvc
mountPath: /data
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
这个YAML文件比较长,内容比较多,不过你只要有点耐心,分功能模块逐个去看也能很快看明白:首先StatefulSet对象的名字是 redis-pv-sts
,表示它使用了PV存储。然后“volumeClaimTemplates”里定义了一个PVC,名字是 redis-100m-pvc
,申请了100MB的NFS存储。在Pod模板里用 volumeMounts
引用了这个PVC,把网盘挂载到了 /data
目录,也就是Redis的数据目录。
下图展示了 StatefulSet 对象完整的关系图:
最后使用 kubectl apply
创建这些对象,一个带持久化功能的“有状态应用”就算是运行起来了:
kubectl apply -f redis-pv-sts.yml
你可以使用命令 kubectl get pvc
来查看 StatefulSet 关联的存储卷状态:
看这两个PVC的命名,不是随机的,是有规律的,用的是PVC名字加上StatefulSet的名字组合而成,所以即使Pod被销毁,因为它的名字不变,还能够找到这个PVC,再次绑定使用之前存储的数据。
那我们就来实地验证一下吧,用 kubectl exec
运行Redis的客户端,在里面添加一些KV数据:
$ kubectl exec -it redis-pv-sts-0 -- redis-cli
127.0.0.1:6379> set a 111
OK
127.0.0.1:6379> set b 222
OK
127.0.0.1:6379> keys *
1) "a"
2) "b"
127.0.0.1:6379> quit
2
3
4
5
6
7
8
9
10
现在我们模拟意外事故,删除这个Pod:kubectl delete pod redis-pv-sts-0
。由于StatefulSet和Deployment一样会监控Pod的实例,发现Pod数量少了就会很快创建出新的Pod,并且名字、网络标识也都会和之前的Pod一模一样:
那Redis里存储的数据怎么样了呢?是不是真的用到了持久化存储,也完全恢复了呢?你可以再用 Redis 客户端登录去检查一下:
因为我们把NFS网络存储挂载到了Pod的 /data
目录,Redis就会定期把数据落盘保存,所以新创建的Pod再次挂载目录的时候会从备份文件里恢复数据,内存里的数据就恢复原状了。
# 3.5 小结
这一章讲了专门部署有状态应用的 API 对象 StatefulSet,它与Deployment非常相似,区别是由它管理的Pod会有固定的名字、启动顺序和网络标识,这些特性对于在集群里实施有主从、主备等关系的应用非常重要。
简单小结:
- StatefulSet的YAML描述和Deployment几乎完全相同,只是多了一个关键字段
serviceName
。 - 要为StatefulSet里的Pod生成稳定的域名,需要定义Service对象,它的名字必须和StatefulSet里的
serviceName
一致。 - 访问StatefulSet应该使用每个Pod的单独域名,形式是“Pod名.服务名”,不应该使用Service的负载均衡功能。
- 在StatefulSet里可以用字段“volumeClaimTemplates”直接定义PVC,让Pod实现数据持久化存储。
课外小贴士:
- StatefulSet 在早期曾经被命名为“PetSet”,意思是应用需要像宠物一样精心照顾,相应地,被Deployment、DaemonSet 管理的应用就是“Cattle”。
- 使用了“clusterlP: None”,没有集群 IP 地址的 Service 对象,也被形象地称为是“Headless Service”
- 有状态的应用管理难度很高,即使定义了StatefulSet 还是有很多麻烦要解决,所以后来又提出了Operator 概念,它有点像是 Kubernetes 里的批处理脚本。