资讯动态

StatefulSet 实战指南:从稳定标识到滚动更新,K8s 有状态应用编排

发布时间:2026/10/21 3:11:56 来源:尧图企业网站定制
干了好几年容器化改造我最大的体会就是Deployment 处理无状态应用是真好用但一碰上数据库、消息队列、缓存这类要认身份的活儿就原形毕露。StatefulSet 就是 K8s 专门为有状态应用的编排与管理设计的控制器它解决的核心问题就三个稳定的网络标识、稳定的存储、有序的部署和缩容。这篇文章我会从原理到 YAML 字段拆解再到我实际踩过的坑把 StatefulSet 一次讲透。不管你是刚接触 K8s 的初学者还是已经在生产环境跑业务的运维开发只要你在某个时刻需要把有状态的服务搬进容器里这篇文章都适合你。我会尽量用大白话讲清楚配合可以抄作业的配置示例让你看完能直接动手。1. StatefulSet 出现之前有状态应用为什么这么难管1.1 Deployment 的默认前提所有 Pod 长得一样、死了就换Deployment 的设计哲学是牛羊模式它假设管理的所有 Pod 都是完全对等的没有名字、没有身份随时可以被替换。你扩容 3 个副本Pod 名称随机生成比如app-7d9f8c4b5-xk3ai、app-7d9f8c4b5-mn2jq缩容或故障重启后新 Pod 又是一个全新的随机名称。这种设计对 Web 后端、API 服务这类无状态应用完全没问题因为流量通过 Service 负载均衡进去请求打到哪个 Pod 都一样。但当你把它用在数据库主从集群上问题立刻暴露客户端通过某个固定地址连接主库如果主库 Pod 一重启IP 变了、名字变了客户端配置就得跟着改这对于数据系统简直是灾难。Deployment 适合的是谁上都一样的场景而有状态应用恰恰是每个副本都不一样。1.2 有状态应用到底有哪几种状态我把有状态拆成三部分来看数据状态数据写在磁盘上Pod 重启后数据必须还在。这是最直观的状态比如数据库的数据目录、消息队列的日志段文件。身份状态集群里的每个节点有固定角色和固定身份比如主库、备库、从库。其他组件和客户端需要通过稳定域名或固定名称来识别和访问它Pod 重建后身份不能变。拓扑与顺序状态集群的启动和关闭有严格顺序。比如先启动主库再启动从库从库才能去主库拉取同步缩容时要先停从库最后才动主库。这类顺序要求是 Deployment 完全无法表达的。生活中打个比方无状态应用像超市收银员哪个收银台空了补哪里有状态应用像银行金库的保险柜每个柜子对应唯一的编号和钥匙编号不能乱柜门不能随便换。1.3 我在没有 StatefulSet 时是怎么硬扛的早些年 K8s 对无状态应用支持得很好但对有状态应用基本靠手工 脚本硬凑。我曾经在一个订单系统项目里用 Deployment 跑一个三节点的数据库集群做法非常原始为每个数据库节点单独建一个独立的 Deploymentreplicas 固定为 1再单独给它配一个 ServicePVC 也单独手动创建好。这套方案能跑但维护成本极高。每次升级镜像我得一个一个 Deployment 轮着来节点出故障需要重新调度时PVC 的节点亲和性经常导致 Pod 调度不上去更麻烦的是主从切换脚本里要写死 IP 或者频繁改配置。代码里到处是节点的名字和地址信息可 Pod 一重启这些就失效。说白了我在用脚本和规章制度去弥补控制器本身缺失的能力。StatefulSet 出现后这门课才真正补上。2. StatefulSet 的核心机制拆解稳定标识是灵魂2.1 Pod 名称里的序号就是每个实例的身份证StatefulSet 管理的 Pod 名称和 Deployment 完全不同它固定为{statefulset 名称}-{序号}的格式。我用db这个名字创建 StatefulSetreplicas 为 3三个 Pod 名称就是db-0、db-1、db-2。序号从 0 开始天然就是每个实例的编号。这个序号的好处你从身份角度理解就顺了集群里的第一个实例永远叫db-0不管它被删掉多少次、重启多少次、被调度到哪个节点只要 StatefulSet 还在重建出来的 Pod 还是叫db-0。这就相当于给每个实例办了张永久身份证其他组件只要记住db-0这个名字就能稳定访问它。我自己做数据库主从脚本时直接约定序号为 0 的实例是主库这个规则永远不会被 Pod 重启破坏写代码舒坦多了。2.2 无头 Service 与稳定网络标识为什么一定要搭配 Headless Service光有稳定名称还不够Pod 还得有稳定可解析的域名。StatefulSet 官方要求你给 Pod 一个serviceName这个 Service 通常是无头 ServiceHeadless Service也就是clusterIP: None的那种。普通 Service 相当于一个总机给你一个稳定的虚拟 IP再把流量转发给后端一组 Pod。而 Headless Service 不创建虚拟 IP它的核心作用是提供 DNS 解析规则当你查询db-0.db-svc这个域名时DNS 会直接返回db-0这个 Pod 的实际 IP。这样每个 Pod 就有了一个稳定的 DNS 记录完整格式是{Pod名称}.{Service名称}.{命名空间}.svc.cluster.local比如db-2.db-svc.default.svc.cluster.local。在同一个命名空间里直接写db-2.db-svc就能访问。这就像给每个数据库节点配了一个固定电话号码而不是给你一个总机号码让你说请转接 2 号分机。节点可以迁移、IP 可以变但电话号不变。我实际部署主从集群时主从配置里填的就是这种 DNS 域名而非 IPPod 重启多少次都不用改配置。2.3 volumeClaimTemplates存储跟着 Pod 的身份走稳定的网络标识解决的是我怎么找到你的问题而稳定的存储解决的是我的数据放哪的问题。StatefulSet 里的存储非常巧妙它用的不是手动创建一堆 PVC而是提供一个volumeClaimTemplates字段你可以把它理解成一个PVC 模板。每次 StatefulSet 扩容控制器就会根据这个模板自动创建一个独立的 PVC。PVC 的命名规则是{模板名称}-{Pod名称}比如模板叫dataPod 叫db-1生成的 PVC 就叫>if [ ${ordinal} -eq 0 ]; then rolemaster else rolereplica fi利用(序号 % 2)还可以做多角色交替分配。这个设计让 StatefulSet 在跑复杂分布式系统时非常灵活角色分配天然基于序号即可完成不需要额外的注册中心。3. 能直接抄作业的 YAML字段含义与配置思路3.1 一个最小可用的完整示例我拿一个三节点的数据库集群举例文件名叫db-statefulset.yaml包含 Headless Service 和 StatefulSet 两部分。这是我在某订单系统里实际用过的简化版本你可以直接改参数套用apiVersion: v1 kind: Service metadata: name: db-svc labels: app: db spec: clusterIP: None selector: app: db ports: - name: tcp port: 3306 targetPort: 3306 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: db spec: serviceName: db-svc replicas: 3 selector: matchLabels: app: db template: metadata: labels: app: db spec: containers: - name: db image: dbx:7.2 ports: - containerPort: 3306 name: tcp volumeMounts: - name: data mountPath: /var/lib/db volumeClaimTemplates: - metadata: name: data spec: accessModes: - ReadWriteOnce resources: requests: storage: 100Gi文件里最核心的三处就是serviceName: db-svc、replicas: 3和volumeClaimTemplates里的data这个模板。3.2 核心字段逐个拆解说清楚为什么要这么配serviceName字段声明这个 StatefulSet 的 Pod 受哪个 Service 管理。不要求 Service 一定存在Pod 也能创建但如果没有配套的 Headless ServicePod 的稳定 DNS 域名就不存在了那db-0.db-svc这种地址就解析不出来。我每次都把这两个对象一起部署否则稳定标识少了一半。selector与metadata.labelsStatefulSet 的 selector 必须和 Pod 模板的 label 匹配这是 K8s 的强制校验。需要注意 selector 一旦创建就不可修改因为它是控制器找到自己管辖 Pod 的依据。volumeClaimTemplates这是 StatefulSet 独有的字段Deployment 里没有。每个模板会生成独立的 PVC比如上面示例中会生成>kubectl describe pod db-0 kubectl get pvc kubectl describe pvc>kubectl get svc db-svc kubectl get endpoints db-svc # 如果 endpoints 为空说明 selector 没匹配上 Pod还要看一眼 Service 的clusterIP是否是None不是的话就要改成 Headless。另外Pod 不一定非要在 Ready 之后才能解析Pod 即使处于未就绪状态只要存在对应的无头 ServiceDNS 是可以解析到 Pod IP 的这点和普通 Service 的 Endpoint 机制略有差异。5.4 滚动更新卡住不往下走了更新 StatefulSet 镜像后最后一个 Pod 已经重建但后面的 Pod 一直不更新。这种情况十有八九是新版本的 Pod 的 readiness 探针没有通过。StatefulSet 的滚动更新规则是只有当前更新后的 Pod 完全 Ready控制器才会继续更新下一个。排查思路分三步kubectl rollout status statefulset/db kubectl get pods -l appdb -o wide kubectl logs db-2 --previous如果发现新版本 Pod 一直 CrashLoopBackOff 或未就绪你要么修复应用的启动问题要么用kubectl rollout undo statefulset/db回滚镜像版本。这里有个好处StatefulSet 的回滚操作本质上是把镜像版本改回去不会像 Deployment 那样产生一堆多余的 ReplicaSet干净利落。5.5 常见问题速查表现象常见原因排查命令Pod 一直 PendingStorageClass 不存在或 PVC 无法调度kubectl describe pod db-0、kubectl get pvc域名无法解析Service 不是 Headless 或 selector 不匹配kubectl get svc db-svc、kubectl get endpoints数据看起来消失PVC 被手动删除或命名不对kubectl get pvc -l appdb、kubectl describe pvc更新卡住readiness 探针未通过kubectl rollout status sts/db、kubectl logs创建顺序混乱误设置了podManagementPolicy: Parallel检查 StatefulSet 的 spec缩容报错 PVC 占用有实例仍挂载该 PVCkubectl describe pvc看 STATUS 和 Used By5.6 一个容易被忽略的删除顺序问题删除 StatefulSet 时也要小心顺序。默认情况下你执行kubectl delete statefulset db会级联删除它管理的所有 Pod但 PVC 和 PV 全部保留。这是 K8s 有意为之的安全策略因为数据盘不能跟着 Pod 一起平白消失。但如果你删除 StatefulSet 时启用了非级联删除也就是kubectl delete statefulset db --cascadeorphanPod 会变成无人管理的野 Pod虽然还运行着但名字和 PVC 的关联关系还在。这种操作很少需要用到一旦用了重建管理关系时会比较麻烦。我的建议是除非特殊情况否则直接用默认级联删除后续按需手动清理 PVC。6. 一些实操中才摸到的细节与心得6.1 不要硬把无状态应用塞进 StatefulSetStatefulSet 虽然功能强大但它是有成本的严格顺序意味着扩容变慢逆序更新意味着发布周期长PVC 模板意味着每增加一个副本就多一块云盘。我见过有人把所有服务都用 StatefulSet 部署理由是好记得名字。这是完全没有必要的。无状态应用用 Deployment 怎么滚动都行扩容也快。把 StatefulSet 用在真正需要稳定身份和稳定存储的场景比如数据库、消息队列、分布式文件系统、ZooKeeper 这类系统才物尽其用。6.2 多个 PVC 模板的实践价值volumeClaimTemplates里可以写多个模板不只是数据盘。比如我要把数据库的数据目录和日志目录分开就写两个模板名字一个data对应mountPath: /var/lib/db一个log对应mountPath: /var/log/db。生成的 PVC 就是>lifecycle: preStop: exec: command: [/bin/sh, -c, db-cli shutdown sleep 5]这套配置在很多生产事故里能救命。我某次做节点维护时因为没加优雅停机数据库异常断电后重启花了一个多小时做日志恢复从那以后每个 StatefulSet 工作负载我都会检查是否有 preStop。6.4 不要忽视 PV 的回收策略创建 PersistentVolume 时回收策略通常默认是Retain、Delete或Recycle已废弃。如果你用云厂商动态供应存储PVC 删除时 PV 通常会被回收并删除。但如果你手动创建静态 PV回收策略是Retain那么删掉 PVC 后PV 还在里面数据也还在但 PV 状态会变成Released无法自动重新绑定。想重新使用这块盘你得手动清理 PV 里的数据并把回收策略改成Delete或重新手动绑定。这个细节直接决定了删掉 PVC 后云盘到底会不会扣费我在成本核算时踩过一次坑后来每次审视 StatefulSet 的存储方案都会专门检查 StorageClass 的 reclaimPolicy。6.5 关于 StatefulSet 的备份与恢复多讲一句StatefulSet 解决了应用层的编排问题但绝对不解决数据备份问题。我见过很多人以为用了 StatefulSet 数据就万无一失了这完全是误解。即使存储盘在如果整个集群地域级故障数据一样会丢。我一般会给所有有状态工作负载配上周期性的逻辑备份把备份产物存到对象存储或跨可用区的地方并且定期做恢复演练。StatefulSet 能保证你的数据跟着实例走但数据不丢是整个架构共同保证的光靠 K8s 控制器远远不够。最后分享一个和 StatefulSet 相关的偏门小技巧排查问题时kubectl exec进到容器里之后可以直接用hostname命令看 Pod 名称验证 StatefulSet 给你分配的序号和预期一致。有时候 DNS 解析的域名不生效但容器环境变量或/etc/hostname会准确地告诉你当前实例是谁这能帮你快速定位是不是 Service 配置出了问题。这是我多次在故障应急现场用得非常顺手的判断方式希望对你有用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