前面了解了ReplicaSet和Deployment两种资源对象的使用,在实际使用过程中,Deployment并不能编排所有类型的应用,对于无状态服务的编排是非常容易的,但是对于有状态服务就无能为力了。先了解下什么是有状态服务和无状态服务:

  • 无状态服务(Stateless Service):该服务运行的实例不会在本地存储需要持久化的数据,并且多个实例对于同一个请求响应是完全一致的。
  • 有状态服务(Stateful Service):该服务运行的实例需要在本地存储持久化数据,比如Mysql数据库,假如现在运行在节点A,那么他的数据就存储在节点A上面,如果这时候把数据库迁到节点B去的话,那么就没有之前的数据了,因为他需要去对应的数据目录里面恢复数据。

比如我们常见的WEB应用,是通过Session来保持用户的登录状态的,如果将Session持久化到节点上,那么该应用就是一个有状态服务。

无状态应用利用Deployment可以很好的进行编排,对于有状态应用,需要考虑的细节就要很多很多了,容器化应用程序最困难的任务之一,就是设计有状态分布式组件的部署体系结构。由于无状态组件没有预定义的启动顺序、集群要求、点对点TCP连接、唯一的网络标识符、正常的启动和终止要求等,因此可以很容易的进行容器化。诸如数据库,大数据分析系统,分布式key/value存储、消息中间件需要有复杂的分布式体系结构,都可能用到上述功能。为此,k8s引入了Stateful这种资源对象来支持这种复杂的需求。Stateful类似于RelicaSet,但是他可以处理Pod的启动顺序,为保留每个Pod的状态设置唯一标识,具有以下几个功能特性:

  • 稳定的、唯一的网络标识符
  • 稳定的、持久化的存储
  • 有序的、优雅的部署和缩放
  • 有序的、优雅的删除和终止
  • 有序的、自动滚动更新

Headless Service

Service是应用服务的抽象,通过Labels为应用提供负载均衡和服务发现,每个Service都会自动分配一个cluster和DNS名,在集群内部可以通过该地址或者通过FDQN的形式来访问服务。比如,一个Deployment有3个Pod,那么就可以定义一个Service,有如下两种方式来访问这个Service:

  • cluster IP的方式,比如:当访问10.109.169.155这个service的IP地址时,10.109.169.155其实就是一个VIP,他会把请求转发到该Service所代理的Endpoints列表中的某一个Pod上。
  • service的Dns方式,比如访问”mysvc.mynamespace.svc.cluster.local”这条DNS记录,就可以访问到mynamespace这个命令空间下名为mysvc的Service所代理的某一个Pod。

对于DNS这种方式实际也有两种情况:

  • 第一种就是普通的Service,访问”mysvc.mynamespace.svc.cluster.local”的时候是通过集群中的DNS服务解析到Service的cluster IP的
  • 第二种就是Headless Service,对于这种情况,访问”mysvc.mynamespace.svc.cluster.local”的时候是直接解析到mysvc带的某一个具体的Pod的IP,中间少了cluster IP的转发,这两者最大区别,Headless Service不需要分配一个VIP,而是可以直接以DNS的记录方式解析到后面的Pod的IP地址。

定义一个如下的Headless Service:(headless-service.yaml)

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

实际上Headless Service在定义上和普通的Service几乎一致,只是他的clusterIP=None,这个Service被创建后并不会被分配一个cluster IP,而是会以DNS记录的方式暴露它所代理的Pod,而且还有一个非常重要的特性,对于Headless Service所代理的所有Pod的IP地址都会绑定一个如下的DNS记录:

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

这个DNS记录正式k8s集群为Pod分配的一个唯一标识,只要知道Pod的名字,以及他对应的Service名字,就可以组装出这样一条DNS记录访问到Pod的IP地址。

StatefulSet

先准备两个1G的存储卷(PV)(pv.yaml):

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv001    
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  hostPath:
    path: /tmp/pv001

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv002
spec:
  capacity:
    storage: 1Gi
  accessModes:
  - ReadWriteOnce
  hostPath:
    path: /tmp/pv002

创建PV:

$ kubectl apply -f pv.yaml
persistentvolume "pv001" created
persistentvolume "pv002" created
$ kubectl get pv
kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM     STORAGECLASS   REASON    AGE
pv001     1Gi        RWO            Recycle          Available                                      12s
pv002     1Gi        RWO            Recycle          Available                                      11s

可以看到成功创建了两个PV对象,状态是:Available。

特性

声明一个StatefulSet资源清单(nginx-sts.yaml):

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
  namespace: default
