在上一节我们了解到了Deployment 的水平扩展/收缩和滚动更新,这足以满足大多数的 web 应用了,但是也不能覆盖到所有的应用。例如数据库的主从这类存在着拓扑关系的应用,Deployment 无法满足现实的应用场景的。
Deployment 满足的容器编排场景,我们称之为无状态应用。而对应的有状态应用中,所谓的有状态表现在以下两个方面:
多个实例之间的顺序关系或者拓扑关系,当容器重启后仍然能够保持顺序关系或者拓扑关系;
多个实例在磁盘上的存储依赖,当容器重启之后依赖关系仍然能够保留,例如对于Mysql,当重启后,master节点对应的存储文件一定是重启前master节点对应的存储。
Kuberenetes 中的 StatefulSet 控制对象正是存储了有状态应用的 “状态”,使得 StatefulSet 对有状态应用容器的支持。
我们通过一个 StatefulSet 的yaml文件来看下其定义:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
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
可以看到与 Deployment 的定义结构完全一样,只是kind为 StatefulSet。同样的 使用apply -f 命令启动该控制对象,并查看 Pod:
kubectl apply -f nginx-statefulset.yaml
statefulset.apps/web created
kubectl get pods
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 9s
web-1 1/1 Running 0 7s
我们再回过头来对比下创建 Deployment 控制对象时所管理的Pod:
kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-deployment-54f57cf6bf-djgwn 1/1 Running 0 79s
nginx-deployment-54f57cf6bf-q4pw2 1/1 Running 0 79s
可以看到Name的明显差异,Deloyment 所管理的 Pod 命名是产生了一个随机的UID,而 StatefulSet 所管理的Pod命名是有明显的顺序的,从0开始依次累加。
不仅仅如此,使用 StatefulSet管理的Pod 启动顺序也有严格的保证,当 n 编号的Pod状态为 Running 之后编号为 n+1 的 Pod 一直都是 Pending 状态。
当Pod A需要被Pod B的访问时,需要定义Pod A的Service,Pod B通过访问 Pod A的Service既可访问到 Pod A。那么Sevice是如何被访问到的呢,有以下两种方式:
Service 拥有自己的虚拟IP(vip);
通过Service的DNS访问,通过DNS访问,又有以下区分:
DNS解析到 Service的VIP,与第一种方式一致,只是多了DNS解析;
Service没有VIP,通过DNS直接解析到Pod的IP。
我们首先看下Service的定义文件:
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx
我们来GET下这个 Service传达的重要点:
定义的类型 kind = Service
暴露 80 端口
看到了熟悉的selector,表示该Service将会应用到所有label含有app=nginx 的Pod;
这里我们需要注意 clusterIP = None,表示这个Service是没有虚拟IP的,对应的 Service访问类型为上文中介绍 Service 的2.b项。
接下来到了本节的重点了,我们结合着 Service 和 StatefulSet 来解释 StatefulSet是如何保证Pod的顺序关系的。
我们在 StatefulSet 的yaml文件的第 6 行插入一段:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: web
spec:
serviceName: "nginx"
replicas: 2
...
这里 serviceName = ngixn 与 上面 Service 定义中的 name = nginx 对应,当 StatefulSet 控制循环 (在上节说到 kubernetes 的所有控制对象,都是使用控制循环来控制其他API 对象的)对Pod进行控制时,使用该Service来保持其解析身份。
解析身份可以理解为当使用固定的DNS访问Pod时,始终保持着 a.demo.com 解析到 web-0 Pod,b.demo.com 解析到 web-1 Pod。
当然在 kubernetes中的DNS并不是这里的 a.demo.com 或者 b.demo.com,而是一组由 pod、service、namespace等拼接起来的唯一标识,其格式如下:
<pod-name>.<svc-name>.<namespace>.svc.cluster.local
当我们在yaml文件中插入 serviceName: "nginx" 后,再次 kubectl apply -f 该文件,同时也创建 上文中的 Service。可以看到两个Pod对应的hostname:
kubectl exec web-0 -- sh -c 'hostname'
web-0
kubectl exec web-1 -- sh -c 'hostname'
web-1
这说明了 web-0容器所对应的DNS记录始终是以 web-0标识的,web-1容器所对应的DNS记录始终也是以 web-1标识的。
我们可以试想下如果我们将这里的 StatefulSet 对象改为 Deployment,由于Pod的名称是随机的,那么也就不会有DNS记录与Pod顺序的关系,这也说明了 StatefulSet 在文章一开始说明的一个特点:
多个实例之间的顺序关系或者拓扑关系,当容器重启后仍然能够保持顺序关系或者拓扑关系
在理解Pod保持存储依赖前我们需要先了解PVC (Persistent Volume Claim)对象和PV(Persistent Volume)对象。Pod的持久化存储离不开 Kubernetes 的 PVC (Persistent Volume Claim)对象和 PV(Persistent Volume)对象。我们先了解下PVC和PV。
作为开发人员可以这样去理解PVC、PV,把PVC看作是接口,PV看作是接口的实现:
PVC是接口,作为使用人员(开发人员),无需关注具体存在哪、怎么存,只需要使用PVC声明自己的Volume就行;
而PV则是接口的实现,具体怎么存、存在哪对于专业的专业的运维人员更熟悉,运维人员“真正”的去实现怎么存、存在哪。
我们先来看一个 PVC的 yaml 定义文件:
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pv-claim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
通过上面的yaml文件我们可以看到,仅仅只是提供了使用者关注的信息,空间大小1GB,权限为可读可写。
对于开发人员,在Pod中声明Volume时,只需要关注我在容器的那个目录需要挂载出来,根据需要的空间是多少,以及权限来选择PVC。我们看一个使用PVC声明Volume的Pod定义文件:
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
在spec.volumeMounts 节点,nginx容器需要将 usr/share/nginx/html目录做持久化存储,使用的volume名称为 pv-storage。
而在 volumes节点声明使用了一个名称为 pv-claim 的PVC。pv-claim正是我们上面示例中的PVC名称。
我们再来看一下PV的yaml文件:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-claim
spec:
capacity:
storage: 5Gi
accessModes:
- ReadWriteOnce
nfs:
path: /data/nfs
server: 192.168.56.103
可以看到PV中需要的空间是5GB,是通过NFS挂载磁盘实现的存储。当创建PVC时,kubernetes会自动绑定符合条件的PV。所以,示例中的PVC会与PV完成绑定。
我们再回过头来看PVC是接口,PV是实现这个比喻。kubernetes 这样把volume的使用和实现通过解耦的方式实现,带来了以下两个好处:
开发者无需关注复杂的存储相关内容,只需要使用就行;
运维人员可以根据需要对存储进行替换(nfs、Ceph RBD等)。
在初步的了解了PV、PVC后,我们回到正题,StatefulSet如何Pod保持存储依赖。
我们首先还是看下一个使用了volume的 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
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
我们可以看到Pod的 user/share/nginx/html 是需要做持久化存储的,而当前StatefulSet管理的 Pod 副本数为2,那么两个Pod是如何保持重启后,Pod读取Volume的顺序是一致的,经过前面对service的讲解,想必大家很容易的就猜想到同样的使用编号顺序。
当我们创建完成上面的StatefulSet后,我们可以看到果然是通过编号区分pvc的。
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESSMODES AGE
www-web-0 Bound pvc-xxx 1Gi RWO 4s
www-web-1 Bound pvc-xxx 1Gi RWO 4s
StatefulSet 处理 PVC 与 Pod的逻辑关系同Service 与 Pod 的逻辑一致,这里就不再详细说明了。但是我们可以从Service 到 Pod 到 PVC整个过程来理解StatefulSet如何保证Pod的顺序/拓扑结构的。
外部访问Servcie时,通过DNS解析到Pod 0 访问Pod 0容器,Pod 0 读取 PVC 0的数据,Pod 1 访问Pod 1容器,Pod 1 读取 PVC 1的数据。当Pod重启,虽然Pod的启动有先后,但是DNS解析到的Pod 0始终读取的是PVC 0的数据,而Pod 1始终读取的是PVC 1的数据。
Deployment通过生成UUID来对管理的对象命名,StatefulSet通过编号来对管理的对象命名,这是Deployment与StatefulSet对管理API对象本质上的不同,正是StatefulSet的编号机制,使得控制对象实现对有状态应用的容器编排管理。