MySQL-10 高可用架构
目录
MySQL 高可用架构
目录
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 复制流程
- 主库执行事务,提交时写入 Binlog(
sync_binlog=1保证落盘) - 从库 IO Thread 连接主库的 Binlog Dump 线程,请求从指定位置开始的 Binlog
- 主库 Binlog Dump 线程发送 Binlog Events
- 从库 IO Thread 将收到的 Events 写入 Relay Log
- 从库 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
(写) (读) (读)
切换流程:
- 检测主库故障(ping 失败 N 次)
- 从所有从库中找到数据最新的(Binlog 位置最靠前)
- 其他从库从这台从库补全缺失的 Binlog
- 提升最新从库为新主库
- 其他从库指向新主库
- 更新 VIP(虚拟 IP)到新主库
- 发送通知
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):数据恢复点目标(最多丢失多少数据)
xingliuhua