K8S 之 Statefulset

摘要

Statefulset 介绍

  • Statefulset(缩写为 sts) 是 K8S 官方提供的一种控制器,用于管理有状态应用。例如,数据库应用,消息队列,等等。

  • Statefulset(有状态) 与 Deployment(无状态) 的区别是:

    • Statefulset 创建的 Pod 会有相同的名称,并且会分配相同的持久卷。
    • Statefulset 创建的 Pod 使用 Headless Service(无头服务,没有ClusterIP)进行通信。

Statefulset 管理

Statefulset 创建

  • Statefulset 只能 通过 yaml 创建,不支持 create 创建

  • 一个简单的 redis-statefulset.yaml 文件说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: v1                      # api版本
kind: Service # 资源类型
metadata: # 元数据
name: redis-svc # service名称
namespace: sts-ns
spec: # 配置
ports: # 端口
- port: 6379 # 集群内访问端口,service的端口,一般配置为与 targetPort 一致,但是非必须
protocol: TCP # 协议
targetPort: 6379 # 容器端口, pod的端口,这个必须与实际容器端口一致
selector: # 选择器
app: redis # pod的标签,即匹配pod的标签 app=redis
clusterIP: None # 集群IP设置为None,即为“Headless Service”
type: ClusterIP # 服务类型,默认为ClusterIP
--- # 分割线
apiVersion: apps/v1 # 指定使用的 API 版本,这里是 apps/v1,适用于 StatefulSet 资源
kind: StatefulSet # Kubernetes 资源类型,这里是部署(StatefulSet)
metadata:
name: redis-sts # 资源名称,必须唯一(在同一命名空间下)
namespace: sts-ns
spec: # 配置项
revisionHistoryLimit: 10 # 保留的历史版本数,默认值为 10,Deployment 和 StatefulSet 都有这个配置项。回滚时有用。
selector: # 选择器,指定要管理的 Pod
matchLabels: # 标签选择器
app: redis # 选择器,指定 StatefulSet 管理哪些 Pod(标签必须与 template 中匹配)
updateStrategy: # 更新策略,这里要注意这个更新策略与Deployment的属性名字不一样
type: RollingUpdate # 1.RollingUpdate:这是默认的更新策略。使用 RollingUpdate 更新策略时,在更新 StatefulSet 模板后, 老的 StatefulSet Pod 将被终止,并且将以受控方式自动创建新的 StatefulSet Pod。 更新期间,最多只能有 StatefulSet 的一个 Pod 运行于每个节点上。
# 2.OnDelete:使用 OnDelete 更新策略时,在更新 StatefulSet 模板后,只有当你手动删除老的 StatefulSet Pod 之后,新的 StatefulSet Pod 才会被自动创建。
rollingUpdate: # 滚动升级的配置
partition: 0 # 用于控制从第几个 Pod 开始滚动升级
serviceName: redis-svc # 服务名称,sts对象使用无头服务,这个是必填项
replicas: 2 # 副本数,默认是 1
template: # 模板,定义 Pod 的内容,具体可以参考 Pod 的配置
metadata:
labels:
app: redis # Pod 的标签,必须与 selector 中的 matchLabels 一致
spec:
containers:
- image: redis:6.2 # 容器使用的镜像,这里是官方的 redis 镜像
name: redis # 容器的名称,实际的名称为 sts-redis-<number>,从 0 开始递增
  • 执行创建

1
2
3
4
# 创建命名空间
kubectl create namespace sts-ns
# 创建 StatefulSet
kubectl apply -f redis-statefulset.yaml

查看 Statefulset

1
2
3
4
5
6
7
8
9
10
$ k get all -n sts-ns
NAME READY STATUS RESTARTS AGE
pod/redis-sts-0 1/1 Running 0 40s
pod/redis-sts-1 1/1 Running 0 24s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/redis-svc ClusterIP None <none> 6379/TCP 40s

NAME READY AGE
statefulset.apps/redis-sts 2/2 40s

访问pod中的redis服务

  • 既然是Headless Service,就只能在集群内部访问

  • 在相同的 namespace 中,可以通过 serviceName 直接访问,访问指定的 pod,可以通过 <pod-name>.<service-name>

  • 在不同的 namespace 中,可以通过 <serviceName>.<namespace> 或者 <serviceName>.<namespace>.svc.cluster.local 访问,访问指定的 pod,可以通过 <pod-name>.<service-name>.<namespace>.svc.cluster.local 访问,因为 pod-name 是固定且唯一的

1
2
3
4
5
6
$ k exec -it pod/redis-sts-0 -n sts-ns -- getent hosts redis-svc
10.244.126.55 redis-svc.sts-ns.svc.cluster.local
10.244.194.110 redis-svc.sts-ns.svc.cluster.local

$ k exec -it pod/redis-sts-0 -n sts-ns -- getent hosts redis-sts-1.redis-svc
10.244.126.55 redis-sts-1.redis-svc.sts-ns.svc.cluster.local
  • 在集群外部访问,需要创建一个新的 Service,类型为 NodePort 或者 LoadBalancer,将redis服务暴露出去

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1                      # api版本
kind: Service # 资源类型
metadata: # 元数据
name: redis-svc-out # service名称
namespace: sts-ns
spec: # 配置
ports: # 端口
- port: 6379 # 集群内访问端口,service的端口,一般配置为与 targetPort 一致,但是非必须
protocol: TCP # 协议
targetPort: 6379 # 容器端口, pod的端口,这个必须与实际容器端口一致
selector: # 选择器
app: redis # pod的标签,即匹配pod的标签 app=redis
type: LoadBalancer # 负载均衡
  • 执行

