三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

MySQL高可用进阶:半同步复制+Keepalived+逻辑判断实现智能主从切换

MySQL高可用进阶:半同步复制+Keepalived+逻辑判断实现智能主从切换

1. 项目缘起:为什么我们需要一个“带脑子”的自动切换方案?

在数据库运维的日常里,MySQL高可用是个老生常谈的话题。一提到主从切换,很多人的第一反应就是Keepalived + VIP漂移,这套组合拳确实经典,部署简单,响应也快。但真正在生产环境里跑过几年的人都知道,这套方案的“傻”也是出了名的——它只管VIP漂不漂,可不管数据库节点本身是不是真的“健康”,更不管数据是不是真的同步到位了。

我就亲身经历过一次“惊心动魄”的切换。当时主库因为一个长事务导致IO线程短暂卡顿,从库的复制延迟瞬间飙升到几十秒。偏偏这个时候,主库的网络又抖了一下,Keepalived的检测脚本只是简单地mysql -h 127.0.0.1 -e ‘select 1’,返回成功,就判定主库健康。结果VIP“唰”一下就漂到那个延迟几十秒的从库上去了。应用连上去一读写,全是旧数据,业务逻辑全乱套,差点酿成一次数据错乱的生产事故。那次之后我就明白,高可用不能只靠“心跳”,还得靠“脑子”。我们需要一个能判断数据一致性、复制状态等逻辑状态的切换方案,这就是今天要聊的“半同步复制+Keepalived+第三方数据逻辑判断”这套组合拳的由来。它不是一个新发明的轮子,而是对经典方案一次至关重要的“智力升级”。

2. 方案核心架构:让VIP漂移学会“看数据”

这套方案的目标很明确:在发生故障时,自动将VIP切换到数据最完整、最可靠的从库上,而不是简单地切换到第一个活着的从库。为了实现这个目标,我们需要三个核心组件各司其职,协同工作。

2.1 组件一:MySQL半同步复制——数据一致性的基石

首先,我们得确保在主库写入时,数据能“安全地”到达至少一个从库。异步复制是“发了就不管”,全同步复制是“等所有人都确认”太慢,半同步复制(Semisynchronous Replication)则是个折中的聪明选择。

它的工作原理是:主库执行完一个事务后,并不立即返回给客户端成功,而是等待至少一个从库接收并写入到自己的中继日志(Relay Log)后,才向客户端确认提交成功。如果超过设定的超时时间(rpl_semi_sync_master_timeout,默认10秒)没有从库确认,主库会自动降级为异步复制模式,保证业务不挂死。

为什么它是基石?因为它从源头极大地降低了数据丢失的风险。在主库宕机的极端情况下,我们几乎可以确定,那个成功ACK了主库的从库,拥有宕机前最后一刻的、已提交的事务数据。这为我们后续选择哪个从库作为新主库,提供了最关键的数据完整性依据。没有这个机制,我们所谓的“逻辑判断”就成了无源之水,因为你无法确认哪个从库的数据最新、最全。

2.2 组件二:Keepalived——VIP漂移的执行者

Keepalived在这里的角色很纯粹,就是一个高可用的IP地址管理工具。它通过VRRP协议,在多个服务器之间虚拟出一个VIP(Virtual IP)。所有应用都通过这个VIP来访问数据库。

Keepalived本身通过vrrp_script定义健康检查脚本。传统做法里,这个脚本只检查MySQL进程是否存在、端口是否可连接。但在我们的方案里,这个脚本被赋予了更重要的使命——它不仅要检查“活着”,还要调用一个外部的“大脑”(第三方逻辑判断脚本),来询问:“我现在这个节点,有资格持有VIP吗?”

Keepalived根据这个脚本的返回码(0为成功,非0为失败)来决定是否放弃或抢占VIP。它不关心判断逻辑有多复杂,只忠实地执行“大脑”的指令。这种解耦的设计非常清晰,Keepalived干它最擅长的网络层故障转移,把复杂的业务逻辑判断交给更专业的脚本去做。

2.3 组件三:第三方逻辑判断脚本——方案的大脑

