目录

MySQL-10 高可用架构

MySQL 高可用架构


目录

  1. 高可用架构概览
  2. 主从复制原理
  3. 主从搭建实践
  4. GTID 复制
  5. 复制延迟与处理
  6. 半同步复制
  7. MGR 组复制
  8. MHA 高可用方案
  9. ProxySQL 读写分离
  10. Go 应用的高可用实践

1. 高可用架构概览

方案对比:

单机           → 无 HA,测试/开发用
主从复制        → 读写分离,异步,主库故障需手动切换
主从 + MHA     → 自动故障转移,秒级切换
MGR(组复制)  → 多主/单主,强一致,内置故障转移
MySQL InnoDB Cluster → MGR + MySQL Router + MySQL Shell,官方 HA 方案
Galera Cluster → 多写同步复制(MariaDB/Percona XtraDB Cluster)
云原生 RDS     → 云厂商托管,自动 HA(推荐生产使用)

选型建议

  • 小团队/创业:云 RDS(RDS/PolarDB/Cloud SQL)
  • 中等规模:主从 + MHA + ProxySQL
  • 大规模/强一致要求:MGR + ProxySQL

2. 主从复制原理

2.1 复制架构

主库(Master)                  从库(Slave/Replica)
┌──────────────┐                ┌──────────────────────────────────────┐
│              │                │                                      │
│  写操作      │                │  IO Thread                            │
│     ↓        │                │  ┌──────────────────────────────┐    │
│  Binlog ─────┼── Binlog ─────►│  │ 监听主库 Binlog,写入 Relay Log│   │
│              │  (网络传输)     │  └──────────────────────────────┘    │
│              │                │            ↓                         │
│              │                │  Relay Log                           │
│              │                │            ↓                         │
│              │                │  SQL Thread                          │
│              │                │  ┌──────────────────────────────┐    │
│              │                │  │ 重放 Relay Log,更新从库数据  │    │
│              │                │  └──────────────────────────────┘    │
└──────────────┘                └──────────────────────────────────────┘

2.2 复制流程

  1. 主库执行事务,提交时写入 Binlogsync_binlog=1 保证落盘)
  2. 从库 IO Thread 连接主库的 Binlog Dump 线程,请求从指定位置开始的 Binlog
  3. 主库 Binlog Dump 线程发送 Binlog Events
  4. 从库 IO Thread 将收到的 Events 写入 Relay Log
  5. 从库 SQL Thread 读取并重放 Relay Log

2.3 复制类型

按同步方式:
  异步复制(默认):主库提交后立即返回,不等从库确认
  半同步复制:等至少一个从库写入 Relay Log 后再返回
  同步复制(MGR 增强半同步):等多数节点确认

按复制内容(binlog_format):
  基于语句(STATEMENT):复制 SQL,有不一致风险
  基于行(ROW):复制数据变更,精确可靠(推荐)
  混合模式(MIXED):自动选择

3. 主从搭建实践

3.1 主库配置

# my.cnf
[mysqld]
server_id        = 1           # 全局唯一,主从不同
log_bin          = mysql-bin
binlog_format    = ROW
binlog_row_image = MINIMAL

# GTID 模式(推荐)
gtid_mode         = ON
enforce_gtid_consistency = ON

# 增强的半同步(可选)
# plugin-load-add = rpl_semi_sync_master=semisync_master.so
# rpl_semi_sync_master_enabled = ON
-- 主库创建复制账号
CREATE USER 'repl'@'10.0.0.%' IDENTIFIED BY 'ReplPass123!';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'10.0.0.%';
FLUSH PRIVILEGES;

-- 查看主库状态(非 GTID 模式需要记录 File 和 Position)
SHOW MASTER STATUS;

3.2 从库配置

# my.cnf
[mysqld]
server_id        = 2           # 与主库不同
log_bin          = mysql-bin   # 从库也开 Binlog(级联复制/MHA需要)
relay_log        = relay-bin
relay_log_purge  = ON
relay_log_recovery = ON        # 崩溃恢复
read_only        = ON          # 从库只读
super_read_only  = ON          # 防止 super 用户也能写(MySQL 5.7.8+)

