有状态应用

对于在ReplicaSet作业副本与水平扩展中使用deployment控制器,其功能并不足以覆盖所有容器编排的场景。因为对于deployment来说一个应用的所有 Pod,是完全一样的。所以,它们互相之间没有顺序,也无所谓运行在哪台宿主机上。 但是实际的应用,特别是分布式应用,它的多个实例之间,是有依赖关系的,比如:主从关系,主备关系。

这种实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态应用”。基于控制器模式的设计思想,对有状态应用的编排有了初步的支持,即StatefulSet

这里的状态抽象成了两种:

  • 拓扑状态。就是应用启动的拓扑关系图,应用的多个实例因为不是对等关系,所以需要按照一定的顺序进行启动。
  • 存储状态。应用的多个实例保存了不同的存储数据,对于一个实例来说,其生命周期读取的数据应该是一致的。

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

拓扑状态

在介绍StatefulSet工作原理之前,需要先了解一下Service对外访问中的Headless Serivce的内容。因为StatefulSet是通过Headless Service的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

这里的StatefulSet其定义与deployment的不同就多了一个serviceName=nginx字段。这个字段就是告诉StatefulSet控制器,在执行循环控制(Control Loop)的时候,使用名为nginx的这个Headless Service来保证Pod的可解析身份。 在上面的StatefulSet的声明当中,创建了副本为2的标签为app=nginx的Pod,然后使用名为nginx的Service来代理这两个Pod。

创建在Service对外访问中的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

查看Pod,可以发现StatefulSet给它所管理的Pod名字进行了编号,为<statefulset name>-<index>,索引从0开始:

$ kubectl get pods
NAME    READY   STATUS    RESTARTS   AGE
web-0   1/1     Running   0          4m43s
web-1   1/1     Running   0          3m49s

Note

StatefulSet所管理的Pod的创建,是严格按照编号顺序进行的。在web-0进入到Running状态,并且细分状态为Ready之前,web-1会一直处于Pending状态。

进入容器查看hostname可以发现,创建的Pod的hostname与Pod的名字是一致的。

$ kubectl exec web-0 -- sh -c 'hostname'
web-0
$ kubectl exec web-1 -- sh -c 'hostname'
web-1

StatefulSet通过给其管理的Pod进行编号,来固定拓扑关系

因为Pod的不对等性,所以使用过程中需要访问特定的一个Pod,而不是任意一个POD,于是StatefulSet还利用Headeless Service的DNS记录来为每一个Pod提供固定且唯一的访问入口

比如:web-0是一个主节点,web-1是一个从节点,编号保证了web-0要咸鱼web-1启动,并且为它们分配的dns,保证了可以想要访问主节点和从节点的需求。

接下来便试着以dns的方式访问这个Headless Service。启动一个Pod,然后在这个Pod的容器里,解析一下StatefulSet创建的两个Pod的dns。

$ 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

因为使用Service对外提供统一的入口,就算这两个Pod重启,ip发生了变化,照样能够通过Service提供的固定地址访问。

删除这两个Pod,让StatefulSet自动重建

$ kubectl delete pod -l app=nginx
pod "web-0" deleted
pod "web-1" deleted

重建之后再一次启动一个Pod,然后解析dns就会发现,就算新的Pod的IP发生了变化,仍然能够通过dns访问。

总结

StatefulSet控制器的主要作用之一,就是在利用Pod模板创建Pod的时候,对它们进行了编号,然后按照编号顺序完成创建工作。在控制循环发现状态不一致需要进行调谐的时候,也会严格按照顺序来完成。此外,还通过Headless Service的方式,来为每一个Pod的实例提供了唯一且稳定的DNS记录作为访问入口。

存储状态

StatefulSet对存储状态的管理,使用了名为Persistent Volume Claim的机制。

在前面的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

这里使用了volumeClaimTemplates字段,与Pod模板作用类似,被这个StatefulSet管理的Pod都会声明一个对应的PVC,这个PVC的名字会分配一个与这个Pod完全一致的编号

自动创建的PVC与PV绑定之后,就会进入Bound状态,Pod可以挂载并使用这个PV了。绑定的前提是,运维人员已经在系统中创建好了符合条件的PVC;或者,k8s集群运行在公有云上,这样k8s就会通过Dynamic Provisioning的方式,自动创建与PVC配的PV

根据新的YAML文件创建StatefulSet,就可以看到k8s集群里的两个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状态。

向Pod挂载的目录中写入文件,就是记录到其对应的PV中去。

就算Pod被删除重建,仍然能够从重建后的Pod中读取到先前存放的文件。这是因为Pod被删除,但是Pod对应的PVC和PV并不会被删除,新的Pod命名方式不会变,其查找的PVC的名称也不会变,所以能够找到旧的Pod留下的同名的PVC,进而找到PVC绑定的PV

这样StatefulSet就实现了对应用存储状态的管理。

总结

StatefulSet是为了管理有状态应用的,就是应用之间是存在不对等关系的,比如主从,主备等。 StatefulSet是一个控制器,不过与Deployment不同的是:

  • StatefulSet的控制器直接管理的是Pod。原因还是在于应用之间是不对等的,不能像ReplicaSet一样将所有应用看成一样的。StatefulSet直接管理Pod,对所有Pod进行编号。
  • 通过Headless Service为这些有编号的Pod,在DNS服务器中生成带有同样编号的DNS记录。因为应用的不对等关系,所以需要具有能够访问特定一个应用的能力。
  • 为每一个Pod分配并创建一个同样编号的PVC。这样就可以通过PVC与PV机制为每一个Pod都分配一个独立的Volume,存储状态。

再简单点说,StatefulSet就是特殊的Deployment,为其管理的Pod引入编号来保存拓扑结构。使用Headless Service来为每一个Pod提供唯一且不变的访问DNS,使用PVC/PV机制来保存每一个Pod的状态。这就是其管理有状态应用的方式。


tags: 容器编排 k8s 控制器