一. 简介

实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,就被称为“有状态应用”(Stateful Application)

Kubernetes的StatefulSet组件就是为了解决如下两种情况:

  • 拓扑状态
    应用的多个实例之间存在不是完全对等的关系。这些应用实例,必须按照某些顺序启动,比如应用的主节点 A 要先于从节点 B 启动。而如果把 A 和 B 两个 Pod 删除掉,它们再次被创建出来时也必须严格按照这个顺序才行。并且,新创建出来的 Pod,必须和原来 Pod 的网络标识一样,这样原先的访问者才能使用同样的方法,访问到这个新 Pod。
  • 存储状态
    应用的多个实例分别绑定了不同的存储数据。对于这些应用实例来说,Pod A 第一次读取到的数据,和隔了十分钟之后再次读取到的数据,应该是同一份,哪怕在此期间 Pod A 被重新创建过。

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

关于本文的项目的代码,都放于链接:GitHub资源

二. 拓扑状态

通过 Headless Service 的方式,StatefulSet 为每个 Pod 创建了一个固定并且稳定的 DNS 记录,来作为它的访问入口。

2.1 Service

Service 类型一般有如下的三种:

  • VIP(Virtual IP,即:虚拟 IP)
    Service提供一个 VIP,我们访问这个IP地址时,它会把请求转发到该 Service 所代理的某一个 Pod 上。
  • DNS (Normal Service)
    这种情况下,访问“my-svc.my-namespace.svc.cluster.local”解析到的,正是 my-svc 这个 Service 的 VIP,后面的流程就跟 VIP 方式一致。
  • DNS (Headless Service)
    这种情况下,访问“my-svc.my-namespace.svc.cluster.local”解析到的,直接就是 my-svc 代理的某一个 Pod 的 IP 地址。所以,Headless Service 不需要分配一个 VIP,而是可以直接以 DNS 记录的方式解析出被代理 Pod 的 IP 地址。

2.2 Headless Service

2.2.1 案例

参考如下的demo-service.yaml案例:

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

创建成功后可以查看到如下的内容:
demo-service

2.2.2 clusterIP

关于于上面的这行配置clusterIP: None
clusterIP 是服务的IP地址,通常由主服务器随机分配还具有如下特点:

  • 如果地址是手动指定的,并且未被其他人使用,则该地址将分配给服务;否则,服务创建将失败
  • 无法通过更新更改此字段。
  • 有效值为“ None”,空字符串(“”)或有效IP地址。
  • 当不需要代理时,可以为无头服务指定“无”。
  • 仅适用于ClusterIP,NodePort和LoadBalancer类型。
  • 如果type为ExternalName,则忽略。

Headless Service 是一个标准 Service 的 YAML 文件。只不过,它的 clusterIP 字段的值是:None。这个 Service 被创建后并不会被分配一个 VIP,而是会以 DNS 记录的方式暴露出它所代理的 Pod。

2.2.3 DNS 记录

当按照这样的方式创建了一个 Headless Service 之后,它所代理的所有 Pod 的 IP 地址,都会被绑定一个这样格式的 DNS 记录,如下所示:

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

这个 DNS 记录,正是 Kubernetes 项目为 Pod 分配的唯一的“可解析身份”(Resolvable Identity)。有了这个“可解析身份”,只需要知道了一个 Pod 的名字,以及它对应的 Service 的名字,就可以非常确定地通过这条 DNS 记录访问到 Pod 的 IP 地址。

2.3 案例

2.3.1 创建Statefulset

执行创建,demo-statefulset.yaml内容如下

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: demo-statefulset
spec:
  serviceName: "demo-service"
  replicas: 3
  selector:
    matchLabels:
      app: demo-nginx
  template:
    metadata:
      labels:
        app: demo-nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
          name: nginx

这个 YAML 文件,和我们在前面文章中用到的 nginx-deployment 的唯一区别,就是多了一个 serviceName=nginx 字段。这个字段的作用,就是告诉 StatefulSet 控制器,在执行控制循环(Control Loop)的时候,请使用 nginx 这个 Headless Service 来保证 Pod 的“可解析身份”。

创建成功后,可以看到如下的状态:
statefulsets

2.3.2 检查状态

执行如下的指令,检查Pods相关状态

kubectl get pods -w -l app=demo-nginx
# result
NAME                 READY   STATUS    RESTARTS   AGE
demo-statefulset-0   1/1     Running   0          5m17s
demo-statefulset-1   1/1     Running   0          5m16s
demo-statefulset-2   1/1     Running   0          5m14s

StatefulSet 给它所管理的所有 Pod 的名字,进行了编号。
编号规则是:statefulset.name +-+ index。这些 Pod 的创建,也是严格按照编号顺序进行的。而且这些编号都是从 0 开始累加,与 StatefulSet 的每个 Pod 实例一一对应,绝不重复。

