大家好,我是腾意。

应用程序存在“有状态”和“无状态”两种类别,因为无状态类应用的Pod资源可按需增加、减少或重构,而不会对由其提供的服务产生除了并发响应能力之外的其他严重影响。Pod资源的常用控制器中,Deployment、ReplicaSet和DaemonSet等常用于管理无状态应用。但实际情况是,很多应用本身就是分布式的集群,各应用实例彼此之间存在着关联关系,甚至是次序、角色方面的相关性,其中的每个实例都有其自身的独特性而无法轻易由其他实例所取代。本篇的主要目标就是描述专用于管理此类应用的Pod资源的控制器StatefulSet。

1.1 StatefulSet概述

ReplicaSet控制器可用来管控无状态应用,例如提供静态内容服务的Web服务器程序等,而对于有状态应用的管控,则是另一项专用控制器的任务—StatefulSet。

1.1.1 Stateful应用和Stateless应用

应用程序与用户、设备、其他应用程序或外部组件进行通信时,根据其是否需要记录前一次或多次通信中的相关事件信息以作为下一次通信的分类标准,可以将那些需要记录信息的应用程序称为有状态(stateful)应用,而无须记录的则称为无状态(stateless)应用。下面我们先来了解下状态和存储的关系。

·状态是进程的时间属性。无状态意味着一个进程不必跟踪过去的交互操作,本质上可以说它是一个纯粹的功能性行为。对应地,有状态则意味着进程存储了以前交互过程的记录,并且可以基于它对新的请求进行响应。至于状态信息被保存在内存中,或者持久保存于磁盘上,则是另外一个问题。

·存储是表述持久保存数据的方法,现今通常是指机械硬盘或SSD设备。若进程仅需操作内存中的数据,则表示其无须进行磁盘I/O操作;如果产生了I/O操作,则通常意味着数据的只读访问或读写访问行为。

如图9-1所示,将状态和存储这两个概念正交于坐标系中,则可以归结出如下几种应用程序类型。

img

图9-1 状态和存储的关系

·象限A中是那些具有读写磁盘需求的有状态应用程序,如支持事务功能的各种RDBMS存储系统;另外各种分布式存储系统也是此类应用程序的典型,如Redis Cluster、MongoDB、ZooKeeper和Cassandra等。

·象限B中包含两类应用程序:一类是那些具有读写磁盘需求的无状态应用程序,如具有幂等性的文件上传类服务程序;另一类是仅需只读类I/O访问的无状态应用程序,例如,从外部存储加载静态资源以响应用户请求的Web服务程序。

·象限C中是无磁盘访问需求的无状态应用程序,如地理坐标转换器应用。

·象限D中是无磁盘访问需求的有状态应用程序,如电子商城程序中的购物车系统。

不过,用户拥有放置应用程序的部分自由度,例如,使用购物车的电子商城系统中,一般需要确保购物车里的物品在整个会话期间均保持可用状态,因此它可能不允许使用纯内存的解决方案。另外,设计有状态应用程序时需要着重考虑的另一个方面是数据持久存储的位置,在应用程序所在的节点发生故障后依然需要确保数据可被访问的场景就需要一个外部的持久存储系统,否则使用节点本地存储卷即可。

1.1.2 StatefulSet控制器概述

前面章节中曾讲到,ReplicaSet控制器能够从一个预置的Pod模板创建一个或多个Pod资源,除了主机名和IP地址等属性之外,这些Pod资源并没有本质上的区别,就连它们的名称也是使用同一种散列模式生成,具有很强的相似性。通常,每一个访问请求都会以与其他请求相隔离的方式被这类应用所处理,不分先后也无须关心它们是否存在关联关系,哪怕它们先后来自于同一个请求者。于是,任何一个Pod资源都可以被ReplicaSet控制器重构出的新版本所替代,管理员更多关注的也是它们的群体特征,而无须过于关注任何一个个体。提供静态内容服务的Web服务器程序是这类应用的典型代表之一。

对应地,另一类应用程序在处理客户端请求时,对当前请求的处理需要以前一次或多次的请求为基础进行,新客户端发起的请求则会被其施加专用标识,以确保其后续的请求可以被识别。电商或社交等一类Web应用站点中的服务程序通常属于此类应用。另外还包含了以更强关联关系处理请求的应用,例如,RDBMS系统上处于同一个事务中的多个请求不但彼此之间存在关联性,而且还要以严格的顺序执行。这类应用一般需要记录请求连接的相关信息,即“状态”,有的甚至还需要持久保存由请求生成的数据,尤其是存储服务类的应用,运行于Kubernetes系统上时需要用到持久存储卷。

若ReplicaSet控制器在Pod模板中包含了某PVC(Persistent Volume Claim)的引用,则由它创建的所有Pod资源都将共享此存储卷。PVC后端的PV访问模型配置为Read-OnlyMany或ReadWriteMany时,这些Pod资源中的容器应用挂载存储卷后也就有了相同的数据集。不过,大多数情况是,一个集群系统的分布式应用中,每个实例都有可能需要存储使用不同的数据集,或者各自拥有其专有的数据副本,例如,分布式文件系统GlusterFS和分布式文档存储MongoDB中的每个实例各自使用专有的数据集,分布式服务框架ZooKeeper以及主从复制集群中的Redis的每个实例各自拥有其专用的数据副本。由于ReplicaSet控制器使用同一个模板生成Pod资源,显然,它无法实现为每个Pod资源创建专用的存储卷。别的可考虑使用的方案中,自主式Pod资源没有自愈能力,而组织多个只负责生成一个Pod资源的ReplicaSet控制器则有规模扩展不便的尴尬。