这是整个方案的灵魂,也是区别于普通方案的核心。这个脚本通常是一个Shell或Python脚本,它需要完成以下几项关键检查:

  1. 本机MySQL服务状态:基础检查,确保mysqld进程正常,可以连接。
  2. 复制状态检查
    • Slave_IO_RunningSlave_SQL_Running是否都为Yes
    • Seconds_Behind_Master复制延迟是否在可接受的阈值内(例如,小于30秒)?
    • 是否有复制错误(Last_IO_Errno,Last_SQL_Errno)?
  3. 半同步复制状态检查(关键!)
    • 查询Rpl_semi_sync_slave_status是否为ON,确认本机半同步插件运行正常。
    • 更重要的是,判断本机是否为“那个”确认了主库事务的从库。这可以通过检查一些内部状态,或者结合时间戳和GTID位置来综合推断。一个简单的思路是:在发生切换时,延迟最小且半同步状态正常的从库,极大概率就是最新的从库。
  4. 只读状态检查:确保read_only=ON(对于从库)。一个持有VIP的从库在提升为主库前,必须保证自己是只读的,避免双写。
  5. 集群状态仲裁(可选但推荐):脚本不应该只检查自己。一个更健壮的“大脑”应该能通过查询其他节点,或者查询一个共享的配置中心(如ZooKeeper、etcd,甚至一个简单的数据库表),来了解整个集群的拓扑和状态,从而做出更全局化的决策,比如避免“脑裂”。

这个脚本的返回值直接决定了Keepalived的行为。只有当一个节点通过了所有逻辑检查,认为自己“健康且数据完整”时,它才返回0,告诉Keepalived:“我有资格成为主库,请把VIP给我。”

3. 部署与配置实战:从零搭建智能高可用集群

理论说再多,不如动手搭一遍。下面我们以一个典型的两节点主从架构为例,手把手走通整个部署流程。假设我们有两台服务器:node1 (192.168.1.101)计划作为初始主库,node2 (192.168.1.102)作为初始从库,VIP为192.168.1.100

3.1 MySQL半同步复制配置

首先,在两台服务器上安装相同版本的MySQL(建议5.7+或8.0),并完成基础的异步主从复制搭建。这里假设你已经配置好了server_idlog_bin,并创建了复制账号。

在主库(node1)上:

-- 安装半同步主插件 INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so'; -- 启用半同步 SET GLOBAL rpl_semi_sync_master_enabled = ON; -- 设置超时时间(毫秒),根据网络情况调整 SET GLOBAL rpl_semi_sync_master_timeout = 1000; -- 持久化到配置文件,防止重启失效 -- 在my.cnf的[mysqld]段添加: -- plugin-load="rpl_semi_sync_master=semisync_master.so" -- rpl_semi_sync_master_enabled=1 -- rpl_semi_sync_master_timeout=1000

在从库(node2)上:

-- 安装半同步从插件 INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so'; -- 启用半同步 SET GLOBAL rpl_semi_sync_slave_enabled = ON; -- 重启IO线程以使半同步生效 STOP SLAVE IO_THREAD; START SLAVE IO_THREAD; -- 持久化配置 -- plugin-load="rpl_semi_sync_slave=semisync_slave.so" -- rpl_semi_sync_slave_enabled=1

配置完成后,在主库执行SHOW STATUS LIKE ‘Rpl_semi_sync%’;,关注Rpl_semi_sync_master_status应为ONRpl_semi_sync_master_yes_tx会随着事务提交而增加,表示事务通过半同步方式确认。

3.2 编写核心“大脑”:逻辑判断脚本

这是最关键的一步。我们在/usr/local/bin/目录下创建一个脚本mysql_ha_check.sh,并赋予执行权限。

