3 副本只活了 1 个——StatefulSet 启动顺序的循环死锁
基于 K8s v1.25+,
apps/v1API。StatefulSet 的podManagementPolicy行为从 v1.7 起稳定,各版本一致场景:3 副本分布式存储的 StatefulSet,部署后 Pod-0 反复 CrashLoopBackOff,Pod-1 和 Pod-2 根本没创建。重启 Pod、调应用代码全没用——整个集群卡在初始化阶段 路径:Pod 状态 → readiness 探针逻辑 → headless Service DNS → StatefulSet 控制器顺序创建策略
上篇讲了 PV 绑定后挂载失败、NFS 版本号写错的坑,这篇我们来看 StatefulSet 启动顺序的循环死锁。
一个 YAML 里配了 replicas: 3,StatefulSet(有状态应用控制器——保证 Pod 的稳定网络标识和顺序启停顺序)部署完等了 10 分钟——kubectl get pod 只看到一个 Pod-0 在 CrashLoopBackOff。集群里只有 1 个 Pod,另外 2 个根本就没出现。不是应用代码的问题——是 StatefulSet 的 podManagementPolicy: OrderedReady(顺序创建策略,前一个 Pod 必须 Running and Ready 才创建下一个)和应用的集群自检逻辑形成了死锁。
【坐标】Pod-0 反复 Crash,Pod-1/2 没出生
现象:刚部署就出了岔子

晚上 10 点,团队把分布式配置中心(类似 etcd/consul 的集群架构)部署到 K8s 集群。
3 副本 StatefulSet,headless Service(clusterIP: None——不分配 ClusterIP,给每个 Pod 分配独立的 DNS 记录 pod-name.service-name.namespace.svc.cluster.local),YAML 检查了三遍,自认为万无一失:
apiVersion: v1
kind: Service
metadata:
name: distributed-config
spec:
clusterIP: None
selector:
app: distributed-config
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: distributed-config
spec:
serviceName: distributed-config
replicas: 3
selector:
matchLabels:
app: distributed-config
template:
metadata:
labels:
app: distributed-config
spec:
containers:
- name: config
image: distributed-config:latest
ports:
- containerPort: 9300
readinessProbe:
exec:
command:
- /health/check_cluster.sh
initialDelaySeconds: 30
periodSeconds: 10
5 分钟后——kubectl get pod -w 刷出来一行红色的 CrashLoopBackOff。更诡异的是,只有 Pod-0 在列表中,Pod-1 和 Pod-2 像没提交过一样,查无此 Pod。
"重启一下看看"。删 Pod-0,StatefulSet 控制器重新创建,一样 CrashLoopBackOff。
改镜像版本、调 JVM 堆大小、看应用日志翻了几百行——进程启动了,集群初始化了,然后 readiness 探针说"不健康"。但为什么?日志里只写了一行 Cluster health: 1/3 nodes online,也没说哪里出错了。
凌晨 1 点,值班的运维拉上了架构师一起看。架构师翻了 20 分钟代码,说:"应用没问题,问题是 K8s 在等什么。"
【分层】逐层排查:从 Pod 到 StatefulSet 控制器
Pod 层:readiness 探针在报什么?
查看 Pod-0 的详细信息:
$ kubectl describe pod distributed-config-0
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Started 45s kubelet Started container config
Warning Unhealthy 15s kubelet Readiness probe failed: cluster health not OK
Warning Unhealthy 5s kubelet Readiness probe failed: cluster health not OK

