
Kubernetes StatefulSet 实战:为什么数据库不能用 Deployment 部署很多人第一次在 K8s 上跑 MySQL、Redis、ZooKeeper,直接抄了个 Deployment 的 YAML,结果 Pod 一重启数据就没了,或者主从复制配了半天 IP 一变就全乱套。问题不在你,而在于有状态应用根本不该用 Deployment。这篇讲清楚 StatefulSet 解决了什么、和 Deployment 差在哪,以及一个能跑起来的完整例子。先看 Deployment 为什么不行Deployment 管理的 Pod 是「牲畜不是宠物」——它假设每个副本完全对等、可随意替换。这带来三个对有状态应用致命的问题:Pod 名字是随机的:web-7d9f8c-x2k9、web-7d9f8c-p4m1,重启后名字全变。主从复制里从库要连主库,连的地址一直变,配置根本没法写死。所有 Pod 共享同一个 PVC(或各自随机):Deployment 的多个副本挂的是同一个存储,或者用volumeClaimTemplates也没法保证 Pod 和存储的固定对应。数据库多实例各写各的盘,Deployment 给不了这种保证。启动/停止是并行、无序的:Deployment 扩容时一把全拉起来。但很多集群软件要求「先起主节点,再起从节点」,顺序错了初始化就失败。StatefulSet 就是为解决这三点而生的。StatefulSet 给的三个稳定保证1. 稳定的网络标识(Pod 名字固定且有序)。StatefulSet 的 Pod 名字是statefulset名-序号,从 0 开始:mysql-0、mysql-1、mysql-2。Pod 挂了重建,名字不变。配上一个 Headless Service,每个 Pod 还能拿到稳定的 DNS 域名:mysql-0.mysql.default.svc.cluster.local mysql-1.mysql.default.svc.cluster.local从此从库连主库,直接写mysql-0.mysql就行,永远指向 0 号 Pod,不管它重建多少次。2. 稳定的独占存储。StatefulSet 用volumeClaimTemplates给每个 Pod 生成独立的 PVC:data-mysql-0、data-mysql-1……Pod 删了再建,还是绑回原来那块盘,数据不丢。这是和 Deployment 最本质的区别。3. 有序部署与伸缩。默认按序号顺序创建(0→1→2),前一个 Ready 了才起下一个;缩容时逆序删除(2→1→0)。这正好满足「先主后从」的启动需求。一个能跑的完整例子先建 Headless Service(clusterIP: None是关键,它让 DNS 直接解析到每个 Pod 而不是负载均衡):apiVersion:v1kind:Servicemetadata:name:mysqllabels:app:mysqlspec:clusterIP:None# Headless:不分配 VIP,DNS 直接返回各 Pod IPselector:app:mysqlports:-name:mysqlport:3306再写 StatefulSet:apiVersion:apps/v1kind:StatefulSetmetadata:name:mysqlspec:serviceName:mysql# 必须指向上面那个 Headless Service,否则拿不到稳定 DNSreplicas:3selector:matchLabels:app:mysqltemplate:metadata:labels:app:mysqlspec:containers:-name:mysqlimage:mysql:8.0ports:-containerPort:3306name:mysqlenv:-name:MYSQL_ROOT_PASSWORDvalueFrom:secretKeyRef:name:mysql-secretkey:root-passwordvolumeMounts:-name:data# 对应下面 volumeClaimTemplates 的 namemountPath:/var/lib/mysqlvolumeClaimTemplates:# 每个 Pod 自动生成独立 PVC-metadata:name:dataspec:accessModes:[ReadWriteOnce]storageClassName:standard# 换成你集群实际的 StorageClassresources:requests:storage:10Giapply 之后观察:kubectl apply-fmysql-statefulset.yaml# Pod 名字是有序的,而且是一个一个起来的kubectl get pods-w# mysql-0 0/1 ContainerCreating# mysql-0 1/1 Running - 0 号 Ready 后才起 1 号# mysql-1 0/1 Pending# ...# 每个 Pod 有独立 PVC,名字带序号kubectl get pvc#>#>#># 从 mysql-1 连 mysql-0,地址写死也永远有效mysql-hmysql-0.mysql-uroot-p三个最容易踩的坑坑一:删了 StatefulSet,PVC 不会自动删。这其实是保护机制——防止误删丢数据。但你重建时要注意,新 StatefulSet 会复用同名的旧 PVC,里面还是老数据。想彻底清空得手动删 PVC:kubectl delete statefulset mysql# Pod 没了,但 PVC 还在kubectl get pvc#># 要清数据得手动删坑二:缩容不会删 PVC。把 replicas 从 3 改成 1,mysql-1/mysql-2的 Pod 被删,但data-mysql-1/data-mysql-2这两个 PVC 保留着。下次扩回 3,这两个 Pod 会绑回原来的盘、拿回原来的数据。这是特性不是 bug,但计费时别忘了这些「孤儿」PVC 还在占存储费。坑三:滚动更新默认也是逆序、一个一个来。StatefulSet 的updateStrategy默认是RollingUpdate,从最大序号往小更新,一次一个。这比 Deployment 慢,但对有状态服务是安全的。如果你要金丝雀验证,可以用partition只更新序号 N 的 Pod:spec:updateStrategy:type:RollingUpdaterollingUpdate:partition:2# 只更新 mysql-2,mysql-0/1 保持旧版本,验证 OK 再把 partition 调到 0什么时候该用 StatefulSet判断标准很简单:Pod 之间是不是需要「区分身份」。需要固定名字/固定存储/固定启动顺序 → StatefulSet:数据库、消息队列(Kafka/RabbitMQ)、分布式协调(ZooKeeper/etcd)、需要 sticky 会话或分片的服务。无状态、副本完全对等、随便杀随便换 → Deployment:HTTP API、前端、无状态 worker。别为了「看起来高级」把无状态服务塞进 StatefulSet,那只会让你的滚动更新变慢、扩缩容变笨。小结Deployment 的 Pod 名字随机、存储不固定、启停无序,这三点让它不适合有状态应用。StatefulSet 提供三个稳定保证:有序固定的 Pod 名字 Headless Service 给的稳定 DNS、每 Pod 独立且重建不丢的 PVC、有序的部署与伸缩。必配serviceName指向一个clusterIP: None的 Headless Service,存储用volumeClaimTemplates而不是共享 PVC。记住三个坑:删 StatefulSet 和缩容都不删 PVC(保护数据,但要手动清理);滚动更新逆序单个进行,可用partition做金丝雀。一句话记忆点:Pod 需要有名有姓有自己的盘,就用 StatefulSet;随便替换的用 Deployment。