Kubernetes StatefulSet 实战:为什么数据库不能用 Deployment 部署

Kubernetes StatefulSet 实战:为什么数据库不能用 Deployment 部署

很多人第一次在 K8s 上跑 MySQL、Redis、ZooKeeper,直接抄了个 Deployment 的 YAML,结果 Pod 一重启数据就没了,或者主从复制配了半天 IP 一变就全乱套。问题不在你,而在于有状态应用根本不该用 Deployment。这篇讲清楚 StatefulSet 解决了什么、和 Deployment 差在哪,以及一个能跑起来的完整例子。

先看 Deployment 为什么不行

Deployment 管理的 Pod 是「牲畜不是宠物」------它假设每个副本完全对等、可随意替换。这带来三个对有状态应用致命的问题:

  • Pod 名字是随机的 :web-7d9f8c-x2k9web-7d9f8c-p4m1,重启后名字全变。主从复制里从库要连主库,连的地址一直变,配置根本没法写死。
  • 所有 Pod 共享同一个 PVC(或各自随机) :Deployment 的多个副本挂的是同一个存储,或者用 volumeClaimTemplates 也没法保证 Pod 和存储的固定对应。数据库多实例各写各的盘,Deployment 给不了这种保证。
  • 启动/停止是并行、无序的:Deployment 扩容时一把全拉起来。但很多集群软件要求「先起主节点,再起从节点」,顺序错了初始化就失败。

StatefulSet 就是为解决这三点而生的。

StatefulSet 给的三个稳定保证

1. 稳定的网络标识(Pod 名字固定且有序)。 StatefulSet 的 Pod 名字是 <statefulset名>-<序号>,从 0 开始:mysql-0mysql-1mysql-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-0data-mysql-1......Pod 删了再建,还是绑回原来那块盘,数据不丢。这是和 Deployment 最本质的区别。

3. 有序部署与伸缩。 默认按序号顺序创建(0→1→2),前一个 Ready 了才起下一个;缩容时逆序删除(2→1→0)。这正好满足「先主后从」的启动需求。

一个能跑的完整例子

先建 Headless Service(clusterIP: None 是关键,它让 DNS 直接解析到每个 Pod 而不是负载均衡):

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  clusterIP: None   # Headless:不分配 VIP,DNS 直接返回各 Pod IP
  selector:
    app: mysql
  ports:
    - name: mysql
      port: 3306

再写 StatefulSet:

yaml 复制代码
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql        # 必须指向上面那个 Headless Service,否则拿不到稳定 DNS
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
        - name: mysql
          image: mysql:8.0
          ports:
            - containerPort: 3306
              name: mysql
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: root-password
          volumeMounts:
            - name: data          # 对应下面 volumeClaimTemplates 的 name
              mountPath: /var/lib/mysql
  volumeClaimTemplates:           # 每个 Pod 自动生成独立 PVC
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: standard   # 换成你集群实际的 StorageClass
        resources:
          requests:
            storage: 10Gi

apply 之后观察:

bash 复制代码
kubectl apply -f mysql-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
# data-mysql-0   Bound   ...
# data-mysql-1   Bound   ...
# data-mysql-2   Bound   ...

验证稳定 DNS(在集群内任意 Pod 里):

bash 复制代码
# 从 mysql-1 连 mysql-0,地址写死也永远有效
mysql -h mysql-0.mysql -uroot -p

三个最容易踩的坑

坑一:删了 StatefulSet,PVC 不会自动删。 这其实是保护机制------防止误删丢数据。但你重建时要注意,新 StatefulSet 会复用同名的旧 PVC,里面还是老数据。想彻底清空得手动删 PVC:

bash 复制代码
kubectl delete statefulset mysql       # Pod 没了,但 PVC 还在
kubectl get pvc                         # data-mysql-0/1/2 仍然 Bound
kubectl delete pvc data-mysql-0 data-mysql-1 data-mysql-2  # 要清数据得手动删

坑二:缩容不会删 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:

yaml 复制代码
spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      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。

相关推荐
欧阳天羲1 小时前
全屋智能家居一体化方案‑配套资料文档合集
云原生·eureka·智能家居
陈聪.2 小时前
Docker 核心知识整理:从入门到企业级应用
运维·docker·容器
01_ice2 小时前
MySQL事务
数据库·mysql
名字还没想好☜2 小时前
Go 的 database/sql 连接池实战:SetMaxOpenConns 怎么配、连接泄漏怎么查
数据库·sql·golang·go·数据库连接池
云原生指北2 小时前
Docker Sandboxes 的隔离例外:共享 Skills 与宿主机 MCP
运维·docker·ai·容器·agent
大眼、不聚光2 小时前
2.oracle--表空间管理
数据库·oracle
字节跳动数据库2 小时前
火山 PostgreSQL Serverless × 飞书妙搭:把 AI 装进数据库,一句话唤醒数据智能
数据库·人工智能·后端
玛丽莲茼蒿2 小时前
Redis(六)—— Redis高级
数据库·redis·缓存
瞬间&永恒~3 小时前
【Kubernetes】(七)Kubernetes控制器
云原生·容器·kubernetes