spec:
  serviceName: "nginx"
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - name: web
          containerPort: 80
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

通过声明的Pod模板来创建Pod,volumeMounts进行关联的不是volume而是一个新的属性:volumeClaimTemplates,该属性会自动创建一个PVC对象,其实就是一个PVC的模板,和Pod模板类似,PVC被创建后会自动去关联当前系统中和他合适的PV进行绑定。除此之外,还多出一个serviceName:"nginx"字段,serviceName就是管理当前StatefulSet的服务名称,该服务必须在StatefulSet之前存在,并且负责该集合的网络标识,Pod会遵循以下格式获取DNS/主机名:pod-specific-string.serviceName.default.svc.cluster.local,其中pod-specific-string由StatefulSet控制器控制。
image.png
StatefulSet 的拓扑结构和其他用于部署的资源对象其实比较类似,比较大的区别在于 StatefulSet 引入了 PV 和 PVC 对象来持久存储服务产生的状态,这样所有的服务虽然可以被杀掉或者重启,但是其中的数据由于 PV 的原因不会丢失。

先创建上面定义的Headless Service:

$ kubectl apply -f headless-svc.yaml
service/nginx created
$ kubectl get service nginx
NAME    TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx   ClusterIP   None         <none>        80/TCP    9s

Headless Service创建完成后可以来创建对应的StatefulSet对象:

$ kubectl apply -f nginx-sts.yaml 
statefulset.apps/web created
$ kubectl get pvc
NAME        STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
www-web-0   Bound    pv001    1Gi        RWO                           10m
www-web-1   Bound    pv002    1Gi        RWO                           6m26s

可以看到通过volume模板自动生成了两个PVC对象,也自动和PV进行了绑定。这时候可以通过—watch参数来查看pod的创建过程:

$ kubectl get pods -l app=nginx --watch 
NAME                      READY   STATUS              RESTARTS   AGE
web-0                     0/1     ContainerCreating   0          1s
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          6s

整个过程出现了两个 Pod:web-0 和 web-1,而且这两个 Pod 是按照顺序进行创建的,web-0 启动起来后 web-1 才开始创建。如同上面 StatefulSet 概念中所提到的,StatefulSet 中的 Pod 拥有一个具有稳定的、独一无二的身份标志。这个标志基于 StatefulSet 控制器分配给每个 Pod 的唯一顺序索引。Pod 的名称的形式为-。这里的对象拥有两个副本,所以它创建了两个 Pod 名称分别为:web-0 和 web-1,使用 kubectl exec 命令进入到容器中查看它们的 hostname:

$ kubectl exec web-0 hostname
web-0
$ kubectl exec web-1 hostname
web-1

StatefulSet中Pod副本的创建会按照序列号升序处理,副本的更新或删除会按照降序处理。

可以看到,这两个Pod的hostname与Pod名字一致,都被分配了对应的编号。查看一个Pod的描述信息:

$ kubectl describe pod web-0
Name:               web-0
Namespace:          default
Priority:           0
PriorityClassName:  <none>
Node:               ydzs-node3/10.151.30.57
Start Time:         Sun, 17 Nov 2019 12:32:50 +0800
Labels:             app=nginx
                    controller-revision-hash=web-6c5c7fd59b
                    statefulset.kubernetes.io/pod-name=web-0
Annotations:        podpreset.admission.kubernetes.io/podpreset-time-preset: 2062768
Status:             Running
IP:                 10.244.3.98
Controlled By:      StatefulSet/web
......

可以看到Controlled By: StatefulSet/web,证明Pod是直接受到StatefulSet控制器管理的。

创建一个busybox的容器,在容器中使用DNS的方式来访问Headless Service:

$ kubectl run -it --image busybox:1.28.3 test --restart=Never --rm /bin/sh
If you don't see a command prompt, try pressing enter.
/ #

使用 kubectl run 命令启动了一个以 busybox 为镜像的 Pod,—rm 参数意味着我们退出 Pod 后就会被删除,和之前的 docker run 命令用法基本一致,现在我们在这个 Pod 容器里面可以使用 nslookup 命令来尝试解析下上面我们创建的 Headless Service:

/ # nslookup nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      nginx
Address 1: 10.244.1.175 web-1.nginx.default.svc.cluster.local
Address 2: 10.244.4.83 web-0.nginx.default.svc.cluster.local
/ # ping nginx
PING nginx (10.244.1.175): 56 data bytes
64 bytes from 10.244.1.175: seq=0 ttl=62 time=1.076 ms
64 bytes from 10.244.1.175: seq=1 ttl=62 time=1.029 ms
64 bytes from 10.244.1.175: seq=2 ttl=62 time=1.075 ms