进一步来说,除了要用到专有的持久存储卷之外,有些集群类的分布式应用实例在运行期间还存在角色上的差异,它们存在单向/双向的基于IP地址或主机名的引用关系,例如主从复制集群中的MySQL从节点对主节点的引用。这类应用实例,每一个都应当作一个独立的个体对待。ReplicaSet对象控制下的Pod资源重构后,其名称和IP地址都存在变动的可能性,因此也无法适配此种场景之需。而StatefulSet(有状态副本集)则是专门用来满足此类应用的控制器类型,由其管控的每个Pod对象都有着固定的主机名和专有存储卷,即便被重构后亦能保持不变。

对比可见,ReplicaSet管控下的Pod资源更像是一群“家畜”(cattle),它们无状态,每个个体均被无区别地对待,因此也就可在任意时刻被另一个具有不同标识的同类事物所取代。而StatefulSet控制器治下的Pod资源更像是多个“宠物”(pet),每一个实例都有着其特有的状态,即使被重构,也得与其前任拥有相同的标识。事实上,在云原生应用的体系里有两组常用的近义词,第一组是无状态(stateless)、牲畜(cattle)、无名(nameless)和可丢弃(disposable),它们都可用于表述无状态应用。另一组是有状态(stateful)、宠物(pet)、具名(having name)和不可丢弃(non-disposable),它们则都可用于称呼有状态应用。

自Kubernetes 1.3起开始通过PetSet控制器支持有状态应用,并于1.5版本中将其重命名为StatefulSet,支持每个Pod对象一个专有索引、有序部署、有序终止、固定的标识符及固定的存储卷等特性。不过在1.9版本之前,它一直处于beta级别。

1.1.3 StatefulSet的特性

StatefulSet是Pod资源控制器的一种实现,用于部署和扩展有状态应用的Pod资源,确保它们的运行顺序及每个Pod资源的唯一性。其与ReplicaSet控制器不同的是,虽然所有的Pod对象都基于同一个spec配置所创建,但StatefulSet需要为每个Pod维持一个唯一且固定的标识符,必要时还要为其创建专有的存储卷。StatefulSet主要适用于那些依赖于下列类型资源的应用程序。

·稳定且唯一的网络标识符。

·稳定且持久的存储。

·有序、优雅地部署和扩展。

·有序、优雅地删除和终止。

·有序而自动地滚动更新。

一般来说,一个典型、完整可用的StatefulSet通常由三个组件构成:Headless Service、StatefulSet和volumeClaimTemplate。其中,Headless Service用于为Pod资源标识符生成可解析的DNS资源记录,StatefulSet用于管控Pod资源,volumeClaimTemplate则基于静态或动态的PV供给方式为Pod资源提供专有且固定的存储。

对于一个拥有N个副本的StatefulSet来说,其Pod对象会被有序创建,顺序依次是{0…N-1},删除则以相反的顺序进行。不过,Kubernetes 1.7及其之后的版本也支持并行管理Pod对象的策略。Pod资源的名称格式为$(statefulset name)-$(ordinal),例如,名称为Web的ReplicaSet资源所生成的Pod对象的名称依次为web-0、web-1、web-2等,其域名后缀可由相关的Headless类型的Service资源给出,格式为$(service name).$(namespace).svc.cluster.local,cluster.local是集群默认使用的域名。

ReplicaSet控制器会为每个VolumeClaim模板创建一个专用的PV,它会从模板中指定的StorageClass中为每个PVC创建PV,未指定时将使用默认的StorageClass资源,而如果存储系统不支持PV的动态供给,就需要管理员事先创建好满足需求的所有PV。删除Pod资源甚至是ReplicaSet控制器并不会删除与其相关的PV资源以确保数据安全,它需要由用户手动删除。这也意味着,Pod资源被重新调度至其他节点时,其PV及数据可复用。Pod名称、PVC和PV关系如图9-2所示。

ReplicaSet也支持规模扩缩容操作,扩容意味着按索引顺序增加更多的Pod资源,而缩容则表示按逆序依次删除索引号最大的Pod资源直到规模数量满足目标设定值。执行扩容操作时,应用至某Pod对象之前必须确保其前的每个Pod对象都已经就绪,反之,终止一个Pod对象时,必须事先确保它的后继者已经终止完成。考虑到不少的有状态应用不支持规模的安全快速缩减,因此,ReplicaSet控制器不支持缩容时的并行操作,一次仅能终止一个Pod资源,以免导致数据讹误。这通常也意味着,存在错误的未恢复的Pod资源时,ReplicaSet控制器也会拒绝启动缩容操作。此外,缩容操作导致的Pod资源终止也不会删除与其相关的PV,以确保数据安全。

img

图9-2 Pod名称、PVC和PV

Kubernetes自1.7版本起还支持用户自定义更新策略,该版本兼容支持之前版本中的删除后更新(OnDelete)策略,以及新的滚动更新策略(RollingUpdate)。OnDelete意味着ReplicaSet不会自动更新Pod资源除非它被删除而激活重建操作。RollingUpdate是默认的更新策略,它支持Pod资源的自动、滚动更新。更新顺序与终止Pod资源的顺序相同,由索引号最大的资源开始,终止一个并完成其更新,而后更新下一个。另外,RollingUpdate还支持分区(partition)机制,用户可基于某个用于分区的索引号对Pod资源进行分区,所以大于等于此索引号的Pod资源会被滚动更新,如图9-3所示。而小于此索引号的Pod资源则不会被更新,即便是此范围内的某Pod资源被删除,它也一样会被基于旧版本的Pod模板重建。

