一、前言
在前面的系列K8S初级入门系列之六-控制器(RC/RS/Deployment),K8S初级入门系列之七-控制器(Job/CronJob/Daemonset)我们已经介绍了多种控制器,今天我们将介绍最后一种控制器--StatefulSet,顾名思义,即有状态Set,那什么是无状态和有状态呢?有个形象的比如,牛和宠物。对于牛来说,只要能干活,在农场主眼里没有区别,一头牛死掉了,可以用另外一头牛顶替上,农场主不会纠结是否还是之前那头牛。对于宠物来说,他们的主人是能感受到个体之间区别的,一只宠物死掉了,是无法用另外一只替换。
前面介绍的RC,RS控制器所创建的都是无状态的实例,即实例个体之间没有差别,一旦某个实例挂掉了,可以启动另一个实例顶替上,比如Ngnix实例。但在实际工程中,还有一种是有状态实例,比如数据库,分库1实例和分库2实例是不一样的,分库1挂掉了,是不能直接启动一个实例顶替上的,而是要确保新启的实例在网络标识,主机标识,存储数据等方面保持一致,才能不影响业务运行。
显然,对于以上有状态的实例控制,RC,RS控制器是无法做到的,针对有状态场景,K8S提供StatuefulSet控制器。
二、StatefulSet原理
StatefulSet适用于有状态实例,那么它是如何确保状态的一致呢?还是以数据库的分库为例,需要保持网络标识,主机标识,存储卷一致。如果用之前的RS来实现,看下实现的原理图:
需要为每个实例创建一个ReplicaSet,分配独立的PVC,以及提供对外访问的Service。这个方式虽然可以实现需求,但并不是完美的解决方案,如果分库数量过大(超过100),那么维护的工作量将非常大。
接下来,我们看下StatefulSet如何实现
StatefuSet负责创建不同实例的Pod,每个实例按照数字顺序进行标识,同时分配与实例关联的PVC,以及Service。
当某个实例挂死后,重建实例,并写入旧实例的标识,并关联就实例的PVC,虽然实例的实体发生变化,但是对外业务感知不到的,还是同一实例,可以理解为旧实例的复制体。如下图所示:
三、StatefulSet创建
在创建前,我们首先预备存储PV,以及StatefulSet所依赖的Headless Service。
1、创建PV
这里我们创建两个PV,分别为a-pv.yaml和b-pv.yaml,以前一章节创建的NFS为存储卷,内容如下:
- a-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: a-pv
labels:
pv: a-pv
spec:
capacity:
storage: 10Mi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
nfs:
path: /nfs/data/a
server: 192.168.16.4
- b-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: b-pv
labels:
pv: b-pv
spec:
capacity:
storage: 10Mi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
nfs:
path: /nfs/data/b
server: 192.168.16.4
执行该文件,并查看状态
[root@k8s-master yaml]# kubectl apply -f a-pv.yaml
persistentvolume/a-pv created
[root@k8s-master yaml]# kubectl apply -f b-pv.yaml
persistentvolume/b-pv created
[root@k8s-master yaml]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
a-pv 10Mi RWX Recycle Available 13s
b-pv 10Mi RWX Recycle Available 4s
PV创建完成,目前处于Available状态。
2、创建Headless Service
StatefulSet需要配套Headless Service,其相关知识可以参见:K8S初级入门系列之八-网络
其yaml内容如下:
[root@k8s-master yaml]# cat stateful-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: stateful-svc
labels:
app: stateful-svc
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx-stateful
其中clusterIP: None表示的就是Headless Service,执行该文件,并查看状态
[root@k8s-master yaml]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
stateful-svc ClusterIP None <none> 80/TCP 23h
3、创建StatefulSet
接下来就来创建今天的主角StatefulSet,其内容如下:
[root@k8s-master yaml]# cat nginx-stateful.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nginx-statefulset
spec:
serviceName: "stateful-svc"
replicas: 2
selector:
matchLabels:
app: nginx-stateful
template:
metadata:
labels:
app: nginx-stateful
spec:
containers:
- name: nginx-stateful
image: nginx:1.14.2
ports:
- containerPort: 80
name: web
volumeMounts:
- name: www
mountPath: /usr/share/nginx/html
volumeClaimTemplates:
- metadata:
name: www
spec:
accessModes: [ "ReadWriteMany" ]
resources:
requests:
storage: 10Mi
StatefulSet与Deployment的结构类似,这里定义了replics为2,也就是两个副本,容器镜像为 nginx:1.14.2,并将目录/usr/share/nginx/html挂载到PVC存储上。
执行该文件,并查看创建状态:
[root@k8s-master yaml]# kubectl apply -f nginx-stateful.yaml
statefulset.apps/nginx-statefulset created
[root@k8s-master yaml]# kubectl get statefulset
NAME READY AGE
nginx-statefulset 2/2 5m14s
我们再看下pod的状态
[root@k8s-master yaml]# kubectl get pod
NAME READY STATUS RESTARTS AGE
nginx-statefulset-0 1/1 Running 0 5s
nginx-statefulset-1 1/1 Running 0 3s
此时,成功创建了2个Pod,并按索引顺序作为名称的后缀。我们再来看下PVC和PV
[root@k8s-master yaml]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
www-nginx-statefulset-0 Bound a-pv 10Mi RWX 6m43s
www-nginx-statefulset-1 Bound b-pv 10Mi RWX 2m26s
[root@k8s-master yaml]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
a-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-0 10m
b-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-1 10m
自动完成了PVC的创建,并且PVC和PV都显示为绑定状态,说明存储卷已经挂载成功。
4、Pod的验证
验证下Pod的访问以及功能是否可用。
在K8S初级入门系列之八-Service核心概念章节,安装了dnsutils工具,使用该工具查看下这两个Pod的ip和域名。
[root@k8s-master ~]# kubectl exec -it dnsutils /bin/sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # nslookup nginx-statefulset-0.stateful-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: nginx-statefulset-0.stateful-svc.default.svc.cluster.local
Address: 10.244.36.71
/ # nslookup nginx-statefulset-1.stateful-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: nginx-statefulset-1.stateful-svc.default.svc.cluster.local
Address: 10.244.36.114
StatefulSet Pod的访问域名格式为Pod名称.Service名称.default.svc.cluster.local。通过nsloolup正确的解析了域名的IP,内部应用就可以使用域名访问不同Pod了。
接下来,我们验证下Pod的访问,首先分别给html写入不同的内容
[root@k8s-master ~]# kubectl exec nginx-statefulset-0 -- sh -c 'echo "this is nginx-statefulset-0" > /usr/share/nginx/html/index.html'
[root@k8s-master ~]# kubectl exec nginx-statefulset-1 -- sh -c 'echo "this is nginx-statefulset-1" > /usr/share/nginx/html/index.html'
为了方便起见,我们使用curl 名称访问这两个Pod
[root@k8s-master ~]# curl http://10.244.36.71
this is nginx-statefulset-0
[root@k8s-master ~]# curl http://10.244.36.114
this is nginx-statefulset-1
能正确的访问到内容。我们再看下NFS挂载目录的内容
[root@k8s-master data]# cd a
[root@k8s-master a]# cat index.html
this is nginx-statefulset-0
...
[root@k8s-master data]# cd b
[root@k8s-master b]# cat index.html
this is nginx-statefulset-1
也已经存储了正确的内容。
四、Pod重建
前面介绍,当其中一个Pod发生异常,为了确保副本数的一致,StatefulSet重建需要重建该Pod,且新建的Pod会保持主机标识,网络标识,以及存储等状态不变。下面我们将通过删除Pod(nginx-statefulset-0)来模拟。
为了监控Pod的重建过程,我们打开两个终端
终端1,执行删除Pod指令
[root@k8s-master b]# kubectl delete pod nginx-statefulset-0
pod "nginx-statefulset-0" deleted
终端2,监控Pod的变化。
[root@k8s-master yaml]# kubectl get pod -w -l app=nginx-stateful
NAME READY STATUS RESTARTS AGE
nginx-statefulset-0 1/1 Running 0 23h
nginx-statefulset-1 1/1 Running 0 23h
nginx-statefulset-0 1/1 Terminating 0 23h
nginx-statefulset-0 1/1 Terminating 0 23h
nginx-statefulset-0 0/1 Terminating 0 23h
nginx-statefulset-0 0/1 Terminating 0 23h
nginx-statefulset-0 0/1 Terminating 0 23h
nginx-statefulset-0 0/1 Pending 0 0s
nginx-statefulset-0 0/1 Pending 0 0s
nginx-statefulset-0 0/1 ContainerCreating 0 0s
nginx-statefulset-0 0/1 ContainerCreating 0 1s
nginx-statefulset-0 1/1 Running 0 2s
可以看到,老的Pod变为Terminating后,新的Pod开始重建,状态变化Pending->ContainerCreating->Running。
重建过程中,Pod的名称保持不变,再看下重建后,新Pod的网络标识。
[root@k8s-master ~]# kubectl exec -it dnsutils /bin/sh
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
/ # nslookup nginx-statefulset-0.stateful-svc.default.svc.cluster.local
Server: 10.96.0.10
Address: 10.96.0.10#53
Name: nginx-statefulset-0.stateful-svc.default.svc.cluster.local
Address: 10.244.36.70
IP发生了变化,域名没有变化,所以在内部访问时,要使用域名访问,不能使用IP。
继续看下挂载的PVC,可以使用详情查看
[root@k8s-master b]# kubectl describe pod nginx-statefulset-0
...
Volumes:
www:
Type: PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
ClaimName: www-nginx-statefulset-0
ReadOnly: false
....
绑定到了之前的PVC www-nginx-statefulset-0。再访问index内容
[root@k8s-master ~]# curl http://10.244.36.70
this is nginx-statefulset-0
也能正确访问到老Pod写入的内容。由此可见,重建的Pod保持了存储标识的一致。
五、扩缩容
StatefulSet与RS一样,支持扩缩容。
1、扩容
先来看下扩容场景,我们需要扩容两个副本,首先创建两个PV,其yaml内容如下:
- c-pv.yaml
[root@k8s-master yaml]# cat c-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: c-pv
labels:
pv: c-pv
spec:
capacity:
storage: 10Mi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
nfs:
path: /nfs/data/c
server: 192.168.16.4
- d-pv.yaml
[root@k8s-master yaml]# cat d-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: d-pv
labels:
pv: d-pv
spec:
capacity:
storage: 10Mi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
nfs:
path: /nfs/data/d
server: 192.168.16.4
执行完成后,查看状态
[root@k8s-master yaml]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
a-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-0 2d10h
b-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-1 2d10h
c-pv 10Mi RWX Recycle Available 6m35s
d-pv 10Mi RWX Recycle Available 2m9s
新创建的两个PV处于Available状态。接下来启动两个终端
终端1,执行扩容指令
[root@k8s-master yaml]# kubectl scale sts nginx-statefulset --replicas=4
statefulset.apps/nginx-statefulset scaled
终端2,监控pod的变化
[root@k8s-master yaml]# kubectl get pod -w -l app=nginx-stateful
NAME READY STATUS RESTARTS AGE
nginx-statefulset-0 1/1 Running 0 2d11h
nginx-statefulset-1 1/1 Running 0 3d11h
nginx-statefulset-2 0/1 Pending 0 0s
nginx-statefulset-2 0/1 Pending 0 0s
nginx-statefulset-2 0/1 Pending 0 1s
nginx-statefulset-2 0/1 ContainerCreating 0 1s
nginx-statefulset-2 0/1 ContainerCreating 0 2s
nginx-statefulset-2 1/1 Running 0 3s
nginx-statefulset-3 0/1 Pending 0 0s
nginx-statefulset-3 0/1 Pending 0 0s
nginx-statefulset-3 0/1 Pending 0 1s
nginx-statefulset-3 0/1 ContainerCreating 0 1s
nginx-statefulset-3 0/1 ContainerCreating 0 2s
nginx-statefulset-3 1/1 Running 0 3s
可以看到,以索引递增的命名方式依次新扩容了两个Pod。
再看下PVC和PV的状态
[root@k8s-master yaml]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
nfs-storage-pvc Bound pvc-f795e09b-1583-436b-869b-88a68d28ce64 1Gi RWX nfs-client 11d
www-nginx-statefulset-0 Bound a-pv 10Mi RWX 3d11h
www-nginx-statefulset-1 Bound b-pv 10Mi RWX 3d11h
www-nginx-statefulset-2 Bound c-pv 10Mi RWX 6m27s
www-nginx-statefulset-3 Bound d-pv 10Mi RWX 6m24s
[root@k8s-master yaml]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
a-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-0 3d11h
b-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-1 3d11h
c-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-2 25h
d-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-3 25h
PVC状态正确,并与PV绑定。
同样,验证下新扩容的nginx-statefulset-2是否可用
[root@k8s-master yaml]# kubectl exec nginx-statefulset-2 -- sh -c 'echo "this is nginx-statefulset-2" > /usr/share/nginx/html/index.html'
[root@k8s-master yaml]# curl http://10.244.36.85
this is nginx-statefulset-2
能正确的写入和读取,扩容成功。
2、缩容
再将这两个Pod缩容掉,同样启用两个终端
终端1,执行缩容指令
[root@k8s-master yaml]# kubectl scale sts nginx-statefulset --replicas=2
statefulset.apps/nginx-statefulset scaled
终端2,监控pod的变化
[root@k8s-master yaml]# kubectl get pod -w -l app=nginx-stateful
NAME READY STATUS RESTARTS AGE
nginx-statefulset-0 1/1 Running 0 2d11h
nginx-statefulset-1 1/1 Running 0 3d11h
nginx-statefulset-2 1/1 Running 0 15m
nginx-statefulset-3 1/1 Running 0 14m
nginx-statefulset-3 1/1 Terminating 0 16m
nginx-statefulset-3 1/1 Terminating 0 16m
nginx-statefulset-3 0/1 Terminating 0 16m
nginx-statefulset-3 0/1 Terminating 0 16m
nginx-statefulset-3 0/1 Terminating 0 16m
nginx-statefulset-2 1/1 Terminating 0 16m
nginx-statefulset-2 1/1 Terminating 0 16m
nginx-statefulset-2 0/1 Terminating 0 16m
nginx-statefulset-2 0/1 Terminating 0 16m
nginx-statefulset-2 0/1 Terminating 0 16m
可以看到,Pod按照倒序索引依次终止。
六、滚动更新
StatefulSet支持更新,可以通过配置相关的更新策略,默认为"RollingUpdate"(滚动更新),如下所示:
spec:
updateStrategy:
type: RollingUpdate
....
接下来,我们通过滚动更新的方式将nginx的镜像版本升级到最新状态
终端1,执行更新执行,修改镜像的版本
[root@k8s-master yaml]# kubectl set image sts nginx-statefulset nginx-stateful=nginx:latest
statefulset.apps/nginx-statefulset image updated
终端2,监控Pod的变化
[root@k8s-master ~]# kubectl get pod -w -l app=nginx-stateful
NAME READY STATUS RESTARTS AGE
nginx-statefulset-0 1/1 Running 0 2d13h
nginx-statefulset-1 1/1 Running 0 3d12h
nginx-statefulset-1 1/1 Terminating 0 3d12h
nginx-statefulset-1 1/1 Terminating 0 3d12h
nginx-statefulset-1 0/1 Terminating 0 3d12h
nginx-statefulset-1 0/1 Terminating 0 3d12h
nginx-statefulset-1 0/1 Terminating 0 3d12h
nginx-statefulset-1 0/1 Pending 0 0s
nginx-statefulset-1 0/1 Pending 0 0s
nginx-statefulset-1 0/1 ContainerCreating 0 0s
nginx-statefulset-1 0/1 ContainerCreating 0 1s
nginx-statefulset-1 1/1 Running 0 71s
nginx-statefulset-0 1/1 Terminating 0 2d13h
nginx-statefulset-0 1/1 Terminating 0 2d13h
nginx-statefulset-0 0/1 Terminating 0 2d13h
nginx-statefulset-0 0/1 Terminating 0 2d13h
nginx-statefulset-0 0/1 Terminating 0 2d13h
nginx-statefulset-0 0/1 Pending 0 0s
nginx-statefulset-0 0/1 Pending 0 0s
nginx-statefulset-0 0/1 ContainerCreating 0 0s
nginx-statefulset-0 0/1 ContainerCreating 0 1s
nginx-statefulset-0 1/1 Running 0 3s
可以看到,按照倒序依次更新。待上一个Pod处于running状态后,才会启动下一个的更新。
查看下升级后Pod实例的版本
[root@k8s-master yaml]# kubectl describe pod nginx-statefulset-1
...
Containers:
nginx-stateful:
Container ID: docker://4caa6d45b6e6423d61bea97f93a680f7171c56462eaaa5e8af83bbe2d1917a6b
Image: nginx:latest
...
可以看到,Pod已经升级到最新版本。
除了 RollingUpdate,还支持
- OnDelete,删除升级策略,StatefulSet并不主动触发升级,而是要用户删除Pod,StatefulSet重建Pod时进行升级,实际是一种手动升级的方式。
- partition,分区升级策略,用户指定一个序号,大于此序号的Pod实例全部升级,而小于此序号的Pod实例则依旧保持老版本不变,即使是这些Pod重建,依然使用老的版本。此策略一般用于按计划分步骤的升级,如金丝雀发布。
七,删除
StatefulSet 同时支持级联和非级联删除。使用非级联方式删除 StatefulSet 时,StatefulSet 的 Pod 不会被删除。使用级联删除时,StatefulSet 和它的 Pod 都会被删除。
这里我们看下级联删除
[root@k8s-master yaml]# kubectl delete sts nginx-statefulset
statefulset.apps "nginx-statefulset" deleted
[root@k8s-master yaml]# kubectl get sts
No resources found in default namespace.
[root@k8s-master yaml]# kubectl get pod -l app=nginx-stateful
No resources found in default namespace.
此时Pod已经被删除,再看下svc,pvc,pv的状态
[root@k8s-master yaml]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
stateful-svc ClusterIP None <none> 80/TCP 4d13h
[root@k8s-master yaml]# kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
www-nginx-statefulset-0 Bound a-pv 10Mi RWX 3d13h
www-nginx-statefulset-1 Bound b-pv 10Mi RWX 3d13h
www-nginx-statefulset-2 Bound c-pv 10Mi RWX 128m
www-nginx-statefulset-3 Bound d-pv 10Mi RWX 127m
[root@k8s-master yaml]# kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
a-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-0 3d13h
b-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-1 3d13h
c-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-2 27h
d-pv 10Mi RWX Recycle Bound default/www-nginx-statefulset-3 27h
与之相关联的Service,PVC,PV并没有删除,需要手动删除。
非级联删除,相对于级联删除,指令上需要增加"--cascade=orphan"后缀参数。
[root@k8s-master yaml]# kubectl delete sts nginx-statefulset --cascade=orphan
八、总结
本章节介绍了有状态控制器StatefulSet,有状态实例有自己的主机标识,网络标识,存储标识的一致,实例之间不能相互替代,Pod实例重建时需要保持状态的一致,使得业务无感。 StatefulSet能根据一份容器镜像完成多个有状态的副本的状态,并自动创建PVC,并绑定PV,同时,与Deployment一样,StatefulSet控制器支持副本重建,扩缩容,滚动更新等。
附:
K8S初级入门系列之一-概述
K8S初级入门系列之二-集群搭建
K8S初级入门系列之三-Pod的基本概念和操作
K8S初级入门系列之四-Namespace/ConfigMap/Secret
K8S初级入门系列之五-Pod的高级特性
K8S初级入门系列之六-控制器(RC/RS/Deployment)
K8S初级入门系列之七-控制器(Job/CronJob/Daemonset)
K8S初级入门系列之八-网络
K8S初级入门系列之九-共享存储
K8S初级入门系列之十-控制器(StatefulSet)
K8S初级入门系列之十一-安全
K8S初级入门系列之十二-计算资源管理