# GTID
gtid_mode         = ON
enforce_gtid_consistency = ON

# 从库 Binlog 格式
log_slave_updates = ON         # 从库的 Relay Log 同步到自身 Binlog(级联复制需要)
-- 从库执行(GTID 模式)
CHANGE MASTER TO
  MASTER_HOST = '10.0.0.1',
  MASTER_PORT = 3306,
  MASTER_USER = 'repl',
  MASTER_PASSWORD = 'ReplPass123!',
  MASTER_AUTO_POSITION = 1;    -- GTID 自动定位

-- 非 GTID 模式
-- CHANGE MASTER TO
--   MASTER_LOG_FILE = 'mysql-bin.000001',
--   MASTER_LOG_POS  = 154;

-- 启动复制
START SLAVE;  -- MySQL 8.0.22+ 用 START REPLICA
-- 查看状态
SHOW SLAVE STATUS\G

3.3 初始数据同步

# 在主库全量备份(InnoDB 用 --single-transaction,不锁表)
mysqldump -u root -p \
  --single-transaction \
  --master-data=2 \     # 记录备份时的 Binlog 位置(非 GTID)
  --gtid-mode=ON \      # GTID 模式加这个
  --all-databases > all.sql

# 传输到从库
scp all.sql replica:/tmp/

# 从库导入
mysql -u root -p < /tmp/all.sql

4. GTID 复制

4.1 什么是 GTID

GTID(Global Transaction ID):全局事务标识符,格式为 server_uuid:transaction_id

GTID = source_id:transaction_id
例如:3E11FA47-71CA-11E1-9E33-C80AA9429562:23