img

图9-3 ReplicaSet分区滚动更新

若给定的分区号大于副本数量,则意味着不会有Pod资源索引号大于此分区号,所有的Pod资源均不会被更新,对于暂存发布、金丝雀发布或分段发布来说,这也是有用的设定。

1.2 StatefulSet基础应用

本节将基于StorageClass及其相关的PV动态供给功能创建一个stateful资源示例,并验证它的各种特性。

1.2.1 创建StatefulSet对象

如前所述,一个完整的StatefulSet控制器需要由一个Headless Service、一个StatefulSet和一个volumeClaimTemplate组成。其中,Headless Service用于为Pod资源标识符生成可解析的DNS资源记录,StatefulSet用于管控Pod资源,volumeClaimTemplate则基于静态或动态的PV供给方式为Pod资源提供专有且固定的存储,如下面的资源清单中的定义所示:


apiVersion: v1
kind: Service
metadata:
  name: myapp-svc
  labels:
    app: myapp-svc
spec:
  ports:
  - port: 80
    name: web
  clusterIP: None
  selector:
    app: myapp-pod
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: myapp
spec:
  serviceName: myapp-svc
  replicas: 2
  selector:
    matchLabels:
      app: myapp-pod
  template:
    metadata:
    labels:
        app: myapp-pod
    spec:
      containers:
      - name: myapp
        image: ikubernetes/myapp:v5
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: myappdata
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: myappdata
    spec:
      accessModes: [ "ReadWriteOnce" ]
      storageClassName: "gluster-dynamic"
      resources:
        requests:
          storage: 2Gi

上面示例中的配置定义在statefulset-demo.yaml文件中。由于StatefulSet资源依赖于一个事先存在的Headless类型的Service资源,因此,这里首先定义了一个名为myapp-svc的Headless Service资源,用于为关联到的每个Pod资源创建DNS资源记录。接着定义了一个名为myapp的StatefulSet资源,它通过Pod模板创建了两个Pod资源副本,并基于volumeClaimTemplates(存储卷申请模板)向gluster-dynamic存储类请求动态供给PV,从而为每个Pod资源提供大小为2GB的专用存储卷。

事实上,定义StatefulSet资源时,spec中必须要嵌套的字段为“serviceName”和“template”,用于指定关联的Headless Service和要使用的Pod模板,“volumeClaimTemplates”

字段用于为Pod资源创建专有存储卷PVC模板,它可内嵌使用的字段即为persistentVolume-Claim资源的可用字段,对StatefulSet资源为可选字段。

在依赖的存储类满足条件之后,即可创建清单中定义的相关资源:


~]$ kubectl apply -f statefulset-demo.yaml
service "myapp-svc" created
statefulset.apps "myapp" created

默认情况下,StatefulSet控制器以串行的方式创建各Pod副本,如果想要以并行方式创建和删除Pod资源,则可以设定.spec.podManagementPolicy字段的值为“Parallel”,默认值为“OrderedReady”。使用默认的顺序创建策略时,可以使用下面的命令观察相关Pod资源的顺次生成过程:


~]$ kubectl get pods -l app=myapp-pod -w
NAME       READY    STATUS               RESTARTS    AGE
myapp-0    0/1      Pending              0           0s
myapp-0    0/1      ContainerCreating    0           0s
myapp-0    1/1      Running              0           42s
myapp-1    0/1      Pending              0           0s
myapp-1    0/1      ContainerCreating    0           0s
myapp-1    1/1      Running              0           14s

待所有的Pod资源都创建完成之后,可以在StatefulSet资源的相关状态中看到相关Pod资源的就绪信息:


~]$ kubectl get statefulsets myapp
NAME     DESIRED    CURRENT    AGE
myapp    2          2          10m

若上述资源的相关信息一切正常,则StatefulSet资源myapp已然就绪,其服务也可由其他依赖方所调用。

1.2.2 Pod资源标识符及存储卷

由StatefulSet控制器创建的Pod资源拥有固定、唯一的标识和专用存储卷,即便重新调度或终止后重建,其名称也依然保持不变,且此前的存储卷及其数据不会丢失。

1.Pod资源的固定标识符

如前所述,由StatefulSet控制器创建的Pod对象拥有固定且唯一的标识符,它们基于唯一的索引序号及相关的StatefulSet对象的名称而生成,格式为“-”,如下面的命令结果所示,两个Pod对象的名称分别为myapp-0和myapp-1,其中myapp为控制器的名称:


~]$ kubectl get pods -l app=myapp-pod
NAME       READY    STATUS     RESTARTS    AGE
myapp-0    1/1      Running    0           4m
myapp-1    1/1      Running    0           3m

Pod资源的主机名同其资源名称,因此也是带索引序号的名称格式,如下面的命令结果所示:


~]$ for i in 0 1; do kubectl exec myapp-$i -- sh -c 'hostname'; done
myapp-0
myapp-1

这些名称标识会由StatefulSet资源相关的Headless Service资源创建为DNS资源记录,其域名格式为$(service_name).$(namespace).svc.cluster.local,其中“cluster.local”是集群的默认域名。在Pod资源创建后,与其相关的DNS资源记录格式为“$(pod_name).$(service_name).$(namespace).svc.cluster.local”,例如前面创建的两个Pod资源的资源记录为myapp-0.