#!/bin/bash # mysql_ha_check.sh # 返回值:0-健康,可持有VIP;非0-不健康,应放弃VIP MYSQL_HOST="127.0.0.1" MYSQL_PORT="3306" MYSQL_USER="check_user" # 创建一个仅有查询权限的监控账号 MYSQL_PASS="YourStrongPassword" MAX_DELAY=30 # 最大允许延迟秒数 # 1. 基础连接检查 if ! mysqladmin -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASS} ping &> /dev/null; then echo "MySQL service is down." exit 1 fi # 2. 查询复制状态和关键指标 SQL="SHOW SLAVE STATUS\G" SLAVE_STATUS=$(mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASS} -e "$SQL" 2>/dev/null) if [ -z "$SLAVE_STATUS" ]; then # 可能这是主库,或者SHOW SLAVE STATUS无输出 # 检查是否只读,如果是从库转主库,这里应该为OFF READ_ONLY=$(mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASS} -sN -e "SELECT @@global.read_only") if [ "$READ_ONLY" -eq 1 ]; then echo "Node is in read-only mode but not a slave? Check topology." exit 2 fi # 如果是主库角色,可以进一步检查半同步主状态、连接数等 # 此处简化处理,认为可写的主库是健康的 echo "Node is a writable master." exit 0 fi # 解析从库状态关键字段(这里使用grep和awk简单处理,生产环境建议用更健壮的方式) IO_RUNNING=$(echo "$SLAVE_STATUS" | grep -i "Slave_IO_Running:" | awk '{print $2}') SQL_RUNNING=$(echo "$SLAVE_STATUS" | grep -i "Slave_SQL_Running:" | awk '{print $2}') SECONDS_BEHIND=$(echo "$SLAVE_STATUS" | grep -i "Seconds_Behind_Master:" | awk '{print $2}') LAST_IO_ERROR=$(echo "$SLAVE_STATUS" | grep -i "Last_IO_Errno:" | awk '{print $2}') SEMI_SYNC_STATUS=$(mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASS} -sN -e "SHOW STATUS LIKE 'Rpl_semi_sync_slave_status';" | awk '{print $2}') # 3. 逻辑判断 # 判断1: 复制线程是否正常 if [ "$IO_RUNNING" != "Yes" ] || [ "$SQL_RUNNING" != "Yes" ]; then echo "Slave threads are not running properly. IO: $IO_RUNNING, SQL: $SQL_RUNNING" exit 3 fi # 判断2: 复制延迟是否过大 if [ "$SECONDS_BEHIND" = "NULL" ] || [ "$SECONDS_BEHIND" -gt $MAX_DELAY ]; then echo "Replication delay is too high: $SECONDS_BEHIND seconds." exit 4 fi # 判断3: 是否有复制错误 if [ "$LAST_IO_ERROR" -ne 0 ]; then echo "There is a replication IO error." exit 5 fi # 判断4: 半同步复制是否正常 if [ "$SEMI_SYNC_STATUS" != "ON" ]; then echo "Semisynchronous replication is not active." exit 6 fi # 判断5: 必须处于只读模式 READ_ONLY=$(mysql -h${MYSQL_HOST} -P${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASS} -sN -e "SELECT @@global.read_only") if [ "$READ_ONLY" -ne 1 ]; then echo "Slave is not in read-only mode. Potential risk for dual-write." exit 7 fi # 所有检查通过 echo "Slave is healthy, replication is semi-sync and within delay limit." exit 0

这个脚本实现了我们之前讨论的核心逻辑检查。它在从库上运行,只有复制完全正常、延迟低、半同步开启且处于只读状态时,才返回0。

3.3 Keepalived配置与集成

在两台节点上安装Keepalived。配置文件通常位于/etc/keepalived/keepalived.conf

在node1 (初始主库) 上的配置:

