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: 2Gi2.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: mysql3.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-svc4. 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 在线热备份工具,是目前各云厂商普遍使用的备份方案。
工作流程:
- 新副本 Pod 启动时,Init Container 从上一个 Pod 的 xtrabackup Sidecar 拉取数据
- xtrabackup Sidecar 在后台监听,随时准备将数据发送给下一个 Pod
- 数据导入完成后,MySQL 自动通过 binlog 同步增量数据
- 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: mysql6.4 StatefulSet(含 Init Container 和 Sidecar)
完整 StatefulSet 配置包含:
- 两个 Init Container(
init-mysql生成 server-id +clone-mysql从上一个 Pod 克隆数据) - 两个运行容器(
mysql主容器 +xtrabackupSidecar 提供备份发送服务) 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 |