有状态应用管理 StatefulSet

StatefulSet (有状态集,缩写为sts) 常用于部署有状态的且需要有序启动的应用程序,比如在进行 SpringCloud 项目容器化时,Eureka 的部署是比较适合用 StatefulSet 部署方式的,可以给每个 Eureka 实例创建一个唯一且固定的标识符,并且每个Eureka 实例无需配置多余的 Service,其余Spring Boot 应用可以直接通过 Eureka 的 Headless Service 即可进行注册。

StatefulSet 基本概念

StatefulSet 主要用于管理有状态应用程序的工作负载 API 对象。比如在生产环境中,可以部署 ElasticSearch 集群、MongoDB 集群或者需要持久化的 RabbitMQ 集群、Redis 集群、Kafka 集群和 ZooKeeper 集群等。

和 Deployment 类似,一个 StatefulSet 也同样管理者基于相同容器规范的 Pod。不同的是, StatefulSet 为每个 Pod 维护一个粘性标识。这些 Pod 是根据相同的规范创建的,但是不可互换,每个 Pod 都有一个持久的标识符,在重新调度时也会保留,一般格式为 StatefulSetName-Number。比如定义了一个名字是 Redis-Sentinel-0、Redis-Sentinel-1、Redis-Sentinel-2。而 StatefulSet 创建的 Pod 一般使用 Headless Service (无头服务)进行通信,和普通的 Service 的区别在于 Headless Service 没有 ClusterIP,它使用的是 Endpoint 进行互相通信,Headless 一般的格式为:

statefulSetName-{0..N-1}.serivceName.namespace.svc.cluster.local

说明:

  • serviceName:Headless Service 的名字,创建 StatefulSet 时,必须指定 Headless Service 名称。
  • 0..N-1 : Pod 所在的序号,从 0 开始到 N-1
  • statefulSetName:StatefulSet 的名字
  • namespace:服务所在的命名空间
  • .cluster.local : Cluster Domain (集群域)

假如公司某个项目需要再 Kubernetes 中部署一个主从模式的 Redis,此时使用 StatefulSet 部署就极为合适,因为 StatefulSet 启动时,只有当前一个容器完全启动时,后一个容器才会被调度,并且每个容器的标识符是固定的,那么就可以通过标识符来断定当前 Pod 的角色。

比如用一个名为 redis-ms 的 StatefulSet 部署主从架构的 Redis,第一个容器启动时,它的标识符为 redis-ms-0,并且 Pod 内主机名也为 redis-ms-0,此时就可以根据主机名来判断,当主机名为 redis-ms-0 的容器作为 Redis 的主节点,其余从节点,那么 Slave 连接 Master 主机配置就可以使用不会更改的 Master 的 Headless Serivce,此时 Redis 从节点(Slave)配置文件如下:

port 6379
slaveof redis-ms-0.redis-ms.public-service.svc.cluster.local 6379
tcp-backlog 511
timeout 0
tcp-keeplive 0
...

其中 redis-ms-0.redis-ms.public-service.svc.cluster.local 是 Redis Master 的 Headless Service,在同一命名空间下只需要写 redis-ms-0.redis-ms 即可,后面的 public-service.svc.cluster.local 可以省略。

StatefulSet 注意事项

一般 StatefulSet 用于有以下一个或者多个需求的应用程序:

  • 需要稳定的独一无二的网络标识符
  • 需要持久化数据
  • 需要有序的、优雅的部署和扩展
  • 需要有序的自动滚动更新

如果应用程序不需要任何稳定的标识符或者有序的部署、删除或者扩展,应该使用无状态的控制器部署应用程序,比如 Deployment 或者 ReplicaSet。

StatefulSet 是 Kubernetes 1.9 版本以前的 beta 资源,在1.5 版本之前的任何 Kubernetes 版本都没有。

Pod 所用的存储必须由 PersistenVolume Provisioner (持久化卷配置器)根据请求配置 StorageClass,或者由管理员预先配置,当然也可以不配置存储。
为了确保数据安全,删除和缩放 StatefulSet 不会删除与 StatefulSet 关联的卷,可以手动选择性地删除 PVC 和PV。

StatefulSet 目前使用 Headless Service (无头服务)负责 Pod 的网络身份和通信,需要提前创建此服务。
删除一个 StatefulSet 时,不保证对 Pod 的终止,要在 StatefulSet 中实现 Pod 的有序和正常终止,可以在删除之前将 StatefulSet 的副本缩减为 0。

定义一个 StatefulSet 资源文件

定义一个简单的 StatefulSet 的示例如下:

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  selector:
    app: nginx
  ports:
    - port: 80
      name: web
  clusterIP: None
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  selector:
    matchLabels:
      app: nginx
  serviceName: "nginx"
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx
          ports:
            - containerPort: 80
              name: web

其中:

  • kind:Service 定义了一个名字为 Nginx 的 Headless Service,创建的 Service格式为 nginx-0.nginx.default.svc.cluster.local,因为没有指定 Namespace (命名空间),所以默认部署在 default。
  • kind:StatefulSet 定义了一个名字为 web 的StatefulSet,replicas 表示部署 Pod 的副本数,本实例为2。

在 StatefulSet 中必须设置 Pod 选择器(.spec.selector)用来匹配其标签(.spec.template.metadata.labels)。在1.8版本之前,如果未配置该字段(.spec.selector),将被设置为默认值。在1.8版本之后,如果为指定匹配Pod Selector,则会导致 StatefulSet 创建错误。

当 StatefulSet 控制器创建 Pod 时,它会添加一个标签 statefulset.kubernetes.io/pod-name , 该标签的值为 Pod的名称,用于匹配 Service。

使用 kubectl apply 创建

