4. 数据库高可用:MySQL主从复制、读写分离、连接池配置、故障自动切换

调度系统里,数据库往往是整个链路中最脆弱的一环。我见过不少调度系统,应用层做得花里胡哨,结果数据库一挂,整个集群直接瘫痪。说白了,数据库的高可用才是调度系统真正的底线。

这一章,我们聚焦 MySQL 的高可用方案。我会从主从复制讲起,逐步深入到读写分离、连接池调优,最后落地到故障自动切换。嗯,这些都是我在生产环境里反复踩过坑之后沉淀下来的东西。

4.1 主从复制:高可用的地基

主从复制是 MySQL 高可用的基础。没有它,后面的一切都是空中楼阁。

MySQL 主从复制的核心机制,说白了就是主库把变更记录到二进制日志(binlog),从库拉取这些日志并重放。整个过程是异步的,所以从库的数据会有一定延迟。

核心流程:

  1. 主库提交事务,写入 binlog
  2. 从库的 I/O 线程连接主库,拉取 binlog 并写入 relay log
  3. 从库的 SQL 线程读取 relay log,重放事务

我个人习惯用 GTID(全局事务标识)模式。相比传统的基于文件名+位置的复制,GTID 让主从切换变得简单很多。你不需要手动去找 binlog 文件名和偏移量,MySQL 自己就能搞定。

配置示例(GTID 模式):

# 主库配置
[mysqld]
server-id = 1
log-bin = mysql-bin
gtid_mode = ON
enforce-gtid-consistency = ON

# 从库配置
[mysqld]
server-id = 2
relay-log = relay-log
gtid_mode = ON
enforce-gtid-consistency = ON
read_only = ON

我在项目中遇到过一个问题:从库延迟太大,导致调度任务读取到过期数据。后来排查发现是从库的 SQL 线程是单线程的,主库写入压力一大,从库就追不上。解决方案是开启并行复制。

# 从库开启并行复制
[mysqld]
slave-parallel-type = LOGICAL_CLOCK
slave-parallel-workers = 4
slave-preserve-commit-order = ON

这里要注意,并行复制不是线程越多越好。我曾经试过开到 16 个线程,结果因为锁冲突反而更慢。一般来说,4-8 个线程是比较合理的范围。

4.2 读写分离:让主库喘口气

调度系统的数据库压力通常是不对称的。写操作集中在任务状态更新、调度记录写入;读操作则集中在任务查询、历史数据检索。如果所有请求都打到主库,主库迟早会扛不住。

读写分离的思路很简单:写请求走主库,读请求走从库。但实现起来有几个坑要注意。

读写分离的常见陷阱:

  • 主从延迟导致脏读:任务刚写入主库,立即从从库读取,可能读不到。解决方案是写后读一致性,关键查询强制走主库。
  • 事务内读写不一致:同一个事务里,先写后读,如果读走从库,可能读到旧数据。我建议事务内的读也走主库。
  • 连接池混乱:主库和从库的连接池要分开管理,不能混用。

我常用的方案是在应用层通过数据源路由来实现读写分离。以 Spring 为例,可以动态切换数据源。

// 读写分离数据源路由示例
public class ReadWriteDataSourceRouter extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DbContextHolder.getDbType(); // 返回 'master' 或 'slave'
    }
}

// 使用注解标记读操作
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}

// AOP 切面自动切换
@Around("@annotation(readOnly)")
public Object route(ProceedingJoinPoint pjp, ReadOnly readOnly) {
    try {
        DbContextHolder.setDbType("slave");
        return pjp.proceed();
    } finally {
        DbContextHolder.clear();
    }
}

你想想看,如果每次读请求都走主库,那主库的压力根本降不下来。读写分离做得好,主库的 QPS 能下降 60%-70%。

4.3 连接池配置:别让连接成为瓶颈

连接池的配置看似简单,但调优空间很大。我见过太多调度系统因为连接池配置不当,导致数据库连接耗尽或者响应变慢。

常用的连接池有 HikariCP、Druid、Tomcat JDBC Pool。我个人偏好 HikariCP,性能好,配置简洁。

参数 推荐值 说明
maximumPoolSize 10-20 不是越大越好,过大反而增加数据库负担
minimumIdle 5-10 保持一定空闲连接,应对突发流量
connectionTimeout 30000ms 连接超时,避免长时间等待
idleTimeout 600000ms 空闲连接超时,释放资源
maxLifetime 1800000ms 连接最大存活时间,防止连接泄漏

HikariCP 配置示例:

spring:
  datasource:
    hikari:
      maximum-pool-size: 15
      minimum-idle: 5
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      pool-name: SchedulePool
      connection-test-query: SELECT 1