myapp-svc.default.svc.cluster.local和myapp-1.myapp-svc.default.svc.cluster.local。下面再创建一个临时的Pod资源,并通过其交互式接口测试上述两个名称的解析,其中粗体部分标识的内容为要运行的测试命令:


~]$ kubectl run -it --image busybox dns-client --restart=Never --rm /bin/sh
If you don't see a command prompt, try pressing enter.
/ # nslookup myapp-0.myapp-svc
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      myapp-0.myapp-svc
Address 1: 10.244.1.25 myapp-0.myapp-svc.default.svc.cluster.local
/ # nslookup myapp-1.myapp-svc
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      myapp-1.myapp-svc
Address 1: 10.244.2.23 myapp-1.myapp-svc.default.svc.cluster.local
/ # nslookup myapp-svc
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      myapp-svc
Address 1: 10.244.1.25 myapp-0.myapp-svc.default.svc.cluster.local
Address 2: 10.244.2.23 myapp-1.myapp-svc.default.svc.cluster.local
/ #

Headless Service资源借助于SRV记录来引用真正提供服务的后端Pod资源的主机名称,进行指向包含Pod IP地址的记录条目。此外,由StatefulSet控制器管控的Pod资源终止后会由控制器自动进行重建,虽然其IP地址存在变化的可能性,但它的名称标识在重建后会保持不变。例如,在另一个终端中删除Pod资源myapp-1:


~]$ kubectl delete pods myapp-1
pod "myapp-1" deleted

删除完成后控制器将随之开始重建Pod资源,由下面的命令结果可知,其名称标识符的确未发生改变:


~]$ kubectl get pods -l app=myapp-pod
NAME       READY   STATUS               RESTARTS    AGE
myapp-0    1/1     Running              0           8m
myapp-1    0/1     ContainerCreating    0           0s

Pod资源重建完成后,再次由此前创建的交互式测试终端尝试进行名称解析测试。由下面的命令结果可知,Pod资源的DNS标识亦未发生改变,但其IP地址会指向重建后的Pod资源地址:


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

Name:      myapp-1.myapp-svc
Address 1: 10.244.3.33 myapp-1.myapp-svc.default.svc.cluster.local
/ #

因此,当客户端尝试向StatefulSet资源的Pod成员发出访问请求时,应该针对HeadlessService资源的CNAME(myapp-svc.default.svc.cluster.local)记录进行,它指向的SRV记录包含了当前处于就绪状态的Pod资源。当然,若在配置Pod模板时定义了Pod资源的liveness probe和readiness probe,考虑到名称标识固定不变,也可以让客户端直接向SRV资源记录(myapp-0.myapp-svc和myapp-1.myapp-svc)发出请求。

2.Pod资源的专有存储卷

前面的StatefulSet资源示例中,控制器通过volumeClaimTemplates为每个Pod副本自动创建并关联一个PVC对象,它们分别绑定了一个动态供给的PV对象:


~]$ kubectl get pvc -l app=myapp-pod -o \
    custom-columns=NAME:metadata.name,VOLUME:spec.volumeName,STATUS:status.phase
NAME                VOLUME                                     STATUS
myappdata-myapp-0   pvc-405cf708-2ddc-11e8-b246-000c29be4e28   Bound
myappdata-myapp-1   pvc-495f87c8-2ddc-11e8-b246-000c29be4e28   Bound

PVC存储卷由Pod资源中的容器挂载到了/usr/share/nginx/html目录,此为容器应用的Nginx进程默认的文档根路径。下面通过kubectl exec命令为每个Pod资源于此目录中生成一个测试页面,用于存储卷持久性测试:


~]$ for i in 0 1; do kubectl exec myapp-$i -- sh -c \ 
    'echo $(date), Hostname: $(hostname) > /usr/share/nginx/html/index.html'; done

接下来基于一个由cirros镜像启动的客户端Pod对象进行访问测试,这样便可通过其DNS名称引用每个Pod资源:


~]$ kubectl run -it --image cirros client --restart=Never --rm /bin/sh
If you don't see a command prompt, try pressing enter.
/ # curl myapp-0.myapp-svc
Fri Mar 23 07:00:46 UTC 2018, Hostname: myapp-0
/ # curl myapp-1.myapp-svc
Fri Mar 23 07:00:46 UTC 2018, Hostname: myapp-1
/ #

删除StatefulSet控制器的Pod资源,其存储卷并不会被删除,除非用户或管理员手动操作移除操作。因此,在另一个终端中删除Pod资源myapp-0,经由StatefulSet控制器重建后,它依然会关联到此前的PVC存储卷上,且此前数据依旧可用:


~]$ kubectl delete pods myapp-0
pod "myapp-0" deleted

另一个终端中监控到的删除及重建过程如下面的命令及其结果所示:


~]$ kubectl get pods -l app=myapp-pod -w
NAME       READY    STATUS               RESTARTS    AGE
myapp-0    1/1      Running              0           1h
myapp-1    1/1      Running              0           1h
myapp-0    0/1      Terminating          0           1h
myapp-0    0/1      Pending              0           0s
myapp-0    0/1      Pending              0           0s
myapp-0    0/1      ContainerCreating    0           0s
myapp-0    1/1      Running              0           11s

而后通过测试用的Pod资源接口再次对其进行访问测试,由如下内容可知,存储卷是复用此前的那个:


/ # curl myapp-0.myapp-svc
Fri Mar 23 07:00:46 UTC 2018, Hostname: myapp-0
/ #

