Kubernetes Operator 开发实战:MySQL 主从自动调谐
基于 kubebuilder 开发 MySQL Operator,实现一主多从自动调谐、主库故障切换、读写分离
Kubernetes Operator 开发实战:MySQL 主从自动调谐
本文详细讲解如何基于 kubebuilder 开发一个支持 GTID 复制模式的 MySQL Operator,实现主从自动调谐、故障切换和读写分离。
一、为什么需要 MySQL Operator
1.1 K8s 原生自愈的局限
Kubernetes 本身具备强大的自愈能力,但对于有状态数据库应用,这种"重启式"的自愈远远不够:
K8s 能做到的:
Pod 挂了 → 重启进程
Node 挂了 → 调度到其他节点
K8s 做不到的:
主库挂了 → 选新主?不知道
从库同步断裂 → 重启有用吗?
旧主恢复 → 怎么追数据?1.2 主从复制 vs K8s 原生
┌────────────────────────────────────────────────────────────────┐
│ K8s vs MySQL Operator │
├────────────────────────────────────────────────────────────────┤
│ │
│ K8s 原生: │
│ - 负责"进程层面"的管理(重启、调度、网络) │
│ - 理解"Pod 活着吗?" │
│ - 不理解"什么是主从复制" │
│ │
│ MySQL Operator: │
│ - 负责"数据库层面"的管理 │
│ - 理解 MySQL 主从协议 │
│ - 理解 GTID/Binlog 语义 │
│ - 实现主从自动切换、数据一致性保障 │
│ │
└────────────────────────────────────────────────────────────────┘二、整体架构设计
2.1 架构图
┌─────────────────────────────────────────────────────────────────────────────┐
│ MySQL Operator 架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ MysqlCluster CRD │ │
│ │ (声明期望:3副本、镜像等) │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Controller 控制器 │ │
│ │ │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ 副本数调谐 │ │ │
│ │ │ 主从状态检测与恢复 │ │ │
│ │ │ 选举逻辑 │ │ │
│ │ │ 初始化集群 │ │ │
│ │ └───────────────────────┘ │ │
│ └──────────────┬──────────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ mysql-01 │ │ mysql-02 │ │ mysql-03 │ │
│ │ (主库) │◄─────►│ (从库) │◄─────►│ (从库) │ │
│ └──────┬──────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │master-svc │ │ slave-svc │ │
│ │(role:master)│ │ (role:slave)│ │
│ │ 写操作 │ │ 读操作 │ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘2.2 读写分离机制
通过两个 Service 实现读写分离:
| Service | 选择标签 | 路由目标 | 用途 |
|---|---|---|---|
| master-service | role=master | 主库 Pod | 所有写操作 |
| slave-service | role=slave | 所有从库 Pod | 所有读操作 |
核心原理:给 Pod 打上不同标签,Service 通过 label selector 自动选中对应的 Pod。主库切换时,只需要给新主打上 role=master 标签,master-service 自动就会选中它。
三、CRD 设计
3.1 资源定义
apiVersion: apps.sirius.com/v1
kind: MysqlCluster
metadata:
name: mysqlcluster-sample
spec:
image: registry.cn-shanghai.aliyuncs.com/.../mysql:5.7
replicas: 3
masterService: master-service # 写操作 Service
slaveService: slave-service # 读操作 Service
storage:
storageClassName: "local-path"
size: 1Gi
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1"
memory: "2Gi"3.2 关键字段说明
| 字段 | 说明 |
|---|---|
| replicas | 副本数,支持扩缩容 |
| image | MySQL 镜像版本 |
| masterService | 主库 Service 名称 |
| slaveService | 从库 Service 名称 |
| storage | PVC 存储配置 |
| resources | 资源限制 |
四、初始化流程
4.1 初始化步骤
1. 创建 master-service、slave-service
↓
2. 为每个 Pod 创建 PVC + ConfigMap
↓
3. 创建 Pod(mysql-01 ~ mysql-0N)
↓
4. 配置主从关系:
- mysql-01 默认为主库
- 其余为从库
↓
5. 打标签:
- 主库 role=master
- 从库 role=slave
↓
6. 标记 initialized=true4.2 主从配置命令
主库执行:
-- 创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;从库执行:
-- GTID 模式不需要指定 binlog 文件和 position
CHANGE MASTER TO
MASTER_HOST='mysql-01',
MASTER_USER='repl',
MASTER_PASSWORD='password',
MASTER_AUTO_POSITION=1;
START SLAVE;GTID 优势:
MASTER_AUTO_POSITION=1让 MySQL 自动判断从哪个事务开始同步,不需要手动指定 binlog 文件和 position,故障切换更简单。
五、主从状态检测与恢复
5.1 主库故障检测
利用 K8s Service 的 endpoint 机制:
master-service 会自动关联带有 role=master 标签且 Ready 的 Pod IP
检测逻辑:
- master-service endpoint 有 IP → 主库存活
- master-service endpoint 为空 → 主库已下线这种方式的优点:K8s 已经维护好了 endpoint 状态,不需要额外的健康检查探针,时效性更好。
5.2 从库同步检测
进入每个从库执行 SHOW SLAVE STATUS\G:
判断条件:
- Slave_IO_Running: Yes → IO 线程正常,从主库拉取 binlog
- Slave_SQL_Running: Yes → SQL 线程正常,执行 relay log
两者同时为 Yes 表示主从同步正常,否则需要重新制作主从关系。5.3 故障切换流程
主库挂了:
┌────────────────────────────────────────────────────────────────┐
│ │
│ 1. 检测 master-svc endpoint 为空 │
│ ↓ │
│ 2. 选举新主 │
│ - 找出所有 role=slave 的从库 │
│ - 健康检查:Pod Running + MySQL 可响应 │
│ - 打分选举:GTID 覆盖率 × 0.6 + 数据量占比 × 0.4 │
│ ↓ │
│ 3. 新主执行 RESET SLAVE ALL(清除旧配置) │
│ 4. 其余从库重新指向新主: │
│ STOP SLAVE │
│ CHANGE MASTER TO MASTER_HOST='新主', MASTER_AUTO_POSITION=1│
│ START SLAVE │
│ ↓ │
│ 5. 打标签:新主 role=master,其余 role=slave │
│ │
└────────────────────────────────────────────────────────────────┘5.4 旧主恢复处理
检测到 master-svc 已有 endpoint(存在存活主库):
- 不进行选举
- 旧主作为从库加入
- 自动执行 CHANGE MASTER TO MASTER_AUTO_POSITION=1
- 旧主追上新主后,自动恢复主从同步
- 打标签 role=slave六、选举打分机制
6.1 打分公式
得分 = GTID覆盖率 × 0.6 + 数据量占比 × 0.46.2 选举决策树
┌────────────────────────────────────────────────────────────────┐
│ 选举决策逻辑 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 第一优先级:GTID 覆盖率 │
│ - 覆盖度 ≥ 99%:直接选,忽略数据量 │
│ - 原因:几乎追平,数据一致性最重要 │
│ │
│ 第二优先级:综合得分(覆盖度差距不大时) │
│ - 覆盖度差距 < 某个阈值:用综合得分 │
│ │
│ 第三优先级:数据量 │
│ - 覆盖度差距 > 阈值:直接选覆盖度最高的 │
│ - 原因:差距太大,选数据最新的 │
│ │
└────────────────────────────────────────────────────────────────┘6.3 实际示例
主库 GTID: {1-100, 2-50, 3-200} 共 350 个事务
从库A: GTID {1-80, 2-30} 覆盖率 31% 数据量 8G
从库B: GTID {1-100, 2-50, 3-180} 覆盖率 94% 数据量 10G
得分计算:
从库A = 0.31×0.6 + (8/10)×0.4 = 0.19 + 0.32 = 0.51
从库B = 0.94×0.6 + 1.0×0.4 = 0.56 + 0.40 = 0.96
→ 从库B 当选新主七、扩缩容处理
7.1 扩容流程
原状态:mysql-01(主) ◄── mysql-02(从)
扩容到 3 副本:
1. Controller 检测到 replicas: 3,当前只有 2 个
2. 创建 mysql-03 的 PVC + ConfigMap + Pod
3. mysql-03 作为新从库加入:
CHANGE MASTER TO MASTER_HOST='mysql-01', MASTER_AUTO_POSITION=1
START SLAVE
4. 打标签 role=slave
新状态:
mysql-01(主) ◄── mysql-02(从)
└── mysql-03(新从)7.2 缩容流程
原状态:mysql-01(主) ◄── mysql-02(从) ◄── mysql-03(从)
缩容到 2 副本:
⚠️ 不能直接删 Pod,否则主从链断裂
正确流程:
1. 如果删的是从库:
a. 先 STOP SLAVE(停止复制)
b. RESET SLAVE ALL(清除主从配置)
c. 然后删除 Pod
2. 如果删的是主库:
a. 先选举新主
b. 让其余从库指向新主
c. 然后才能删旧主八、GTID 原理详解
8.1 GTID 格式
GTID = server_uuid : transaction_id
示例:3E11FA47-71CA-11E1-9E33-C80AA9429562:23
- server_uuid:服务器唯一标识(每台 MySQL 实例都有)
- transaction_id:事务序号,递增8.2 GTID vs 传统 binlog+position
传统方式(需要知道文件和位置):
CHANGE MASTER TO
MASTER_HOST='mysql-01',
MASTER_LOG_FILE='mysql-bin.000003',
MASTER_LOG_POS=1045;
GTID 方式(不需要知道文件和位置):
CHANGE MASTER TO
MASTER_HOST='mysql-01',
MASTER_AUTO_POSITION=1;8.3 GTID 同步原理
主库每次事务提交:
生成 GTID → 写入 binlog → 通知从库
从库记录:
"我已经执行过 GTID: AAAA:1, AAAA:2"
"现在主库发来 AAAA:3,我执行它"
主库告诉从库:
"你已经执行到 AAAA:2 了,我从 AAAA:3 开始发给你"8.4 GTID 集合
从库的 GTID_EXECUTED 表示已执行过的事务集合
SHOW SLAVE STATUS\G
...
Executed_Gtid_Set: AAAA:1-3
...
含义:从库已执行 AAAA 这个服务器的 1、2、3 号事务
还差 AAAA:4、AAAA:5... 等后续事务九、面试常见问题
9.1 K8s 不是本身就可以恢复故障吗?
K8s 只能做"进程层面"的自愈(重启 Pod),无法理解数据库主从协议。MySQL Operator 实现的是"应用层面"的恢复:故障检测、主从切换、数据同步。
9.2 GTID 是快照吗?
不是。GTID 是事务的编号,不是数据本身。快照是某一时刻的完整数据副本(如 mysqldump 导出的文件)。GTID 集合记录的是"执行过哪些事务",用于判断同步进度。
9.3 从库追不上主库怎么办?
等级1:短期(几十分钟内追上)
→ 等待,给从库减压,停止其他大查询
等级2:长期(性能瓶颈)
→ 告警,扩容从库或升级硬件
等级3:完全断裂(relay log 丢失)
→ 重建从库(运维介入)9.4 扩缩容时主从关系怎么处理?
扩容简单,新增从库直接 CHANGE MASTER TO 指向主库。缩容复杂,必须先断开主从关系再删 Pod,直接删主库更不行,必须先选举新主。
十、后续优化方向
- 支持缩容:先移除主从关系再删 Pod
- 备份功能:定时全量备份 + 基于 GTID 的增量备份
- Raft 选主:采用分布式一致性算法实现真正分布式选主
- 双主架构:支持双主多从
十一、核心亮点总结
┌────────────────────────────────────────────────────────────────┐
│ MySQL Operator 核心亮点 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1. 声明式设计 │
│ 用户只需声明副本数、镜像、存储,Operator 自动完成剩余工作 │
│ │
│ 2. 利用 K8s Service endpoint 检测主库故障 │
│ 不需要额外探针,时效性好 │
│ │
│ 3. GTID 模式简化主从切换 │
│ MASTER_AUTO_POSITION=1,不需要指定文件和位置 │
│ │
│ 4. 标签机制实现 Service 动态路由 │
│ 打标签 = 改变路由,IP 变但标签不变 │
│ │
│ 5. GTID 覆盖率评估数据一致性 │
│ 选举时选择数据最新的从库作为新主 │
│ │
└────────────────────────────────────────────────────────────────┘