2.3.3 检查hostname

可以看到,这两个 Pod 的 hostname 与 Pod 名字是一致的,都被分配了对应的编号。
使用 kubectl exec 命令进入到容器中查看它们的 hostname:

kubectl exec demo-statefulset-0 -- sh -c 'hostname'
kubectl exec demo-statefulset-1 -- sh -c 'hostname'
kubectl exec demo-statefulset-2 -- sh -c 'hostname'

我们将看到如下结果:
Host name

2.3.4 检查DNS

我们启动了一个一次性的 Pod,因为–rm 意味着 Pod 退出后就会被删除掉,创建并进入Pod:

kubectl run -i --tty --image busybox:1.28.4 dns-test --restart=Never --rm /bin/sh 

进入这个 Pod 的容器里面,尝试用 nslookup 命令,解析一下 Pod 对应的 Headless Service

nslookup demo-service
# result
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name:      demo-service
Address 1: 10.1.3.77 demo-statefulset-1.demo-service.default.svc.cluster.local
Address 2: 10.1.3.76 demo-statefulset-0.demo-service.default.svc.cluster.local
Address 3: 10.1.3.78 demo-statefulset-2.demo-service.default.svc.cluster.local

实际执行结果可以看到如下内容:
DNS

2.4 滚动更新

滚动更新时,把这几个 Pod 删除之后,Kubernetes 会按照原先编号的顺序,创建出了两个新的 Pod。并且,Kubernetes 依然为它们分配了与原来相同的“网络身份”:demo-statefulset-0 ,demo-statefulset-1 和demo-statefulset-2。通过这种严格的对应规则,StatefulSet 就保证了 Pod 网络标识的稳定性。
可以理解为:Podname就是唯一主键。

2.5 小结

Kubernetes 成功地将 Pod 的拓扑状态,按照 Pod 的“名字 + 编号”的方式固定了下来。此外,Kubernetes 还为每一个 Pod 提供了一个固定并且唯一的访问入口,即:这个 Pod 对应的 DNS 记录。这些状态,在 StatefulSet 的整个生命周期里都会保持不变,绝不会因为对应 Pod 的删除或者重新创建而失效。
当 StatefulSet 的“控制循环”发现 Pod 的“实际状态”与“期望状态”不一致,需要新建或者删除 Pod 进行“调谐”的时候,它会严格按照这些 Pod 编号的顺序,逐一完成这些操作。

三. 存储状态

Kubernetes 项目引入了一组叫作 Persistent Volume Claim(PVC)Persistent Volume(PV)的 API 对象,大大降低了用户声明和使用持久化 Volume 的门槛。

3.1 PVC

3.1.1 定义

PVC 其实就是一种特殊的 Volume。只不过一个 PVC 具体是什么类型的 Volume,要在跟某个 PV 绑定之后才知道。这些 PVC,都以“--< 编号 >”的方式命名,并且处于 Bound 状态。这个 StatefulSet 创建出来的所有 Pod,都会声明使用编号的 PVC。

3.1.2 案例

‘demo-pv-pod.yam’l案例的如下:

apiVersion: v1
kind: Pod
metadata:
  name: demo-pv-pod
spec:
  containers:
    - name: demo-nginx
      image: nginx
      ports:
        - containerPort: 80
          name: "http-server"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: pv-storage
  volumes:
    - name: pv-storage
      persistentVolumeClaim:
        claimName: pv-claim
  • persistentVolumeClaim
    在这个 Pod 的 Volumes 定义中,只需要指定 PVC 的名字,而完全不必关心 Volume 本身的定义。

3.2 PV

3.2.1 定义

PV来自于由运维人员维护的 PV(Persistent Volume)对象。PVC 和 PV 的设计,实际上类似于“接口”和“实现”的思想。

  • 开发者只要知道并会使用“接口”,即:PVC。
  • 而运维人员则负责给“接口”绑定具体的实现,即:PV。

3.2.2 案例

参考‘demo-pv.yaml’相关代码:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: demo-pv
  labels:
    type: local
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  rbd:
    monitors:
    - 'IPs'
    pool: kube
    image: foo
    fsType: ext4
    readOnly: true
    user: admin
    keyring: /etc/ceph/keyring

PV 对象的 spec.rbd 字段,是 Ceph RBD Volume 的详细定义。而且,它还声明了这个 PV 的容量是 5 GiB。这样,Kubernetes 就会为我们刚刚创建的 PVC 对象绑定这个 PV。

3.2.3 PV状态

PersistentVolume(PV)的状态:

  • Available(可用): 块空闲资源还没有被任何声明绑定
  • Bound(已绑定): 卷已经被声明绑定, 注意:但是不一定不能继续被绑定,看accessModes而定
  • Released(已释放): 声明被删除,但是资源还未被集群重新声明
  • Failed(失败): 该卷的自动回收失败

