k8s系列-StatefulSet-拓扑状态

K8S-StatefulSet

引入

Deployment不足以满足所有的应用编排问题,造成这个问题的根本原因,因为Deployment对应用做了一个简单的假设,一个应用的所有Pod是完全一样的,它们之间没有顺序,也无所谓到底运行在哪一台宿主机上,需要的时候Deployment可以根据Pod模板创建新的Pod,不需要的时候Deployment就可以杀掉任意一个Pod。

这样的话并不能满足我们的所有场景,多个实例之间可能存在依赖关系,比如主从关系、主备关系。还有一些自身实例中自己带有一些缓存数据的,实例一旦被杀掉虽然能够重建出来但是数据已经丢了。

这种实例之间有不对等关系,以及实例对外部数据有依赖的,称为有状态应用,Deployment适合用来部署无状态应用,容器技术对于有状态应用来说,困难程度直线上升,一开始有状态应用几乎成了容器技术圈子的忌讳,不过Kubernetes还是成为了第一个吃螃蟹的人,这个吃螃蟹的工具就是StatefulSet

介绍

StatefulSet的设计非常容易理解,将真实世界里的应用状态分为如下两种:

拓扑状态

拓扑状态意味着应用的多个实例之间不是完全的对等关系,这些应用的实例必须按照一定的顺序启动,比如主节点A和从节点B,主节点A必须先于从节点B启动,删除了重新创建的时候也必须满足这个顺序,并且新创建出来的Pod必须和原来的Pod标识一样,这样原先的访问者才能使用原先的方法,访问到新的Pod

存储状态

队友应用的多个实例来说,应用的多个实例绑定了不同的存储数据,对于这些应用实例来说,Pod A第一次读取到的数据,和隔了十分钟读取到的数据应该是同一份,哪怕Pod A在这个期间被重新创建过,典型例子比如数据库应用的多个存储实例

StatefulSet的核心功能就是通过某种方式记录Pod的这些状态,让Pod在重建时能够为新Pod恢复这些状态。

Service和Headless Service介绍

讲述StatefulSet工作原理之前,必须先讲解一下Headless Service,Service是Kubernetes中将Pod暴露访问的一种机制,定义好了Pod,只要能访问到这个Service就能访问到Pod,Service如何被访问的呢?

VIP

第一种方式就是Service的VIP(Virtual IP)方式,即虚拟IP的方式,比如我看到Service的IP地址是10.0.23.1,我访问这个10.0.23.1其实就是个虚拟IP,它会把请求转发到Service所代理的一个Pod上

DNS

Service的DNS方式,比如这时候只要我访问my-svc.my-namespace.svc.cluster.local这条DNS记录,就可以访问电脑名为my-svc代理的Pod。

Service DNS方式下,分为两种处理方法:

第一种处理方法,是Normal Service。这种情况下访问my-svc.my-namespace.svc.cluster.local解析到的就是my-svc的VIP,然后就是VIP的方式继续访问。

第二种处理方法,就是Headless Service,这种情况下你访问my-svc.my-namespace.svc.cluster.local解析到的就是my-svc代理的某一个Pod的IP地址,这里的区别就在于,你不需要为这个Service分配虚拟IP,而是可以直接以DNS记录的方式解析出被代理的Pod的IP地址。

这么样设计有何作用,先从Headless Service的定义方式看看再解释

Headless Service定义

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None
selector:
app: nginx

将上面这个yaml文件使用kubectl create -f xxx.yaml创建,这里的字段大多和普通的Service一样,只是clusterIP字段的值是None,也就是这个Service没有一个VIP作为头,这样这个Service被创建后就没有VIP,而是会以DNS记录的方式暴露出它所代理的Pod,他所代理的Pod就是Pod上携带了app=nginx标签的Pod。

创建完这个Headless Service之后,他所代理的所有的Pod的IP地址,都被绑定一个这样的DNS记录:

1
<pod-name>.<svc-name>.<namespace>.svc.cluster.local

这个DNS记录,就是Kubernetes项目为Pod分配的唯一的可解析身份,有个这种访问方式,知道pod的名字和service的名字就能直接通过这条DNS记录访问到IP地址了。

StatefulSet定义

我们再来编写一个StatefulSet的YAML文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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文件的定义就是比Deployment一文中的nginx-deployment多了一个serviceName=nginx的属性,这个字段就是告诉StatefulSet控制器在执行控制循环的时候请使用nginx这个Headless Service来保证Pod的可解析身份。

创建这个StatefulSet

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@localhost home]# kubectl create -f statefulSet_test1.yml
statefulset.apps/web created

# 查看StatefulSet
[root@localhost home]# kubectl get statefulset
NAME READY AGE
web 2/2 8m13s

#查看Pod
[root@localhost home]# kubectl get pods
NAME READY STATUS RESTARTS AGE
web-0 1/1 Running 0 9m
web-1 1/1 Running 0 66s

从上面pod观察可以发现,StatefulSet新建的Pod后面都有个编号,从0开始,Pod的创建也是按照顺序来创建的,比如web-0进入Running状态,并且READY之前,web-1都会处于Pending状态,两个Pod都进入了Running状态之后,可以查看到它们唯一的网络身份了。

现在来试试用之前说到的dns记录能不能访问到这些pod,使用如下命令创建一个一次性Pod,在里面使用nslookup解析Pod对应的Headless Service

1
2
3
4
5
#创建一个一次性的Pod,-rm命令意味着Pod退出后就会被删除掉(busybox的latest镜像一直解析dns失败,用老版本就可以)
[root@localhost home]# kubectl run -i --tty --image busybox:1.28.3 dns-test --restart=Never --rm /bin/sh

#执行如下命令可以查看到这个web-0的pod地址
nslookup web-0.nginx

上面的命令执行成功之后会自动进入pod中
image

StatefulSet的Pod重启

我们先删除掉这两个Pod,因为存在StatefulSet的缘故,检测到这两个Pod下线会重新启动Pod起来

1
2
3
[root@localhost ~]# kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted

接下来我们使用如下命令来观测这个app=nginx的Pod的状态,(执行完删除得快点执行这条命令不然看不到效果了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[root@localhost ~]# kubectl get pod -w -l app=nginx
NAME READY STATUS RESTARTS AGE
web-0 0/1 Terminating 2 8d
web-1 0/1 Terminating 2 8d
web-1 0/1 Terminating 2 8d
web-1 0/1 Terminating 2 8d
web-0 0/1 Terminating 2 8d
web-0 0/1 Terminating 2 8d
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 2s
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 1s

从上面我们可以发现,web-0和web-1被停掉以后,web-0经过PendingContainerCreatingRunning三个阶段重新运行起来,接下来才是web-1有开始经过这三个阶段,可以看出来,Kubernetes会按照原先编号的顺序,创建两个新的Pod,并且现在他们的网络身份依然是web-0.nginx

再去执行上面的创建busybox的pod

1
[root@localhost home]# kubectl run -i --tty --image busybox:1.28.3 dns-test --restart=Never --rm /bin/sh

可以看到结果如下,web-0.nginx的解析的ip address已经有了修改
image

所以我们对于有状态应用的实例访问,应该使用DNS记录或者hostname,而不是Pod的IP地址。

总结如下:StatefulSet这个控制器在创建Pod的时候会对Pod进行编号,并且按照编号顺序逐一完成创建工作,而当StatefulSet的控制循环发现期望状态和实际状态不一致的时候,需要新建、删除Pod操作的时候,会按照Pod的顺序完成这些操作。