有状态应用管理 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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容