K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

第九课:StatefulSet、Headless Service、稳定身份与存储原理

上一课我们已经把存储主链路讲清楚了: 但只懂“数据不丢”还不够。 你以后做数据库、中间件、监控、日志、消息队列时,真正难的往往不是“有没有卷”,而是下面这几个问题: 1. 这个副本到底是谁,它是不是固定的那个成员 2. 它的同伴怎么找到它 3. 它重建后还是不是“原来的那个身份” 4. 缩容之后,那一份副本的数据要不要保留 5. 多个副本能不能按顺序启动,而

Markdown09-第九课-StatefulSet-Headless-Service-稳定身份与存储原理.md2026年4月10日 02:27

第九课:StatefulSet、Headless Service、稳定身份与存储原理

为什么这一课必须接在存储后面

上一课我们已经把存储主链路讲清楚了:

  • PVC 是需求声明
  • PV 是真实卷
  • StorageClass 决定供给方式
  • NFS 让数据从 Pod 生命周期里解耦出来

但只懂“数据不丢”还不够。

你以后做数据库、中间件、监控、日志、消息队列时,真正难的往往不是“有没有卷”,而是下面这几个问题:

  1. 这个副本到底是谁,它是不是固定的那个成员
  2. 它的同伴怎么找到它
  3. 它重建后还是不是“原来的那个身份”
  4. 缩容之后,那一份副本的数据要不要保留
  5. 多个副本能不能按顺序启动,而不是一窝蜂一起冲上来

这时只会 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-0
  • web-1
  • web-2

这里的重点不是“名字好看”。

重点是:

  • 序号有语义
  • 同一个 ordinal 表示同一个逻辑成员

以后你看到:

  • broker-0
  • redis-2
  • alertmanager-1

你就要立刻联想到:

  • 这是某个稳定成员
  • 不是匿名副本

2. 稳定的网络身份

StatefulSet 通常要配合 Headless Service。

这样每个 Pod 就会有稳定 DNS:

  • web-0.web-hl.stateful-lab.svc.cluster.local
  • web-1.web-hl.stateful-lab.svc.cluster.local
  • web-2.web-hl.stateful-lab.svc.cluster.local

这非常关键。

因为很多分布式组件在做:

  • 集群引导
  • 主从同步
  • peer 通信
  • 仲裁选主

时,需要“按固定名字找同伴”。

3. 稳定的每副本独立存储

通过 volumeClaimTemplates,StatefulSet 会自动按副本序号生成 PVC:

  • data-web-0
  • data-web-1
  • data-web-2

这意味着:

  • 每个成员都有自己的数据卷
  • 数据和序号绑定,而不是跟随机 Pod 绑定

4. 有序创建 / 删除 / 更新

默认 podManagementPolicy: OrderedReady

这表示:

  • 先创建 web-0
  • web-0 Ready
  • 再创建 web-1
  • web-1 Ready
  • 再创建 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-lab
  • Service/web-hl
  • StatefulSet/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: Retain
  • whenScaled: Retain

这表示:

  • 删除 StatefulSet 时,PVC 不自动跟着删
  • 缩容时,被缩掉 ordinal 的 PVC 也保留

volumeClaimTemplates

告诉 StatefulSet:

  • 每个副本都要自动生成一份名为 data 的 PVC

最终名称会变成:

  • data-web-0
  • data-web-1
  • data-web-2

这个命名规则非常重要,排障时要一眼认出来。


我们拿到了哪些真实证据

证据 1:创建顺序是严格递进的

我实际观察到 Pod 的创建时间是:

  • web-02026-04-10T02:17:31Z
  • web-12026-04-10T02:17:48Z
  • web-22026-04-10T02:18:05Z

这是非常标准的 OrderedReady 现象。

不是一次性并发出来三个。

证据 2:PVC 也是按序号生成的

生成出的 PVC 是:

  • data-web-0
  • data-web-1
  • data-web-2

分别绑定到各自不同的 PV。

这说明不是多个成员抢同一块盘。

而是:

  • 成员身份和卷是一一对应的

证据 3:Headless Service 解析到所有 Pod

我从 web-0 / web-1 内部执行 nslookup,看到:

  • web-hl.stateful-lab.svc.cluster.local 返回了三个 Pod IP
  • web-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-0
  • web-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: OrderedReady
  • serviceName: gitea-postgresql-hl
  • persistentVolumeClaimRetentionPolicy: Retain / Retain
  • volumeClaimTemplates: data 10Gi nfs-dynamic

这非常符合数据库场景。

数据库类组件通常更偏向:

  • 稳定启动顺序
  • 明确的存储归属

2. monitoring/prometheus-monitoring-kube-prometheus-prometheus

它的关键配置是:

  • podManagementPolicy: Parallel
  • serviceName: prometheus-operated
  • persistentVolumeClaimRetentionPolicy: Retain / Retain

这说明 StatefulSet 不一定非得顺序创建。

如果应用本身不依赖严格顺序,或者更追求快速拉起,就可以选:

  • Parallel

3. monitoring/alertmanager-monitoring-kube-prometheus-alertmanager

它的关键点更有教学意义:

  • podManagementPolicy: Parallel
  • serviceName: alertmanager-operated
  • 参数里有:
    • --cluster.peer=alertmanager-monitoring-kube-prometheus-alertmanager-0.alertmanager-operated:9094

而对应的 Headless Service:

  • clusterIP: None
  • publishNotReadyAddresses: 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:缩容了,以为数据也自动清了

不一定。

要看:

  • persistentVolumeClaimRetentionPolicy
  • PV 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>

你要重点找:

  • FailedScheduling
  • Provisioning
  • ProvisioningSucceeded
  • SuccessfulCreate
  • SuccessfulDelete
  • 探针失败

看 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

你要最终追到:

  • 这块卷究竟是哪种后端
  • 真正的数据路径在哪
  • 节点切换是否仍可挂载

本课你必须真正掌握的结论

如果你把下面几句话说顺了,这一课才算真的学会。

  1. StatefulSet 解决的核心不是“Pod 有盘”,而是“成员身份、网络发现、存储归属、生命周期顺序”。
  2. Headless Service 的本质是服务发现,不是 VIP 转发。
  3. serviceName + ordinal + headless DNS 一起构成稳定成员地址。
  4. volumeClaimTemplates 会为每个副本生成独立 PVC,而不是共享一块卷。
  5. 缩容删除的是最大 ordinal,保不保留 PVC 要看 retention policy。
  6. 稳定名称不等于稳定 IP,稳定存储也不等于稳定节点。
  7. StatefulSet 只是编排模型,不自动等于高可用和备份能力。

给你的专家化训练题

你可以试着不用看文档,自己回答下面这些问题。

  1. 为什么 Alertmanager 这种组件更适合用 Headless Service 来做 peer 发现?
  2. 为什么 web-2 缩容后数据还在,但 Pod IP 却会变化?
  3. RWO + NFS 为什么仍然可能跨节点重建成功?
  4. 为什么 Pod 级 FQDN 能解析成功,但不带端口访问仍可能失败?
  5. 如果把 persistentVolumeClaimRetentionPolicy.whenScaled 改成 Delete,你预期缩容后会发生什么?

你要能把这 5 个问题讲明白,才算真正入门 StatefulSet。