3.3 volumeClaimTemplates

3.3.1 定义

凡是被这个 StatefulSet 管理的 Pod,都会声明一个对应的 PVC;而这个 PVC 的定义,就来自于 volumeClaimTemplates 这个模板字段。

3.3.2 案例

参考‘demo-statefulset-v2.yaml’相关代码:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: demo-statefulset
spec:
  serviceName: "demo-service"
  replicas: 3
  selector:
    matchLabels:
      app: demo-nginx
  template:
    metadata:
      labels:
        app: demo-nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: tmp
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: tmp
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi

这个自动创建的 PVC,与 PV 绑定成功后,就会进入 Bound 状态,这就意味着这个 Pod 可以挂载并使用这个 PV 了。

执行如下查询指令:

kubectl get pvc -l app=demo-nginx

成功后的情况如下:
PVC bouding
这个 PVC 的名字,会被分配一个与这个 Pod 完全一致的编号。

3.3.3 滚动更新

当进行滚动更新或者手动删除一个Pod时,PVC 维持有状态的经历如下的流程:

  • 删除一个Pod
    这个 Pod 对应的 PVC 和 PV,并不会被删除,而这个 Volume 里已经写入的数据,也依然会保存在远程存储服务里。
  • 创建一个‘新的且同名’的Pod
    StatefulSet 控制器就会重新创建一个‘新的且同名’的Pod,“纠正”这个不一致的情况。
  • 基于 volumeClaimTemplates 模版创建
    在这个新的 Pod 对象的定义里,它声明使用的 PVC 的名字与旧 PVC名称一致。这个 PVC 的定义,还是来自于 PVC 模板(volumeClaimTemplates),这是 StatefulSet 创建 Pod 的标准流程。
  • PVC与PV绑定
    在这个新的 Pod 被创建出来之后,Kubernetes 为它查找对应 PVC 时,就会直接找到旧 Pod 遗留下来的同名的 PVC,进而找到跟这个 PVC 绑定在一起的 PV。

基于上面的流程,新的 Pod 就可以挂载到旧 Pod 对应的那个 Volume,并且获取到保存在 Volume 里的数据。
通过这种方式,Kubernetes 的 StatefulSet 就实现了对应用存储状态的管理。

3.3.4 命名规则

根据 volumeClaimTemplates ,为每个Pod 创建一个pvo,pvc的命名规则匹配模式:

(volumeClaimTemplates.name)-(pod_name)

比如上面的 volumeMounts.name=tmp, Podname=demo-statefulset-[0-2],因此创建出来的PVC是 tmp-demo-statefulset-0、tmp-demo-statefulset-1、 tmp-demo-statefulset-2。

3.4 小结

PVC 和 PV 的设计,就是“接口”和“实现”的思想,‘volumeClaimTemplates’进行了再一层的封装抽象,提供了更加模版的开发方式,降低开发与运维之间的耦合性。

四. 总结

4.1 启停顺序

  • 有序部署
    部署Statefulset时,如果有多个Pod副本,它们会被顺序地创建(从0到N-1)并且,在下一个Pod运行之前所有之前的Pod必须都是Running和Ready状态。
  • 有序删除
    当Pod被删除时,它们被终止的顺序是从N-1到0。
  • 有序扩展
    当对Pod执行扩展操作时,与部署一样,它前面的Pod必须都处于Running和Ready状态。

4.2 使用场景

  • 稳定的持久化存储
    即Pod重新调度后还是能访问到相同的持久化数据,基于PVC 来实现。
  • 稳定的网络标识符
    即Pod 重新调度后其iPodName 和 HostName不变。
  • 有序部署,有序扩展
    基于init containers 来实现。
  • 有序收缩

最后,StatefulSet 其实是一种特殊的 Deployment,只不过这个“Deployment”的每个 Pod 实例的名字里,都携带了一个唯一并且固定的编号。这个编号的顺序,固定了 Pod 的拓扑关系;这个编号对应的 DNS 记录,固定了 Pod 的访问方式;这个编号对应的 PV,绑定了 Pod 与持久化存储的关系。所以,当 Pod 被删除重建时,这些“状态”都会保持不变。

欢迎收藏个人博客: Wyatt's Blog ,非常感谢~

个人博客: Wyatt's Blog个人公众号:Wyatt的成长之路
微信号公众号

Reference

https://www.cnblogs.com/baoshu/p/13281876.html
https://time.geekbang.org/column/article/41154?utm_campaign=guanwang&utm_source=baidu-ad&utm_medium=ppzq-pc&utm_content=title&utm_term=baidu-ad-ppzq-title
https://kubernetes.io/zh/docs/tutorials/stateful-application/basic-stateful-set/
https://blog.csdn.net/renfeigui0/article/details/101214865