概述

Kubernetes中有很多的控制器,比如常用的ReplicaSet,Deployment,DaemonSet,StatefulSet等。以这四种为例,我们可以将它们分为两种,一种为有状态控制器,一种为无状态控制器,StatefulSet则为有状态控制器,通常用于管理有状态的服务,如:MySQL,Redis,MongoDB等。

有状态控制器通常有以下几个特点:

  • 稳定的,唯一的网络标识

  • 稳定的,持久的存储

  • 有序的,优雅的部署和伸缩

  • 有序的,优雅的删除和停止

  • 有序的,自动的滚更新

也就是说如果我们的应用如有以上的任何一个特点我们就可以使用statefulset控制器来完成应用的部署。


网络标识:

Pod名称:唯一且不会发生变化,由 $(statefulset name)-(0-N)组成,N为一个无限大的正整数,从0开始。

容器内计算机名:与Pod名称一至且不会发生变化。

DNS A记录:每一个副本都拥有唯一且不变的一条A记录指定自己,格式组成:$(pod name).$(service name).$(namespace name).svc。

唯一的Pod标签,通过StatefulSet创建的每个副本都拥有一个唯一的标签,格式为:statefulset.kubernetes.io/pod-name=$(pod name),通常可以将新的service单独关联到此标签,来解决某个Pod的问题等。

以上的标识只要配置不发生改变,发生重启,升级,删除后再创建这些标识都不会发生变化。

备注:Pod的IP地址会发生变化,但我们也可以使用另外的方案来结合StatefulSet实现固定IP的方案,通常不推荐这么做。


持久存储:

每个Pod都对应一个PVC,PVC的名称组成:$(volumeClaimTemplates name)-$(pod name)-(0-N),N为一个无限大的正整数,从0开始。

当Pod被删除的时候不会自动删除对应的PVC,需要手动删除。

每次Pod升级,重启,删除重新创建后都呆以保证使用的是首次使用的PVC,保证数据的唯一性与持久化。


有序:

当部署有N个副本的StatefulSet时候,严格按照从0到N的顺序创建,并且下一个Pod创建的前提是上一个Pod已经Running状态。

当删除有N个副本的StatefulSet时候,严格按照从N到0的顺序删除,并且下一个Pod删除的前提是上一个Pod已经完全Delete。

当扩容StatefulSet副本的时候,每增加一个pod前提是上一个Pod已经Running状态。

当减少StatefulSet副本的时候,每删除一个pod前提是上一个Pod已经完全Delete。

当升级StatefulSet的时候,严格按照从N到1的顺序升级,并且下一个Pod升级的前提是上一个Pod已经Running状态。


无头服务(headless service)

无头服务是Kubernetes中service类型的一种,为什么需要无头服务?通常我们在使用Deployment部署Pod的时候名称是随机的字符串,Pod也是无序的。但 StatefulSet是有序且名称唯一,每一个Pod不可以被取代,所以这时候就需要使用无头服务来实现。

无头服务特点:当使用Kubernetes内DNS解析去访问Service的时候,会将解析的IP指向Service后端的Pod。当无头服务与StatefulSet控制器使用的时候,因为每个Pod的数据可能是不一样的,所以在Service名前面再加了一个Pod名,这样我们的每个StatefulSet控制器的Pod就拥有了唯一的网络标识(A记录)。

无头服务案例:

apiVersion: v1
kind: Service
metadata:
  namespace: test
  name: mysql-test
spec:
  selector:
    app: mysql-test
  clusterIP: None
  ports:
  - port: 3306
    targetPort: 3306

ClusterIP:当此项的值为“None”的时候,此服务就为无头服务,Kubernetes不会给此服务分配IP地址。当使用type为NodePort的时候不可以使用无头服务。


volumeClaimTemplates:

此为Kubernetes中的一个对象,对象的路径为“statefulset.spec.volumeClaimTemplates”,用于给StatefulSet的Pod申请PVC,称为卷申请模板,它会为每个Pod生成不同的PVC,关绑定到PV,从而实现Pod的专有存储。

通常我们在使用Deployment中使用“deployment.spec.template.spec.volumes”与PVC建立关联的时候,所有的副本使用的是相同的PV与PVC,他们的数据是相同的。而StatefulSet为实现各Pod的专有存储,所以才有了“statefulset.spec.volumeClaimTemplates”此方法。


实际应用场景

以MySQL主从为例,我们将实现通过同一个StatefulSet控制器创建出主库和从库,主库和从库的my.cnf配置自定义,和从库的自动扩容(目前实现半自动)

思路:

  • 使用ConfigMap实现主库和从库的自定义配置

  • 使用initContainers实现主容器在启动之前初始化配置


关于initContainers简单介绍

StatefulSet在Kubernetes中的应用_第1张图片

从上图我们可以看出一个Pod的生命周期包含:initContainer,PostStart,PreStop,Liveness和Readiness。其中容器(Container)的生命周期只包含PostStart,PreStop,Liveness和Readiness,而initContainer是属于在容器启动之前的。