由此表明,重建的Pod资源被重新调度至哪个节点,此前的PVC资源就会被分配至哪个节点,这样就真正实现了数据的持久化。

1.3 StatefulSet资源扩缩容

StatefulSet资源的扩缩容与Deployment资源相似,即通过修改资源的副本数来改动其目标Pod资源数量。对StatefulSet资源来说,kubectl scale和kubectl patch命令均可实现此功能,也可以使用kubectl edit命令直接修改其副本数,或者在修改配置文件之后,由kubectlapply命令重新声明。

例如,下面的命令即能将myapp中的Pod副本数量扩展至6个:


~]$ kubectl scale statefulset myapp --replicas=6
statefulset.apps "myapp" scaled

StatefulSet资源的扩展过程与创建过程的Pod资源生成策略相同,默认为顺次进行,而且其名称中的序号也将以现有Pod资源的最后一个序号向后进行:


~]$ kubectl get pods -l app=myapp-pod -w
NAME        READY    STATUS               RESTARTS    AGE
myapp-0     1/1      Running              0           5m
myapp-1     1/1      Running              0           4m
myapp-2     0/1      ContainerCreating    0           10s
myapp-2     1/1      Running              0           25s
myapp-3     0/1      ContainerCreating    0           10s


与扩容操作相对,执行缩容操作只需要将其副本数量调低即可,例如,这里可使用kubectl patch命令将StatefulSet资源myapp的副本数量修补为3个:


~]$ kubectl patch statefulset myapp -p '{"spec":{"replicas":3}}'
statefulset.apps "myapp" patched

缩减规模时终止Pod资源的默认策略也以Pod顺序号逆序逐一进行,直到余下的数量满足目标为止:


~]$ kubectl get pods -l app=myapp-pod -w
NAME       READY    STATUS         RESTARTS    AGE
myapp-0    1/1      Running        0           7m
myapp-1    1/1      Running        0           6m
myapp-2    1/1      Running        0           2m
myapp-3    1/1      Running        0           2m
myapp-4    1/1      Running        0           1m
myapp-5    1/1      Running        0           1m
myapp-5    1/1      Terminating    0           1m
myapp-5    0/1      Terminating    0           1m
myapp-4    1/1      Terminating    0           2m
myapp-4    0/1      Terminating    0           2m
myapp-3    1/1      Terminating    0           2m
myapp-3    0/1      Terminating    0           2m

另外,终止Pod资源后,其存储卷并不会被删除,因此缩减规模后若再将其扩展回来,那么此前的数据依然可用,且Pod资源名称保持不变。

1.4 StatefulSet资源升级

自Kubernetes 1.7版本起,StatefulSet资源支持自动更新机制,其更新策略将由spec.updateStrategy字段定义,默认为RollingUpdate,即滚动更新。另一个可用策略为OnDelete,

即删除Pod资源重建以完成更新,这也是Kubernetes 1.6及之前版本唯一可用的更新策略。StatefulSet资源的更新机制可用于更新Pod资源中的容器镜像、标签、注解和系统资源配额等。

1.4.1 滚动更新

滚动更新StatefulSet控制器的Pod资源以逆序的形式从其最大索引编号的Pod资源逐一进行,它在终止一个Pod资源、更新资源并待其就绪后启动更新下一个资源,即索引号比当前号小1的Pod资源。对于主从复制类的集群应用来说,这样也能保证起主节点作用的Pod资源最后进行更新,确保兼容性。

StatefulSet的默认更新策略为滚动更新,通过“kubectl get statefulset NAME”命令中的输出可以获取相关的信息,myapp控制器的输出如下所示:


updateStrategy:
    rollingUpdate:
      partition: 0
    type: RollingUpdate

更新Pod中的容器镜像可以使用“kubectl set image”命令进行,例如下面的命令可将myapp控制器下的Pod资源镜像版本升级为“ikubernetes/myapp:v6”:


~]$ kubectl set image statefulset myapp myapp=ikubernetes/myapp:v6
statefulset.apps "myapp" image updated

更新操作过程,可在另一终端中使用如下命令进行监控,其逆序操作Pod资源的过程如下面的命令结果所示:


~]$ kubectl get pods -l app=myapp-pod -w
NAME       READY    STATUS               RESTARTS    AGE
myapp-0    1/1      Running              0           11m
myapp-1    1/1      Running              0           11m
myapp-2    1/1      Running              0           6m
myapp-2    1/1      Terminating          0           7m
myapp-2    0/1      Terminating          0           7m
myapp-2    0/1      Pending              0           0s
myapp-2    0/1      ContainerCreating    0           0s
myapp-2    1/1      Running              0           11s
myapp-1    1/1      Terminating          0           11m
myapp-1    0/1      Terminating          0           11m
myapp-1    0/1      Pending              0           0s
myapp-1    0/1      ContainerCreating    0           2s
myapp-1    1/1      Running              0           18s
myapp-0    1/1      Terminating          0           12m
myapp-0    0/1      Terminating          0           12m
myapp-0    0/1      Pending              0           0s
myapp-0    0/1      ContainerCreating    0           0s
myapp-0    1/1      Running              0           11s

待更新完成后,获取每个Pod资源中的镜像文件版本即可验证其升级结果:


~]$ for i in 0 1 2; do kubectl get po myapp-$i \
   --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
ikubernetes/myapp:v6
ikubernetes/myapp:v6
ikubernetes/myapp:v6

另外,用户也可以使用“kubectl rollout status”命令跟踪StatefulSet资源滚动更新过程中的状态信息。

1.4.2 暂存更新操作