global_defs { router_id mysql_node1 # 唯一标识 } vrrp_script chk_mysql_ha { script "/usr/local/bin/mysql_ha_check.sh" # 调用我们的“大脑”脚本 interval 2 # 检查间隔秒数 weight -5 # 如果检查失败,优先级降低5 fall 2 # 连续2次失败才认为失败 rise 1 # 1次成功就认为恢复 } vrrp_instance VI_1 { state BACKUP # 两台都设为BACKUP,通过优先级竞争,避免脑裂 interface eth0 # 网卡名称 virtual_router_id 51 # 虚拟路由ID,集群内唯一 priority 100 # 初始优先级,node1稍高 advert_int 1 authentication { auth_type PASS auth_pass 1111 # 密码,集群内一致 } virtual_ipaddress { 192.168.1.100/24 dev eth0 # VIP } track_script { chk_mysql_ha # 跟踪健康检查脚本 } # 当本节点成为MASTER时执行的脚本,用于提升为主库(关闭只读,确保写权限) notify_master "/etc/keepalived/notify_master.sh" # 当本节点成为BACKUP时执行的脚本,用于降级为从库(开启只读) notify_backup "/etc/keepalived/notify_backup.sh" }

在node2 (初始从库) 上的配置:与node1基本一致,主要修改router_idmysql_node2priority设置为一个低于100的值,比如98。这样在双方都健康时,VIP会落在优先级更高的node1上。

notify_master.shnotify_backup.sh脚本用于在VIP漂移时,自动执行数据库角色切换。这是实现自动故障转移(Failover)的关键一步,而不仅仅是VIP切换。

notify_master.sh (当节点获得VIP,需提升为主库时执行):

#!/bin/bash # 停止复制,重置主从关系 mysql -e "STOP SLAVE; RESET SLAVE ALL;" # 关闭只读模式 mysql -e "SET GLOBAL read_only=OFF;" # 刷新权限表,确保更改生效(某些版本需要) mysql -e "FLUSH PRIVILEGES;" # 记录日志 logger "Keepalived: This node is now the MASTER. VIP acquired, read_only disabled."

notify_backup.sh (当节点失去VIP,需降级为从库时执行):

#!/bin/bash # 开启只读模式 mysql -e "SET GLOBAL read_only=ON;" # 注意:这里不会自动重新配置主从。需要额外的逻辑,或者依靠MHA、orchestrator等工具来重构复制拓扑。 # 记录日志 logger "Keepalived: This node is now a BACKUP. VIP lost, read_only enabled."

配置完成后,启动两边的Keepalived服务:systemctl start keepalived && systemctl enable keepalived。使用ip addr show eth0命令查看VIP在哪台机器上。

4. 故障模拟与切换逻辑全解析

纸上谈兵终觉浅,我们来模拟几种典型的故障场景,看看这套“带脑子”的方案如何应对。

4.1 场景一:主库MySQL进程崩溃

这是最直接的故障。node1上的mysqld进程意外终止。

  1. node1上的mysql_ha_check.sh脚本执行,mysqladmin ping失败,脚本返回非0。
  2. Keepalived检测到chk_mysql_ha脚本失败,根据weight -5的配置,将node1的优先级从100降至95。
  3. node2的优先级为98,高于node1的95。node2在下一个VRRP通告周期中,发现自己的优先级更高,于是发起选举,将VIP192.168.1.100抢占到自己身上。
  4. node2的Keepalived状态变为MASTER,触发notify_master.sh脚本执行。
  5. 脚本停止node2的复制线程,并关闭read_only模式。此时,node2正式成为可读写的新主库。
  6. 所有应用程序通过VIP连接到新的主库node2,写入继续。

关键点:切换的发生,不仅仅因为node1的MySQL挂了,更因为node2通过了所有逻辑检查(复制正常、延迟低、半同步开启),证明自己是一个合格的继任者。

4.2 场景二:主库网络中断(脑裂风险防范)

node1所在服务器网络完全断开,但MySQL进程正常。

  1. node1无法与node2通信,但它自己检查自己是“健康”的(脚本返回0)。它认为自己是唯一存活的节点,会继续持有VIP。但由于网络隔离,应用实际上无法访问到这个VIP。
  2. node2也收不到node1的VRRP通告,它会认为node1下线了。node2检查自身状态通过,于是将自身优先级提升(因为认为没有竞争对手),并宣告自己为MASTER,抢占VIP。
  3. 此时,网络分区导致出现了两个“主库”,都认为VIP在自己这里,这就是“脑裂”。

我们的方案如何缓解?

  • 第三方脚本的增强:我们的mysql_ha_check.sh可以进一步增强,加入对原主库可达性的判断。例如,脚本可以尝试连接原主库的IP,或者查询一个共享的仲裁节点(如一个轻量级的consul服务)。如果发现原主库还活着(只是网络不通),那么当前节点即使自身健康,也主动放弃竞选(脚本返回非0),从而避免在分区时形成双主。这需要更复杂的脚本逻辑,但能极大提升集群的健壮性。
  • Keepalived的nopreemptpriority设计:我们将两台机器初始状态都设为BACKUP,并设置不同的priority,这本身比一主一备(MASTER/BACKUP)的模式更能减少脑裂概率。结合严格的脚本检查,能在大多数网络抖动场景下保持稳定。

4.3 场景三:从库复制延迟过大

这是传统方案最容易出错的地方。假设node2(从库)因为一个大的批量操作,复制延迟达到了2分钟。

  1. 此时node1(主库)依然健康。
  2. node2上的mysql_ha_check.sh脚本执行。检查到Seconds_Behind_Master大于我们设定的MAX_DELAY(30秒),脚本返回退出码4(非0)。
  3. Keepalived检测到脚本失败,降低node2的优先级。
  4. 即使此时node1发生故障,node2因为自身检查不通过,优先级被降低,它也不会去抢占VIP。VIP可能会漂移到其他健康的从库(如果有多于两个节点),或者停留在故障的主库上(直到脚本超时或检测到主库故障)。这阻止了一次危险的数据不一致切换
  5. 运维人员会收到报警(监控应监控脚本的退出码和MySQL延迟),然后人工介入处理延迟问题。这体现了“自动切换”与“人工兜底”的结合,自动化处理明确、简单的故障,将复杂、有风险的场景留给人工判断。

5. 方案优化与生产环境进阶思考

上面的基础方案已经能解决大部分问题,但在严苛的生产环境中,我们还需要考虑更多。

5.1 判断脚本的增强与仲裁机制

基础的本地检查脚本存在局限性,它只有一个节点的视角。一个更健壮的“大脑”应该具备集群视角。

  • 探针式检查:脚本可以内置一个小型探针,主动去连接集群中的其他节点,询问“你认为谁是主库?”、“我的数据是不是最新的?”。这需要节点间有一个简单的HTTP接口或共享的数据库表来交换信息。
  • 引入外部仲裁器:这是更常见的做法。可以使用ZooKeeper、etcd或Consul作为分布式协调服务。每个MySQL节点定期向仲裁器注册自己的状态(GTID、角色、健康状态)。判断脚本不再只检查自己,而是去仲裁器查询:“根据集群全局状态,我现在应该是主库吗?”仲裁器可以根据预设策略(如GTID最大者胜出)给出权威答案,彻底解决脑裂问题。许多成熟的高可用工具如Orchestrator,其核心就是一个强大的拓扑管理和仲裁器。

5.2 多从库环境下的优先级设计

当集群有多个从库(比如一主三从)时,VIP应该漂移到哪个从库?这就需要更精细的优先级设计。

  1. 基于半同步ACK的优先级:哪个从库的Rpl_semi_sync_slave_statusON且最近有ACK,哪个优先级就最高。这需要脚本能查询或推断出ACK信息。
  2. 基于GTID/位点的优先级:脚本比较各从库(包括自己)的Executed_Gtid_Set,拥有最靠后GTID的从库优先级最高。这需要脚本能访问其他节点的信息(通过仲裁器或直接查询)。
  3. 基于延迟的优先级Seconds_Behind_Master最小的从库优先级最高。
  4. 配置化优先级:在Keepalived配置中为不同从库设置不同的基础priority,再通过检查脚本的weight进行调整。例如,同机房从库优先级高于跨机房从库。

在实际配置中,可以将这些因素综合成一个分数,最终通过脚本的返回值或修改Keepalived的priority动态调整来实现。

5.3 与专业高可用工具的对比与选型

我们的方案本质上是“Keepalived + 自定义脚本”,它的优点是轻量、透明、可控性强,适合对运维有较强把控能力的团队。但它也有明显的缺点:

  • 功能有限:只实现了故障转移(Failover),缺乏自动的故障恢复(Failback)、主从拓扑自动重构、在线切换等高级功能。
  • 运维复杂度:脚本的逻辑需要自己开发和维护,可靠性需要经过充分测试。
  • 监控与可视化:需要自行搭建完善的监控来跟踪脚本状态、复制延迟、半同步状态等。

因此,对于更复杂或要求更高的场景,可以考虑成熟的第三方工具:

  • Orchestrator:功能非常强大,支持自动故障检测、恢复、拓扑可视化、中间件支持等,是当前MySQL高可用领域的热门选择。
  • MHA (Master High Availability):经典工具,自动化程度高,但近年来活跃度下降。
  • ProxySQL + Group Replication:利用MySQL组复制(InnoDB Cluster)提供内置的高可用和自动选主,再通过ProxySQL实现读写分离和连接路由,是一套更现代、更集成的方案。

选择自研方案还是成熟工具,取决于团队的运维能力、业务对RTO(恢复时间目标)/RPO(恢复点目标)的要求以及技术栈的整体规划。

5.4 监控与告警:让运维睡个安稳觉

再好的自动化方案也离不开监控。对于这套方案,必须监控以下核心指标:

  • Keepalived状态:各节点的VRRP状态(MASTER/BACKUP)。
  • VIP漂移历史:记录VIP切换的时间点和原因。
  • MySQL健康检查脚本退出码:非0退出必须立即告警。
  • 半同步复制状态Rpl_semi_sync_master_status,Rpl_semi_sync_slave_status
  • 复制延迟Seconds_Behind_Master,设置不同级别的阈值(如>10s警告,>30s严重)。
  • GTID增长情况:监控主从的GTID集合差异。

将这些指标接入Prometheus+Grafana或Zabbix等监控系统,并配置相应的告警规则(如企业微信、钉钉、短信),才能确保在出现问题时,运维团队能第一时间被通知,甚至在自动切换发生前就介入处理潜在风险。

这套“半同步复制+Keepalived+第三方逻辑判断”的方案,是我在经历了多次“傻切换”的教训后,总结出的一套务实且有效的改进方案。它不是在否定经典,而是在经典之上增加了至关重要的“数据逻辑层”。它告诉我们,高可用的核心不仅仅是“可用”,更是“数据正确的可用”。在实际部署时,请务必在测试环境进行充分的故障演练,摸清各种边界情况下的行为,并根据自己业务的特点,调整脚本的判断逻辑和阈值。只有这样,才能真正让数据库层的高可用,成为业务稳定运行的坚实后盾。

← 返回列表