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

Repository Reading Site

本轮操作记录:StatefulSet、Headless Service 与稳定身份实验

这一轮我不是只想“创建一个 StatefulSet”。 我要把下面这些事情做成可验证的事实: 1. StatefulSet 为什么不是 Deployment 的简单替代品 2. Headless Service 到底如何提供成员发现 3. `serviceName`、Pod ordinal、DNS 之间是什么关系 4. `volumeClaimTemplat

Markdown09-操作记录-StatefulSet与Headless-Service实验.md2026年4月10日 02:27

本轮操作记录:StatefulSet、Headless Service 与稳定身份实验

本轮目标

这一轮我不是只想“创建一个 StatefulSet”。

我要把下面这些事情做成可验证的事实:

  1. StatefulSet 为什么不是 Deployment 的简单替代品
  2. Headless Service 到底如何提供成员发现
  3. serviceName、Pod ordinal、DNS 之间是什么关系
  4. volumeClaimTemplates 如何把状态和副本序号绑定起来
  5. 缩容时为什么删的是最大 ordinal
  6. 缩容后数据为什么还能保留
  7. 真正排障时应该沿着哪条链路查

Step 1:先核对仓库现状和上一课风格

实际命令

ls -la
find . -maxdepth 2 -type d | sort
sed -n '1,220p' 08-第八课-存储持久化PV-PVC-StorageClass与NFS原理.md
sed -n '1,240p' 08-操作记录-存储持久化与NFS实验.md

为什么先做这一步

因为这套学习路径已经形成了固定结构:

  • 一份教学文档
  • 一份操作记录
  • 一组对应的 manifests

如果我不先看前一课格式,后面的内容就会断层,学习体验会变差。

我确认到的事实

当前仓库里已经完成了:

  • 环境验证
  • kubectl apply
  • 调度
  • 网络
  • NetworkPolicy
  • 认证授权
  • ConfigMap / Secret
  • 存储 / NFS

所以第九课最自然的下一步就是:

  • StatefulSet

也就是把“有盘”继续推进到“有身份的状态副本”。


Step 2:先看真实集群里已经有哪些 StatefulSet 样本

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl get statefulset -A -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl get svc -A | rg 'headless|hl|operated'
sed -n '1,240p' phase-1/03-statefulset-daemonset-job.md

为什么必须先看真实样本

因为我不想把这课教成“纸上理解”。

我需要先确认这套集群里,StatefulSet 到底是不是实际在用。

只有这样,后面讲:

  • Pod 身份
  • 稳定 DNS
  • 每副本独立卷
  • 顺序控制

这些内容才不是空讲概念。

我看到的结果

真实集群里已经有:

  • gitea/gitea-postgresql
  • monitoring/prometheus-monitoring-kube-prometheus-prometheus
  • monitoring/alertmanager-monitoring-kube-prometheus-alertmanager
  • monitoring/loki
  • dev/web

Headless Service 也真实存在:

  • dev/web-headless
  • gitea/gitea-postgresql-hl
  • monitoring/alertmanager-operated
  • monitoring/prometheus-operated

原理解释

这一步证明了:

  • StatefulSet 不是教材里的特殊资源
  • 你的集群里正在用它承载真实状态型组件

这对学习非常重要。

因为你以后排障,排的不是示例,而是这些真实对象。


Step 3:抽出真实生产样本的关键字段

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n gitea get sts gitea-postgresql -o jsonpath='{.spec.podManagementPolicy}{"\n"}{.spec.serviceName}{"\n"}{.spec.persistentVolumeClaimRetentionPolicy.whenDeleted}{"\n"}{.spec.persistentVolumeClaimRetentionPolicy.whenScaled}{"\n"}{range .spec.volumeClaimTemplates[*]}{.metadata.name}{" "}{.spec.resources.requests.storage}{" "}{.spec.storageClassName}{"\n"}{end}'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n monitoring get sts prometheus-monitoring-kube-prometheus-prometheus -o jsonpath='{.spec.podManagementPolicy}{"\n"}{.spec.serviceName}{"\n"}{.spec.persistentVolumeClaimRetentionPolicy.whenDeleted}{"\n"}{.spec.persistentVolumeClaimRetentionPolicy.whenScaled}{"\n"}'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n monitoring get sts alertmanager-monitoring-kube-prometheus-alertmanager -o yaml | \
  rg 'podManagementPolicy|serviceName|cluster.peer|persistentVolumeClaimRetentionPolicy' -n -C 2

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n monitoring get svc alertmanager-operated -o yaml | sed -n '1,120p'

为什么这一步很关键

因为我想把“示例 YAML”升级为“生产式样本”。

我希望你看到:

  • 为什么有的组件选 OrderedReady
  • 为什么有的组件选 Parallel
  • 为什么 Alertmanager 需要稳定 peer DNS
  • 为什么 Headless Service 在真实系统里不是可有可无

我确认到的事实

gitea-postgresql

  • podManagementPolicy: OrderedReady
  • serviceName: gitea-postgresql-hl
  • persistentVolumeClaimRetentionPolicy: Retain / Retain
  • volumeClaimTemplates: data 10Gi nfs-dynamic

Prometheus

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

Alertmanager

  • podManagementPolicy: Parallel
  • serviceName: alertmanager-operated
  • --cluster.peer=alertmanager-monitoring-kube-prometheus-alertmanager-0.alertmanager-operated:9094
  • alertmanager-operatedclusterIP: None
  • publishNotReadyAddresses: true

原理解释

这一步告诉我们:

  • StatefulSet 不是只有一种固定玩法
  • 它既可以表达“顺序严格”的数据库类系统
  • 也可以表达“成员发现稳定,但启动顺序不严格”的监控类系统

这才是工程视角,而不是死背字段。


Step 4:设计我们自己的实验对象

我创建了哪些文件

路径:

  • manifests/09-statefulset/00-namespace-stateful-lab.yaml
  • manifests/09-statefulset/10-web-headless-service.yaml
  • manifests/09-statefulset/20-web-statefulset.yaml

为什么实验要这样设计

我需要一个足够简单,但又能同时验证:

  • 稳定名字
  • Headless DNS
  • 自动生成 PVC
  • 顺序启动
  • 缩扩容与数据保留

的对象。

所以我用了:

  • 一个 Headless Service
  • 一个 3 副本 StatefulSet
  • volumeClaimTemplates
  • busybox httpd

YAML 里最关键的设计点

Headless Service

clusterIP: None

表示:

  • DNS 返回后端 Pod IP
  • 不使用 ClusterIP VIP

StatefulSet

serviceName: web-hl
podManagementPolicy: OrderedReady
persistentVolumeClaimRetentionPolicy:
  whenDeleted: Retain
  whenScaled: Retain

表示:

  • 稳定 DNS 挂在 web-hl 名下
  • 创建顺序必须递进
  • 缩容 / 删除时,PVC 不自动清理

volumeClaimTemplates

metadata:
  name: data

意味着后续会自动生成:

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

原理解释

我这里不是追求“最短 YAML”。

我是故意把关键字段写全,好让每个实验现象都能回到具体字段。

这才是能学会原理的写法。


Step 5:应用实验资源,观察最初始状态

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl apply \
  -f manifests/09-statefulset/00-namespace-stateful-lab.yaml \
  -f manifests/09-statefulset/10-web-headless-service.yaml \
  -f manifests/09-statefulset/20-web-statefulset.yaml

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab get all,pvc

我为什么立刻看 all,pvc

因为 StatefulSet 和无状态控制器的差异,一上来就会体现在:

  • Pod 是否一次性都出现
  • PVC 是否按序号生成

我看到的第一个关键结果

刚应用完时,集群里只出现了:

  • data-web-0

而不是一下子就出来:

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

原理解释

这说明 StatefulSet 的控制器行为不是:

  • “想要 3 个副本,就全都并发造出来”

而是:

  • 先处理第 0 号成员
  • 后续成员要等前面的状态满足条件再推进

这就是 StatefulSet 和 Deployment 的第一层直观区别。


Step 6:第一次实验就遇到故障,我按真实排障路径处理

我看到的问题

继续观察后,web-0 卡在了:

  • Init:CrashLoopBackOff

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab get pod,pvc -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab get events --sort-by=.lastTimestamp | tail -n 60
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab describe pod web-0
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab logs web-0 -c bootstrap --previous

为什么这套排障顺序是标准动作

当 Pod 起不来时,你不能一上来就猜。

标准顺序应该是:

  1. get 看全局状态
  2. events 看控制器和 kubelet 报了什么
  3. describe 看容器状态、最近事件、探针、挂载
  4. logs 直接看容器自己怎么报错

我定位到的直接原因

bootstrap init 容器日志明确报:

sh: syntax error: unexpected end of file (expecting "fi")

故障的本质是什么

这不是 Kubernetes 机制问题。

而是我在 init 容器脚本里用了 here-doc 写文件,在 busybox sh 里最终解析失败,导致脚本语法不完整。

我怎么修复

我把写文件逻辑改成了更稳妥的 printf

printf 'pod=%s\nordinal=%s\nfirst_boot=%s\nnode=%s\n' ...

然后重新应用 StatefulSet 模板,再删除 web-0 让控制器按新模板重建。

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/09-statefulset/20-web-statefulset.yaml
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab delete pod web-0 --wait=false

原理解释

这一段非常值得你学:

  • init 容器失败,会阻止主容器启动
  • StatefulSet 不会跳过坏掉的 ordinal 继续造后面的成员
  • 所以前一个成员卡住,后续成员也会跟着卡住

这正是有序控制的另一面:

  • 它能保护依赖顺序的系统
  • 也会让前序错误阻塞整条链路

Step 7:修复后,验证 OrderedReady 的顺序创建

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab rollout status statefulset/web --timeout=240s

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab get pods -o 'custom-columns=NAME:.metadata.name,PHASE:.status.phase,READY:.status.containerStatuses[0].ready,NODE:.spec.nodeName,IP:.status.podIP,CREATED:.metadata.creationTimestamp'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab get pvc -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,VOLUME:.spec.volumeName,STORAGECLASS:.spec.storageClassName,SIZE:.spec.resources.requests.storage

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab get events --sort-by=.lastTimestamp | tail -n 80

命令解释

rollout status

用来观察控制器是否完成本轮模板下发。

custom-columns

把最关心的字段按列打印出来,适合观察:

  • 创建时间
  • IP
  • 节点
  • Ready 状态

为什么这里给 custom-columns 加了引号

因为当前 shell 是 zsh

[0] 这种写法如果不加引号,zsh 会把它当作通配模式解析,报:

  • no matches found

这是 shell 层面的坑,不是 kubectl 的坑。

我看到的关键结果

创建时间是:

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

PVC 也依次出现:

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

事件里能看到典型过程:

  • create Claim data-web-0 Pod web-0
  • ProvisioningSucceeded
  • Scheduled
  • Created container

然后才轮到 1 号、2 号。

原理解释

这一步说明:

  • StatefulSet 的“副本数 = 3”不是并行目标
  • 而是一个有顺序约束的目标

在默认策略下,控制器会先保证:

  • web-0 完整可用

然后才会继续。


Step 8:验证 Headless Service 与稳定 DNS

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab get svc,endpoints,endpointslice

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab exec web-0 -- sh -c 'nslookup web-hl.stateful-lab.svc.cluster.local'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab exec web-1 -- sh -c 'nslookup web-0.web-hl.stateful-lab.svc.cluster.local'

为什么先看 endpointsendpointslice

因为 Headless Service 的本质就是:

  • DNS 返回后端成员

而后端成员列表实际来自:

  • Endpoints / EndpointSlice

如果这里就不对,DNS 后面也不会对。

我看到的结果

web-hl 的:

  • clusterIP = None

endpoints 里有三个地址:

  • 10.244.119.242:8080
  • 10.244.169.12:8080
  • 10.244.242.42:8080

nslookup web-hl.stateful-lab.svc.cluster.local 返回了三个 Pod IP。

nslookup web-0.web-hl.stateful-lab.svc.cluster.local 返回的是单个:

  • 10.244.119.242

原理解释

这正是 Headless Service 的经典行为:

  • 集合域名返回全体成员
  • 成员域名返回具体成员

这就为:

  • peer 发现
  • 主从通信
  • 集群成员寻址

提供了基础。


Step 9:故意踩一个最容易误解的坑,讲清楚端口语义

第一次尝试的实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab exec web-0 -- sh -c 'wget -qO- http://web-2.web-hl.stateful-lab.svc.cluster.local'

我看到的结果

报错:

  • connection refused

为什么这一步特别有教学价值

很多人会误以为:

  • 既然 Service 写了 port: 80 -> targetPort: 8080
  • 那访问 web-2.web-hl... 就会自动走 80 转 8080

这是不对的。

真正的原理

web-2.web-hl... 这个名字解析出来的是:

  • web-2 这个 Pod 的真实 IP

客户端随后是直接去连这个 Pod。

这时默认端口是:

  • HTTP 的 80

但我们的应用实际监听的是:

  • 8080

所以你不写端口,当然会拒绝连接。

正确验证命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab exec web-0 -- sh -c 'wget -qO- http://web-2.web-hl.stateful-lab.svc.cluster.local:8080'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab exec web-0 -- sh -c 'wget -qO- http://web-hl.stateful-lab.svc.cluster.local:8080'

我看到的成功结果

访问具体成员 web-2 时,返回内容里有:

  • pod=web-2
  • ordinal=2

访问集合域名 web-hl 时,也能取回某个成员的内容。

原理解释

这一段你一定要记住:

Headless Service 重点是发现成员,不是替你做普通 Service 那种 VIP/NAT 转发。

如果你未来团队里有人把这块混了,后面做 Kafka、Redis Cluster、数据库副本集时会掉进很多坑里。


Step 10:验证每个副本都有自己的独立卷

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab exec web-0 -- cat /data/index.html

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab exec web-2 -- cat /data/index.html

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab get pvc -o custom-columns=NAME:.metadata.name,STATUS:.status.phase,VOLUME:.spec.volumeName,STORAGECLASS:.spec.storageClassName,SIZE:.spec.resources.requests.storage

我看到的结果

web-0 的卷里记录的是:

  • pod=web-0
  • ordinal=0

web-2 的卷里记录的是:

  • pod=web-2
  • ordinal=2

PVC 也是一一对应:

  • data-web-0 -> pvc-be680ea6-afd0-4bc9-b9e0-a8ca7fda6e54
  • data-web-1 -> pvc-4c85cc11-0c81-428a-8002-a92fd06cabd9
  • data-web-2 -> pvc-ea7bd08e-17c2-4f8c-9a7e-41dcbb393e12

原理解释

这证明 volumeClaimTemplates 的行为是:

  • 每个 ordinal 自动派生自己的 PVC

不是:

  • 大家共用一块卷

这对数据库类系统至关重要。

因为每个副本的账本、索引、WAL、缓存都应当独立。


Step 11:把 web-2 的状态一路追到真实 NFS 目录

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab exec web-2 -- sh -c 'echo lesson9-marker=$(date +%Y%m%dT%H%M%S) > /data/marker.txt; ls -lah /data; echo; cat /data/marker.txt'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab get pvc data-web-2 -o jsonpath='{.spec.volumeName}{"\n"}{.metadata.name}{"\n"}'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl get pv pvc-ea7bd08e-17c2-4f8c-9a7e-41dcbb393e12 -o jsonpath='{.spec.nfs.server}{"\n"}{.spec.nfs.path}{"\n"}'

ssh root@154.219.104.66 \
  'sudo ls -lah /srv/nfs/k8s/stateful-lab-data-web-2-pvc-ea7bd08e-17c2-4f8c-9a7e-41dcbb393e12 && echo && sudo cat /srv/nfs/k8s/stateful-lab-data-web-2-pvc-ea7bd08e-17c2-4f8c-9a7e-41dcbb393e12/marker.txt'

为什么一定要追到 NFS 服务器

因为很多人学到 PVC 就停了。

但如果你以后要做故障排查,你必须能继续往下追:

  • PVC 绑定到了哪个 PV
  • PV 背后是哪种存储
  • 实际数据目录在什么地方

我看到的结果

data-web-2 绑定到:

  • pvc-ea7bd08e-17c2-4f8c-9a7e-41dcbb393e12

对应的真实后端是:

  • NFS server:10.10.0.5
  • path:/srv/nfs/k8s/stateful-lab-data-web-2-pvc-ea7bd08e-17c2-4f8c-9a7e-41dcbb393e12

我写入的文件是:

  • lesson9-marker=20260410T022019

在 NFS 服务器上也能直接看到同样内容。

原理解释

这一步把“状态”从抽象概念落成了真实对象:

  • 这不是某个 Pod 私有的幻觉文件系统
  • 它已经是后端存储里一份真实目录

所以后面只要这块卷还在,Pod 换了,数据也可能还在。


Step 12:缩容到 2,验证删除顺序和 PVC 保留

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab scale statefulset web --replicas=2

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab get pods -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab get pvc -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab get events --sort-by=.lastTimestamp | tail -n 40

ssh root@154.219.104.66 \
  'sudo ls -lah /srv/nfs/k8s/stateful-lab-data-web-2-pvc-ea7bd08e-17c2-4f8c-9a7e-41dcbb393e12 && echo && sudo cat /srv/nfs/k8s/stateful-lab-data-web-2-pvc-ea7bd08e-17c2-4f8c-9a7e-41dcbb393e12/marker.txt'

我为什么同时看 pods、pvc、events、NFS

因为只看一个维度会误判。

我要同时确认:

  • 控制器删掉的是哪个 Pod
  • PVC 有没有一起消失
  • 事件里控制器到底宣布了什么
  • 底层目录是不是真的还在

我看到的结果

缩容后剩下的是:

  • web-0
  • web-1

被删掉的是:

  • web-2

事件里明确写着:

  • delete Pod web-2 in StatefulSet web successful

与此同时,data-web-2 仍然是:

  • Bound

而且 NFS 目录和 marker.txt 还在。

原理解释

这一步完全验证了两条规则:

  1. StatefulSet 缩容按逆序删除最大 ordinal
  2. whenScaled: Retain 会保留被缩掉成员的 PVC

这就是为什么你以后缩数据库副本时,不能想当然地以为“缩掉就清干净了”。


Step 13:再扩回 3,验证同一 ordinal 重新挂回原状态

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab scale statefulset web --replicas=3

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab get pods -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n stateful-lab get pvc data-web-2 -o wide

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n stateful-lab exec web-2 -- sh -c 'hostname; echo; cat /data/index.html; echo; cat /data/marker.txt'

我看到的结果

新的 web-2 又回来了。

它现在的 Pod IP 是:

  • 10.244.242.43

而之前老的 web-2 是:

  • 10.244.242.42

但它进入容器后看到的状态仍是原来的:

  • hostname = web-2
  • first_boot = 2026-04-10T02:18:25+0000
  • lesson9-marker=20260410T022019

对应的 PVC 仍然是:

  • data-web-2 -> pvc-ea7bd08e-17c2-4f8c-9a7e-41dcbb393e12

原理解释

这里你必须彻底建立一个正确模型:

  • StatefulSet 保的是“逻辑成员身份”
  • 不是“永远同一 IP”

这个实验里:

  • web-2 这个成员名保住了
  • 它对应的卷保住了
  • 它的旧数据保住了

但:

  • Pod IP 可以变

这非常符合 Kubernetes 的设计哲学。


Step 14:把实验现象和真实生产组件对应起来

实际命令

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n gitea get sts gitea-postgresql -o jsonpath='{.spec.podManagementPolicy}{"\n"}{.spec.serviceName}{"\n"}{.spec.persistentVolumeClaimRetentionPolicy.whenDeleted}{"\n"}{.spec.persistentVolumeClaimRetentionPolicy.whenScaled}{"\n"}{range .spec.volumeClaimTemplates[*]}{.metadata.name}{" "}{.spec.resources.requests.storage}{" "}{.spec.storageClassName}{"\n"}{end}'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n monitoring get sts prometheus-monitoring-kube-prometheus-prometheus -o jsonpath='{.spec.podManagementPolicy}{"\n"}{.spec.serviceName}{"\n"}{.spec.persistentVolumeClaimRetentionPolicy.whenDeleted}{"\n"}{.spec.persistentVolumeClaimRetentionPolicy.whenScaled}{"\n"}'

KUBECONFIG=~/.kube/config-k8s-lab \
kubectl -n monitoring get sts alertmanager-monitoring-kube-prometheus-alertmanager -o yaml | \
  rg 'podManagementPolicy|serviceName|cluster.peer|persistentVolumeClaimRetentionPolicy' -n -C 2

我得到的结论

数据库类样本

gitea-postgresql 选择:

  • OrderedReady
  • Headless Service
  • 独立卷模板
  • PVC Retain

这说明数据库更强调:

  • 成员顺序
  • 数据归属

监控类样本

Prometheus / Alertmanager 选择:

  • Parallel
  • 稳定 serviceName
  • PVC Retain

Alertmanager 甚至直接把固定 peer 地址写进启动参数:

  • alertmanager-monitoring-kube-prometheus-alertmanager-0.alertmanager-operated:9094

原理解释

这一步非常关键。

因为它把我们的教学实验,从:

  • “一个简单 demo”

接到了:

  • 你集群里真实在跑的架构模式

以后你看到这些 YAML,就不会只觉得“字段很多”,而是会知道每个字段是在解决什么系统问题。


Step 15:本轮最值得你背下来的排障链路

如果以后某个 StatefulSet 起不来,我建议你先按这个顺序查。

1. 先看控制器和副本整体

kubectl -n <ns> get sts,pod,pvc

先回答:

  • 哪个 ordinal 没起来
  • 对应 PVC 有没有创建出来

2. 再看事件

kubectl -n <ns> get events --sort-by=.lastTimestamp

先区分:

  • 是调度失败
  • 是卷没绑上
  • 还是容器自己挂了

3. 看 Pod 细节和容器日志

kubectl -n <ns> describe pod <pod>
kubectl -n <ns> logs <pod> -c <container> --previous

尤其别忘了:

  • init 容器单独看

4. 看 DNS 和成员发现

kubectl -n <ns> get svc,endpoints,endpointslice
kubectl -n <ns> exec <pod> -- nslookup <svc>
kubectl -n <ns> exec <pod> -- nslookup <pod>.<svc>.<ns>.svc.cluster.local

5. 看存储真实后端

kubectl -n <ns> get pvc <claim> -o wide
kubectl get pv <pv> -o yaml

必要时继续追到:

  • NFS 目录
  • 云盘 ID
  • 节点挂载状态

本轮结论

这一轮我已经把 StatefulSet 最关键的四个承诺都做成了真实证据:

  1. 稳定序号和有序创建:web-0 -> web-1 -> web-2
  2. 稳定 DNS:web-0.web-hl... 这类成员级域名可用
  3. 每副本独立卷:data-web-0/1/2 各自绑定不同 PV
  4. 缩容后状态保留:web-2 删除,但 data-web-2 和后端 NFS 数据仍在

这意味着你已经不只是“会敲 StatefulSet YAML”了。

你已经开始真正理解:

  • Kubernetes 怎样把“成员身份”
  • “网络发现”
  • “存储生命周期”

组合成有状态系统的基础编排模型。