Events 段写得很清楚:Readiness probe failed: cluster health not OK。
注意——这是 readiness 探针失败,不是容器退出。容器的 Liveness 没有报错,说明应用进程一直在跑,没挂。问题卡在探针脚本 /health/check_cluster.sh 的返回值上。
这个脚本每 10 秒跑一次,看看集群是否健康:
# /health/check_cluster.sh — 检查集群是否已形成(简化版)
PEER_COUNT=$(curl -s http://localhost:9300/_cluster/health | jq '.number_of_nodes')
if [ "$PEER_COUNT" -lt 3 ]; then
exit 1 # 集群成员不足 3 → 不健康
fi
exit 0
问题一目了然:readiness 要求集群至少有 3 个节点才通过。但 Pod-0 是第一个也是唯一一个启动的节点,集群只有它自己,所以永远 return 1。探针永远不通过——Pod 永远不 Ready。
应用层:谁在等谁?
分布式配置中心的启动流程是一个典型的"先有鸡还是先有蛋"困境:
Pod-0 启动
→ 进程初始化(成功)
→ 尝试加入集群(自己作为种子节点)
→ 检查集群健康状态 → 只有 1 个节点 < 3 → "不健康"
→ readiness 探针检测到不健康 → kubelet 标记 Pod 为 NotReady

这里涉及一个关键的设计权衡:分布式应用的 readiness 检查为什么要检查"集群已完全形成"(3 节点全在线)?
这个设计在滚动更新或扩缩容场景下是合理的——防止流量打到未同步的节点。但在首次部署场景下,这个逻辑把自己堵死了。readiness 的判断标准没有区分"首次初始化"和"日常健康检查"两种模式。
这是 K8s 有状态服务排障中一个反复出现的模式:一个在运行时合理的检查逻辑,在初始化时恰好成了死锁的具体实现。
StatefulSet 控制器层:OrderedReady 的严格顺序
StatefulSet 和 Deployment 的核心区别是什么?Deployment 下的 Pod 是"无名的",谁先启动、谁后启动都一样。但 StatefulSet 下的 Pod 是有序号的(-0、-1、-2),它们的身份和启动顺序是有意义的。
所以 StatefulSet 默认采用 podManagementPolicy: OrderedReady:
控制器创建 Pod 的规则:
1. 创建 Pod-0 → 等待它变为 Running and Ready
2. Pod-0 Ready → 创建 Pod-1 → 等待 Ready
3. Pod-1 Ready → 创建 Pod-2 → 等待 Ready
4. 全部 Ready → 部署完成
这种顺序保证对数据库类的有状态应用至关重要——比如 MySQL 主从复制,Pod-0 必须先初始化为主库,Pod-1 才能以从库身份加入。
但这里的 Pod-0 因为 readiness 不过,一直 NotReady。控制器重复等待——不创建 Pod-1。

看 Events 的第二条——waiting for pod distributed-config-0 to be running and ready。StatefulSet 控制器承认它看到了 Pod-0 NotReady,正在等它 Ready。但 Pod-0 等的却是 Pod-1 和 Pod-2 存在才能通过 readiness。这就形成了经典的循环依赖(circular dependency):
StatefulSet 控制器:等 Pod-0 Ready 才创建 Pod-1/2
Pod-0 readiness 探针:等 Pod-1/2 存在才通过
→ 双方都等对方先走 → 死锁
CRI 层:容器运行时确认
有人会问:"那容器到底在不在跑?万一容器运行时卡住了呢?"
K8s 分层排查的第二个原则:不要只看 kubectl,用 crictl 直接从 CRI 层面确认容器状态:
$ crictl ps --name config
CONTAINER IMAGE STATUS NAME NAMESPACE
a1b2c3d4e distributed-config@sha256:abc Running config default
$ crictl logs a1b2c3d4e | tail -3
2026-07-29 10:00:03 INFO Node started, attempting to join cluster
2026-07-29 10:00:04 WARN Cluster health: 1/3 nodes online, quorum not met
容器状态 Running,日志看起来也正常——排除 CRI 层的问题。如果只看 kubectl 查不到,crictl 是第二层防线。
Node/集群层:确认不是资源问题
这个场景下 Node 层和集群层没有参与死锁,但排查时仍然要走完这一层,避免漏判:

资源充足——不是资源不足导致的调度失败。
三层排查到这里,根因锁定:readiness 探针逻辑与 StatefulSet 顺序创建策略的冲突。
【路径】🔍 下次先跑这些命令
五步排查法
遇到 StatefulSet Pod 一半起不来的情况,按这个顺序排查。不要跳层,不要只查 Pod 不看控制器:

【定位】最常误判
❌ 第一层:Naive 想法
"Pod-0 CrashLoopBackOff → 应用有 bug → 调代码、换镜像、看启动日志。"
这是大多数人踩的第一个坑。团队花 2-3 天排查应用层问题:找性能瓶颈、调 JVM 参数、加调试日志、升级 SDK 版本——结果什么都没解决。因为问题根本不在应用本身,应用进程一直在正常运行。
❌ 第二层:有经验但跳层
"Pod-0 起不来 → 看 Node 资源 → kubectl describe node 看 Allocated resources。"
比上一步好一点,至少知道要去排查资源层面。但跳过了 Pod 层的 Events 和 CRI 层的容器状态,直接跳到 Node 层——然后发现 Node 资源还有很多,就回到原地打转。
✅ 第三层:正确排查方向(逐层不跳步)
步骤 1:看 Events → 发现是 readiness 探针失败(不是应用 crash)
步骤 2:看 readiness 探针定义 → 发现它检查集群状态而非进程存活
步骤 3:看 StatefulSet 控制器 → 发现它在等 Pod-0 Ready 才创建后续 Pod
步骤 4:连接三个信息 → 死锁的根因就出来了

关键判断:CrashLoopBackOff 不等于应用 Crash。K8s 的 CrashLoopBackOff 会在以下两种截然不同的情况下触发:
| 触发原因 | 现象 | 判断方法 |
|---|---|---|
| 容器进程退出(exit code ≠ 0) | 进程真正挂了 | kubectl logs pod 日志中途截断退出 |
| readiness 探针连续失败(进程仍在运行) | 进程活着但探针断言失败 | kubectl describe pod 只看得到 readiness 失败,logs 日志正常 |
这次是第二种——容器进程一直在跑,只是 readiness 的检查逻辑太严格,把自己锁死了。
【标点】修复 + Check-list
三种修复方案及其取舍
方案一:区分首次部署和后续维护的 readiness 逻辑

剥离集群健康检查到 livenessProbe(进程真的挂了才重启),readiness 只检查端口存活(接受流量但不一定要完整的集群)。初始延迟 120 秒留给所有 Pod 启动和形成集群的时间。
方案二:使用 Parallel 策略绕过顺序限制
apiVersion: apps/v1
kind: StatefulSet
spec:
podManagementPolicy: Parallel # 不再等待 Ready,并行创建
Parallel 策略让 StatefulSet 控制器一次性创建所有 Pod,不等待 Ready。Pod-0/Pod-1/Pod-2 同时启动,集群同时形成,readiness 检查就能通过了。
但要注意——Parallel 策略放弃了"顺序启动"的语义保证。如果你的应用严格依赖 Pod-0 先初始化(比如 MySQL 主从),Parallel 可能引入竞态条件。
方案三:在应用层面做首次部署的判断
修改 check_cluster.sh,让它区分首次部署和后续维护:
# 如果自己是 ordinal=0 的节点,且集群只有自己 → 通过
if [ "$(hostname)" = "distributed-config-0" ]; then
PEER_COUNT=$(curl -s http://localhost:9300/_cluster/health | jq '.number_of_nodes')
if [ "$PEER_COUNT" -eq 1 ]; then
exit 0 # 首次部署,单节点正常
fi
fi
| 方案 | 优势 | 代价 | 适用场景 |
|---|---|---|---|
| 方案一:分层探针 | 保持 OrderedReady 语义,不改应用代码 | 需要理解探针设计模式 | 大多数情况,推荐首选 |
| 方案二:Parallel 策略 | 最快速,一行 YAML | 放弃有序启动保证 | 集群形成不依赖节点顺序(如 Cassandra) |
| 方案三:应用层判断 | 不改 YAML,保持最大灵活性 | 需要改应用脚本,增加逻辑复杂度 | 应用团队自主可控 |
金句:"排查 StatefulSet 问题不是在查 Pod——是在查 Pod 和控制器之间的那条等待链。"
Check-list:StatefulSet 启动故障排查
| 步骤 | 命令/操作 | 判断标准 |
|---|---|---|
| 看 Pod 列表 | kubectl get pod -w |
部分 Pod 没出现 → 控制器可能在等 |
| 看控制器状态 | kubectl describe statefulset |
有 waiting for pod ... to be running and ready |
| 看 Pod Events | kubectl describe pod <name>-0 |
Events 段有 Readiness probe failed |
| 看 readiness 定义 | kubectl get statefulset -o yaml \| grep -A 15 readinessProbe |
检查逻辑是否依赖其他 Pod |
| CRI 层确认 | crictl ps \| crictl logs <container-id> |
容器真的在运行?还是 kubectl 骗了你? |
| 测 headless DNS | kubectl run dns-test --rm -it --image=busybox:1.28 -- nslookup <name>-0.<service> |
未 Ready 的 Pod 没有 DNS 记录 |
| 看资源是否充足 | kubectl describe node \| grep -A 5 'Allocated resources' |
CPU/Memory 百分比不高,排除资源因素 |
附:完整命令清单
排查与修复命令一览
# 状态查看
kubectl get pod -l app=distributed-config -w
kubectl describe statefulset distributed-config
kubectl describe pod distributed-config-0
# readiness 探针查看
kubectl get statefulset distributed-config -o yaml | grep -A 15 readinessProbe
# CRI 层容器确认
crictl ps --name config
crictl logs <container-id> | tail -20
# 节点资源确认
kubectl describe node | grep -A 5 'Allocated resources'
# 修复应用
kubectl patch statefulset distributed-config -p '{"spec":{"podManagementPolicy":"Parallel"}}'
# DNS 验证
kubectl run dns-test --rm -it --image=busybox:1.28 -- nslookup distributed-config-0.distributed-config
复盘:这次事故教会了我们什么
StatefulSet 和 Deployment 最大的区别是什么?Deployment 认为所有 Pod 都是等价的,谁先谁后无所谓。StatefulSet 认为 Pod 是有身份的,顺序本身是一种语义。
当你把有状态应用搬到 K8s 上,你要做的不是"把 Docker 命令转成 YAML",而是重新审视你的应用的初始化假设——启动时到底在等什么?初始化顺序有没有隐式的依赖?readiness 的正确粒度是什么?
三个值得带走的思考:
-
K8s 的 readiness 探针不是一个简单的"进程是否在跑",它是流量开关——它的判定逻辑应该回答"是否准备好接收流量",而不是"整个系统是否健康"。把系统健康交给 liveness。
-
OrderedReady 是 StatefulSet 的默认策略,但不是唯一策略。Parallel 并不是一个"偷懒"选项,它适用于那些节点之间不存在严格启动依赖的分布式系统。
-
排查 K8s 问题时,Events 总是第一手现场。不看 Events 直接调代码,就跟警察不勘查现场直接开始推理一样——你可能会找到一个答案,但大概率是错的。
下篇我们聊另一个存储类常见问题:容器存储空间不足——emptyDir 和 hostPath 的使用误区。