我曾经犯过一个错误:把 maximumPoolSize 设成了 100,觉得这样并发能力更强。结果数据库的连接数被打满,其他服务连不上数据库,整个系统都受影响。后来我总结了一个经验公式:连接数 = (核心数 * 2) + 有效磁盘数。对于调度系统,10-20 个连接通常就够用了。

4.4 故障自动切换:MHA vs Orchestrator

主从复制和读写分离都做好了,但如果主库挂了,谁来接管?手动切换太慢,而且容易出错。我们需要一个自动故障切换工具。

目前主流的选择有两个:MHA 和 Orchestrator。我两个都用过,说说我的感受。

特性 MHA Orchestrator
架构 Manager + Node 单进程 + Raft 集群
部署复杂度 中等 简单
切换速度 10-30秒 5-15秒
拓扑管理 手动配置 自动发现
Web UI 无 有
社区活跃度 低(已停止维护) 高

我个人现在更倾向于 Orchestrator。原因很简单:MHA 已经停止维护了,而 Orchestrator 还在持续更新,而且它的自动拓扑发现功能非常实用。

4.4.1 MHA 的配置要点

虽然 MHA 已经过时,但很多老系统还在用。如果你不得不维护 MHA,有几个关键点要注意。

# MHA 配置文件示例
[server default]
manager_workdir=/var/log/mha
manager_log=/var/log/mha/manager.log
ssh_user=root
repl_user=repl
repl_password=repl_password
ping_interval=3
secondary_check_script=masterha_secondary_check -s 192.168.1.2 -s 192.168.1.3

[server1]
hostname=192.168.1.1
candidate_master=1

[server2]
hostname=192.168.1.2
candidate_master=1

[server3]
hostname=192.168.1.3
no_master=1

MHA 避坑指南:

  • SSH 免密登录必须配好,否则切换时会失败
  • ping_interval 不要设得太小,否则网络抖动会误触发切换
  • candidate_master 要合理设置,不是所有从库都适合升为主库

4.4.2 Orchestrator 的部署与使用

Orchestrator 的部署比 MHA 简单得多。一个二进制文件就能跑起来,而且支持 Raft 集群部署,自身就是高可用的。

# Orchestrator 配置文件核心部分
{
  "Topology": {
    "Master": "192.168.1.1:3306",
    "User": "orchestrator",
    "Password": "orchestrator_password"
  },
  "Recovery": {
    "PeriodicCheckInterval": 5,
    "FailureDetectionPeriod": 10,
    "RecoveryPeriodBlockSeconds": 300
  },
  "Raft": {
    "Enabled": true,
    "DataDir": "/var/lib/orchestrator/raft",
    "Nodes": [
      "192.168.1.10:10008",
      "192.168.1.11:10008",
      "192.168.1.12:10008"
    ]
  }
}

Orchestrator 的自动切换逻辑很智能。它会检测主库是否真的挂了,然后自动选出最合适的从库升为主库,并重新配置其他从库的复制关系。整个过程不需要人工干预。

Orchestrator 的优势:

  • 自动发现 MySQL 拓扑,不需要手动维护节点列表
  • 支持故障恢复后的自动重连
  • 提供 Web UI,可以直观地查看集群状态
  • 支持 HTTP API,方便集成到调度系统的运维平台

4.5 整体架构图

下面这张图展示了调度系统数据库高可用的整体架构。从应用层到数据库层,每一层都有对应的高可用保障措施。

调度系统数据库高可用架构 应用层(调度服务集群) 读写分离路由 | 连接池(HikariCP) | 事务管理 中间层(故障检测与切换) Orchestrator / MHA | 心跳检测 | 自动选主 主库(Master) 写请求 | 事务提交 binlog 输出 从库1(Slave) 读请求 | 报表查询 relay log 重放 从库2(Slave) 读请求 | 历史数据 relay log 重放 故障切换(自动选主) 说明: 1. 应用层通过读写分离路由,写请求发往主库,读请求发往从库 2. Orchestrator 持续检测主库健康状态,故障时自动将从库提升为新主库

4.6 总结与避坑清单

数据库高可用不是一蹴而就的。从主从复制到读写分离,再到连接池调优和故障自动切换,每一步都需要仔细打磨。

核心要点回顾:

  • 主从复制优先使用 GTID 模式,简化切换流程
  • 读写分离要注意主从延迟,关键查询强制走主库
  • 连接池大小不是越大越好,10-20 个连接通常够用
  • 故障切换工具推荐 Orchestrator,部署简单且功能强大

我曾经踩过的坑:

  • 连接池设得太大,数据库连接数被打满,其他服务受影响
  • MHA 的 SSH 免密没配好,故障时切换失败
  • 读写分离没处理事务内一致性,导致业务逻辑出错
  • 从库并行复制线程数设得太多,反而因为锁冲突变慢

嗯,数据库高可用这块内容比较多,但都是实打实的生产经验。你把这些配置和方案落地到自己的调度系统里,数据库层面的高可用基本就稳了。