← 返回文章列表

Kubernetes Operator 开发实战:MySQL 主从自动调谐

基于 kubebuilder 开发 MySQL Operator,实现一主多从自动调谐、主库故障切换、读写分离

13 分钟阅读
字号

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-servicerole=master主库 Pod所有写操作
slave-servicerole=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副本数,支持扩缩容
imageMySQL 镜像版本
masterService主库 Service 名称
slaveService从库 Service 名称
storagePVC 存储配置
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=true

4.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.4

6.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 覆盖率评估数据一致性                                    │
│     选举时选择数据最新的从库作为新主                              │
│                                                                │
└────────────────────────────────────────────────────────────────┘

参考资料


相关代码仓库:https://gitee.com/sirius/mysql-operator

分享

// RELATED_POSTS

0%