当用户需要设定一个更新操作,但又不希望它立即执行时,可将更新操作予以“暂存”,待条件满足后再手动触发其执行更新。StatefulSet资源的分区更新机制能够实现此项功 能。在设定更新操作之前,将.spec.update-Strategy.rollingUpdate.partition字段的值设置为Pod资源的副本数量,即比Pod资源的最大索引号大1,这就意味着,所有的Pod资源都不会处于可直接更新的分区之内(如图9-4所示),那么于其后设定的更新操作也就不会真正执行,直到用户降低分区编号至现有Pod资源索引号范围之内。

img

图9-4 暂存更新

下面测试滚动更新的暂存更新操作,首先将StatefulSet资源myapp的滚动更新分区值设定为3:


~]$ kubectl patch statefulset myapp \
    -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":3}}}}'
statefulset.apps "myapp" patched

而后,将myapp控制器的Pod资源镜像版本更新为“ikubernetes/myapp:v7”:


~]$ kubectl set image statefulset myapp myapp=ikubernetes/myapp:v7
statefulset.apps "myapp" image updated

接着检测各Pod资源的镜像文件版本信息,可以发现其版本并未发生改变:


$ kubectl get pods -l app=myapp-pod \
    -o custom-columns=NAME:metadata.name,IMAGE:spec.containers[0].image
NAME      IMAGE
myapp-0   ikubernetes/myapp:v6
myapp-1   ikubernetes/myapp:v6
myapp-2   ikubernetes/myapp:v6

此时,即便删除某Pod资源,它依然会基于旧的版本镜像进行重建。例如,下面首先删除了Pod对象myapp-1,随后待其重建操作启动后,再获取与其相关的镜像信息,结果依然显示了旧的版本:


~]$ kubectl delete pods myapp-1
pod "myapp-1" deleted
~]$ kubectl get pods myapp-1 \
    -o custom-columns=NAME:metadata.name,IMAGE:spec.containers[0].image
NAME      IMAGE
myapp-1   ikubernetes/myapp:v6

由此可见,暂存状态的更新操作对所有的Pod资源均不产生影响。

1.4.3 金丝雀部署

将处于暂存状态的更新操作的partition定位于Pod资源的最大索引号,即可放出一只金丝雀,由其测试第一轮的更新操作,在确认无误后通过修改partition属性的值更新其他的Pod对象是一种更为稳妥的更新操作。

将1.4.2节暂停的更新StatefulSet控制器myapp资源的分区号设置为Pod资源的最大索引号2,将会触发myapp-2的更新操作:


~]$ kubectl patch statefulset myapp -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
statefulset.apps "myapp" patched

其执行结果如图9-5所示。

img

图9-5 金丝雀发布

待其更新完成后,检测所有Pod资源使用的镜像文件版本,对比下面命令的输出,可以看出其更新操作再次暂停于myapp-2的更新操作完成之后:


~]$ kubectl get pods -l app=myapp-pod \
    -o custom-columns=NAME:metadata.name,IMAGE:spec.containers[0].image
NAME      IMAGE
myapp-0   ikubernetes/myapp:v6
myapp-1   ikubernetes/myapp:v6
myapp-2   ikubernetes/myapp:v7

此时,位于非更新分区内的其他Pod资源仍不会被更新到新的镜像版本,哪怕它们被删除后重建亦是如此。

1.4.4 分段更新

金丝雀安然度过测试阶段之后,用户便可启动后续其他Pod资源的更新操作。在待更新的Pod资源数量较少的情况下,直接将partition属性的值设置为0,它将逆序完成后续所有Pod资源的更新。而当待更新的Pod资源较多时,用户也可以将Pod资源以线性或指数级增长的方式来分阶段完成更新操作,操作过程无非是分步更新partition属性值,例如,将myapp控制器的分区号码依次设置为1、0以完成剩余Pod资源的线性分步更新,如图9-6所示。

img

图9-6 分阶段更新

待更新全部完成后,用户便可根据需求筹划下一轮的更新操作。

1.4.5 其他话题

同其他类型的Pod控制器资源类似,StatefulSet也支持级联或非级联的删除操作。默认的删除类型为级联删除,即同时删除StatefulSet和相关的Pod资源。若要执行非级联删除,为删除命令使用“--cascade=false”选项即可。

另外,StatefulSet控制器管理Pod资源的策略除了默认的OrderedReady(顺次创建及逆序删除)之外,还支持并行的创建和删除操作,即同时创建所有的Pod资源以及同时删除所有的Pod资源,完成这一点,只需要将spec.podManagementPolicy字段的值设置为Parallel即可,不过对于有角色之分的分布式应用来说,为了保证数据安全可靠,建议使用默认策略,除非数据完整性是可以不用考虑在内的因素。

另外,不同的有状态应用的运维操作过程差别巨大,因此StatefulSet控制器本身几乎无法为此种类型的应用提供完善的通用管理控制机制,现实中的各种有状态应用通常是使用专用的自定义控制器专门封装特定的运维操作流程,这些自定义控制器有时也被统一称为Operator,这些内容将在后面的章节介绍。

1.5 案例:etcd集群

Kubernetes的所有对象都需要持久化存储于etcd存储系统中,以确保系统重启或故障恢复后能将它们予以还原。

etcd是一个分布式键值数据存储系统,具有可靠、快速、强一致性等特性,它通过分布式锁、leader选举和写屏障(write barriers)来实现可靠的分布式协作。因此,Kubernetes系统管理员应该将etcd部署为集群工作模型,以实现其服务高可用,并提供更好的性能表现。