kubectl apply -f statefulset.yaml

创建 busybox 验证

apiVersion: v1
kind: Pod
metadata:
  name: busybox
spec:
  containers:
    - name: busybox
      image: busybox:1.28.4
      command:
        - sleep
        - "3600"
      resources:
        limits:
          memory: "128Mi"
          cpu: "500m"
  restartPolicy: Always
$ kubectl exec -it busybox -- sh
/ # 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.1.0.198 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.1.0.199 web-1.nginx.default.svc.cluster.local
$ kubectl get pods -o wide
NAME      READY   STATUS    RESTARTS   AGE     IP           NODE             NOMINATED NODE   READINESS GATES
busybox   1/1     Running   0          6m52s   10.1.0.202   docker-desktop   <none>           <none>
web-0     1/1     Running   0          14m     10.1.0.198   docker-desktop   <none>           <none>
web-1     1/1     Running   0          14m     10.1.0.199   docker-desktop   <none>           <none>

nslookup 命令的输出结果中,我们可以看到,在访问 web-0.nginx 的时候,最后解析
到的,正是 web-0 这个 Pod 的 IP 地址;而当访问 web-1.nginx 的时候,解析到的则是
web-1 的 IP 地址。

如果你把这两个 StatefulSet 的 Pod 删除掉

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

再查看这两个Pod 的状态变化

$ kubectl get pod -w -l app=nginx
NAME    READY   STATUS    RESTARTS   AGE
web-0   0/1     ContainerCreating   0          0s
web-0   1/1     Running             0          5s
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          4s

当我们删除这两个Pod 后,Kubernetes 会按照原来的编号的顺序,创建出两个新的Pod。依旧可以使用 web-0.nginxweb-1.nginx 访问,StatefuleSet 保证了 Pod 网络标识的稳定性

$ kubectl exec -it busybox -- sh
/ # 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.1.0.209 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.1.0.210 web-1.nginx.default.svc.cluster.local

$ kubectl get pod -l app=nginx -owide
NAME    READY   STATUS    RESTARTS   AGE     IP           NODE             NOMINATED NODE   READINESS GATES
web-0   1/1     Running   0          7m21s   10.1.0.209   docker-desktop   <none>           <none>
web-1   1/1     Running   0          7m16s   10.1.0.210   docker-desktop   <none>           <none>

扩容缩容

$ kubectl scale --replicas=3 sts web
statefulset.apps/web scaled
$ kubectl get pods
NAME      READY   STATUS    RESTARTS      AGE
web-0     1/1     Running   0             40m
web-1     1/1     Running   0             40m
web-2     1/1     Running   0             11m

$ kubectl scale --replicas=2 sts web
statefulset.apps/web scaled
$ kubectl get pods
NAME      READY   STATUS    RESTARTS      AGE
web-0     1/1     Running   0             41m
web-1     1/1     Running   0             41m

StatefulSet 更新策略

更新策略:

  • rollingUpdate: 当updateStrategy的值被设置为RollingUpdate时,StatefulSet Controller会删除并创建StatefulSet相关的每个Pod对象,其处理顺序与StatefulSet终止Pod的顺序一致,即从序号最大的Pod开始重建,每次更新一个Pod。

  • onDeleted:当updateStrategy的值被设置为OnDelete时,StatefulSet Controller并不会自动更新StatefulSet中的Pod实例,而是需要用户手动删除这些Pod并触发StatefulSet Controller创建新的Pod实例来弥补,因此这其实是一种手动升级模式。

  • Partitioned : updateStrategy也支持特殊的分区升级策略(Partitioned),在这种模式下,用户指定一个序号,StatefulSet中序号大于等于此序号的Pod实例会全部被升级,小于此序号的Pod实例则保留旧版本不变,即使这些Pod被删除、重建,也仍然保持原来的旧版本。这种分区升级策略通常用于按计划分步骤的系统升级过程中。

灰度发布

灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。

Partitioned 可以用于灰度发布

$ kubectl edit sts web
updateStrategy:
    rollingUpdate:
      partition: 2
    type: RollingUpdate
    
#修改 yaml文件
image: nginx:1.23.1

$ kubectl apply -f nginx.yaml

$ kubectl get pod web-2 -oyaml | grep image
  - image: nginx:1.23.1
    imagePullPolicy: Always
    image: nginx:1.23.1
    imageID: docker-pullable://nginx@sha256:1761fb5661e4d77e107427d8012ad3a5955007d997e0f4a3d41acc9ff20467c7

$ kubectl get pod web-0 -oyaml | grep image
  - image: nginx
    imagePullPolicy: Always
    image: nginx:latest
    imageID: docker-pullable://nginx@sha256:1761fb5661e4d77e107427d8012ad3a5955007d997e0f4a3d41acc9ff20467c7

可以看出,序号大于等于2,都更新了,实现了灰度发布

级联删除和非级联删除

级联删除:删除 StatefulSet 同时删除 Pod
非级联删除:删除 StatefulSet 不删除 Pod

# 级联删除
$ kubectl delete sts web
statefulset.apps "web" deleted
# 非级联删除
$ kubectl delete sts web --cascade=false
warning: --cascade=false is deprecated (boolean value) and can be replaced with --cascade=orphan.
statefulset.apps "web" deleted

# 删除 statefulset 后,再删除pod,pod不会重新创建
$ kubectl delete pod web-0 web-1
pod "web-0" deleted
pod "web-1" deleted

$ kubectl get pods -l app=nginx
NAME    READY   STATUS    RESTARTS   AGE
web-2   1/1     Running   0          4m31s
web-3   1/1     Running   0          4m28s
web-4   1/1     Running   0          4m26s

推荐阅读更多精彩内容