优势

  • 主从切换无需手动指定 Binlog 位置(MASTER_AUTO_POSITION=1
  • 从库可以自动跳过已执行的事务(防重放)
  • 更易于搭建级联复制

4.2 GTID 查看

-- 当前执行过的所有 GTID 集合
SELECT @@global.gtid_executed;

-- 已接收但未执行的 GTID(从库)
SELECT @@global.gtid_purged;

-- 主从延迟(GTID 维度)
-- 主库已执行
SELECT @@global.gtid_executed AS master_gtid;
-- 从库已执行
SHOW SLAVE STATUS\G -- 看 Executed_Gtid_Set

4.3 GTID 注意事项

-- GTID 模式下的限制:
-- 1. 不能使用 CREATE TABLE ... SELECT
--    (会产生两个事务 ID,违反 GTID 一事务一 GTID 原则)

-- ✅ 替代写法
CREATE TABLE new_table LIKE old_table;
INSERT INTO new_table SELECT * FROM old_table;

-- 2. 不支持在事务中混用事务表和非事务表(MyISAM)

-- 3. sql_log_bin=0 设置下执行的操作不会记录 GTID
--    从库需要手动处理这些操作(或避免使用)

5. 复制延迟与处理

5.1 延迟的原因

1. 主库并发写入,从库单线程重放(MySQL 5.6 以前)
2. 大事务(一个事务几百MB的 Binlog)
3. 从库机器性能差
4. 网络抖动
5. 从库有复杂查询占用 I/O

5.2 监控延迟

-- 查看从库延迟
SHOW SLAVE STATUS\G
-- Seconds_Behind_Master: 延迟秒数(基于事务时间戳,不一定准确)

-- 更准确的延迟:比较主从的 Binlog 位置
-- 主库
SHOW MASTER STATUS;  -- 记录 File 和 Position
-- 从库
SHOW SLAVE STATUS\G  -- 看 Exec_Master_Log_Pos

5.3 多线程复制(并行复制)

# MySQL 5.7+ 基于逻辑时钟的并行复制
slave_parallel_type     = LOGICAL_CLOCK   # 按 binlog 写入时间并行
slave_parallel_workers  = 4               # 并行线程数(建议 4-8)

# MySQL 8.0+
replica_parallel_type    = LOGICAL_CLOCK
replica_parallel_workers = 8

# 主库配置(提高 Group Commit 的并行度)
binlog_group_commit_sync_delay = 100      # 微秒,让更多事务进入同一 Group Commit

5.4 应用层处理延迟

// 方案1:读主库(强一致性需求)
func GetOrderStrong(ctx context.Context, masterDB, slaveDB *sql.DB, id int64) (*Order, error) {
    // 下单后立即查询,走主库
    return queryOrder(ctx, masterDB, id)
}

// 方案2:等待从库追上(写后读一致性)
func GetOrderWithWait(ctx context.Context, masterDB, slaveDB *sql.DB, id int64) (*Order, error) {
    // 先尝试从从库读
    order, err := queryOrder(ctx, slaveDB, id)
    if err == nil && order != nil {
        return order, nil
    }
    // 从库没有(可能延迟),降级到主库
    return queryOrder(ctx, masterDB, id)
}

// 方案3:写后路由(在 context 中标记)
type contextKey string
const useMasterKey contextKey = "use_master"

func MarkUseMaster(ctx context.Context) context.Context {
    return context.WithValue(ctx, useMasterKey, true)
}

func getDB(ctx context.Context, master, slave *sql.DB) *sql.DB {
    if v, ok := ctx.Value(useMasterKey).(bool); ok && v {
        return master
    }
    return slave
}

6. 半同步复制

6.1 原理

标准异步复制的问题:主库崩溃时,从库可能还没收到最新的 Binlog → 数据丢失

半同步复制:主库等待至少一个从库的 ACK(从库已写入 Relay Log)后再提交。

# 主库
plugin-load-add = rpl_semi_sync_master=semisync_master.so
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_master_timeout = 1000  # 等待 ACK 超时时间(毫秒)
                                      # 超时则自动降级为异步复制

# 从库
plugin-load-add = rpl_semi_sync_slave=semisync_slave.so
rpl_semi_sync_slave_enabled  = 1

6.2 增强半同步(AFTER SYNC,MySQL 5.7+)

# MySQL 5.7 引入 AFTER SYNC 模式(也叫 Loss-Less Semi-Sync)
rpl_semi_sync_master_wait_point = AFTER_SYNC  # 等从库 ACK 后再写 Binlog(更安全)
# vs AFTER_COMMIT: 先写 Binlog 再等 ACK(可能主库挂了从库没 ACK,但数据已在主库)

7. MGR 组复制

7.1 MGR 介绍

MySQL Group Replication(MGR) 是 MySQL 5.7.17 引入的原生分布式高可用方案:

  • 基于 Paxos 协议,多数节点投票
  • 单主模式(Single-Primary):自动选主,其余只读
  • 多主模式(Multi-Primary):所有节点可写(需处理冲突)
MGR 集群(3节点,单主模式):

  MySQL-1 (Primary)     MySQL-2 (Secondary)    MySQL-3 (Secondary)
    ↕                      ↕                      ↕
         Paxos 协议通信(所有节点投票确认事务)

7.2 MGR 搭建

# 三台机器都加的配置
[mysqld]
server_id = 1  # 每台不同:1, 2, 3
gtid_mode = ON
enforce_gtid_consistency = ON
binlog_format = ROW
log_slave_updates = ON
master_info_repository = TABLE
relay_log_info_repository = TABLE

# MGR 插件
plugin_load_add = 'group_replication.so'
loose-group_replication_group_name = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'  # UUID
loose-group_replication_start_on_boot = OFF
loose-group_replication_local_address = '10.0.0.1:33061'  # 每台不同
loose-group_replication_group_seeds = '10.0.0.1:33061,10.0.0.2:33061,10.0.0.3:33061'
loose-group_replication_bootstrap_group = OFF
loose-group_replication_single_primary_mode = ON
-- 第一台(引导节点)
SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;

-- 其余节点
START GROUP_REPLICATION;

-- 查看成员状态
SELECT * FROM performance_schema.replication_group_members;

7.3 MGR 优缺点

优点

  • 原生支持,无需额外工具
  • 自动故障转移(主库故障,自动选出新主)
  • 强一致性(事务在多数节点确认后才提交)

缺点

  • 性能损耗(每个事务需要网络往返确认)
  • 大事务影响大(Paxos 消息体大)
  • 多主模式下冲突检测复杂
  • 网络要求高(低延迟、稳定)

8. MHA 高可用方案

8.1 MHA 架构

MHA(Master High Availability Manager):自动化主从切换工具。

          ┌──────────────┐
          │  MHA Manager │  (监控脚本,可在独立服务器)
          └──────┬───────┘
                 │ 监控 + 切换
      ┌──────────┼────────────┐
      ▼          ▼            ▼
  Master       Slave1       Slave2
  (写)         (读)         (读)

切换流程

  1. 检测主库故障(ping 失败 N 次)
  2. 从所有从库中找到数据最新的(Binlog 位置最靠前)
  3. 其他从库从这台从库补全缺失的 Binlog
  4. 提升最新从库为新主库
  5. 其他从库指向新主库
  6. 更新 VIP(虚拟 IP)到新主库
  7. 发送通知

8.2 VIP 漂移

# MHA 配合 VIP 实现透明切换
# 主库持有 VIP(如 10.0.0.100)
# 应用连接 VIP,无感知切换

# MHA 切换时的 master_ip_failover_script 示例
/usr/local/bin/master_ip_failover \
  --command=start --ssh_user=root \
  --orig_master_host=10.0.0.1 \
  --new_master_host=10.0.0.2 \
  --new_master_ip=10.0.0.2 \
  --new_master_port=3306

9. ProxySQL 读写分离

9.1 ProxySQL 简介

ProxySQL 是高性能 MySQL 中间件,支持:

  • 读写分离
  • 连接池
  • 查询路由
  • 查询缓存
  • 故障切换
  • 负载均衡

9.2 ProxySQL 配置

-- 连接 ProxySQL 管理接口(默认 6032 端口)
mysql -u admin -padmin -h 127.0.0.1 -P 6032

-- 添加 MySQL 服务器
INSERT INTO mysql_servers (hostgroup_id, hostname, port, max_connections) VALUES
  (10, '10.0.0.1', 3306, 1000),   -- 主库(hostgroup 10)
  (20, '10.0.0.2', 3306, 1000),   -- 从库1(hostgroup 20)
  (20, '10.0.0.3', 3306, 1000);   -- 从库2(hostgroup 20)

-- 配置用户
INSERT INTO mysql_users (username, password, default_hostgroup) VALUES
  ('appuser', 'AppPass123!', 10);  -- 默认走主库

-- 配置读写分离规则(正则匹配)
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup, apply) VALUES
  (1, 1, '^SELECT.*FOR UPDATE$', 10, 1),  -- SELECT FOR UPDATE 走主库
  (2, 1, '^SELECT',              20, 1);  -- 其他 SELECT 走从库