1
2
3
4
5
6
7
8
9
10
11
# 创建
$ k create -f redis-svc-out.yaml
# 查看
$ k get svc -n sts-ns
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
redis-svc ClusterIP None <none> 6379/TCP 70m
redis-svc-out LoadBalancer 10.96.21.96 10.211.55.202 6379:31806/TCP 54m

# 在集群外的机器上访问 redis,每次连接会轮询访问各个pod
$ redis-cli -h 10.211.55.202 info server | grep redis_version
redis_version:8.0.3

查看 Statefulset 详情

  • 当 Statefulset 运行错误时,可以通过该命令查看 Statefulset 的详情,找到错误原因

1
2
kubectl describe sts <sts-name>
kubectl describe sts <sts-name> -n <namespace-name>

删除 Statefulset

1
2
3
4
5
kubectl delete sts <sts-name>
kubectl delete sts <sts-name> -n <namespace-name>

# 通过 yaml 文件删除
kubectl delete -f <yaml-file>

查看 Statefulset 日志

1
2
kubectl logs sts/<sts-name>
kubectl logs sts/<sts-name> -n <namespace-name>

滚动升级与回滚 Statefulset

  • RollingUpdate 类型的 Statefulset.spec.template 的任何更新都将触发滚动更新。

  • 我们修改过 Statefulset.spec.template,并保存后,重新运行 kubectl apply -f <yaml-file>即可触发滚动升级。

  • 如果只是更新容器的镜像,也可以通过如下命令触发滚动升级

1
2
3
4
5
6
7
8
$ k exec -it pod/redis-sts-0 -n sts-ns -- redis-server -v
Redis server v=6.2.19 sha=00000000:0 malloc=jemalloc-5.1.0 bits=64 build=c51c65e7ae83f735

# kubectl set image sts/<sts-name> <container-name>=<image>:<tag> --record=true
kubectl set image sts redis-sts redis=redis:8.0 --record=true -n sts-ns

$ k exec -it pod/redis-sts-0 -n sts-ns -- redis-server -v
Redis server v=8.0.3 sha=00000000:1 malloc=jemalloc-5.3.0 bits=64 build=d4cb0aa008da4ca9
  • 查看滚动升级状态

1
2
3
# kubectl rollout status sts/<sts-name>
$ kubectl rollout status sts redis-sts -n sts-ns
partitioned roll out complete: 2 new pods have been updated...
  • 查看历史版本

1
2
3
4
# 前面的序号表示版本号
kubectl rollout history sts redis-sts -n sts-ns
# 查看指定版本的详情
kubectl rollout history sts redis-sts -n sts-ns --revision=1
  • 回滚

1
2
3
4
# 回退到前一个版本
kubectl rollout undo sts redis-sts -n sts-ns
# 回到指定版本,这里 --to-revision=1 表示回到版本1
kubectl rollout undo sts redis-sts -n sts-nsx --to-revision=1

Deployment 和 StatefulSet 的对比总结

特性 Deployment StatefulSet
用途 无状态应用(如 Web 服务、API 服务) 有状态应用(如数据库、缓存:Redis、MySQL、Kafka)
Pod 名称 Pod 名字随机生成,如 nginx-abc123 Pod 名字有序,格式为 name-0name-1name-2
稳定的标识 不提供,每次重建 Pod 名字可能不同 提供,每个 Pod 的名字、网络 ID、存储持久化不变
存储卷 通常配合 PVC,但 Pod 重建时挂载卷可能变化 配置 volumeClaimTemplates,每个 Pod 独立持久卷
滚动更新策略 支持 maxSurgemaxUnavailable,可并行更新 Pod 默认 逐个顺序更新 Pod(可用 partition 控制分批更新)
可用性 通常设计为 无状态多副本高可用,不保证 Pod 启动顺序或固定 IP 有状态且有序部署、删除、更新,通常单副本逐个维护可用性
服务访问 通过 ClusterIP、NodePort、LoadBalancer 访问 推荐用 Headless Service (clusterIP: None),Pod 有固定 DNS
典型应用场景 Web 应用、REST API、无状态微服务 数据库(MySQL、PostgreSQL)、缓存(Redis)、队列(Kafka、RabbitMQ)
Pod 伸缩特性 灵活伸缩,增加/减少副本无特定顺序 有序伸缩name-0 -> name-1 -> name-2
重建策略 Pod 崩溃后重建的是新 Pod,无持久存储数据可能丢失 Pod 崩溃后重建的是相同 Pod 名字,挂载相同 PVC,不丢数据

后记

  • StatefulSet 类似于 Deployment,同样支持扩缩容(scale 或者 hpa),但因为其是有状态的,所以需谨慎

1
2
3
4
5
# scale
kubectl scale sts redis-sts --replicas=5 -n sts-ns

# hpa,要求pod配置中有资源限制
kubectl autoscale sts redis-sts --name="hpa-redis" --cpu-percent=50 --min=3 --max=10 -n sts-ns
操作 可行性 推荐情况
扩容(增加副本数) 推荐 安全,新增 Pod 按序从 N-1 开始创建(sts-name-0 -> sts-name-1 -> sts-name-2
缩容(减少副本数) ⚠️ 小心使用 可能导致 Pod 及其关联 PVC 被废弃,数据可能丢失