背景

用户通过 Deployment、ReplicationController 可以方便地在 kubernetes 中部署一套高可用、可扩展的分布式无状态服务。这类应用不在本地存储数据,通过简单的负载均衡策略可实现请求分发。随着 k8s 的普及和云原生架构的兴起,越来越多的人希望把数据库这类有状态服务也通过 k8s 进行编排。但因为有状态服务的复杂性,这一过程并不容易。本文将以最流行的开源数据库 MySQL 为例,介绍如何在 k8s 上部署运维有状态服务。本文所作的调研基于k8s 1.18

StatefulSet 简介

DeploymentReplicationController是为无状态服务而设计的,它们中 pod 的名称、主机名、存储都是不稳定的,且 pod 的启动、销毁顺序随机,并不适合数据库这样的有状态应用。为此,k8s 推出了面向有状态服务的工作负载StatefulSet。其管理的 pod 具有如下特点:

  1. 唯一性 - 对于包含 N 个副本的 StatefulSet,每个 pod 会被分配一个 [0,N)范围内的唯一序号。1. 顺序性 - StatefulSet 中 pod 的启动、更新、销毁默认都是按顺序进行的。1. 稳定的网络身份标识 - pod 的主机名、DNS 地址不会随着 pod 被重新调度而发生变化。1. 稳定的持久化存储 - 当 pod 被重新调度后,仍然能挂载原有的 PersistentVolume,保证了数据的完整性和一致性。 StatefulSet在Headless Service的基础上又为StatefulSet控制的每个Pod副本创建了一个DNS域名,这个域名的格式为:
$(podname).(headless server name)   
FQDN: $(podname).(headless server name).namespace.svc.cluster.local

创建 StatefulSet

作为开始,使用如下示例创建一个 StatefulSet。它和StatefulSets概念中的示例相似。它创建了一个Headless Servicenginx用来发布 StatefulSetweb中的 Pod 的 IP 地址。

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  ports:
    - port: 80
      name: web
  clusterIP: None
  selector:
    app: nginx

---

apiVersion: v1
kind: PersistentVolume
metadata:
  name: "www-data-pv"
  labels:
    name: www-data-pv
    release: stable
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Recycle
  nfs:
    path: /nfs/www/data
    server: 192.168.84.75

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: www-data-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  selector:
    matchLabels:
      name: www-data-pv
      release: stable

---


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
          ports:
            - containerPort: 80
              name: web
          volumeMounts:
            - name: www
              mountPath: /usr/share/nginx/html
      volumes:
        - name: www
          persistentVolumeClaim:
            claimName:  www-data-pvc

通过该配置文件,可看出StatefulSet的三个组成部分:

  • Headless Service:名为nginx,用来定义Pod网络标识( DNS domain)。- StatefulSet:定义具体应用,名为Nginx,有三个Pod副本,并为每个Pod定义了一个域名。- persistentVolumeClaim: 是由用户进行存储的请求。它类似于pod。Pod消耗节点资源,PVC消耗PV资源。Pod可以请求特定级别的资源(CPU和内存)。声明可以请求特定的大小和访问模式(例如,可以一次读/写或多次只读)。【persistentVolumeClaim的细节请看k8s的持久化存储PVC】

为什么需要 headless service 无头服务?
在用Deployment时,每一个Pod名称是没有顺序的,是随机字符串,因此是Pod名称是无序的,但是在statefulset中要求必须是有序 ,每一个pod不能被随意取代,pod重建后pod名称还是一样的。而pod IP是变化的,所以是以Pod名称来识别。pod名称是pod唯一性的标识符,必须持久稳定有效。这时候要用到无头服务,它可以给每个Pod一个唯一的名称 。

使用kubectl apply中的 Headless Service 和 StatefulSet。

kubectl apply -f web.yaml
service/nginx created
statefulset.apps/web created

最后三个Pod全部running且ready, 且顺序是web-0.web-1,web-2

测试pod间是否相通

每个 Pod 都拥有一个基于其顺序索引的稳定的主机名。使用kubectl exec在每个 Pod 中执行hostname

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

使用kubectl run运行一个提供nslookup命令的容器,该命令来自于dnsutils包。通过对 Pod 的主机名执行nslookup,你可以检查他们在集群内部的 DNS 地址。

$ kubectl run -i --tty --image busybox:1.28 dns-test --restart=Never --rm

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.6

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.6

另外一种方法是可以直接在web-0 pod中执行 nslookup + 域名 指令。

kubectl exec web-0 -- nslookup web-1.nginx.svc.cluster.local

格式是:${podname}.${servername}.svc.cluster.local

扩容/缩容 StatefulSet

扩容/缩容 StatefulSet 指增加或减少它的副本数。这通过更新replicas字段完成。你可以使用kubectl scale或者kubectl patch来扩容/缩容一个 StatefulSet。

扩容

在一个终端窗口观察 StatefulSet 的 Pod。

kubectl get pods -w -l app=nginx

在另一个终端窗口使用kubectl scale扩展副本数为 5。

kubectl scale sts web --replicas=5
statefulset.apps/web scaled

