3 副本只活了 1 个——StatefulSet 启动顺序的循环死锁

基于 K8s v1.25+apps/v1 API。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 没出生

现象:刚部署就出了岔子

Pod 列表截图 — 仅 Pod-0 CrashLoopBackOff, 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

Pod Events 截图 — Readiness probe failed

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

启动死锁流程图 — OrderedReady + quorum 死锁

这里涉及一个关键的设计权衡:分布式应用的 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。

StatefulSet Events 截图 — 控制器在等 Pod-0 Ready

看 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 层和集群层没有参与死锁,但排查时仍然要走完这一层,避免漏判:

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:连接三个信息 → 死锁的根因就出来了

排查对比截图 — 日志正常 vs readiness 失败

关键判断:CrashLoopBackOff 不等于应用 Crash。K8s 的 CrashLoopBackOff 会在以下两种截然不同的情况下触发:

触发原因 现象 判断方法
容器进程退出(exit code ≠ 0) 进程真正挂了 kubectl logs pod 日志中途截断退出
readiness 探针连续失败(进程仍在运行) 进程活着但探针断言失败 kubectl describe pod 只看得到 readiness 失败,logs 日志正常

这次是第二种——容器进程一直在跑,只是 readiness 的检查逻辑太严格,把自己锁死了。

【标点】修复 + Check-list

三种修复方案及其取舍

方案一:区分首次部署和后续维护的 readiness 逻辑

YAML 修复对比 — before/after diff

剥离集群健康检查到 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 的正确粒度是什么?

三个值得带走的思考:

  1. K8s 的 readiness 探针不是一个简单的"进程是否在跑",它是流量开关——它的判定逻辑应该回答"是否准备好接收流量",而不是"整个系统是否健康"。把系统健康交给 liveness。

  2. OrderedReady 是 StatefulSet 的默认策略,但不是唯一策略。Parallel 并不是一个"偷懒"选项,它适用于那些节点之间不存在严格启动依赖的分布式系统。

  3. 排查 K8s 问题时,Events 总是第一手现场。不看 Events 直接调代码,就跟警察不勘查现场直接开始推理一样——你可能会找到一个答案,但大概率是错的。

下篇我们聊另一个存储类常见问题:容器存储空间不足——emptyDir 和 hostPath 的使用误区。