大多数教程教你用 Deployment 跑无状态服务,但数据库、消息队列、缓存、协调器这些有状态组件,才是生产里最难的那部分。StatefulSet 解决了它们最痛的三件事:稳定的网络身份(名字不变)、稳定的存储绑定(PVC 不丢)、有序的部署升级。本指南从 StatefulSet 核心语义讲起,落到 etcd / Kafka / Elasticsearch 的真实部署模式与治理。
目录
- 1. 为什么 Deployment 不够:StatefulSet 的三板斧
- 2. 稳定的网络身份:headless Service 与 hostname
- 3. 稳定的存储:volumeClaimTemplates 与 PVC 生命周期
- 4. 有序部署与滚动更新
- 5. 优雅缩容与终止
- 6. 分布式有状态系统部署模式
- 7. Operator:有状态应用的自治治理
- 8. 故障排查与生产避坑
- 9. 生产最佳实践
1. 为什么 Deployment 不够:StatefulSet 的三板斧
1.1 Deployment 的"无状态假设"
Deployment 的三个假设,对有状态应用全部不成立:
1. Pod 可任意替换(名字/网络身份变了无所谓)
→ 数据库节点换了 hostname,客户端连接全部失效
2. Pod 可共享同一存储(或不用存储)
→ 每个数据库副本必须有自己的数据盘
3. 副本间无顺序依赖
→ 主从、投票、法定人数都要求"谁先谁后"
StatefulSet 的对应机制:
1. 稳定标识:Pod 名字固定(<name>-0, -1, ...)+ 稳定的网络身份
2. 稳定存储:每个 Pod 绑定自己的 PVC(volumeClaimTemplates 自动生成)
3. 有序管理:部署/升级/缩容按序号有序进行
ℹ️ 核心洞察:StatefulSet 不是"带存储的 Deployment",而是把"每个副本的身份与数据"当作一等公民对待——这正是有状态应用的生命线。
2. 稳定的网络身份:headless Service 与 hostname
2.1 稳定的 hostname 与 DNS
apiVersion: v1
kind: Service
metadata:
name: etcd
namespace: infra
spec:
clusterIP: None # headless:不分配虚拟 IP
selector:
app: etcd
publishNotReadyAddresses: true # 未 Ready 也发布地址(成员发现需要)
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: etcd
namespace: infra
spec:
serviceName: etcd # 关联 headless Service
replicas: 3
selector:
matchLabels: { app: etcd }
template:
metadata:
labels: { app: etcd }
spec:
containers:
- name: etcd
image: quay.io/coreos/etcd:v3.5
command: ["etcd", "--name=$(POD_NAME)"]
env:
- name: POD_NAME
valueFrom:
fieldRef: { fieldPath: metadata.name }
生成的稳定 DNS 名:
etcd-0.etcd.infra.svc.cluster.local
etcd-1.etcd.infra.svc.cluster.local
etcd-2.etcd.infra.svc.cluster.local
特点:
- 名字在重建/滚动升级后不变(身份稳定)
- headless Service 无 VIP,DNS 直接解析到各 Pod IP
- etcd/Kafka 等成员发现直接靠"固定的成员清单"
2.2 有状态应用怎么用这个身份
etcd 初始化成员列表(静态成员发现):
--initial-cluster etcd-0=http://etcd-0.etcd:2380,etcd-1=http://etcd-1.etcd:2380,...
Kafka(KRaft 模式):
broker.id = 序号;controller 用稳定 hostname 做 Quorum
注意:
- headless Service 的 publishNotReadyAddresses=true 很重要:
未 Ready 的 Pod 也要能被 DNS 发现(初始化阶段成员要先互连)
- 不要给 headless Service 配 LoadBalancer / NodePort
3. 稳定的存储:volumeClaimTemplates 与 PVC 生命周期
3.1 volumeClaimTemplates 自动生成 PVC
spec:
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ReadWriteOnce]
storageClassName: fast-ssd # 每副本独立盘
resources:
requests:
storage: 100Gi
自动生成的 PVC:
data-etcd-0 → 绑定 PV-A(专属 etcd-0)
data-etcd-1 → 绑定 PV-B(专属 etcd-1)
data-etcd-2 → 绑定 PV-C(专属 etcd-2)
关键语义:
- PVC 与 Pod 解耦:Pod 删除重建,PVC 还在,数据不丢
- Pod 从 etcd-2 缩到 etcd-1 → 只删 Pod 保留 PVC(谨慎,见第 5 节)
- 不同节点故障:StatefulSet 不会把 etcd-0 的盘自动挪到别的节点
(这由存储层决定,如跨节点的分布式存储)
3.2 PVC 重建与数据恢复
恢复流程(Pod 误删后的数据找回):
1. Pod 删除 → PVC 仍在
2. StatefulSet 重建同名 Pod → 自动挂回同一 PVC → 数据完好
→ 这就是"身份 + 存储绑定"的价值:删了也能原样回来
要真正"清掉数据":
删除 PVC(kubectl delete pvc data-etcd-2)
→ 注意:这会把数据也删掉,需要明确的运维动作
→ 建议给关键 PVC 打 `retain` 回收策略
4. 有序部署与滚动更新
4.1 有序部署
创建顺序:
etcd-0 → etcd-1 → etcd-2(依次,前一个 Ready 才部署下一个)
删除顺序:
etcd-2 → etcd-1 → etcd-0(倒序)
为什么:
- 分布式系统常需要"先有首个节点"再扩展
- 缩容从序号最大的开始,避免打乱身份映射
(如 Kafka:broker 缩容有"controller 优先级")
滚动更新:
- 默认按序号倒序更新(从最后一个开始),逐个进行
- 每次只更新一个,等它 Ready 再继续
- 可通过 partition 参数做"金丝雀式"升级
4.2 升级策略参数
spec:
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 2 # 只更新序号 >= 2 的 Pod(先升级一个做灰度)
maxUnavailable: 1
partition 的用途:
- partition=N:只更新序号 >= N 的 Pod
- 先设 partition = replicas-1 → 只更新最后一个做试点
- 验证 OK → 逐步降低 partition → 全量升级
- 出问题 → 改回 partition 控制,或回滚镜像版本
4.3 升级的稳杀先序
升级有状态组件前必须:
1. 有完整备份(etcd snapshot / Kafka 备份)
2. 了解升级的滚动约束(如 ES 需要先 master 后 data)
3. 在隔离环境先跑一遍同版本升级
4. 准备回滚路径(镜像版本 + partition)
教训:没有备份就升级数据库,等于把生产赌在运气上。
5. 优雅缩容与终止
5.1 缩容不是"删个 Pod"那么简单
缩容 StatefulSet 的语义:
kubectl scale sts etcd --replicas=2
→ 删除 etcd-2(序号最大)
→ PVC data-etcd-2 保留(数据仍在盘上)
→ 再次扩回 3 → 新建 etcd-2 挂回原 PVC
但分布式应用缩容本身有状态操作:
- etcd 缩容:要先 etcdctl member remove(不然成员列表还认为有 3 个)
- Kafka 缩容:要先迁移 partition 副本,再 graceful stop
- ES 缩容:要先禁用该节点分配,再 remove node
→ 缩容应由 Operator / 管理脚本先做"应用层移除",再 scale sts
5.2 优雅终止与 PodDisruptionBudget
# PDB:保证维护/缩容时至少有多少个可用
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: etcd-pdb
spec:
minAvailable: 2
selector:
matchLabels: { app: etcd }
PDB 的作用:
- 节点维护(drain)时,保证 etcd 同时下线不超过 1 个
- 防止"法定人数"跌破(3 节点最少 2 个存活)
- 结合 terminationGracePeriodSeconds:给优雅退出留时间
(etcd 停止前要选新 leader / Kafka 要迁移 leader)
建议:
minAvailable = 多数(2/3、3/5)保护 Quorum
配合 lifecycle preStop hook 做优雅退出
6. 分布式有状态系统部署模式
6.1 etcd(协调器,3-5 节点)
推荐模式:
- headless + 静态成员发现(前述示例)
- 3 或 5 节点(奇数是法定多数)
- PDB minAvailable = 多数
- 每个节点独立 fast-ssd PVC
- 定期 snapshot 到对象存储(etcdctl snapshot save)
避坑:
- 不要随便"动态加成员"——用静态清单更稳
- 节点数据盘丢了 = 成员丢失 → 及时移除并补新节点
6.2 Kafka(KRaft 模式,Controller + Broker)
KRaft(去掉 ZooKeeper)的 StatefulSet 模式:
- Controller Quorum:3 节点(controller.quorum.voters 静态清单)
- Brokers:N 节点(每个有自己的 data PVC)
- broker.id 用序号;listener 用稳定 hostname
- 缩容前先 reassign partition 副本
关键:
- 数据盘用 throughput-optimized 存储(吞吐敏感)
- 顺序升级:先 Controller 后 Broker
6.3 Elasticsearch(Master / Data 分工)
ES 在 K8s 常见两种:
方案 A:单 StatefulSet(节点同时 master+data),适合中小规模
方案 B:多 StatefulSet 分工(master、data、ingest 各一个 set)
- master:3 节点(小盘,稳定性优先)
- data:按热/温/冷分层多个 set(不同 storageClass)
- 每个 set 的 PDB 保证法定人数
避坑:
- 节点亲和/反亲和:master 分散到不同可用区
- ES 缩容前要先 disable allocation
7. Operator:有状态应用的自治治理
7.1 为什么需要 Operator
StatefulSet 只保证"身份+存储+顺序"的底座,
"应用级操作"(备份、扩缩、升级、健康自愈)仍需人写脚本。
Operator 把这些操作变成"控制器":
- 监听自定义资源(EtcdCluster / KafkaCluster)
- 自动执行:部署、成员管理、滚动升级、备份恢复
- 失败自愈:节点挂了 → 自动隔离/重建
生态:
- etcd-operator / coreos/etcd-operator
- strimzi-kafka-operator(Kafka 事实标准)
- elastic-cloud-on-k8s(ECK,官方)
- prometheus-operator、rook(Ceph)...
7.2 示例:Strimzi 声明式管理 Kafka
apiVersion: kafka.strimzi.io/v1beta2
kind: Kafka
metadata:
name: orders-kafka
spec:
kafka:
replicas: 3
listeners:
- name: plain
port: 9092
type: internal
storage:
type: persistent-claim
size: 500Gi
class: kafka-ssd
authorization:
type: simple
zookeeper:
replicas: 3
storage:
type: persistent-claim
size: 20Gi
用 Operator 后:
改 replicas / 升级版本 → 只改 CR,Operator 全自动处理
→ 有状态应用从"手工运维"升级为"声明式自治"
8. 故障排查与生产避坑
8.1 常见故障
| 症状 | 根因 | 排查 |
|---|---|---|
| Pod Pending | PVC 未绑定 / 节点无匹配存储 | kubectl get pvc kubectl describe pod |
| 成员发现失败 | headless publishNotReadyAddresses 未开 | 检查 DNS nslookup |
| 升级卡住 | 前一个 Pod 未 Ready | kubectl get sts -o yaml 看 condition |
| 缩容后数据不删 | PVC retain | 明确 PVC 生命周期 |
| Quorum 丢失 | 节点同时宕 > 法定人数 | 检查 PDB / 节点亲和 |
8.2 避坑清单
坑一:给 StatefulSet 配了普通 Service
→ 换成 headless,否则成员发现不了稳定 hostname
坑二:缩容直接 scale sts,没做应用层移除
→ etcd member remove / Kafka reassign 先做
坑三:升级无备份
→ 有状态升级前必须快照/备份
坑四:PV 回收策略 delete
→ 误删 PVC = 数据毁灭,用 Retain 或确认后手动删
坑五:3 个副本挤在同一节点/可用区
→ 节点亲和 + 反亲和 / PodTopologySpread 分散
9. 生产最佳实践
9.1 部署前 Checklist
□ 用 headless Service(publishNotReadyAddresses 按需开启)
□ 每个副本独立 volumeClaimTemplates + 合适 storageClass
□ PDB 保护法定人数(minAvailable = 多数)
□ 升级有备份 + partition 灰度 + 回滚路径
□ 缩容走"应用层移除 → scale sts"顺序
□ 节点/可用区分散(反亲和 + topologySpread)
□ 关键组件接 Operator(Strimzi/ECK/etcd-operator)
□ 定期备份演练(快照恢复验证)
□ 监控:member 健康、存储水位、Quorum 状态
9.2 一句话原则
StatefulSet 给你"身份、存储、顺序"的底座,但"成员、备份、升级"
这些应用级操作,交给 Operator——别用裸 StatefulSet 硬扛数据库。
小结
StatefulSet 是有状态应用在 K8s 上立足的底座:headless Service 给稳定的网络身份,volumeClaimTemplates 给每副本专属存储,有序滚动给安全的升级路径。但底座之上还有大量应用级责任——成员管理、备份恢复、法定人数、优雅缩容——这些正是 Operator 存在的意义。落地记住四件事:身份用 headless、存储用 volumeClaimTemplates、法定人数用 PDB 保护、应用级操作交给 Operator。当你用 Operator 声明式地管理 etcd / Kafka / ES 时,有状态服务才算真正融入了云原生的"声明式自治"哲学。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。