直接解析 Headless Service 的名称,可以看到得到的是两个 Pod 的解析记录,但实际上如果通过nginx这个 DNS 去访问服务的话,并不会随机或者轮询背后的两个 Pod,而是访问到一个固定的 Pod,所以不能代替普通的 Service。如果分别解析对应的 Pod 呢?

$ / # nslookup web-0.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.4.83 web-0.nginx.default.svc.cluster.local
/ # nslookup web-1.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.1.175 web-1.nginx.default.svc.cluster.local

可以看到解析 web-0.nginx 的时候解析到了 web-0 这个 Pod 的 IP,web-1.nginx 解析到了 web-1 这个 Pod 的 IP,而且这个 DNS 地址还是稳定的,因为 Pod 名称就是固定的,比如我们这个时候去删掉 web-0 和 web-1 这两个 Pod:

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

删除完成后才看 Pod 状态:

$ kubectl get pods -l app=nginx  
NAME    READY   STATUS    RESTARTS   AGE
web-0   1/1     Running   0          42s
web-1   1/1     Running   0          39s

可以看到 StatefulSet 控制器仍然会安装顺序创建出两个 Pod 副本出来,而且 Pod 的唯一标识依然没变,所以这两个 Pod 的网络标识还是固定的,我们依然可以通过web-0.nginx去访问到web-0这个 Pod,虽然 Pod 已经重建了,对应 Pod IP 已经变化了,但是访问这个 Pod 的地址依然没变,并且他们依然还是关联的之前的 PVC,数据并不会丢失:

/ # nslookup web-0.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-0.nginx
Address 1: 10.244.3.98 web-0.nginx.default.svc.cluster.local
/ # nslookup web-1.nginx
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      web-1.nginx
Address 1: 10.244.1.176 web-1.nginx.default.svc.cluster.local

通过 Headless Service,StatefulSet 就保证了 Pod 网络标识的唯一稳定性,由于 Pod IP 并不是固定的,所以访问有状态应用实例的时候,就必须使用 DNS 记录的方式来访问了,偶尔有固定的 Pod IP 的需求,或许可以用这种方式来代替。

最后我们可以通过删除 StatefulSet 对象来删除所有的 Pod,仔细观察也会发现是按照倒序的方式进行删除的:

$ kubectl delete statefulsets  web
statefulset.apps "web" deleted
$ kubectl get pods --watch
NAME    READY   STATUS    RESTARTS   AGE
web-1   1/1   Terminating   0     3h/31m
web-0   1/1   Terminating   0     3h/31m

管理策略

对于某些分布式系统来说,StatefulSet的顺序性保证是不必要或者不应该的,这些系统仅仅要求唯一性和身份标识。为了解决这个问题,只需要在声明StatefulSet的时候重新设置spec.PodManagementPolicy的策略即可。

默认的管理策略是orderedReady,表示让StatefulSet控制器遵循上文演示的顺序性保障。除此之外,还可以设置为Parallel管理模式,表示让StatefulSet控制器并行的终止所有Pod,在启动或终止另外一个Pod前,不必等待这些Pod变成Running和Ready或者完全终止状态。

更新策略

在StatefulSet中同样也支持两种升级策略:onDeleteRollingUpdate,同样可以通过设置.spec.updateStrategy.type进行指定。

  • OnDelete:该策略表示当更新了StatefulSet的模板后,只有手动删除旧的Pod才会创建新的Pod。
  • RollingUpdate:该策略表示当更新StatefulSet模板后会自动删除旧的Pod并创建新的Pod,如果更新发生了错误,这次”滚动更新”就会停止。不过需要注意StatefulSet的Pod在部署时是顺序从0-n的,在滚动更新时,这些Pod则是按照逆序即n-0依次删除并创建。

另外StatefulSet的滚动升级还支持Partitions的特性,可以通过.spec.updatestrategy.rollingUpdate.partitions进行设置,在设置partitions后,StatefulSet的Pod中序号大于或等于partitions的Pod会在StatefulSet的模板更新后进行滚动升级,而其余的Pod保持不变。

在实际项目中,其实很少会去直接通过StatefulSet来部署有状态服务,对于一些特定的服务,可能会使用更加高级的Operator来部署,比如etcd-operator、prometheus-operator等等,这些应用都能够很好的管理有状态的服务,而不是单纯的使用一个StatefulSet来部署一个Pod就行,因为对于有状态的应用最重要的还是数据恢复、故障转移等等。