Skip to content
StatefulSet 与有状态应用

StatefulSet 与有状态应用

StatefulSet 管理有状态应用的完整指南:Headless Service、Init Container、Sidecar 模式、MySQL 主从复制实战。


1. StatefulSet 概述

StatefulSet(简称 STS)用于管理有状态应用,与 Deployment 类似但提供额外保证。

1.1 与 Deployment 的核心区别

特性 Deployment StatefulSet
Pod 标识 随机名称(如 app-5d4f-9xk2m 有序编号(如 app-0, app-1
启动顺序 同时启动 按序启动 0 → 1 → 2
停止顺序 同时停止 逆序停止 2 → 1 → 0
网络标识 无稳定标识 稳定的 DNS 名称 <pod>.<svc>
存储 共享或无存储 每个 Pod 独立的 PVC(volumeClaimTemplates)
扩缩容顺序 并行 顺序进行

1.2 适用场景

适用 不适用
数据库集群(MySQL、PostgreSQL) 简单的无状态 Web 应用
消息队列(Kafka、RabbitMQ) 可通过 Deployment + 共享 PVC 解决的场景
缓存集群(需要持久化标识) 非常复杂的有状态系统(建议迁出 K8s)
分布式存储(etcd、Elasticsearch)

💡 最佳实践:不是所有有状态应用都适合 StatefulSet。对某些复杂系统(如大型数据库集群),推荐将其从 K8s 抽离出去,使用云服务或独立部署。


2. StatefulSet 基本配置

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql-sts
spec:
  selector:
    matchLabels:
      app: mysql
  serviceName: "mysql-svc"       # 🚨 必须与 Headless Service 的 name 一致
  replicas: 3
  minReadySeconds: 10
  template:
    metadata:
      labels:
        app: mysql
    spec:
      terminationGracePeriodSeconds: 10
      containers:
        - name: mysql
          image: mysql:8.0.31
          env:
            - name: MYSQL_ROOT_PASSWORD
              value: "123456"
          ports:
            - containerPort: 3306
          volumeMounts:
            - mountPath: /var/lib/mysql
              name: data-volume
  volumeClaimTemplates:             # 每个 Pod 独立的 PVC 模板
    - metadata:
        name: data-volume
      spec:
        accessModes: ["ReadWriteOnce"]
        storageClassName: "local-path"
        resources:
          requests:
            storage: 2Gi

2.1 关键字段说明

字段 说明
serviceName 🚨 必须与 Headless Service 的 metadata.name 完全一致
volumeClaimTemplates 为每个 Pod 独立创建 PVC,命名格式 <volumeName>-<podName>-<序号>
podManagementPolicy OrderedReady(默认,按序)/ Parallel(并行)

3. Headless Service

3.1 为什么需要

普通 Service 带有负载均衡,请求会被随机转发到不同的 Pod。对有状态应用来说,我们需要直接访问特定的实例(如写操作必须到主库 mysql-0)。

Headless Service 通过设置 clusterIP: None 禁用负载均衡,让 DNS 查询直接返回所有 Pod 的 IP 地址

3.2 配置

apiVersion: v1
kind: Service
metadata:
  name: mysql-svc
  labels:
    app: mysql
spec:
  ports:
    - port: 3306
      name: mysql
  clusterIP: None       # 关键:设为 None 即为 Headless Service
  selector:
    app: mysql

3.3 DNS 访问方式

创建后每个 Pod 获取固定 DNS 名称:

DNS 名称 指向
mysql-0.mysql-svc 第 0 个 Pod(主库)
mysql-1.mysql-svc 第 1 个 Pod
mysql-2.mysql-svc 第 2 个 Pod
mysql-svc 所有 Pod IP(A 记录列表)
# 从临时 Pod 中测试 DNS 解析
kubectl run -it --rm debug --image=busybox -- nslookup mysql-0.mysql-svc

4. Init Container(初始化容器)

4.1 概念

Init Container 是一种在应用容器之前运行的特殊容器。它定义在 spec.initContainers 中。

特性 说明
执行顺序 多个 Init Container 按定义顺序依次执行
阻塞性 前一个未完成或失败,后一个不启动;Init Container 未完成,应用容器不启动
重启策略 失败后按 restartPolicy 重试

4.2 常见用途

用途 示例
生成配置文件 根据 Pod 序号生成 server-id
等待依赖就绪 等待数据库或缓存服务可用
初始化数据 从主库克隆数据到副本
权限设置 修改挂载卷的目录权限

4.3 控制启动顺序

spec:
  initContainers:
    - name: wait-for-db
      image: busybox:1.36
      command:
        - sh
        - -c
        - |
          until nslookup mysql-0.mysql-svc; do
            echo "Waiting for mysql-0...";
            sleep 3;
          done
          echo "mysql-0 is ready!"
  containers:
    - name: my-app
      # ...

🚨 注意:使用无限循环 until ... do 的风险是永远不会超时失败。更好的方式是用重试计数或 timeout 控制。


5. Sidecar(边车模式)

在同一个 Pod 中运行一个辅助容器,配合主容器完成特定任务。

5.1 常见 Sidecar 模式

┌─────────────── Pod ───────────────┐
│  ┌──────────┐    ┌──────────────┐ │
│  │ 主容器   │    │   Sidecar    │ │
│  │ (MySQL)  │◄──►│ (xtrabackup) │ │
│  └──────────┘    └──────────────┘ │
│       │               │           │
│    /var/lib/mysql  (共享卷)       │
└───────────────────────────────────┘

5.2 MySQL + xtrabackup 示例

xtrabackup 是开源的 MySQL 在线热备份工具,是目前各云厂商普遍使用的备份方案。

工作流程

  1. 新副本 Pod 启动时,Init Container 从上一个 Pod 的 xtrabackup Sidecar 拉取数据
  2. xtrabackup Sidecar 在后台监听,随时准备将数据发送给下一个 Pod
  3. 数据导入完成后,MySQL 自动通过 binlog 同步增量数据
  4. Sidecar 只负责初次启动时的历史数据导入,后续同步由 MySQL 自行完成

🔬 原理:MySQL binlog 有保留期限,当数据库运行一段时间后再新增副本,可能追不到 binlog 源头。因此需要先将全量数据导入副本,再开启增量同步。


6. MySQL 主从复制完整示例

🚨 注意:此例仅用于演示 K8s 概念,不能直接用于生产。生产环境推荐使用 Helm chart 部署。

6.1 整体架构

┌─────────────┐   ┌─────────────┐   ┌─────────────┐
│  mysql-0    │   │  mysql-1    │   │  mysql-2    │
│  (Primary)  │──▶│  (Replica)  │──▶│  (Replica)  │
│  Read/Write │   │  Read Only  │   │  Read Only  │
└──────┬──────┘   └──────┬──────┘   └──────┬──────┘
       │                 │                 │
       └────────┬────────┴────────┬────────┘
                │                 │
        ┌───────┴──────┐  ┌───────┴──────┐
        │ mysql (HS)   │  │  mysql-read  │
        │ 直接访问 Pod  │  │ 负载均衡读   │
        └──────────────┘  └──────────────┘

6.2 配置文件

# mysql-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql
  labels:
    app: mysql
data:
  primary.cnf: |
    [mysqld]
    log-bin                         # 主库开启 binlog
  replica.cnf: |
    [mysqld]
    super-read-only                 # 从库设置只读

6.3 Headless Service

# 用于直接访问特定实例(如 mysql-0.mysql)
apiVersion: v1
kind: Service
metadata:
  name: mysql
spec:
  ports:
    - name: mysql
      port: 3306
  clusterIP: None
  selector:
    app: mysql
---
# 用于只读负载均衡
apiVersion: v1
kind: Service
metadata:
  name: mysql-read
spec:
  ports:
    - name: mysql
      port: 3306
  selector:
    app: mysql

6.4 StatefulSet(含 Init Container 和 Sidecar)

完整 StatefulSet 配置包含:

  • 两个 Init Container(init-mysql 生成 server-id + clone-mysql 从上一个 Pod 克隆数据)
  • 两个运行容器(mysql 主容器 + xtrabackup Sidecar 提供备份发送服务)
  • volumeClaimTemplates 为每个 Pod 创建独立 PVC

6.5 验证

# 启动临时 MySQL 客户端
kubectl run mysql-client --image=mysql:5.7 -it --rm -- bash

# 连接主库写入数据
mysql -h mysql-0.mysql
CREATE DATABASE test;
USE test;
CREATE TABLE messages (message VARCHAR(100));
INSERT INTO messages VALUES ("hello from primary");

# 连接只读 Service 验证复制
mysql -h mysql-read
SELECT * FROM test.messages;

# 在从库上尝试写入(应该失败)
INSERT INTO test.messages VALUES ("should fail");  # 失败!

7. 端口转发(port-forward)

当集群内数据库不直接对外暴露时,可通过端口转发进行调试:

# 将本地 3306 转发到 mysql-0 Pod 的 3306
kubectl port-forward pod/mysql-0 3306:3306

# 指定监听地址
kubectl port-forward pod/mysql-0 --address=0.0.0.0 3306:3306

之后就可以用本地数据库工具(如 Navicat、DBeaver)连接 localhost:3306


8. StatefulSet 常见陷阱

陷阱 说明
serviceName 不匹配 StatefulSet 的 serviceName 必须与 Headless Service 的 name 完全一致
PVC 不自动删除 删除 StatefulSet 不会自动删除 PVC,需手动清理
网络标识依赖 DNS 集群 DNS(CoreDNS)必须正常运行
缩容数据丢失 缩容会删除 Pod,对应 PVC 保留但需要手动处理数据
不适用于所有有状态应用 复杂数据库集群建议使用 Operator 或迁出 K8s