etcd将键存储于层级组织的键空间中,这使得它看起来非常类似于文件系统的倒置树状结构,于是,它的每个键要么是一个包含有其他键的目录,要么是一个含有数据的常规键。Kubernetes将其所有数据存储于etcd的/registry目录中,虽然API Server是以JSON格式组织数据资源对象,但对于底层使用etcd存储系统的场景来说,它们可类比为存储于文件系统内的JSON文件中。另外,Kubernetes的系统组件中,仅API Server直接与etcd进行通信,其他所有组件的数据读写操作都要经由API Server进行,从而确保了数据的高度一致性。

在分布式模型中,etcd采用raft算法,实现了分布式系统数据的可用性和一致性。若发生网络分区或节点故障,则raft算法就会通过quorum机制处理集群状态转移,其中成员数量大于半数的一方可继续工作,若leader存在,则它们可继续提供读写服务,否则就要选举出新的leader,并且在此期间写操作是被禁止的,而成员数量少于半数的节点将停止任何的写入操作。本节将描述如何于Kubernetes集群中基于StatefulSet控制器托管部署一个分布式的etcd集群,它只是一个部署示例,并不会取代Kubernetes系统现有的etcd。

1.5.1 创建Service资源

StatefulSet资源依赖于Headless Service为各Pod资源提供名称解析服务,其他Pod资源可直接使用DNS名称来获取相关的服务,如etcd-0.etcd、etcd-1.etcd等,下面的资源清单(etcd-services.yaml)中定义的第一个资源etcd就是此类的Service资源。第二个名为etcd-client的Service资源是一个正常的Service,它拥有ClusterIP,并可通过NodePort向Kubernetes集群外部的etcd客户端提供服务:


apiVersion: v1
kind: Service
metadata:
  name: etcd
  annotations:
    # Create endpoints also if the related pod isn't ready
    service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"
spec:
  ports:
  - port: 2379
    name: client
  - port: 2380
    name: peer
  clusterIP: None
  selector:
    app: etcd-member
---
apiVersion: v1
kind: Service
metadata:
  name: etcd-client
spec:
  ports:
  - name: etcd-client
    port: 2379
    protocol: TCP
    targetPort: 2379
  selector:
    app: etcd-member
  type: NodePort

首先创建上述两个Service对象:


~]$ kubectl apply -f etcd-services.yaml
service "etcd-svc" created
service "etcd-client" created

而后,可于default名称空间中看到如下两个Service资源记录:


~]$ kubectl get svc -l app=etcd
NAME          TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)             AGE
etcd          ClusterIP   None           <none>        2379/TCP,2380/TCP   5s
etcd-client   NodePort    10.244.60.53   <none>        2379:30290/TCP      5s

由上面输出的信息可以看出,etcd-client通过10.91.175.53向客户端提供服务,它是一个标准类型的Service资源,类型为NodePort,可通过各工作节点的30290端口将服务暴露到集群外部。服务类型的资源创建完成后,接下来便可创建StatefulSet控制器,构建分布式etcd集群。

1.5.2 etcd StatefulSet

etcd依赖于持久存储设备保存数据,这里为其配置了使用自定义的、具有动态PV供给功能的gluster-dynamic存储类为各Pod资源的PVC提供存储后端,大小可由用户按需求进行定义。另外,etcd镜像文件的版本也可由用户修改为与实际需求匹配的版本,如3.3.1等,不过StatefulSet本就支持自动更新机制,用户也可以于必要时启动更新机制切换其镜像版本。下面是定义在etcd-statefulset.yaml文件中的配置示例,它定义了一个名为etcd的StatefulSet资源:


apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: etcd
  labels:
    app: etcd
spec:
  serviceName: etcd
  # changing replicas value will require a manual etcdctl member remove/add
  # command (remove before decreasing and add after increasing)
  replicas: 3
  selector:
    matchLabels:
      app: etcd-member
  template:
    metadata:
      name: etcd
      labels:
        app: etcd-member
    spec:
      containers:
      - name: etcd
        image: "quay.io/coreos/etcd:v3.2.16"
        ports:
        - containerPort: 2379
          name: client
        - containerPort: 2380
          name: peer
        env:
        - name: CLUSTER_SIZE
          value: "3"
        - name: SET_NAME
          value: "etcd"
        volumeMounts:
        - name: data
          mountPath: /var/run/etcd
        command:
          - "/bin/sh"
          - "-ecx"
          - |
            IP=$(hostname -i)
            PEERS=""
            for i in $(seq 0 $((${CLUSTER_SIZE} - 1))); do
                PEERS="${PEERS}${PEERS:+,}${SET_NAME}-${i}=http://${SET_NAME}-
                    ${i}.${SET_NAME}:2380"
            done
            # start etcd. If cluster is already initialized the `--initial-*` 
                options will be ignored.
            exec etcd --name ${HOSTNAME} \
              --listen-peer-urls http://${IP}:2380 \
              --listen-client-urls http://${IP}:2379,http://127.0.0.1:2379 \
              --advertise-client-urls http://${HOSTNAME}.${SET_NAME}:2379 \
              --initial-advertise-peer-urls http://${HOSTNAME}.${SET_NAME}:2380 \
              --initial-cluster-token etcd-cluster-1 \
              --initial-cluster ${PEERS} \
              --initial-cluster-state new \
              --data-dir /var/run/etcd/default.etcd
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      storageClassName: gluster-dynamic
      accessModes:
        - "ReadWriteOnce"
      resources:
        requests:
          storage: 1Gi