-- 应用配置
LOAD MYSQL SERVERS TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
LOAD MYSQL USERS TO RUNTIME;
SAVE MYSQL USERS TO DISK;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL QUERY RULES TO DISK;

9.3 ProxySQL 监控

-- 查看连接池状态
SELECT * FROM stats.stats_mysql_connection_pool;

-- 查看查询统计
SELECT hostgroup, schemaname, username, digest_text, count_star, sum_time
FROM stats.stats_mysql_query_digest
ORDER BY sum_time DESC LIMIT 10;

-- 查看服务器状态
SELECT * FROM monitor.mysql_server_ping_log ORDER BY time_start_us DESC LIMIT 20;

10. Go 应用的高可用实践

10.1 多数据源配置

type DBPool struct {
    master  *sql.DB
    slaves  []*sql.DB
    counter uint64  // 轮询计数器
}

func NewDBPool(masterDSN string, slaveDSNs []string) (*DBPool, error) {
    master, err := sql.Open("mysql", masterDSN)
    if err != nil {
        return nil, err
    }
    configurePool(master)

    slaves := make([]*sql.DB, 0, len(slaveDSNs))
    for _, dsn := range slaveDSNs {
        slave, err := sql.Open("mysql", dsn)
        if err != nil {
            return nil, err
        }
        configurePool(slave)
        slaves = append(slaves, slave)
    }

    return &DBPool{master: master, slaves: slaves}, nil
}

