K8S StatefulSet 快速开始
其实这篇文章的梗概已经写了很久了,中间我小孩出生了,从此人间多了一份牵挂。抽出一些时间去办理新生儿相关手续。初为人父确实艰辛,就像学技术一样,都需要有极大的耐心,付出很多的时间。
一、引子
1.1、独立的存储
我们来思考一下我们曾经学过的一些分布式中间件它们具有什么特点。
一些分布式中间件它们为了避免单点故障往往采用集群的方式,部署在不同的服务器上向外提供服务。同时为了防止单节点数据文件过大,造成磁盘IO瓶颈,追求更高性能和更大的吞吐量,数据往往在每个节点分片存储。
如下图的三个节点的 Kafka 集群,主题 TopicA 就有 Partition0、Partition1、Partition2 三个数据分区。集群的每个节点,都需要有独立的存储。
1.2、稳定的网络 ID
同时,为了保障分区容错性,避免单个节点宕机后它上面的数据片完全丢失,不同节点上的数据片又会互为备份。这样不同节点之间就需要有大量的网络请求,集群中的每个节点,都需要知道彼此的存在。
就比如一个三节点的 Zookeeper 集群配置就会有:
...
server.1=192.168.1.121:2888:3888
server.2=192.168.1.122:2888:3888
server.3=192.168.1.123:2888:3888
一个三节点的 RockeMQ 集群配置就会有:
...
namesrvAddr=worker1:9876;worker2:9876;worker3:9876
1.3、启动的先后顺序
很多分布式中间件,为了提升读取性能,往往采用主从复制的方式。主节点专注于处理写操作,而读操作则分散到多个从节点。同时,限制从节点的写操作,也能一定程度上保证数据一致性。
下图是三个节点的 Zookeeper 集群,客户端的写操作只能由主节点处理,而从节点负责同步主节点数据并处理读请求。
主从复制往往需要在节点启动的时候通过选举算法来决定谁是主,谁是从。若多个节点同时尝试成为 Leader,可能因网络延迟导致选举耗时增加或脑裂(Split Brain)问题。
若节点能顺序启动,则当前两个节点选出 Leader 后,第三个节点直接加入集群成为 Follower 就可以了。
1.4、总结
我们总结一下,分布式中间件的一些特点:
- 集群的每个节点,都需要有独立的存储。
- 集群中的每个节点,都需要知道彼此的存在。
- 集群节点的启动,有先后顺序的要求。
前面两个是硬性的,而启动的先后顺序,并不是所有分布式系统都有的特点。
二、StatefulSet 的前世今生
从 K8S 的角度来思考如何部署这种有状态的服务。
Deployment 可以吗?
Deployment 通过⼀个 pod 模板创建多个 pod 副本。这些副本除了它们的名字和 IP 地址不同外,没有别的差异。如果 pod 模板里描述了一个关联到特定数据卷的持久卷声明,那么 Deployment 的所有副本都将共享这个持久卷声明,也就是绑定到同一个持久卷声明,也就是绑定到同⼀个持久卷。
Deployment 里的所有 pod 共享相同的持久卷声明和持久卷:
因为是在 pod 模板里关联声明的,又会依据 pod 模板创建多个 pod 副本,则不能对每个副本都指定独立的持久卷声明。所以也不能通过一个 Deployment 来运行一个每个实例都需要独立存储的分布式数据存储服务,至少通过单个 Deployment 是做不到的。
Pod 间通过 IP 访问?
我们知道,在分布式中间件中,往往需要在每个集群成员的配置文件中列出所有其他集群成员和它们的 IP地址(或主机名)。但是在 Kubernetes 中,每次重新调度⼀个 pod,这个新的 pod 就有⼀个新的主机名和IP地址,这样就要求当集群中任何⼀个成员被重新调度后,整个应用集群都需要重新配置。这显然是不现实的,这就需为每个 Pod 维护一个稳定的网络标识。
Pod 间通过 Service 访问?
那 Pod 之间能不能通过 Service 互相访问呢?
我们知道可以通过 Service 的 FQDN(Fully Qualified Domain Name,完全限定域名)来访问它所代理的后端 Pod,而且这个 FQDN 是不变的。
这个方案显然也不太现实。通过 Service 访问时,请求会被随机转发到任意后端 Pod,无法指定特定 Pod。
PodA 发出去的请求,甚至都可能发回给自己。
那能不能给每个 Pod 一个固定的 FQDN?
带着这些疑问,我们进入今天的 StatefulSet 的学习。
三、StatefulSet 是什么
StatefulSet 是 Kubernetes(K8s)中用于管理有状态应用的资源对象,是继 Deployment(无状态应用管理)之后的重要补充。它主要解决有状态应用的稳定性、唯一性、有序性和数据持久化等问题,适用于需要唯一标识、顺序启动、可靠存储的场景(如数据库、消息队列等)。
# StatefulSet 规范
kubectl explain sts.spec
属性名 | 类型 | 必填 | 说明 |
replicas | <integer> | 副本数 | |
revisionHistoryLimit | <integer> | 保留的历史版本数 | |
selector | <LabelSelector> | required | 标签选择器, 选择它所关联的 Pod |
serviceName | <string> | required | Headless Service 的名字 |
template | <PodTemplateSpec> | required | 生成 Pod 的模板 |
volumeClaimTemplates | <[]PersistentVolumeClaim> | 存储卷申领模板 | |
minReadySeconds | <integer> | 最小就绪秒数 | |
updateStrategy | <StatefulSetUpdateStrategy> | 更新策略 |
四、实战案例
我们先来看一个官网的例子:
1)创建 Headless Service
创建无头服务以便为 Pod 提供网络标识
apiVersion: v1
kind: Service
metadata:name: nginxlabels:app: nginx
spec:ports:- port: 80name: webclusterIP: Noneselector:app: nginx
- 创建一个名为 nginx 的无头服务(Headless Service) 用来控制网络域名。
- 所谓无头服务是 clusterIP 属性指定为 None。
- 标签选择器查找具有 app:nginx 标签的 Pod。
2)创建 StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:name: mysts
spec:selector:matchLabels:app: nginx # 必须匹配 .spec.template.metadata.labelsserviceName: "nginx"replicas: 3 # 默认值是 1minReadySeconds: 10 # 默认值是 0template:metadata:labels:app: nginx # 必须匹配 .spec.selector.matchLabelsspec:terminationGracePeriodSeconds: 10containers:- name: nginximage: nginx:1.14.2imagePullPolicy: IfNotPresentports:- containerPort: 80name: webvolumeMounts:- name: vctmountPath: /usr/share/nginx/htmlvolumeClaimTemplates:- metadata:name: vctspec:accessModes: [ "ReadWriteOnce" ]storageClassName: "managed-nfs-storage"resources:requests:storage: 100Mi
- 创建一个名为 mysts 的 StatefulSet 控制器,在独立的 3 个 Pod 副本中启动 nginx 容器。
- serviceName 指定前面创建的 Headless Service 的名字,即 nginx。
- volumeClaimTemplates 为每个 Pod 提供持久卷申领(PVC)的模板。
- accessModes 持久卷访问模式,ReadWriteOnce 卷可以被一个节点以读写方式挂载。因为每个 Pod 需要有独立的存储,所以官方建议用 ReadWriteOncePod 更准确。
- storageClassName 动态供应的存储类名称。像我的系统中就安装了 NFS 的动态供应商。
- containers.volumeMounts 容器的持久卷需挂载 volumeClaimTemplates 申领模板。
- 查看 StatefulSet
# sts 是 StatefulSet 缩写
kubectl get sts
- 查看 Pod
kubectl get pod -l app=nginx -owide
StatefulSet 会为关联的 Pod 保持一个不变的 Pod Name。
对于包含 N 个 副本的 StatefulSet,当部署 Pod 时,它们是依次创建的,顺序为 {0..N-1}。
Pod 主机名格式:$(StatefulSet 名称)-$(序号) 序号从 0 开始。
for i in {0..2}; do kubectl exec mysts-$i -- sh -c 'hostname';done
3)Pod FQDN
StatefulSet 创建的 Pod 会得到一个 DNS 子域,格式为: $(pod 名称).$(所属服务的 DNS 域名)。
例如,上面无头服务 nginx 的完全限定域名是 nginx.default.svc.cluster.local,则 Pod 的 DNS 为mysts-{0..2}.nginx.default.svc.cluster.local。
不同的中间件节点之间,就可以用这个 Pod DNS 进行通信。
4)无头服务与普通服务的区别
假设现在有这么一个 ClusterIP 的 Service:
对服务名进行解析,会得到 Service 的 ClusterIP:
而对无头服务名进行解析,会得到所有关联的 Pod IP:
5)自动创建 PVC-PV
使用 StatefulSet 会自动帮我们创建 PVC 与 PV,并完成它们的绑定。
# 查看 PVC
kubectl get pvc
- pvc 的名称格式 $(volumeClaimTemplates 名称)-$(pod 名称)
# 查看 PV
kubectl get pv | grep pvc
- pv 的名称格式 pvc-$(PVC 的 uid)
6)独立的存储
# 分别给三个 nginx 设置首页
kubectl exec -it mysts-0 -- sh -c "echo 'mysts-0' > /usr/share/nginx/html/index.html"
kubectl exec -it mysts-1 -- sh -c "echo 'mysts-1' > /usr/share/nginx/html/index.html"
kubectl exec -it mysts-2 -- sh -c "echo 'mysts-2' > /usr/share/nginx/html/index.html"
进入到 NFS 存储路径下,会发现三个文件夹,里面的 index.html 内容各不相同。
六、动态扩容与缩容
1)缩容
StatefulSet 的扩缩容和 Demployment 一样,只需要修改 replica 副本数量就可以。
就比如我们现在 mysts 是 3 个副本数,我们把它改成 2 个。
kubectl edit sts mysts
replica: 2
- 当删除 Pod 时,它们是逆序终止的,顺序为 {N-1..0}。所以 mysts-2 最先被删除。
- 当删除 Pod 时,存储资源默认是保留的。PV、PVC 以及磁盘数据都会被保留。
2)扩容
将 replica 由 2 改为 3
kubectl edit sts mysts
replica: 3
新的 Pod 名称没变,但是 IP 已经变了。原来 10.244.9.62。
访问这个 IP,之前创建的数据都在,而且能正确地挂载进来。
七、更新策略
kubectl explain sts.spec.updateStrategy
有两种策略:RollingUpdate(默认)、OnDelete。
1)RollingUpdate 滚动更新策略
RollingUpdate 更新策略会更新一个 StatefulSet 中的所有 Pod,采用与序号索引相反的顺序并遵循 StatefulSet 的保证。你可以通过指定 .spec.updateStrategy.rollingUpdate.partition 将使用 RollingUpdate 策略的 StatefulSet 的更新拆分为多个分区。
- 镜像准备
ctr -n=k8s.io images pull docker.1ms.run/library/nginx:1.16.1
ctr -n=k8s.io images tag docker.1ms.run/library/nginx:1.16.1 docker.io/library/nginx:1.16.1
在一个终端窗口中对 mysts StatefulSet 执行 patch 操作来改变容器镜像:
# 将 nginx:1.14.2 更新为 nginx:1.16.1
kubectl patch statefulset mysts --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"nginx:1.16.1"}]'
在另一个终端监控 StatefulSet 中的 Pod:
kubectl get pod -l app=nginx --watch
StatefulSet 里的 Pod 采用和序号相反的顺序更新。在更新下一个 Pod 前,StatefulSet 控制器终止每个 Pod 并等待它们变成 Running 和 Ready。 请注意,虽然在顺序后继者变成 Running 和 Ready 之前 StatefulSet 控制器不会更新下一个 Pod,但它仍然会重建任何在更新过程中发生故障的 Pod,使用的是它们现有的版本。
- 查看容器镜像
for p in 0 1 2; do kubectl get pod "mysts-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
镜像版本都已被替换为 nginx:1.16.1。
- 分区更新
你可以使用 .spec.updateStrategy.rollingUpdate 中的 partition 字段对 StatefulSet 执行更新的分段操作。
在一个终端窗口中对 mysts StatefulSet 执行 patch 操作来改变分区:
# 分区从默认值 0->2
kubectl patch sts mysts -p '{"spec":{"updateStrategy":{"rollingUpdate":{"maxUnavailable":0,"partition":2}}}}'
再次 Patch StatefulSet 来改变此 StatefulSet 使用的容器镜像:
# 将 nginx:1.16.1 变回 nginx:1.14.2
kubectl patch statefulset mysts --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"nginx:1.14.2"}]'
在另一个终端监控 StatefulSet 中的 Pod:
kubectl get pod -l app=nginx --watch
Pod 在更新的时候,只是更新了 mysts-2 这个 Pod。
partition: 2 表示更新的时候把 Pod 序号大于等于 2 的进行更新。
- 查看容器镜像
for p in 0 1 2; do kubectl get pod "mysts-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
- StatefulSet 的金丝雀发布
可以使用分区更新来实现 StatefulSet 简单的金丝雀发布,先把 partition 设置为 replica-1,然后更新镜像,这个时候序号最大的 Pod 就会被更新,同时观察这个 Pod 的日志和监控情况,如果运行平稳,再继续修改 partition 为 0:
kubectl patch sts mysts -p '{"spec":{"updateStrategy":{"rollingUpdate":{"maxUnavailable":0,"partition":0}}}}'
此时会触发其余 Pod 的滚动更新:
- 查看容器镜像
for p in 0 1 2; do kubectl get pod "mysts-$p" --template '{{range $i, $c := .spec.containers}}{{$c.image}}{{end}}'; echo; done
此时所有的镜像版本都更新为 1.14.2 了。
2)OnDelete 策略
在一个终端窗口中对 mysts StatefulSet 执行 patch 操作,以使用 OnDelete 更新策略
kubectl patch statefulset mysts -p '{"spec":{"updateStrategy":{"type":"OnDelete", "rollingUpdate": null}}}'
再次 Patch StatefulSet 来改变此 StatefulSet 使用的容器镜像:
# 将 nginx:1.14.2 更新为 nginx:1.16.1
kubectl patch statefulset mysts --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/containers/0/image", "value":"nginx:1.16.1"}]'
在另一个终端监控 StatefulSet 中的 Pod:
此时终端没有任何变化。
如果更新策略是 OnDelete,那不会自动更新 Pod,需要手动删除 Pod,重新创建的 Pod 才会实现更新。
- 删除 mysts-2:
kubectl delete pod mysts-2
此时 StatefulSet 帮我们又重新创建了一个 mysts-2:
此时 mysts-2 的镜像版本,已经更新为 1.16.1:
其他两个,还是 1.14.2: