2.5. StatefulSet 与有状态集群
Deployment 把 Pod 当作可替换的零件,StatefulSet 则承认「每个副本都不一样」:名字、DNS、存储卷都必须跟着序号走。这一章用 etcd 三副本集群,把稳定身份、有序启停、PVC 模板和 partition 灰度一次讲透。
- 层级:
L2 · 核心
- 难度:
进阶
- 预计:
70 分钟
- 前置:
工作负载、存储与持久化、Service 与 DNS
2.5.1. 你将学到
StatefulSet 相对 Deployment 的三个额外保证
Headless Service、稳定 DNS 与 PVC 模板的对应关系
扩缩容顺序、PVC 保留策略、滚动升级与
partition灰度可运行的 etcd 三副本集群及其恢复注意事项
2.5.2. 为什么 Deployment 不够
维度 |
Deployment |
StatefulSet |
|---|---|---|
Pod 名称 |
随机后缀,重建即变 |
固定序号 |
网络标识 |
不稳定,只能靠 Service |
|
存储 |
共享卷或无卷 |
每副本独立 PVC,跟随序号 |
启停顺序 |
并行,无保证 |
默认按序号有序 |
数据库需要「我知道我是谁、我的数据在哪、我的同伴叫什么」,分别对应稳定网络标识、稳定存储、有序生命周期。
2.5.3. 最小骨架
apiVersion: v1
kind: Service
metadata:
name: demoapp-sts
spec:
clusterIP: None
selector: {app: demoapp}
ports: [{name: http, port: 80}]
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sts-demo
spec:
serviceName: demoapp-sts
replicas: 2
selector: {matchLabels: {app: demoapp}}
template:
metadata: {labels: {app: demoapp}}
spec:
containers:
- name: demoapp
image: ikubernetes/demoapp:v1.0
volumeMounts: [{name: appdata, mountPath: /app/data}]
volumeClaimTemplates:
- metadata: {name: appdata}
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources: {requests: {storage: 2Gi}}
PVC 的命名规则是 <模板名>-<StatefulSet 名>-<序号>,因此会看到 appdata-sts-demo-0、appdata-sts-demo-1。删除 Pod 不会删除 PVC,重建后仍挂回原来的卷。
2.5.4. Headless Service 与 DNS
clusterIP: None 的 Service 不分配虚拟 IP,DNS 直接返回后端 Pod 的 A 记录:
记录 |
解析结果 |
|---|---|
|
所有 Ready Pod 的 IP 列表 |
|
仅 |
kubectl run dns-test --rm -it --restart=Never --image=busybox:1.36 -- \
nslookup sts-demo-0.demoapp-sts
警告
引导阶段要发布未就绪地址
默认只有 Ready 的 Pod 才进入 Endpoints,而 etcd 引导时所有成员都还没 Ready。给 Headless Service 加 publishNotReadyAddresses: true,peer 之间才能先解析到彼此,再组成集群。
2.5.5. 扩缩容与滚动升级
kubectl scale sts sts-demo --replicas=3
缩容默认从最大序号开始,并且保留 PVC。若希望随对象删除自动清理存储,声明 persistentVolumeClaimRetentionPolicy 的 whenDeleted / whenScaled,默认都是 Retain。
升级策略有两种:RollingUpdate(默认)从最大序号开始逐个删除重建,等前一个 Ready 才继续;OnDelete 只更新模板,由你手工删 Pod 触发重建。partition 是灰度开关:只有序号 大于等于 partition 的 Pod 会被更新。
# 只更新 etcd-2
kubectl patch sts etcd -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'
kubectl set image sts/etcd etcd=quay.io/coreos/etcd:v3.5.16
kubectl get pod -l app=etcd -w
# 观察无误后放开其余序号
kubectl patch sts etcd -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":0}}}}'
回滚用 kubectl rollout undo sts/etcd,StatefulSet 会从高序号往低序号逐个替换回旧版本。把 partition 保持在高位,就能让「只有部分副本升级」,这是有状态服务最实用的灰度手段。
2.5.6. 案例:三副本 etcd 集群
etcd 依赖多数派(quorum)工作:3 个成员可容忍 1 个故障,5 个成员可容忍 2 个。
apiVersion: v1
kind: Service
metadata:
name: etcd-headless
spec:
clusterIP: None
publishNotReadyAddresses: true
selector: {app: etcd}
ports: [{name: client, port: 2379}, {name: peer, port: 2380}]
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: etcd
spec:
serviceName: etcd-headless
replicas: 3
podManagementPolicy: Parallel
selector: {matchLabels: {app: etcd}}
template:
metadata: {labels: {app: etcd}}
spec:
containers:
- name: etcd
image: quay.io/coreos/etcd:v3.5.15
env: [{name: POD_NAME, valueFrom: {fieldRef: {fieldPath: metadata.name}}}]
command:
- /usr/local/bin/etcd
- --name=$(POD_NAME)
- --data-dir=/var/lib/etcd/data
- --listen-client-urls=http://0.0.0.0:2379
- --advertise-client-urls=http://$(POD_NAME).etcd-headless:2379
- --listen-peer-urls=http://0.0.0.0:2380
- --initial-advertise-peer-urls=http://$(POD_NAME).etcd-headless:2380
- --initial-cluster=etcd-0=http://etcd-0.etcd-headless:2380,etcd-1=http://etcd-1.etcd-headless:2380,etcd-2=http://etcd-2.etcd-headless:2380
- --initial-cluster-token=etcd-k8s-demo
readinessProbe:
httpGet: {path: /health, port: 2379}
initialDelaySeconds: 5
periodSeconds: 10
volumeMounts: [{name: data, mountPath: /var/lib/etcd}]
volumeClaimTemplates:
- metadata: {name: data}
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: standard
resources: {requests: {storage: 1Gi}}
三个设计点:
podManagementPolicy: Parallel:所有成员同时创建。若用默认的OrderedReady,etcd-0会因为等不到另外两个成员组成多数派而永远不 Ready,etcd-1也就永远不会被创建,形成死锁。--initial-cluster写全三个成员:配合稳定 DNS 名,任一副本重建后都能用同一份配置重新加入。就绪探针只探本机:
/health只反映本成员状态,不会因为某个同伴故障就把全部 Pod 标记为未就绪。
storageClassName 换成 kubectl get sc 里的实际名字;quay.io 拉不动时换成内网镜像仓库中的 etcd 3.5 镜像。
kubectl apply -f etcd.yaml
kubectl get pod -l app=etcd -w
kubectl exec etcd-0 -- etcdctl \
--endpoints=http://etcd-0.etcd-headless:2379,http://etcd-1.etcd-headless:2379,http://etcd-2.etcd-headless:2379 \
endpoint health --cluster
kubectl exec etcd-0 -- etcdctl --endpoints=http://127.0.0.1:2379 member list -w table
kubectl exec etcd-0 -- etcdctl --endpoints=http://127.0.0.1:2379 put /demo/hello world
kubectl exec etcd-2 -- etcdctl --endpoints=http://127.0.0.1:2379 get /demo/hello
kubectl delete pod etcd-1
kubectl get pod -l app=etcd -w
etcd-1 会以同名重建并挂回 data-etcd-1,自动重新加入集群。
警告
扩缩容不等于成员变更
StatefulSet 的 replicas 只控制 Pod 数量。对 etcd 这类自带成员管理的系统,扩容必须先 etcdctl member add etcd-3 --peer-urls=http://etcd-3.etcd-headless:2380,再 kubectl scale;缩容要先 member remove 再减副本。直接改副本数会让成员列表与 Pod 列表不一致。
2.5.6.1. 备份与恢复注意事项
kubectl exec etcd-0 -- etcdctl --endpoints=http://127.0.0.1:2379 snapshot save /var/lib/etcd/snap.db
快照文件落在 PVC 上;etcd 镜像通常不带 tar,kubectl cp 可能失败,可改用临时 Pod 挂载 PVC 或上传到对象存储。
删除 StatefulSet 不会删 PVC,但删除 PVC 就等于删数据;恢复时用
etcdutl snapshot restore把快照还原到新的数据目录,并换一个新的--initial-cluster-token,先以单成员启动验证数据,再用member add把其他成员逐个加回;--force-new-cluster只用于「只剩一个成员且无法恢复多数派」的灾难场景,它会让该成员成为唯一成员,之后必须手工补齐副本;副本数缩到低于多数派(3 副本只剩 1 个)时集群只读不可写,这不是故障,是 quorum 在起作用。
2.5.7. 验证清单
kubectl get sts etcd显示READY 3/3Pod 名为
etcd-0、etcd-1、etcd-2,重建后序号不变endpoint health --cluster三个成员全部 healthy,member list均为started删除
etcd-1后自动重建并重新加入设置
partition=2后只有etcd-2被更新
2.5.8. 排错表
现象 |
可能原因 |
处理 |
|---|---|---|
Pod 一直 |
PVC 未绑定、StorageClass 不存在、反亲和过严 |
|
只创建了 |
|
先修 0 号;引导阶段改用 |
etcd 反复重启 |
|
看容器日志首行报错 |
|
peer 端口不通、DNS 解析失败 |
|
滚动升级卡住 |
新 Pod 不 Ready,或 |
|
2.5.9. 进阶挑战
把
podManagementPolicy改成OrderedReady,观察 etcd 引导为什么失败,再改回Parallel。用
partition做一次真实灰度:只升级etcd-2,验证读写正常后再放开其余副本,最后用rollout undo回滚。用 local PV +
volumeBindingMode: WaitForFirstConsumer给每个 etcd 成员绑定本地盘,观察调度与绑定的先后关系。
2.5.10. 小结
StatefulSet 的核心是三个保证:稳定网络标识、稳定存储、有序生命周期;扩缩容按序号进行,PVC 默认保留,升级按序号倒序,
partition控制灰度范围。etcd 这类系统要额外处理成员关系与 quorum,
replicas不等于member;恢复的第一原则是「先确认数据还在,再谈重建」。
2.5.11. 延伸阅读
原书示例:chapter8