首先,将上述资源定义创建于集群中。当依赖的镜像文件不存在时,下载镜像的过程会导致创建时长略长,此处需要耐心等待:


~]$ kubectl apply -f etcd-statefulset.yaml
statefulset.apps "etcd" created

在另一终端,可使用下列命令监控各Pod资源的创建过程,直到它们都转为正常的运行状态为止:


~]$ kubectl get pods -l app=etcd-member -w
NAME      READY    STATUS               RESTARTS    AGE
etcd-0    0/1      Pending              0           0s
etcd-0    0/1      Pending              0           0s
etcd-0    0/1      ContainerCreating    0           0s
etcd-0    1/1      Running              0           11s
etcd-1    0/1      Pending              0           0s
etcd-1    0/1      Pending              0           0s
etcd-1    0/1      ContainerCreating    0           0s
etcd-1    1/1      Running              0           11s
etcd-2    0/1      Pending              0           0s
etcd-2    0/1      Pending              0           0s
etcd-2    0/1      ContainerCreating    0           1s
etcd-2    1/1      Running              0           15s

而后,检测1.5.1节中创建的Service资源etcd-svc和etcd-client是否已经正常关联到相关的Pod资源。下面的命令结果表示它们都已经通过标签选择器关联到了由Pod资源生成的Endpoints资源之上,这些Pod资源是由StatefulSet控制器etcd创建而成的:


~]$ kubectl get endpoints -l app=etcd
NAME        ENDPOINTS                                                      AGE
etcd        10.244.1.50:2380,10.244.2.50:2380,10.244.3.57:2380 + 3 more...  1m
etcd-client 10.244.1.50:2379,10.244.2.50:2379,10.244.3.57:2379             1m

对比由etcd控制器创建的Pod资源的相关信息可知,上面的Endpoints中的IP地址均属于由etcd控制创建的Pod资源:


~]$ kubectl get pods -l app=etcd-member -o wide
NAME     READY   STATUS    RESTARTS   AGE   IP            NODE
etcd-0   1/1     Running   0          1m    10.244.3.57   node03.ilinux.io
etcd-1   1/1     Running   0          1m    10.244.1.50   node01.ilinux.io
etcd-2   1/1     Running   0          1m    10.244.2.50   node02.ilinux.io

使用etcd的客户端工具etcdctl检测集群的健康状态:


~]$ kubectl exec etcd-0 -- etcdctl cluster-health
member 2e80f96756a54ca9 is healthy: got healthy result from http://etcd-0.etcd:2379
member 7fd61f3f79d97779 is healthy: got healthy result from http://etcd-1.etcd:2379
member b429c86e3cd4e077 is healthy: got healthy result from http://etcd-2.etcd:2379
cluster is healthy

至此,一个由三节点(Pod资源)组成的etcd集群已经正常运行起来了,客户端可从各工作节点的30290端口在Kubernetes集群外部进行访问,而各Pod资源既可以直接向Service资源etcd-client的名称或10.106.60.53(ClusterIP)发出访问请求,也可以向Headless Service资源的名称etcd进行请求。

需要注意的是,前面的配置文件中提供的StatefulSet资源配置要求进行规模变动时,需要手动对etcd集群执行命令以完成节点的添加或删除,而且缩容时要先移除节点再缩减副本数量,扩容时要先提升副本数量再添加节点,因此它的主要目标里不包含应用规模的自由变动,但对于应用升级的支持完好。例如,将镜像文件版本升级至v3.2.17的版本:


~]$ kubectl set image statefulset etcd etcd=quay.io/coreos/etcd:v3.2.17
statefulset.apps "etcd" image updated

而后使用“kubectl rollout status”命令监控其滚动升级过程中的状态变动:


~]$ kubectl rollout status statefulset etcd
Waiting for 1 pods to be ready...
Waiting for partitioned roll out to finish: 1 out of 3 new pods have been updated...
Waiting for 1 pods to be ready...
Waiting for partitioned roll out to finish: 2 out of 3 new pods have been updated...
Waiting for 1 pods to be ready...
partitioned roll out complete: 3 new pods have been updated...

升级完成后,可于相关的任一Pod资源中运行etcdctl命令查看应用程序的版本是否已经升级完成:


~]$ kubectl exec etcd-0 -- etcdctl -v
etcdctl version: 3.2.17
API version: 2

由上面命令结果可见,应用程序已经成功升级至3.2.17的版本。滚动升级过程中,处于更新过程的Pod资源对象无法正常使用,不过,客户端是通过Service资源提供的入口而非Pod资源本身的标识(如名称或IP地址)来进行访问,因此并不会出现服务不可用问题。

1.6 本篇小结

本篇着重讲解了有状态应用的Pod资源控制器StatefulSet,有状态应用相较于无状态应用来说,在管理上有着特有的复杂之处,甚至不同的有状态应用的管理方式各不相同,在部署时需要予以精心组织。

·StatefulSet依赖于Headless Service资源为其Pod资源创建DNS资源记录。

·每个Pod资源均拥有固定且唯一的名称,并且需要由DNS服务解析。

·Pod资源中的应用需要依赖于PVC和PV持久保存其状态数据。

·支持扩容和缩容,但具体的实现机制依赖于应用本身。

·支持自动更新,默认的更新策略为滚动更新机制。

版权声明:如无特殊说明,文章均为本站原创,版权所有,转载需注明本文链接

本文链接:http://www.bianchengvip.com/article/Kubernetes-StatefulSet-Controller/