Repository Reading Site
第九课:StatefulSet、Headless Service、稳定身份与存储原理
上一课我们已经把存储主链路讲清楚了: 但只懂“数据不丢”还不够。 你以后做数据库、中间件、监控、日志、消息队列时,真正难的往往不是“有没有卷”,而是下面这几个问题: 1. 这个副本到底是谁,它是不是固定的那个成员 2. 它的同伴怎么找到它 3. 它重建后还是不是“原来的那个身份” 4. 缩容之后,那一份副本的数据要不要保留 5. 多个副本能不能按顺序启动,而
第九课:StatefulSet、Headless Service、稳定身份与存储原理
为什么这一课必须接在存储后面
上一课我们已经把存储主链路讲清楚了:
PVC是需求声明PV是真实卷StorageClass决定供给方式- NFS 让数据从 Pod 生命周期里解耦出来
但只懂“数据不丢”还不够。
你以后做数据库、中间件、监控、日志、消息队列时,真正难的往往不是“有没有卷”,而是下面这几个问题:
- 这个副本到底是谁,它是不是固定的那个成员
- 它的同伴怎么找到它
- 它重建后还是不是“原来的那个身份”
- 缩容之后,那一份副本的数据要不要保留
- 多个副本能不能按顺序启动,而不是一窝蜂一起冲上来
这时只会 Deployment + PVC 已经不够了。
StatefulSet 解决的不是“多加几个有盘的 Pod”。
它解决的是:
有状态系统里的副本,不是可随意互换的匿名实例,而是有编号、有身份、有历史的数据成员。
这就是你从“会部署应用”走向“能搭系统架构”的分水岭。
先把最重要的一句话刻进脑子里
Deployment 的副本是“可互换”的
如果你部署一个无状态 Web 服务:
- 谁是第 1 个 Pod 不重要
- 谁被删掉也不重要
- 反正后面会补一个新的
- 新旧实例只要都能处理请求即可
这就是 Deployment 的世界观。
StatefulSet 的副本是“不可随意互换”的
如果你部署的是:
- PostgreSQL
- MySQL 主从
- Redis Sentinel / Cluster
- Kafka Broker
- ZooKeeper
- Elasticsearch
- Prometheus
- Alertmanager
那么副本往往不是匿名的。
它们通常需要:
- 固定成员名
- 固定启动序号
- 每个成员自己的数据目录
- 通过稳定地址互相发现
这就是 StatefulSet 的世界观。
为什么说 Deployment + PVC 不等于 StatefulSet
很多初学者会说:
我给 Deployment 挂一个 PVC,不就也有状态了吗?
这句话只对了一小部分。
它确实解决了“有卷”
如果 Deployment 只有一个副本,而且你给它挂上一个 PVC,那么:
- Pod 重建后
- 新 Pod 再挂回这个 PVC
数据确实可能还在。
但它没有解决“成员身份”
Deployment 没有给你这些保证:
- Pod 名称稳定
- 0 号、1 号、2 号这样的序号语义
- 固定 DNS 成员地址
- 每个副本自己一块独立的卷
- 默认有序创建 / 有序删除
你可以把它理解成:
PVC解决“数据放哪”StatefulSet解决“谁在用这份数据,以及它在集群里叫什么名字”
这两者不是替代关系,而是组合关系。
StatefulSet 到底提供了哪四类核心能力
你以后面试、设计、排障,都可以围绕这四条来讲。
1. 稳定的 Pod 身份
StatefulSet 的 Pod 不是随机名字,而是:
web-0web-1web-2
这里的重点不是“名字好看”。
重点是:
- 序号有语义
- 同一个 ordinal 表示同一个逻辑成员
以后你看到:
broker-0redis-2alertmanager-1
你就要立刻联想到:
- 这是某个稳定成员
- 不是匿名副本
2. 稳定的网络身份
StatefulSet 通常要配合 Headless Service。
这样每个 Pod 就会有稳定 DNS:
web-0.web-hl.stateful-lab.svc.cluster.localweb-1.web-hl.stateful-lab.svc.cluster.localweb-2.web-hl.stateful-lab.svc.cluster.local
这非常关键。
因为很多分布式组件在做:
- 集群引导
- 主从同步
- peer 通信
- 仲裁选主
时,需要“按固定名字找同伴”。
3. 稳定的每副本独立存储
通过 volumeClaimTemplates,StatefulSet 会自动按副本序号生成 PVC:
data-web-0data-web-1data-web-2
这意味着:
- 每个成员都有自己的数据卷
- 数据和序号绑定,而不是跟随机 Pod 绑定
4. 有序创建 / 删除 / 更新
默认 podManagementPolicy: OrderedReady。
这表示:
- 先创建
web-0 - 等
web-0Ready - 再创建
web-1 - 等
web-1Ready - 再创建
web-2
删除时相反:
- 先删最大 ordinal
- 再删次大的
这对数据库、主从复制、需要先有 leader / seed / primary 的系统特别重要。
你必须学会区分的 4 组概念
这一部分是很多人学到后面还会混淆的地方。
稳定名称,不等于稳定 IP
在我们的实验里:
- 第一次
web-2的 IP 是10.244.242.42 - 缩容再扩容后,
web-2变成了10.244.242.43
但它仍然叫:
web-2
而且它仍然挂着:
data-web-2
所以 StatefulSet 保证的是:
- 稳定逻辑身份
不是:
- 永远固定同一个 Pod IP
稳定存储,不等于固定节点
web-2 是否一定回到同一个节点?
不一定。
是否回到原节点,取决于:
- 调度结果
- 底层卷类型
- 是否有 node affinity
如果底层是:
- NFS
- 网络盘
- 分布式存储
那 Pod 可以换节点,卷仍然能挂上。
如果底层是:
- local PV
- 只在某一台机器上的本地盘
那它就可能被节点亲和性限制住。
Headless Service,不等于“没有 Service”
Headless Service 仍然是 Service。
只是它:
clusterIP: None- 不再给你一个虚拟 VIP
- DNS 直接返回 Pod IP 列表
所以它更像是:
一个“服务发现入口”
而不是:
一个“负载均衡转发器”
PVC 保留,不等于备份
如果 data-web-2 没被删,只能说明:
- 这块卷还在
- 这份目录还在
它不等于:
- 做了快照
- 做了异地备份
- 有版本回滚能力
保留和备份是两回事。
这点 CTO 级别必须分清。
我们这次实验到底做了什么
实验目录:
manifests/09-statefulset
对象非常简单,但刚好覆盖 StatefulSet 的核心原理:
Namespace/stateful-labService/web-hlStatefulSet/web
Headless Service
关键字段是:
spec:
clusterIP: None
这表示:
- 没有 ClusterIP 虚拟地址
- DNS 解析会直接给出后端 Pod IP
StatefulSet
关键字段是:
spec:
serviceName: web-hl
podManagementPolicy: OrderedReady
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
volumeClaimTemplates:
- metadata:
name: data
你要逐个读懂这些字段。
serviceName: web-hl
告诉 StatefulSet:
- 它的稳定网络身份要挂在哪个 Service 名字下面
没有这个字段,就没有我们后面用到的:
web-0.web-hl...
这类稳定 DNS。
podManagementPolicy: OrderedReady
告诉控制器:
- 必须按顺序创建
- 前一个副本没 Ready,不要继续
persistentVolumeClaimRetentionPolicy
这里我们明确写成:
whenDeleted: RetainwhenScaled: Retain
这表示:
- 删除 StatefulSet 时,PVC 不自动跟着删
- 缩容时,被缩掉 ordinal 的 PVC 也保留
volumeClaimTemplates
告诉 StatefulSet:
- 每个副本都要自动生成一份名为
data的 PVC
最终名称会变成:
data-web-0data-web-1data-web-2
这个命名规则非常重要,排障时要一眼认出来。
我们拿到了哪些真实证据
证据 1:创建顺序是严格递进的
我实际观察到 Pod 的创建时间是:
web-0:2026-04-10T02:17:31Zweb-1:2026-04-10T02:17:48Zweb-2:2026-04-10T02:18:05Z
这是非常标准的 OrderedReady 现象。
不是一次性并发出来三个。
证据 2:PVC 也是按序号生成的
生成出的 PVC 是:
data-web-0data-web-1data-web-2
分别绑定到各自不同的 PV。
这说明不是多个成员抢同一块盘。
而是:
- 成员身份和卷是一一对应的
证据 3:Headless Service 解析到所有 Pod
我从 web-0 / web-1 内部执行 nslookup,看到:
web-hl.stateful-lab.svc.cluster.local返回了三个 Pod IPweb-1.web-hl.stateful-lab.svc.cluster.local返回了单个web-1的 IP
这说明:
- Service 域名代表整个成员集合
- Pod 域名代表某个具体成员
证据 4:Pod 级 DNS 只是“名字到 IP”
这里有个很关键的细节。
我第一次访问:
http://web-2.web-hl.stateful-lab.svc.cluster.local
结果报:
connection refused
为什么?
因为这个名字解析到的是:
web-2这个 Pod 的 IP
而我们的容器监听的是:
8080
不是:
80
所以访问 Pod 级 DNS 时,显式带上 :8080 才成功。
这件事非常值得你记住:
Pod FQDN 解决的是“找到哪个 Pod”,不是“自动做 Service 级端口转发”。
这是很多人第一次学 Headless Service 时最容易误判的点。
证据 5:缩容删的是最大 ordinal,而不是随机 Pod
当我把副本数从 3 缩到 2 时:
- 消失的是
web-2
不是:
web-0web-1
这符合 StatefulSet 的删除规则:
- 逆序删除
证据 6:缩容后 PVC 没被删
虽然 web-2 被删了,但:
data-web-2仍然是Bound- 仍然绑定到原来的 PV
这正是:
persistentVolumeClaimRetentionPolicy.whenScaled = Retain
的真实效果。
证据 7:数据目录在 NFS 后端仍然存在
我把 web-2 的 PVC 映射到的 PV 查出来后,看到它对应:
- NFS server:
10.10.0.5 - NFS path:
/srv/nfs/k8s/stateful-lab-data-web-2-pvc-ea7bd08e-17c2-4f8c-9a7e-41dcbb393e12
我还直接 SSH 到 NFS 服务器确认了目录和文件确实存在。
这一步的意义很大。
因为它把抽象的:
- PVC
- PV
落到了真实的:
- 一台服务器
- 一个目录
证据 8:扩容回来的是同一逻辑成员,不是一个全新匿名实例
我先在 web-2 的卷里写入:
marker.txt
内容是:
lesson9-marker=20260410T022019
缩容到 2 后再扩回 3,我重新进入 web-2,看到:
- 它仍然叫
web-2 marker.txt仍然存在index.html里记录的first_boot仍是首次创建时的时间
更关键的是,它的 Pod IP 已经变了:
- 旧 IP:
10.244.242.42 - 新 IP:
10.244.242.43
这正好证明:
- IP 可以变
- 逻辑身份不变
- 数据也不变
这就是 StatefulSet 最核心的工程价值。
真实集群里有哪些现成样本
这套集群不是玩具集群,里面已经有几个很好的 StatefulSet 样本。
1. gitea/gitea-postgresql
我检查到它的关键配置是:
podManagementPolicy: OrderedReadyserviceName: gitea-postgresql-hlpersistentVolumeClaimRetentionPolicy: Retain / RetainvolumeClaimTemplates: data 10Gi nfs-dynamic
这非常符合数据库场景。
数据库类组件通常更偏向:
- 稳定启动顺序
- 明确的存储归属
2. monitoring/prometheus-monitoring-kube-prometheus-prometheus
它的关键配置是:
podManagementPolicy: ParallelserviceName: prometheus-operatedpersistentVolumeClaimRetentionPolicy: Retain / Retain
这说明 StatefulSet 不一定非得顺序创建。
如果应用本身不依赖严格顺序,或者更追求快速拉起,就可以选:
Parallel
3. monitoring/alertmanager-monitoring-kube-prometheus-alertmanager
它的关键点更有教学意义:
podManagementPolicy: ParallelserviceName: alertmanager-operated- 参数里有:
--cluster.peer=alertmanager-monitoring-kube-prometheus-alertmanager-0.alertmanager-operated:9094
而对应的 Headless Service:
clusterIP: NonepublishNotReadyAddresses: true
这说明它在做什么?
它在做:
- 成员级别的稳定发现
- 集群 peer 互联
而且为了让成员在完全 Ready 之前也能互相发现,它启用了:
publishNotReadyAddresses: true
这是比入门示例更真实、更高级的一种状态型集群设计。
什么时候该用 OrderedReady,什么时候该用 Parallel
这不是背概念,而是要结合业务行为判断。
更适合 OrderedReady 的场景
- 主从初始化顺序敏感
- 需要先有 seed / primary / leader
- 前一个成员健康后,后一个成员才能正常加入
- 初始化脚本容易彼此干扰
常见例子:
- 某些数据库
- 需要串行引导的复制集群
更适合 Parallel 的场景
- 成员彼此相对独立
- 不要求严格按序启动
- 希望更快拉起集群
- 应用自己有成员发现 / 重试 / 自愈能力
常见例子:
- Prometheus
- Alertmanager
- 某些日志存储或监控组件
Headless Service 的原理,你要讲得明白
这块是 CTO 级别必须能解释给团队听的。
普通 Service 在干什么
普通 ClusterIP Service 的心智模型是:
客户端 -> ClusterIP(VIP) -> kube-proxy/IPVS/iptables -> 某个后端 Pod
核心是:
- 有一个虚拟入口
- kube-proxy 负责转发
- 客户端通常不关心具体落到哪个 Pod
Headless Service 在干什么
Headless Service 的心智模型是:
客户端 -> DNS 查询 -> 直接得到后端 Pod IP 列表 -> 客户端自己连某个 Pod
核心变化是:
- 不再经由 VIP
- 不再由 kube-proxy 代表你做负载均衡
- 重点变成“服务发现”
所以它特别适合:
- StatefulSet
- 需要直接面向成员节点通信的系统
- 需要自己维护 peer 列表的集群
它的限制也要讲清楚
- 客户端必须知道怎么处理多个 A 记录
- 不能指望它像普通 Service 一样提供统一转发体验
- 端口语义也更接近“你直连这个 Pod 的那个端口”
如果团队成员把 Headless Service 当成“普通 Service 少了个 IP”,那大概率会踩坑。
volumeClaimTemplates 的本质是什么
它不是“给所有副本共用一块卷”。
恰恰相反。
它是在表达:
每个副本都按同一模板,生成一份属于自己的 PVC。
所以 StatefulSet 的卷模型通常是:
web-0 <-> data-web-0 <-> pv-0
web-1 <-> data-web-1 <-> pv-1
web-2 <-> data-web-2 <-> pv-2
这与 Deployment 挂单个共享 PVC 的思路完全不同。
这种设计特别适合:
- 每个成员有自己账本
- 每个成员有自己 WAL / data dir
- 每个成员有自己的缓存或索引
你以后设计系统时必须警惕的误区
误区 1:把 StatefulSet 当成“带编号的 Deployment”
不够准确。
它的重点不在“名字带编号”,而在:
- 身份稳定
- 存储稳定
- 网络发现稳定
- 生命周期顺序可控
误区 2:以为 RWO 就一定不能跨节点重建
不对。
ReadWriteOnce 说的是:
- 同一时刻挂载语义
不是:
- 这块盘只能永远留在某个节点
如果底层是 NFS 这种网络存储,那么即使 PVC 是 RWO,Pod 仍然可能在别的节点重新挂载成功。
误区 3:缩容了,以为数据也自动清了
不一定。
要看:
persistentVolumeClaimRetentionPolicyPV reclaimPolicy- 外部 provisioner 的行为
在我们的实验里,缩容后:
- Pod 没了
- PVC 还在
- 后端 NFS 目录还在
误区 4:以为 StatefulSet 自带高可用
也不对。
StatefulSet 提供的是“成员编排模型”。
真正的高可用还要看:
- 应用自己是否支持主从 / 仲裁 / 副本复制
- 存储后端是否可靠
- 网络是否可靠
- 探针是否合理
- 备份、恢复、扩缩容策略是否完善
StatefulSet 不是 HA 魔法棒。
你以后排障时最该先看哪些命令
看整体
kubectl -n <ns> get sts,pod,pvc
kubectl -n <ns> get svc,endpoints,endpointslice
你要先回答:
- StatefulSet 目标副本数是多少
- 哪个 ordinal 没起来
- 对应的 PVC 有没有创建和绑定
- Headless Service 有没有把端点暴露出来
看控制器事件
kubectl -n <ns> get events --sort-by=.lastTimestamp
kubectl -n <ns> describe sts <name>
kubectl -n <ns> describe pod <pod-name>
你要重点找:
FailedSchedulingProvisioningProvisioningSucceededSuccessfulCreateSuccessfulDelete- 探针失败
看 DNS 和成员发现
kubectl -n <ns> exec <pod> -- nslookup <svc>
kubectl -n <ns> exec <pod> -- nslookup <pod>.<svc>.<ns>.svc.cluster.local
这里要确认:
- 集合域名是否返回多条 A 记录
- 单 Pod 域名是否返回固定成员地址
看卷的真实去向
kubectl -n <ns> get pvc <claim> -o wide
kubectl get pv <pv-name> -o yaml
你要最终追到:
- 这块卷究竟是哪种后端
- 真正的数据路径在哪
- 节点切换是否仍可挂载
本课你必须真正掌握的结论
如果你把下面几句话说顺了,这一课才算真的学会。
StatefulSet解决的核心不是“Pod 有盘”,而是“成员身份、网络发现、存储归属、生命周期顺序”。Headless Service的本质是服务发现,不是 VIP 转发。serviceName + ordinal + headless DNS一起构成稳定成员地址。volumeClaimTemplates会为每个副本生成独立 PVC,而不是共享一块卷。- 缩容删除的是最大 ordinal,保不保留 PVC 要看 retention policy。
- 稳定名称不等于稳定 IP,稳定存储也不等于稳定节点。
- StatefulSet 只是编排模型,不自动等于高可用和备份能力。
给你的专家化训练题
你可以试着不用看文档,自己回答下面这些问题。
- 为什么 Alertmanager 这种组件更适合用 Headless Service 来做 peer 发现?
- 为什么
web-2缩容后数据还在,但 Pod IP 却会变化? RWO + NFS为什么仍然可能跨节点重建成功?- 为什么 Pod 级 FQDN 能解析成功,但不带端口访问仍可能失败?
- 如果把
persistentVolumeClaimRetentionPolicy.whenScaled改成Delete,你预期缩容后会发生什么?
你要能把这 5 个问题讲明白,才算真正入门 StatefulSet。