// 获取从库(轮询负载均衡)
func (p *DBPool) Slave() *sql.DB {
    if len(p.slaves) == 0 {
        return p.master
    }
    idx := atomic.AddUint64(&p.counter, 1) % uint64(len(p.slaves))
    return p.slaves[idx]
}

func (p *DBPool) Master() *sql.DB {
    return p.master
}

10.2 重试与故障切换

func (p *DBPool) QueryWithFallback(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
    // 先尝试从库
    rows, err := p.Slave().QueryContext(ctx, query, args...)
    if err == nil {
        return rows, nil
    }

    // 从库失败,检查是否是连接错误
    if isConnectionError(err) {
        // 降级到主库
        return p.Master().QueryContext(ctx, query, args...)
    }
    return nil, err
}

func isConnectionError(err error) bool {
    if err == nil {
        return false
    }
    var mysqlErr *mysql.MySQLError
    if errors.As(err, &mysqlErr) {
        // 连接相关错误码
        switch mysqlErr.Number {
        case 2003, 2006, 2013:  // Can't connect, MySQL server has gone away
            return true
        }
    }
    return errors.Is(err, driver.ErrBadConn)
}

10.3 使用 ProxySQL 的 Go 配置

// 使用 ProxySQL 时,Go 应用配置非常简单
// ProxySQL 透明处理读写分离

masterDSN := "appuser:AppPass123!@tcp(proxysql:6033)/myapp?parseTime=True&loc=Local"

db, err := sql.Open("mysql", masterDSN)
// ProxySQL 自动将 SELECT 路由到从库,写操作路由到主库
// Go 应用无需感知主从

// 需要强制走主库时,可以使用注释提示
db.QueryContext(ctx, "SELECT /*+ MASTER */ * FROM users WHERE id = ?", id)
// ProxySQL 可配置规则:含 MASTER 注释的走主库

10.4 健康检查与熔断

import "github.com/sony/gobreaker"

type DBPool struct {
    master  *sql.DB
    slave   *sql.DB
    breaker *gobreaker.CircuitBreaker
}

func NewDBPoolWithBreaker(masterDSN, slaveDSN string) (*DBPool, error) {
    // ...
    cb := gobreaker.NewCircuitBreaker(gobreaker.Settings{
        Name:        "mysql-slave",
        MaxRequests: 5,
        Interval:    60 * time.Second,
        Timeout:     30 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            return counts.ConsecutiveFailures >= 3
        },
    })
    return &DBPool{slave: slave, master: master, breaker: cb}, nil
}

func (p *DBPool) Query(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
    result, err := p.breaker.Execute(func() (interface{}, error) {
        return p.slave.QueryContext(ctx, query, args...)
    })
    if err != nil {
        // 熔断开启,降级到主库
        return p.master.QueryContext(ctx, query, args...)
    }
    return result.(*sql.Rows), nil
}

小结

方案 适用场景 RTO RPO 复杂度
单机 开发/测试 N/A N/A
主从(手动切换) 读写分离 分钟级 可能丢失
主从 + MHA 中小规模 30s~2min 接近0
MGR 单主 强一致 秒级 0
云 RDS 全场景 秒级 0
  • RTO(Recovery Time Objective):恢复时间目标
  • RPO(Recovery Point Objective):数据恢复点目标(最多丢失多少数据)