在第一个 终端中检查kubectl get命令的输出,等待增加的 3 个 Pod 的状态变为 Running 和 Ready。
StatefulSet 控制器扩展了副本的数量。StatefulSet 按序号索引顺序的创建每个 Pod,并且会等待前一个 Pod 变为 Running 和 Ready 才会启动下一个 Pod。

缩容

在一个终端观察 StatefulSet 的 Pod。

kubectl get pods -w -l app=nginx

在另一个终端使用kubectl patch将 StatefulSet 缩容回三个副本。

kubectl patch sts web -p '{"spec":{"replicas":3}}'
statefulset.apps/web patched

顺序终止 Pod

控制器会按照与 Pod 序号索引相反的顺序每次删除一个 Pod。在删除下一个 Pod 前会等待上一个被完全关闭。

规律总结:

  • 匹配Pod name(网络标识)的模式为:
          (
         
         
          s
         
         
          t
         
         
          a
         
         
          t
         
         
          e
         
         
          f
         
         
          u
         
         
          l
         
         
          s
         
         
          e
         
         
          t
         
         
          名
         
         
          称
         
         
          )
         
         
          −
         
        
        
         (statefulset名称)-
        
       
      (statefulset名称)−(序号)</strong>,比如上面的示例:web-0,web-1,web-2。</li>- StatefulSet为每个Pod副本创建了一个DNS域名,这个域名的格式为:**$(podname).(headless server name)**,也就意味着服务间是通过Pod域名来通信而非Pod IP,因为当Pod所在Node发生故障时,Pod会被飘移到其它Node上,Pod IP会发生变化,但是Pod域名不会有变化。<li>StatefulSet使用Headless服务来控制Pod的域名,这个域名的FQDN为:<strong>
      
       
        
         
          (
         
         
          s
         
         
          e
         
         
          r
         
         
          v
         
         
          i
         
         
          c
         
         
          e
         
         
          n
         
         
          a
         
         
          m
         
         
          e
         
         
          )
         
         
          .
         
        
        
         (service name).
        
       
      (servicename).(namespace).svc.cluster.local</strong>,其中,“cluster.local”指的是集群的域名。</li>- 根据volumeClaimTemplates,为每个Pod创建一个pvc,pvc的命名规则匹配模式:**(volumeClaimTemplates.name)-(pod_name)**,比如上面的volumeMounts.name=www, Pod name=web-[0-2],因此创建出来的PVC是www-web-0、www-web-1、www-web-2。- 删除Pod不会删除其pvc,手动删除pvc将自动释放pv。
    

    域名示例

    关于Cluster Domain、headless service名称、StatefulSet 名称如何影响StatefulSet的Pod的DNS域名的示例:

    |Cluster Domain|Service (ns/name)|StatefulSet (ns/name)|StatefulSet Domain|Pod DNS|Pod Hostname |—— |cluster.local|default/nginx|default/web|nginx.default.svc.cluster.local|web-{0…N-1}.nginx.default.svc.cluster.local|web-{0…N-1} |cluster.local|foo/nginx|foo/web|nginx.foo.svc.cluster.local|web-{0…N-1}.nginx.foo.svc.cluster.local|web-{0…N-1} |kube.local|foo/nginx|foo/web|nginx.foo.svc.kube.local|web-{0…N-1}.nginx.foo.svc.kube.local|web-{0…N-1}

    更新策略

    在Kubernetes 1.7及更高版本中,通过.spec.updateStrategy字段允许配置或禁用Pod、labels、source request/limits、annotations自动滚动更新功能。
    **OnDelete:**通过.spec.updateStrategy.type 字段设置为OnDelete,StatefulSet控制器不会自动更新StatefulSet中的Pod。用户必须手动删除Pod,以使控制器创建新的Pod。
    **RollingUpdate:**通过.spec.updateStrategy.type 字段设置为RollingUpdate,实现了Pod的自动滚动更新,如果.spec.updateStrategy未指定,则此为默认策略。
    StatefulSet控制器将删除并重新创建StatefulSet中的每个Pod。它将以Pod终止(从最大序数到最小序数)的顺序进行,一次更新每个Pod。在更新下一个Pod之前,必须等待这个Pod Running and Ready。
    **Partitions:**通过指定 .spec.updateStrategy.rollingUpdate.partition 来对 RollingUpdate 更新策略进行分区,如果指定了分区,则当 StatefulSet 的 .spec.template 更新时,具有大于或等于分区序数的所有 Pod 将被更新。
    具有小于分区的序数的所有 Pod 将不会被更新,即使删除它们也将被重新创建。如果 StatefulSet 的 .spec.updateStrategy.rollingUpdate.partition 大于其 .spec.replicas,则其 .spec.template 的更新将不会传播到 Pod。在大多数情况下,不需要使用分区。

    外部访问集群内部

    土方法

    对pod打标签,然后映射成服务

    kubectl label pod web-0 webInst=0 -n sy-platform-demo
    kubectl expose pod web-0 --port=80--target-port=80--name=web-0 --selector=web=0 --type=NodePort -n sy-platform-demo
    

    修改web-0 的nodeport 端口号

    kubectl edit  service web-0 -n sy-platform-demo
    

    --完--