文章目录
- 前言
- 一、StatefulSet拓扑状态
- 1.1 Service访问的两种方式
- 1.2 无头Service为下面的每个Pod提供了可解析身份
- 1.3 StatefulSet 使用 Service DNS 域名来维持 Pod 的拓扑状态
- StatefulSet按顺序启动,保持拓扑状态(主从关系)
- 删除两个Pod重新启动,保持拓扑状态(主从关系)
- 1.4 StatefulSet拓扑状态小结
- 二、StatefulSet存储状态
- 2.1 Pod直接持久化
- 2.2 Pod使用PV/PVC持久化
- 2.3 StatefulSet使用PV/PVC持久化
- 2.4 删除Pod数据不丢失
- 2.5 StatefulSet 进行“滚动更新”
- 尾声
前言
Deployment 实际上并不足以覆盖所有的应用编排问题,造成这个问题的根本原因,在于 Deployment 对应用做了一个简单化假设。它认为,一个应用的所有 Pod,是完全一样的。所以,它们互相之间没有顺序,也无所谓运行在哪台宿主机上。需要的时候,Deployment 就可以通过 Pod 模板创建新的 Pod;不需要的时候,Deployment 就可以“杀掉”任意一个 Pod。 但是,在实际的场景中,并不是所有的应用都可以满足这样的要求。
- 分布式应用,它的多个实例,往往有依赖关系,比如:主从关系、主备关系。这些实例一旦被杀掉,重建出来,主从关系需要继续保持;
- 数据存储类应用,它的多个实例,往往都会在本地磁盘上保存一份数据。这些实例一旦被杀掉,重建出来,实例与数据之间的对应关系需要继续保持,否则导致应用失败。
所以,这种实例之间有不对等关系,或者实例对外部数据有依赖关系的应用,就被称为“有状态应用”(Stateful Application)。
容器技术诞生后,大家很快发现,它用来封装“无状态应用”(Stateless Application),尤其是 Web 服务,非常好用。但是,一旦你想要用容器运行“有状态应用”,其困难程度就会直线上升。而且,这个问题解决起来,单纯依靠容器技术本身已经无能为力,这也就导致了很长一段时间内,“有状态应用”几乎成了容器技术圈子的“忌讳”,大家一听到这个词,就纷纷摇头。
不过,Kubernetes 项目还是成为了“第一个吃螃蟹的人”。
- 拓扑状态。这种情况意味着,应用的多个实例之间不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果你把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。
- 存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。这种情况最典型的例子,就是一个数据库应用的多个存储实例。
所以,StatefulSet 的核心功能,就是通过某种方式记录这些状态,然后在 Pod 被重新创建时,能够为新 Pod 恢复这些状态。
一、StatefulSet拓扑状态
1.1 Service访问的两种方式
在开始讲述 StatefulSet 的工作原理之前,我就必须先为你讲解一个 Kubernetes 项目中非常实用的概念:Headless Service。
那么,这个 Service 又是如何被访问的呢?
第一种方式,是以 Service 的 VIP(Virtual IP,即:虚拟 IP)方式。比如:当我访问 10.0.23.1 这个 Service 的 IP 地址时,10.0.23.1 其实就是一个 VIP,它会把请求转发到该 Service 所代理的某一个 Pod 上。
第二种方式,就是以 Service 的 DNS 方式。比如:这时候,只要我访问“my-svc.my-namespace.svc.cluster.local”这条 DNS 记录,就可以访问到名叫 my-svc 的 Service 所代理的某一个 Pod。
第一种处理方法,是 Normal Service。这种情况下,你访问“my-svc.my-namespace.svc.cluster.local”解析到的,正是 my-svc 这个 Service 的 VIP,后面的流程就跟 VIP 方式一致了。
而第二种处理方法,正是 Headless Service 。这种情况下,你访问“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是 my-svc 代理的某一个 Pod 的 IP 地址。可以看到,这里的区别在于,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址。
小结:
普通Service有两种访问方式: VIP(集群内的虚拟IP) 和 “my-svc.my-namespace.svc.cluster.local”
无头Service只有一种访问方式: “my-svc.my-namespace.svc.cluster.local” ,因为无头Service 没有 VIP
无头Service和普通Service区别在于 nslookup 域名,普通Service是到Service IP,无头Service是直接到Pod IP.
那么,这样的设计又有什么作用呢?
1.2 无头Service为下面的每个Pod提供了可解析身份
想要回答这个问题,我们需要从 Headless Service 的定义方式看起。
下面是一个标准的 Headless Service 对应的 YAML 文件:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
可以看到,所谓的 Headless Service,其实仍是一个标准 Service 的 YAML 文件。只不过,它的 clusterIP 字段的值是:None,即:这个 Service,没有一个 VIP 作为“头”。这也就是 Headless 的含义。所以,这个 Service 被创建后并不会被分配一个 VIP,而是会以 DNS 记录的方式暴露出它所代理的 Pod。
而它所代理的 Pod,依然是Label Selector 机制选择出来的,即:所有携带了 app=nginx 标签的 Pod,都会被这个 Service 代理起来。
然后关键来了。
当你按照这样的方式创建了一个 Headless Service 之后,它所代理的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录,如下所示:
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
这个 DNS 记录,正是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”(Resolvable Identity)。
小结:无头Service为下面的每个Pod提供了可解析身份,即 <pod-name>.<svc-name>.<namespace>.svc.cluster.local
, 有了这个“可解析身份”,只要你知道了一个 Pod 的名字,以及它对应的 Service 的名字,你就可以非常确定地通过这条 DNS 记录访问到 Pod 的 IP 地址。
1.3 StatefulSet 使用 Service DNS 域名来维持 Pod 的拓扑状态
StatefulSet按顺序启动,保持拓扑状态(主从关系)
那么,StatefulSet 又是如何使用这个 DNS 记录来维持 Pod 的拓扑状态的呢?
为了回答这个问题,现在我们就来编写一个 StatefulSet 的 YAML 文件,如下所示:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.9.1
ports:
- containerPort: 80
name: web
这个 YAML 文件,和我们在前面文章中用到的 nginx-deployment 的唯一区别,就是多了一个 serviceName=nginx 字段。
这个字段的作用,就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。
所以,当你通过 kubectl create 创建了上面这个 Service 和 StatefulSet 之后,就会看到如下两个对象:
$ kubectl create -f svc.yaml
$ kubectl get service nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP None <none> 80/TCP 10s
$ kubectl create -f statefulset.yaml
$ kubectl get statefulset web
NAME DESIRED CURRENT AGE
web 2 1 19s
这时候,如果你手比较快的话,还可以通过 kubectl 的 -w 参数,即:Watch 功能,实时查看 StatefulSet 创建两个有状态实例的过程:
备注:如果手不够快的话,Pod 很快就创建完了。不过,你依然可以通过这个 StatefulSet 的 Events 看到这些信息。
$ kubectl get pods -w -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 0/1 Pending 0 0s
web-0 0/1 Pending 0 0s
web-0 0/1 ContainerCreating 0 0s
web-0 1/1 Running 0 19s
web-1 0/1 Pending 0 0s
web-1 0/1 Pending 0 0s
web-1 0/1 ContainerCreating 0 0s
web-1 1/1 Running 0 20s
通过上面这个 Pod 的创建过程,我们不难看到,StatefulSet 给它所管理的所有 Pod 的名字,进行了编号,编号规则是:<statefulset name>-<ordinal index>
。
而且这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,绝不重复。
更重要的是,这些 Pod 的创建,也是严格按照编号顺序进行的。比如,在 web-0 进入到 Running 状态、并且细分状态(Conditions)成为 Ready 之前,web-1 会一直处于 Pending 状态。
备注:Ready 状态再一次提醒了我们,为 Pod 设置 livenessProbe 和 readinessProbe 的重要性。
当这两个 Pod 都进入了 Running 状态之后,你就可以查看到它们各自唯一的“网络身份”了。
我们使用 kubectl exec 命令进入到容器中查看它们的 hostname:
$ kubectl exec web-0 -- sh -c 'hostname'
web-0
$ kubectl exec web-1 -- sh -c 'hostname'
web-1
通过这条命令,我们启动了一个一次性的 Pod,因为 --rm 意味着 Pod 退出后就会被删除掉。然后,在这个 Pod 的容器里面,我们尝试用 nslookup 命令,解析一下 Pod 对应的 Headless Service:
$ kubectl run -i --tty --image busybox:1.28.4 dns-test --restart=Never --rm /bin/sh
$ nslookup web-0.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx
Address 1: 10.244.1.7
$ nslookup web-1.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-1.nginx
Address 1: 10.244.2.7
从 nslookup 命令的输出结果中,我们可以看到,在访问 web-0.nginx 的时候,最后解析到的,正是 web-0 这个 Pod 的 IP 地址;而当访问 web-1.nginx 的时候,解析到的则是 web-1 的 IP 地址。
删除两个Pod重新启动,保持拓扑状态(主从关系)
这时候,如果你在另外一个 Terminal 里把这两个“有状态应用”的 Pod 删掉:
$ kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
然后,再在当前 Terminal 里 Watch 一下这两个 Pod 的状态变化,就会发现一个有趣的现象:
$ kubectl get pod -w -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 0/1 ContainerCreating 0 0s
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 2s
web-1 0/1 Pending 0 0s
web-1 0/1 ContainerCreating 0 0s
web-1 1/1 Running 0 32s
可以看到,当我们把这两个 Pod 删除之后,Kubernetes 会按照原先编号的顺序,创建出了两个新的 Pod。并且,Kubernetes 依然为它们分配了与原来相同的“网络身份”:web-0.nginx 和 web-1.nginx。
通过这种严格的对应规则,StatefulSet 就保证了 Pod 网络标识的稳定性。
比如,如果 web-0 是一个需要先启动的主节点,web-1 是一个后启动的从节点,那么只要这个 StatefulSet 不被删除,你访问 web-0.nginx 时始终都会落在主节点上,访问 web-1.nginx 时,则始终都会落在从节点上,这个关系绝对不会发生任何变化。
所以,如果我们再用 nslookup 命令,查看一下这个新 Pod 对应的 Headless Service 的话:
$ kubectl run -i --tty --image busybox dns-test --restart=Never --rm /bin/sh
$ nslookup web-0.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-0.nginx
Address 1: 10.244.1.8
$ nslookup web-1.nginx
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: web-1.nginx
Address 1: 10.244.2.8
我们可以看到,在这个 StatefulSet 中,这两个新 Pod 的“网络标识”(比如:web-0.nginx 和 web-1.nginx),再次解析到了正确的 IP 地址(比如:web-0 Pod 的 IP 地址 10.244.1.8)。
通过这种方法,Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。
这些状态,在 StatefulSet 的整个生命周期里都会保持不变,绝不会因为对应 Pod 的删除或者重新创建而失效。
不过,相信你也已经注意到了,尽管 web-0.nginx 这条记录本身不会变,但它解析到的 Pod 的 IP 地址,并不是固定的。这就意味着,对于“有状态应用”实例的访问,你必须使用 DNS 记录或者 hostname 的方式,而绝不应该直接访问这些 Pod 的 IP 地址。
删除重建Pod,Pod的网络标识不变,通过这种方式,Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。
1.4 StatefulSet拓扑状态小结
问题:StatefulSet 如何保证应用实例的拓扑状态,在 Pod 删除和再创建的过程中保持稳定?
回答:StatefulSet 这个控制器的主要作用之一,就是使用 Pod 模板创建 Pod 的时候,对它们进行编号,并且按照编号顺序逐一完成创建工作。而当 StatefulSet 的“控制循环”发现 Pod 的“实际状态”与“期望状态”不一致,需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作。所以,StatefulSet 其实可以认为是对 Deployment 的改良。
与此同时,通过 Headless Service 的方式,StatefulSet 为每个 Pod 创建了一个固定并且稳定的 DNS 记录,来作为它的访问入口。实际上,在部署“有状态应用”的时候,应用的每个实例拥有唯一并且稳定的“网络标识”,是一个非常重要的假设。
二、StatefulSet存储状态
2.1 Pod直接持久化
介绍 StatefulSet 对存储状态的管理机制。这个机制,主要使用的是一个叫作 Persistent Volume Claim 的功能。在前面介绍 Pod 的时候,我曾提到过,要在一个 Pod 里声明 Volume,只要在 Pod 里加上 spec.volumes 字段即可。然后,你就可以在这个字段里定义一个具体类型的 Volume 了,比如:hostPath。
可是,你有没有想过这样一个场景:如果你并不知道有哪些 Volume 类型可以用,要怎么办呢?
比如,下面这个例子,就是一个声明了 Ceph RBD 类型 Volume 的 Pod:
apiVersion: v1
kind: Pod
metadata:
name: rbd
spec:
containers:
- image: kubernetes/pause
name: rbd-rw
volumeMounts:
- name: rbdpd
mountPath: /mnt/rbd
volumes:
- name: rbdpd
rbd:
monitors:
- '10.16.154.78:6789'
- '10.16.154.82:6789'
- '10.16.154.83:6789'
pool: kube
image: foo
fsType: ext4
readOnly: true
user: admin
keyring: /etc/ceph/keyring
imageformat: "2"
imagefeatures: "layering"
其一,如果不懂得 Ceph RBD 的使用方法,那么这个 Pod 里 Volumes 字段,你十有八九也完全看不懂。
其二,这个 Ceph RBD 对应的存储服务器的地址、用户名、授权文件的位置,也都被轻易地暴露给了全公司的所有开发人员,这是一个典型的信息被“过度暴露”的例子。
这也是为什么,在后来的演化中,Kubernetes 项目引入了一组叫作 Persistent Volume Claim(PVC)和 Persistent Volume(PV)的 API 对象,大大降低了用户声明和使用持久化 Volume 的门槛。
问题:pod - hostPath 、pod - NFS、pod - Ceph 就可以完成持久化,为什么还需要使用 pod - pvc - pv - hostPath 、pod - pvc - pv - NFS、pod - pvc - pv - Ceph ?
回答:(1) 是为了开发与运维相拆分,运维只写 pv - hostPath/NFS/Ceph ,开发只写 pod - pvc ,通过 pvc 来连接运维的 pv
(2) 服务器端具体的存储交给运维,可以保证数据安全性。
如果自己既是开发又是运维,可以直接 pod - hostPath/NFS/Ceph,服务端结构越简单,健壮性越强。
(1) pod - hostPath/NFS/Ceph: 只需要一个 Pod/Deployment/StatefulSet YAML文件就可以了, 定义 Controller 并关联 hostPath/NFS/Ceph;
(2) pod - pvc - pv - hostPath/NFS/Ceph:
需要一个 Pod/Deployment/StatefulSet YAML文件,定义 Controller 并关联 PVC;
需要一个 PVC YAML文件,定义 PVC (设置accessModes和resources/requests, 这两个属性用来自动关联PV);
需要一个 PV YAML文件,定义 PV 并关联 hostPath/NFS/Ceph;
问题:在三层绑定中,Pod与PVC的绑定直接写在 YAML 文件中,没问题;PV与hostPath/NFS/Ceph的绑定直接写在 YAML 文件中,也没问题,但是PVC与PV如何绑定成功,如何判断是否绑定成功?
回答:(1) 如何绑定成功:PVC中有两个属性
- storage: 1Gi,表示我想要的 Volume 大小至少是 1 GiB;(PV要能和这个PVC绑定,需要大于等于1 GiB)
- accessModes: ReadWriteOnce,表示这个 Volume 的挂载方式是可读写,并且只能被挂载在一个节点上而非被多个节点共享。 (PV 提供的权限也应该是这样)
(2) 判断是否绑定成功:输入 kubectl get pvc -o wide -A 命令,PVC与PV绑定成功,状态是 Bound;否则状态是 UnBound。
2.2 Pod使用PV/PVC持久化
举个例子,有了 PVC 之后,一个开发人员想要使用一个 Volume,只需要简单的两步即可。
第一步:定义一个 PVC,声明想要的 Volume 的属性:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
可以看到,在这个 PVC 对象里,不需要任何关于 Volume 细节的字段,只有描述性的属性和定义。比如,
storage: 1Gi,表示我想要的 Volume 大小至少是 1 GiB;(PV要能和这个PVC绑定,需要大于等于1 GiB)
accessModes: ReadWriteOnce,表示这个 Volume 的挂载方式是可读写,并且只能被挂载在一个节点上而非被多个节点共享。 (PV 提供的权限也应该是这样)
备注:关于哪种类型的 Volume 支持哪种类型的 AccessMode,你可以查看 Kubernetes 项目官方文档中的详细列表。
Persistent Volumes | Kubernetes
https://kubernetes.io/docs/concepts/storage/persistent-volumes/#access-modes
第二步:在应用的 Pod 中,声明使用这个 PVC:
apiVersion: v1
kind: Pod
metadata:
name: pv-pod
spec:
containers:
- name: pv-container
image: nginx
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: pv-storage
volumes:
- name: pv-storage
persistentVolumeClaim:
claimName: pv-claim
可以看到,在这个 Pod 的 Volumes 定义中,我们只需要声明它的类型是persistentVolumeClaim,然后指定 PVC 的名字,而完全不必关心 Volume 本身的定义。
这时候,只要我们创建这个 PVC 对象,Kubernetes 就会自动为它绑定一个符合条件的 Volume。可是,这些符合条件的 Volume 又是从哪里来的呢?
答案是,它们来自于由运维人员维护的 PV(Persistent Volume)对象。接下来,我们一起看一个常见的 PV 对象的 YAML 文件:
kind: PersistentVolume
apiVersion: v1
metadata:
name: pv-volume
labels:
type: local
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
rbd:
monitors:
# 使用 kubectl get pods -n rook-ceph 查看 rook-ceph-mon- 开头的 POD IP 即可得下面的列表
- '10.16.154.78:6789'
- '10.16.154.82:6789'
- '10.16.154.83:6789'
pool: kube
image: foo
fsType: ext4
readOnly: true
user: admin
keyring: /etc/ceph/keyring
可以看到,这个 PV 对象的 spec.rbd 字段,正是我们前面介绍过的 Ceph RBD Volume 的详细定义。而且,它还声明了这个 PV 的容量是 10 GiB。这样,Kubernetes 就会为我们刚刚创建的 PVC 对象绑定这个 PV。
所以,Kubernetes 中 PVC 和 PV 的设计,实际上类似于“接口”和“实现”的思想。开发者只要知道并会使用“接口”,即:PVC;而运维人员则负责给“接口”绑定具体的实现,即:PV。
这种解耦,就避免了因为向开发者暴露过多的存储系统细节而带来的隐患。此外,这种职责的分离,往往也意味着出现事故时可以更容易定位问题和明确责任,从而避免“扯皮”现象的出现。
2.3 StatefulSet使用PV/PVC持久化
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.9.1
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
上面是 Pod 使用 PVC ,这里是 StatefulSet 使用 PVC
Pod 关联/使用 PVC: 使用 volumes 属性实现,给当前 Pod 声明一个 PVC
StatefulSet 关联/使用 PVC: 使用 volumeClaimTemplates 属性实现,给 StatefulSet 管理的每个Pod 都声明一个 PVC
这次,我们为这个 StatefulSet 额外添加了一个 volumeClaimTemplates 字段。从名字就可以看出来,它跟 Deployment 里 Pod 模板(PodTemplate)的作用类似。也就是说,凡是被这个 StatefulSet 管理的 Pod,都会声明一个对应的 PVC;而这个 PVC 的定义,就来自于 volumeClaimTemplates 这个模板字段。更重要的是,这个 PVC 的名字,会被分配一个与这个 Pod 完全一致的编号。
这个自动创建的 PVC,与 PV 绑定成功后,就会进入 Bound 状态,这个我们需要的稳定的状态,这就意味着这个 Pod 可以挂载并使用这个 PV 了。
如果你还是不太理解 PVC 的话,可以先记住这样一个结论:PVC 其实就是一种特殊的 Volume。只不过一个 PVC 具体是什么类型的 Volume,要在跟某个 PV 绑定之后才知道。关于 PV、PVC 更详细的知识,我会在容器存储部分做进一步解读。
当然,PVC 与 PV 的绑定得以实现的前提是,运维人员已经在系统里创建好了符合条件的 PV(比如,我们在前面用到的 pv-volume);或者,你的 Kubernetes 集群运行在公有云上,这样 Kubernetes 就会通过 Dynamic Provisioning 的方式,自动为你创建与 PVC 匹配的 PV (storageClass也可以根据PVC的需求自动创建PV)。
问题:开发这边声明一个PVC,运维就需要一个PV,很难核对?
回答:运维端使用storageClass可以根据PVC的需求自动创建PV.
所以,我们在使用 kubectl create 创建了 StatefulSet 之后,就会看到 Kubernetes 集群里出现了两个 PVC:
$ kubectl create -f statefulset.yaml
$ kubectl get pvc -l app=nginx
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
www-web-0 Bound pvc-15c268c7-b507-11e6-932f-42010a800002 1Gi RWO 48s
www-web-1 Bound pvc-15c79307-b507-11e6-932f-42010a800002 1Gi RWO 48s
可以看到,这些 PVC,都以“<PVC 名字 >-<StatefulSet 名字 >-< 编号 >”的方式命名,并且处于 Bound 状态。
我们前面已经讲到过,这个 StatefulSet 创建出来的所有 Pod,都会声明使用编号的 PVC。比如,在名叫 web-0 的 Pod 的 volumes 字段,它会声明使用名叫 www-web-0 的 PVC,从而挂载到这个 PVC 所绑定的 PV。
所以,我们就可以使用如下所示的指令,在 Pod 的 Volume 目录里写入一个文件,来验证一下上述 Volume 的分配情况:
$ for i in 0 1; do kubectl exec web-$i -- sh -c 'echo hello $(hostname) > /usr/share/nginx/html/index.html'; done
如上所示,通过 kubectl exec 指令,我们在每个 Pod 的 Volume 目录里,写入了一个 index.html 文件。这个文件的内容,正是 Pod 的 hostname。比如,我们在 web-0 的 index.html 里写入的内容就是"hello web-0"。
此时,如果你在这个 Pod 容器里访问“http://localhost”,你实际访问到的就是 Pod 里 Nginx 服务器进程,而它会为你返回 /usr/share/nginx/html/index.html 里的内容。这个操作的执行方法如下所示:
$ for i in 0 1; do kubectl exec -it web-$i -- curl localhost; done
hello web-0
hello web-1
2.4 删除Pod数据不丢失
如果你使用 kubectl delete 命令删除这两个 Pod,这些 Volume 里的文件会不会丢失呢?
$ kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted
可以看到,正如我们前面介绍过的,在被删除之后,这两个 Pod 会被按照编号的顺序被重新创建出来。而这时候,如果你在新创建的容器里通过访问“http://localhost”的方式去访问 web-0 里的 Nginx 服务:
# 在被重新创建出来的Pod容器里访问http://localhost
$ kubectl exec -it web-0 -- curl localhost
hello web-0
就会发现,这个请求依然会返回:hello web-0。也就是说,原先与名叫 web-0 的 Pod 绑定的 PV,在这个 Pod 被重新创建之后,依然同新的名叫 web-0 的 Pod 绑定在了一起。对于 Pod web-1 来说,也是完全一样的情况。
为什么新建出来的Pod,可以恢复数据?
回答:StatefulSet 控制器恢复这个 Pod 的过程
(1) 当你把一个 Pod,比如 web-0,删除之后,这个 Pod 对应的 PVC 和 PV,并不会被删除,而这个 Volume 里已经写入的数据,也依然会保存在远程存储服务里(比如,我们在这个例子里用到的 Ceph 服务器)。
(2) StatefulSet 控制器发现,一个名叫 web-0 的 Pod 消失了。所以,控制器就会重新创建一个新的、名字还是叫作 web-0 的 Pod 来,“纠正”这个不一致的情况。
(3) 在这个新的 Pod 对象的定义里,它声明使用的 PVC 的名字,还是叫作:www-web-0。这个 PVC 的定义,还是来自于 PVC 模板(volumeClaimTemplates),这是 StatefulSet 创建 Pod 的标准流程。
(4) 在这个新的 web-0 Pod 被创建出来之后,Kubernetes 为它查找名叫 www-web-0 的 PVC 时,就会直接找到旧 Pod 遗留下来的同名的 PVC,进而找到跟这个 PVC 绑定在一起的 PV。
这样,新的 Pod 就可以挂载到旧 Pod 对应的那个 Volume,并且获取到保存在 Volume 里的数据。
2.5 StatefulSet 进行“滚动更新”
修改 StatefulSet 的 Pod 模板,就会自动触发“滚动更新(rolling update)”:
$ kubectl patch statefulset mysql --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"mysql:5.7.23"}]'
statefulset.apps/mysql patched
在这里,我使用了 kubectl patch 命令。它的意思是,以“补丁”的方式(JSON 格式的)修改一个 API 对象的指定字段,也就是我在后面指定的“spec/template/spec/containers/0/image”。
这样,StatefulSet Controller 就会按照与 Pod 编号相反的顺序,从最后一个 Pod 开始,逐一更新这个 StatefulSet 管理的每个 Pod。而如果更新发生了错误,这次“滚动更新”就会停止。此外,StatefulSet 的“滚动更新”还允许我们进行更精细的控制,比如金丝雀发布(Canary Deploy)或者灰度发布,这意味着应用的多个实例中被指定的一部分不会被更新到最新的版本。
这个字段,正是 StatefulSet 的 spec.updateStrategy.rollingUpdate 的 partition 字段。比如,现在我将前面这个 StatefulSet 的 partition 字段设置为 2:
$ kubectl patch statefulset mysql -p '{"spec":{"updateStrategy":{"type":"RollingUpdate","rollingUpdate":{"partition":2}}}}'
statefulset.apps/mysql patched
其中,kubectl patch 命令后面的参数(JSON 格式的),就是 partition 字段在 API 对象里的路径。所以,上述操作等同于直接使用 kubectl edit 命令,打开这个对象,把 partition 字段修改为 2。
这样,我就指定了当 Pod 模板发生变化的时候,比如 MySQL 镜像更新到 5.7.23,那么只有序号大于或者等于 2 的 Pod 会被更新到这个版本。并且,如果你删除或者重启了序号小于 2 的 Pod,等它再次启动后,也会保持原先的 5.7.2 版本,绝不会被升级到 5.7.23 版本。
尾声
StatefulSet 的设计思想:StatefulSet 其实就是一种特殊的 Deployment,而其独特之处在于,它的每个 Pod 都被编号了。而且,这个编号会体现在 Pod 的名字和 hostname 等标识信息上,这不仅代表了 Pod 的创建顺序,也是 Pod 的重要网络标识(即:在整个集群里唯一的、可被访问的身份)。
StatefulSet 有了这个编号后(相对于Deployment),StatefulSet 就使用 Kubernetes 里的两个标准功能:Headless Service 和 PV/PVC,实现了对 Pod 的拓扑状态和存储状态的维护。
StatefulSet 下的每个Pod有了这个编号后,删除重建Pod,Pod的网络标识不变,通过这种方式,Kubernetes 就成功地将 Pod 的拓扑状态(比如:哪个节点先启动,哪个节点后启动),按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。
StatefulSet 下的每个Pod有了这个编号后,删除重建Pod,Pod的网络标识不变,新的 Pod 对象的定义里,它声明使用的 PVC 的名字,还是叫作:www-web-0,就会直接找到旧 Pod 遗留下来的同名的 PVC,进而找到跟这个 PVC 绑定在一起的 PV。
普通Service有两种访问方式: VIP(集群内的虚拟IP) 和 “my-svc.my-namespace.svc.cluster.local”
无头Service只有一种访问方式: “my-svc.my-namespace.svc.cluster.local” ,因为无头Service 没有 VIP
无头Service和普通Service区别在于 nslookup 域名,普通Service是到Service IP,无头Service是直接到Pod IP.
问题:pod - hostPath 、pod - NFS、pod - Ceph 就可以完成持久化,为什么还需要使用 pod - pvc - pv - hostPath 、pod - pvc - pv - NFS、pod - pvc - pv - Ceph ?
回答:(1) 是为了开发与运维相拆分,运维只写 pv - hostPath/NFS/Ceph ,开发只写 pod - pvc ,通过 pvc 来连接运维的 pv
(2) 服务器端具体的存储交给运维,可以保证数据安全性。
如果自己既是开发又是运维,可以直接 pod - hostPath/NFS/Ceph,服务端结构越简单,健壮性越强。
(1) pod - hostPath/NFS/Ceph: 只需要一个 Pod/Deployment/StatefulSet YAML文件就可以了, 定义 Controller 并关联 hostPath/NFS/Ceph;
(2) pod - pvc - pv - hostPath/NFS/Ceph:
需要一个 Pod/Deployment/StatefulSet YAML文件,定义 Controller 并关联 PVC;
需要一个 PVC YAML文件,定义 PVC (设置accessModes和resources/requests, 这两个属性用来自动关联PV);
需要一个 PV YAML文件,定义 PV 并关联 hostPath/NFS/Ceph;
问题:在三层绑定中,Pod与PVC的绑定直接写在 YAML 文件中,没问题;PV与hostPath/NFS/Ceph的绑定直接写在 YAML 文件中,也没问题,但是PVC与PV如何绑定成功,如何判断是否绑定成功?
回答:(1) 如何绑定成功:PVC中有两个属性
- storage: 1Gi,表示我想要的 Volume 大小至少是 1 GiB;(PV要能和这个PVC绑定,需要大于等于1 GiB)
- accessModes: ReadWriteOnce,表示这个 Volume 的挂载方式是可读写,并且只能被挂载在一个节点上而非被多个节点共享。 (PV 提供的权限也应该是这样)
(2) 判断是否绑定成功:输入 kubectl get pvc -o wide -A 命令,PVC与PV绑定成功,状态是 Bound;否则状态是 UnBound。
Pod 关联/使用 PVC: 使用 volumes 属性实现,给当前 Pod 声明一个 PVC
StatefulSet 关联/使用 PVC: 使用 volumeClaimTemplates 属性实现,给 StatefulSet 管理的每个Pod 都声明一个 PVC
问题:开发这边声明一个PVC,运维就需要一个PV,很难核对?
回答:运维端使用storageClass可以根据PVC的需求自动创建PV.
参考资料:Statefulset拓扑状态Statefulset存储状态