initContainer应用场景:

  • 可以解决服务之间的依赖问题,如我们有一个Web服务和MySQL,如果我们将两个服务同时启动有可能会出现数据库连接异常等问题,那么我们可以通过initContainer去监测当只有MySQL启动成功的时候再启动Web。

  • 做初始化配置,如我们现在做的MySQL主从案例,主库与从库my.cnf的server_id不一样,我们无法使用同一配置,为减少管理上的麻烦也不推荐使用两个StatefulSet控制器,那么这时候我们就可以使用initContainer来完成我们的需求。我们可以将数据卷挂载到initContainer,在initContainer内产生的数据是可以被主容器所使用的,所以我们server_id通过initContainer内生成并写入my.cnf。

在此不对初始化容器做过多介绍,详情可参考:初始化容器,或者查看官方文档。


首先创建名称空间

apiVersion: v1
kind: Namespace
metadata:
  name: test

定义ConfigMap配置文件

apiVersion: v1
kind: ConfigMap
metadata:
  namespace: test
  name: mysql-cnf
data:
  master.cnf: |
    [mysqld]
    log-bin=mysql-bin
    binlog-ignore-db=information_schema,performance_schema,mysql,sys
    expire_logs_days=7
  slave.cnf: |
    [mysqld]
    slave_parallel_workers=16
    slave_parallel_type=logical_clock
    master_info_repository=TABLE
    relay_log_info_repository=TABLE
    relay_log_recovery=ON

根据自身情况,可自定义自己的选项。其中server_id不需要指定,将在初始化容器的时候写入。


定义StatefulSet配置文件

apiVersion: apps/v1
kind: StatefulSet
metadata:
  namespace: test
  name: mysql-test
spec:
  serviceName: mysql-test
  replicas: 2
  selector:
    matchLabels:
      app: mysql-test
  template:
    metadata:
      namespace: test
      name: mysql-test
      labels:
        app: mysql-test
    spec:
      initContainers:
      - name: init-mysql
        image: mysql:5.7
        command:
        - bash
        - "-c"
        - | 
          hostname=`echo $HOSTNAME | awk -F '-' '{print $NF}'`
          server_id=$[hostname+100]
          if [[ $hostname -eq 0 ]]; then
            cp /mnt/my.cnf.d/master.cnf /mnt/conf.d/my.cnf
          else
            cp /mnt/my.cnf.d/slave.cnf /mnt/conf.d/my.cnf
          fi
          echo "server_id=${server_id}" >> /mnt/conf.d/my.cnf
        volumeMounts:
        - name: mysql-cnf
          mountPath: /mnt/my.cnf.d/
        - name: conf
          mountPath: /mnt/conf.d/
      containers:
      - name: mysql-test
        image: mysql:5.7
        args: ["--character_set_server=utf8","--collation-server=utf8_unicode_ci"]
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: "!gogen123"
        volumeMounts:
        - name: conf
          mountPath: /etc/mysql/conf.d/
        - name: mysql-data
          mountPath: /var/lib/mysql/
      volumes:
      - name: mysql-cnf
        configMap:
          name: mysql-cnf
      - name: conf
        emptyDir: {}
  volumeClaimTemplates:
  - metadata:
      namespace: test
      name: mysql-data
    spec:
      accessModes: ["ReadWriteOnce"] 
      resources:
        requests:
          storage: 2Gi
      storageClassName: nas
      
---

apiVersion: v1
kind: Service
metadata:
  namespace: test
  name: mysql-test
spec:
  selector:
    app: mysql-test
  clusterIP: None
  ports:
  - port: 3306
    targetPort: 3306

initContainers:该配置段主要实现主容器启动时的配置初始化,初始化容器。因为StatefulSet控制器拥有唯一的网络标识,如计算机机名是唯一,所以我们根据计算机名做判断 ,让一个副本做Master,其余的做Slave,从而将不同的配置复制到共享数据卷“conf”。此卷由我们自己定义,并采用emptyDir方式,前面我们讲了在initContainers中产生的数据可以在主容器中使用,那么同样主容器也挂载此数据卷,那么就可以使用我们自定义的配置,我们通过volumes直接将conf挂载到/etc/mysql/conf.d/目录即可使用。

volumeClaimTemplates:关于此块配置用于自动申请pvc并于pv进行关联,因为我们使用的是阿里云的NAS,默认情况下无法自动申请成功,需要定义一个存储类(storageClassName),具体此参数的值可以通过 pv的配置获取,可以将pv输出为yaml查看。


最后:
我们的应用已经启动成功,还剩下最后一步。进入从库的MySQL Shell内执行以下命令。

change master to master_host='mysql-test-0.mysql-test.test.svc',master_port=3306,master_user='root',master_password='!gogen123',master_log_file='mysql-bin.000003',master_log_pos=154;

start slave;

正常情况下这时候查看我们的从库状态就已经成功,如果需要扩容,扩容后的从库同样也只需要执行上面两条SQL命令即可。


总结:最后一步还需要手动完成,不是特别完善,也大大提高了我们部署时的麻烦,扩容也相对方便,后续考虑将最后一步也实现自动化。