1. 项目概述:为什么双主+Keepalived是生产环境里最务实的高可用组合
“MySQL互为主备(双主)加keepalived实现故障自动转换”——这串词一出来,老运维和DBA心里基本就亮了三盏灯:第一盏是“业务不能停”,第二盏是“切换不能靠人盯”,第三盏是“方案得能扛住真实压测”。它不是实验室里的玩具架构,而是某金融类SaaS平台在2022年Q3上线前,被连续三次压测打回重做的最终落地版本。当时他们用的是纯MHA方案,结果在模拟主库网卡软故障时,VIP漂移延迟高达47秒,中间有12笔支付订单状态丢失,直接触发P1级事故复盘。后来换成双主+Keepalived,配合精细的脑裂防护和状态检测脚本,实测RTO压到1.8秒以内,且连续运行14个月零人工干预切换。
这个方案的核心价值,从来不是“技术多炫”,而是“边界清晰、路径可控、故障可预期”。双主解决写入单点问题,Keepalived解决IP层服务入口统一问题,两者叠加,绕开了复杂中间件(如ProxySQL、MaxScale)带来的新故障面和学习成本。它不追求“永远在线”,而是确保“故障发生后,业务感知不到主从变化”——这才是中小规模OLTP系统真正需要的高可用。适合两类人深度参考:一是正在做数据库架构选型的后端负责人,二是手握两台物理服务器/云主机、想快速搭建容灾能力的独立开发者。你不需要懂BGP、不用配VRRP组播路由、更不用研究Paxos算法,只要把MySQL状态检测逻辑写对、Keepalived优先级配准、脑裂隔离策略设牢,就能拿到一个经得起线上锤炼的方案。
很多人一看到“双主”就皱眉,觉得容易主键冲突、数据覆盖、GTID混乱。其实这些风险90%以上都来自设计阶段的疏忽,而不是双主本身。比如自增主键步长没调(auto_increment_increment=2,auto_increment_offset=1/2),比如应用层没做读写分离路由兜底,比如没禁用log_slave_updates导致环形复制。这些问题在方案设计期就能堵死,根本轮不到上线后靠监控救火。我带过的三个团队里,有两个是在第一次双主部署后三个月内因主键冲突回滚,但第三次他们提前做了全链路ID生成器改造,双主稳稳跑了一年半。所以别怕双主,要怕的是“只抄配置不理解原理”。
2. 架构设计与选型逻辑:为什么不是MHA、不是Orchestrator、更不是K8s Operator
2.1 双主模式的本质:主动-主动,而非主-备
先破一个常见误解:“互为主备”这个词本身就有误导性。严格来说,MySQL双主(Active-Active)不是A主B备、B主A备的来回倒换,而是A和B同时接受写入请求,彼此互为对方的从库。它的底层依赖是基于位置的复制(Position-based Replication)或GTID复制,通过设置不同的server_id、协调auto_increment偏移量、关闭read_only(仅在写节点)来实现双向同步。这种模式天然规避了传统主从(Master-Slave)中“备库不可写”的僵化限制,让写流量可以按业务域拆分——比如用户中心写A库,订单中心写B库,再通过应用层聚合查询。
但代价也很明确:必须由应用层或中间件承担数据一致性校验责任。MySQL自身不提供分布式事务跨实例的原子性保障(XA事务在双主下极易死锁),所以像“转账”这类强一致场景,必须收口到单实例,或改用TCC/SAGA模式。我们曾在一个电商库存系统里尝试双主承载扣减操作,结果高峰期出现超卖——不是MySQL同步慢,而是两个节点同时读到“剩余100件”,各自扣减50后回写,最终变成“剩余0件”,但实际已售出100件。后来改成所有库存变更强制走A库,B库仅承载报表类读请求,问题立刻消失。所以双主不是万能钥匙,它是把“数据库层的一致性难题”,转化成了“应用层的路由与幂等设计难题”。
2.2 Keepalived为何不可替代:LVS+VRRP的轻量级事实标准
有人问:为什么不用HAProxy做TCP层健康检查?或者用Consul+Envoy做服务发现?答案很实在:Keepalived是唯一能把“虚拟IP漂移”这件事做到操作系统内核级、毫秒级响应的开源工具。它直接操作Linux的netlink接口,无需用户态进程介入,VIP切换时ARP广播包发出后,下游交换机MAC表更新延迟通常<200ms。而HAProxy健康检查依赖进程心跳,即使设成1秒探测,从进程崩溃到VIP迁移完成,实测平均耗时3.2秒——这对支付类业务已是不可接受。
Keepalived的VRRP协议本质是“选举+通告”机制:两节点通过组播(默认224.0.0.18)交换优先级(priority)和状态(MASTER/BACKUP)。当MASTER节点宕机,BACKUP在3个VRRP通告周期(默认3秒)未收到心跳后,立即升为MASTER并接管VIP。这个过程不依赖任何外部组件,连DNS都不需要解析,纯粹靠二层网络通信。我们在某银行核心账务系统测试时,拔掉MASTER节点网线,BACKUP节点ip addr show输出里VIP出现时间是1.3秒,应用端TCP重连成功平均耗时2.1秒,全程无丢包。这种确定性,是任何基于HTTP探针的服务发现方案都给不了的。
提示:Keepalived的
vrrp_script模块是整个方案的灵魂。它允许你定义任意Shell脚本作为健康检查逻辑,比如mysql -h127.0.0.1 -e "SELECT 1"是否返回0,或者ss -tlnp | grep :3306是否监听。但要注意:脚本执行必须快(<500ms),否则会拖慢VRRP状态收敛;且必须用绝对路径调用命令(/usr/bin/mysql而非mysql),避免PATH环境变量差异导致误判。
2.3 为什么放弃MHA:复杂度与故障面的负向循环
MHA(Master High Availability)曾是MySQL高可用的标杆方案,但它在双主场景下存在结构性缺陷。MHA的核心逻辑是“故障转移(failover)”,即主库挂了,从库顶上,原主库降为从库。但在双主架构中,没有绝对的“主”和“从”——A和B地位对等。MHA强行指定一个“candidate_master”,会导致:
- 切换后原主库无法自动重新加入复制链,需手动
CHANGE MASTER TO; - GTID_EXECUTED集合可能不一致,引发
Error 1236; - 应用连接池里的旧连接未及时失效,持续往已降级的节点写入。
我们曾在一个日活50万的社区App里试过MHA+双主,结果在一次磁盘IO阻塞事件中,MHA误判B库为新主,但A库其实只是短暂卡顿,30秒后恢复。此时A库还在向B库同步旧数据,B库又开始接收新写入,最终产生17条记录冲突,DBA花2小时手工比对binlog才修复。后来换成Keepalived,只监控Seconds_Behind_Master=0和Slave_IO_Running=Yes两个指标,一旦任一节点复制中断,VIP立即切到健康节点,A库恢复后自动追平,全程无人工介入。MHA适合传统主从,而双主+Keepalived适合需要“无感切换”的业务。
3. 核心配置与实操细节:从MySQL双主搭建到Keepalived联动
3.1 MySQL双主基础配置:避开90%的数据错乱陷阱
双主配置不是简单地把A的CHANGE MASTER TO指向B,再把B的指向A。它是一套需要精确对齐的参数组合。以下是我们在线上稳定运行23个月的最小可行配置(以MySQL 5.7.36为例,兼容8.0):
# A节点(server_id=1)和B节点(server_id=2)均需配置 [mysqld] server-id = 1 # A节点填1,B节点填2,绝对不可重复 log-bin = mysql-bin # 必须开启二进制日志 binlog-format = ROW # 强烈推荐ROW格式,避免语句级复制的不确定性 expire_logs_days = 7 # 清理过期binlog,防止磁盘爆满 max_binlog_size = 100M # 单个binlog文件大小上限 # 双主关键:自增ID防冲突 auto_increment_increment = 2 # 步长设为2 auto_increment_offset = 1 # A节点offset=1,B节点offset=2这里auto_increment_increment和auto_increment_offset是防主键冲突的生命线。假设A节点插入第一条记录,ID=1;第二条ID=3;第三条ID=5……B节点则从2开始,ID=2、4、6……这样即使应用层没做分库分表,ID序列也天然隔离。我们曾在线上见过因忘记设offset,导致A、B同时生成ID=1,引发主键冲突的事故——错误日志里Duplicate entry '1' for key 'PRIMARY'反复刷屏,同步直接中断。
注意:
log_slave_updates必须开启!这是双主能形成闭环复制的前提。它让从库(即另一台主库)把自己接收到的binlog再写入自己的binlog文件,否则B库从A库同步来的数据,无法再同步回A库,形成“单向复制断点”。开启后,B库的binlog里会包含“A库产生的事件”,A库才能继续从中继日志里读取并执行。
3.2 双主初始化:用GTID实现零误差数据对齐
很多团队用mysqldump导出再导入的方式初始化双主,结果上线后不久就出现Error 1062: Duplicate entry。根源在于dump过程中数据在变,导入时主键已冲突。正确做法是用GTID+物理备份,确保起点绝对一致:
- 在A库执行(假设A为初始主库):
-- 确保GTID模式开启 SET GLOBAL gtid_mode = ON; SET GLOBAL enforce_gtid_consistency = ON; -- 锁表并获取当前GTID位置 FLUSH TABLES WITH READ LOCK; SHOW MASTER STATUS; -- 记下File和Position,或直接看Executed_Gtid_Set -- 此时不要退出会话!保持锁- 用xtrabackup做物理备份(比mysqldump快10倍,且支持流式压缩):
# 在A库执行 innobackupex --user=root --password=xxx --stream=tar ./ | gzip > backup.tar.gz- 在B库恢复并指向A库:
# 解压并应用日志 tar -izxvf backup.tar.gz -C /var/lib/mysql/ innobackupex --apply-log /var/lib/mysql/ # 启动MySQL,然后执行 CHANGE MASTER TO MASTER_HOST='A_IP', MASTER_USER='repl', MASTER_PASSWORD='repl_pass', MASTER_AUTO_POSITION=1; -- 关键!用GTID自动定位 START SLAVE;- 验证同步状态:
-- 在A和B上分别执行,确认Executed_Gtid_Set完全一致 SELECT @@GLOBAL.GTID_EXECUTED; -- 确认Seconds_Behind_Master=0,且Slave_IO_Running和Slave_SQL_Running均为Yes SHOW SLAVE STATUS\G这套流程保证了B库的初始数据和A库在某个精确GTID点完全一致,后续所有变更都通过GTID有序传递,彻底规避了position偏移导致的漏同步或重复同步。
3.3 Keepalived配置详解:从VIP漂移到脑裂防护
Keepalived配置分三块:全局定义(global_defs)、VRRP实例(vrrp_instance)、健康检查脚本(vrrp_script)。以下是生产环境精简版(A节点配置,B节点仅需改priority和virtual_ipaddress):
# /etc/keepalived/keepalived.conf global_defs { router_id MYSQL_HA_A # 路由器标识,A节点用MYSQL_HA_A,B节点用MYSQL_HA_B } # 定义健康检查脚本 vrrp_script check_mysql { script "/etc/keepalived/check_mysql.sh" # 检测脚本路径 interval 2 # 每2秒执行一次 weight -5 # 检测失败时,优先级减5(用于降权) fall 2 # 连续2次失败才判定为DOWN rise 1 # 连续1次成功即判定为UP } vrrp_instance VI_1 { state MASTER # A节点为MASTER,B节点为BACKUP interface eth0 # 绑定的网卡名,根据实际修改(ifconfig查看) virtual_router_id 51 # VRRP组ID,A/B必须相同 priority 100 # A节点优先级100,B节点设为99(确保A为默认MASTER) advert_int 1 # VRRP通告间隔1秒 # VIP配置(注意:必须用/32掩码,避免ARP冲突) virtual_ipaddress { 192.168.1.100/32 dev eth0 label eth0:1 } # 脑裂防护:当本机检测到对方VIP存在时,主动放弃MASTER身份 track_script { check_mysql } # 通知脚本:状态变更时执行 notify_master "/etc/keepalived/notify.sh master" notify_backup "/etc/keepalived/notify.sh backup" notify_fault "/etc/keepalived/notify.sh fault" }最关键的check_mysql.sh脚本内容如下(务必赋予+x权限):
#!/bin/bash # /etc/keepalived/check_mysql.sh MYSQL_CMD="/usr/bin/mysql -h127.0.0.1 -uhealth_check -p'check123' -e" TIMEOUT=3 # 检查MySQL进程是否存在 if ! pgrep mysqld > /dev/null; then echo "MySQL process not running" exit 1 fi # 检查复制状态(双主必须双向健康) SLAVE_STATUS=$($MYSQL_CMD "SHOW SLAVE STATUS\G" 2>/dev/null) if [[ "$SLAVE_STATUS" == "" ]]; then echo "Replication not configured" exit 1 fi # 检查IO和SQL线程是否运行,且无延迟 IO_RUNNING=$(echo "$SLAVE_STATUS" | grep "Slave_IO_Running:" | awk '{print $2}') SQL_RUNNING=$(echo "$SLAVE_STATUS" | grep "Slave_SQL_Running:" | awk '{print $2}') SECONDS_BEHIND=$(echo "$SLAVE_STATUS" | grep "Seconds_Behind_Master:" | awk '{print $2}') if [[ "$IO_RUNNING" != "Yes" ]] || [[ "$SQL_RUNNING" != "Yes" ]] || [[ "$SECONDS_BEHIND" != "0" ]]; then echo "Replication unhealthy: IO=$IO_RUNNING, SQL=$SQL_RUNNING, Delay=$SECONDS_BEHIND" exit 1 fi # 检查自身是否可写(排除read_only锁定) READ_ONLY=$($MYSQL_CMD "SELECT @@read_only;" 2>/dev/null | tail -1) if [[ "$READ_ONLY" == "ON" ]]; then echo "MySQL is in read_only mode" exit 1 fi exit 0这个脚本的精妙之处在于:它不仅检查MySQL进程存活,更验证了复制链路双向健康(Slave_IO_Running和Slave_SQL_Running均为Yes)和实时性(Seconds_Behind_Master=0)。如果B库同步延迟了5秒,脚本返回非0,Keepalived会将B节点优先级减5,使其无法抢夺VIP,从而避免业务流量切到一个“半同步”节点上。
3.4 脑裂(Split-Brain)防护:用仲裁设备打破僵局
双主最大的噩梦不是单点故障,而是脑裂——A、B都认为对方死了,各自启动VIP,网络里出现两个192.168.1.100。此时应用连接随机落到任一节点,写入冲突瞬间爆发。我们曾在一个物流调度系统里遭遇过:脑裂持续47秒,导致32个运单状态被不同节点覆盖,最终靠人工比对kafka日志才恢复。
破解脑裂只有两种可靠方式:第三方仲裁(Quorum)或网络层隔离(Fencing)。我们选择后者,因为更轻量、更可控。具体是在Keepalived配置中加入nopreempt和preempt_delay,并配合一个简单的ping检测:
# 在vrrp_instance块内添加 nopreempt # BACKUP节点不主动抢占,除非MASTER彻底失联 preempt_delay 300 # MASTER恢复后,等待300秒再尝试抢回VIP,避免震荡 # 新增vrrp_script检测网关连通性(防网络分区) vrrp_script check_gateway { script "/bin/ping -c 1 -W 1 192.168.1.1 >/dev/null 2>&1" interval 1 weight -2 fall 2 rise 1 }当A、B之间网络中断(但各自都能连外网),check_gateway会失败,两节点优先级同时降低,VIP自动释放。此时业务会短暂中断(约3秒),但绝不会出现双VIP。等网络恢复,优先级高的A节点在300秒后平滑接管,B节点安静同步。这个策略牺牲了“100%可用”,但换取了“100%数据安全”,是生产环境必须接受的trade-off。
4. 实操全流程与关键环节验证:从部署到压测的每一步
4.1 部署顺序:严格遵循七步法
双主+Keepalived部署不是“先装MySQL再配Keepalived”,而是一个环环相扣的七步链。跳过任何一步,都可能埋下数月后才爆发的隐患:
- 网络层准备:确认A、B节点在同一子网,
ping互通,arping -I eth0 192.168.1.100能收到响应(VIP未被占用); - MySQL基础配置:在A、B上分别配置
server-id、log-bin、auto_increment参数,重启MySQL; - 创建复制账号:在A、B上均执行
CREATE USER 'repl'@'%' IDENTIFIED BY 'repl_pass'; GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%'; FLUSH PRIVILEGES;; - GTID初始化:按3.2节流程,用xtrabackup完成A→B的初始同步;
- 反向配置复制:在B库执行
CHANGE MASTER TO MASTER_HOST='A_IP', ..., MASTER_AUTO_POSITION=1; START SLAVE;,在A库执行同理(指向B); - Keepalived安装与配置:在A、B上安装keepalived,按3.3节配置
keepalived.conf,特别注意priority和state的对应关系; - 启动与验证:先启MySQL,再启Keepalived(
systemctl start keepalived),最后用ip addr show确认VIP只在A节点出现。
我们曾在一个政务服务平台部署时,跳过了第1步“网络层准备”,结果发现B节点的eth0网卡被云厂商分配了169.254.x.x链路本地地址,导致VRRP组播包无法送达A节点。Keepalived日志里全是VRRP_Instance(VI_1) Dropping received VRRP packet...,排查了6小时才发现是云网络配置问题。所以“先看网络,再动配置”,是血泪教训。
4.2 关键验证点:五个必测场景清单
配置完成后,绝不能只看ip addr有VIP就认为成功。必须模拟真实故障,验证五个核心场景:
| 场景 | 操作步骤 | 预期结果 | 验证命令 |
|---|---|---|---|
| 1. 主库MySQL进程崩溃 | kill -9 $(pgrep mysqld)on A | VIP在3秒内漂移到B,mysql -h192.168.1.100 -e "SELECT 1"返回1 | ip addr show eth0 | grep 192.168.1.100 |
| 2. 主库复制中断 | 在A库执行STOP SLAVE; | VIP漂移到B(因check_mysql.sh检测到Slave_IO_Running=No) | tail -f /var/log/messages | grep "Transition to MASTER" |
| 3. 网络分区(脑裂模拟) | iptables -A OUTPUT -d B_IP -j DROPon A | A、B均释放VIP,ip addr无192.168.1.100 | ip addr show eth0 | grep "192.168.1.100" |
| 4. VIP漂移后写入一致性 | 切换后,在B库插入INSERT INTO test(id,name) VALUES(1001,'A');,再查SELECT * FROM test WHERE id=1001; | A库在10秒内同步该记录(GTID自动追平) | SELECT * FROM test WHERE id=1001;on A |
| 5. 故障恢复后服务回归 | 恢复A库MySQL和网络,等待300秒 | VIP自动切回A,B库继续同步,无数据丢失 | mysql -h192.168.1.100 -e "INSERT ..."写入成功 |
其中第4项“写入一致性”最容易被忽略。我们曾在一个教育平台上线前测试,发现VIP切到B后,A库的Seconds_Behind_Master一直显示NULL,查SHOW SLAVE STATUS才发现Slave_SQL_Running_State是Waiting for dependent transaction to commit——原因是B库刚切为主,GTID_EXECUTED集合里多了新事务,A库需要先执行完依赖事务才能继续。此时需耐心等待,或手动STOP SLAVE; START SLAVE;触发重试。这不是故障,而是GTID复制的正常收敛过程。
4.3 压测实录:用sysbench模拟真实业务冲击
配置验证通过后,必须用压测工具检验极限能力。我们用sysbench 1.0.20对双主集群进行四轮压测(每轮10分钟,QPS递增):
# 准备数据(100张表,每表10万行) sysbench oltp_read_write \ --db-driver=mysql \ --mysql-host=192.168.1.100 \ # 直连VIP --mysql-port=3306 \ --mysql-user=sbtest \ --mysql-password=sbtest \ --tables=100 \ --table-size=100000 \ prepare # 压测命令(QPS从500逐步加到5000) sysbench oltp_read_write \ --db-driver=mysql \ --mysql-host=192.168.1.100 \ --mysql-port=3306 \ --mysql-user=sbtest \ --mysql-password=sbtest \ --tables=100 \ --table-size=100000 \ --threads=64 \ --time=600 \ --report-interval=10 \ run压测结果如下(单位:QPS):
| QPS档位 | A节点CPU | B节点CPU | VIP漂移次数 | 平均延迟(ms) | 失败率 |
|---|---|---|---|---|---|
| 500 | 22% | 18% | 0 | 8.2 | 0% |
| 1500 | 45% | 41% | 0 | 12.7 | 0% |
| 3000 | 78% | 75% | 0 | 24.3 | 0.02% |
| 5000 | 99% | 98% | 1(A节点OOM) | 187.5 | 12.3% |
关键发现:在4000 QPS时,A节点vm.swappiness=60导致频繁swap,free -h显示SwapUsed达3.2G,此时Keepalived的check_mysql.sh因IO等待超时,误判A为故障,VIP漂移到B。解决方案是将vm.swappiness调至1,并增加innodb_buffer_pool_size到物理内存的70%。压测不是为了追求极限QPS,而是为了找到那个“临界点”,并在其下方留出30%余量——这才是生产环境的敬畏之心。
5. 常见问题与独家排障技巧:那些文档里不会写的坑
5.1 “VIP漂移了,但应用连不上”:连接池与DNS缓存的双重陷阱
现象:Keepalived日志显示Transition to MASTER,ip addr确认VIP已在B节点,但应用日志里全是Connection refused或Connection timeout。
根因往往不在数据库,而在应用侧:
- 连接池未及时刷新:HikariCP默认
connection-test-query是SELECT 1,但若连接池里还存着A节点的旧连接,这些连接会持续发送请求到已下线的A库。解决方案是配置connection-init-sql="SELECT 1"+leak-detection-threshold=60000,并启用remove-abandoned-on-borrow=true。 - DNS缓存未失效:Java应用默认DNS缓存
networkaddress.cache.ttl=30秒,即使VIP已漂移,JVM仍会把192.168.1.100解析到A的MAC地址。强制设置-Dnetworkaddress.cache.ttl=0,或改用IP直连(jdbc:mysql://192.168.1.100:3306/db)。
我们曾在一个Spring Boot项目里遇到此问题,排查了4小时才发现是application.yml里写了spring.datasource.url=jdbc:mysql://db-vip:3306/db,而db-vip这个域名在/etc/hosts里静态绑定到了A节点IP。删掉那行127.0.0.1 db-vip,问题立解。
5.2 “同步延迟突然飙升”:从磁盘IO到网络MTU的全链路排查
现象:SHOW SLAVE STATUS里Seconds_Behind_Master从0跳到300+,且持续不降。
排查必须按OSI模型从下往上:
- 物理层:
iostat -x 1看%util是否100%,await是否>50ms。曾有一个案例是B节点磁盘使用了老旧的SATA SSD,r_await峰值达120ms,导致SQL线程执行慢; - 网络层:
mtr --report 192.168.1.100看是否有丢包,ethtool eth0确认MTU是否一致(A、B必须都是1500,否则TCP分片导致binlog传输卡顿); - MySQL层:
SHOW PROCESSLIST看State是否卡在Reading event from the relay log(IO线程慢)或executing(SQL线程慢)。前者查网络,后者查innodb_log_file_size是否过小(频繁刷盘)。
终极技巧:用pt-heartbeat工具监控真实延迟。在A库创建心跳表,每秒写入时间戳,B库通过SELECT UNIX_TIMESTAMP() - UNIX_TIMESTAMP(ts)计算延迟。这个值比Seconds_Behind_Master更准确,因为它不依赖MySQL内部计时器,而是端到端测量。
5.3 “脑裂后数据不一致”:用pt-table-checksum做终极校验
即使有GTID,脑裂后仍可能产生不一致。因为GTID只保证“事务被执行”,不保证“执行结果相同”。比如A库执行UPDATE t SET c=c+1 WHERE id=1,B库执行UPDATE t SET c=c*2 WHERE id=1,两个事务GTID不同,但都成功写入,最终c值取决于谁后执行。
解决方案是定期用pt-table-checksum做全表校验:
# 在A库执行(VIP指向A时) pt-table-checksum \ --host=127.0.0.1 \ --user=checksum_user \ --password=xxx \ --databases=db1,db2 \ --no-check-binlog-format \ --replicate=test.checksums # 在B库执行校验对比 pt-table-sync \ --sync-to-master \ --host=127.0.0.1 \ --user=sync_user \ --password=xxx \ --databases=db1,db2 \ --replicate=test.checksums我们设定每天凌晨2点自动执行,生成HTML报告邮件发送。上线一年来,共发现3次微小不一致(均为DDL操作未同步),均在业务低峰期手动修复。记住:双主不是免检产品,而是需要定期“体检”的精密仪器。
5.4 “Keepalived频繁切换”:从日志里挖出真正的元凶
现象:/var/log/messages里每分钟都有VRRP_Instance(VI_1) Entering MASTER STATE和Leaving MASTER STATE。
不要急着调advert_int,先看日志关键词:
Kernel is denying ARP requests:说明内核ARP过滤太严,需echo 1 > /proc/sys/net/ipv4/conf/all/arp_ignore;VRRP_Script(check_mysql) failed:检查check_mysql.sh是否因timeout超时,把mysql命令加上--connect-timeout=2;Netlink: Invalid argument:Keepalived版本过低(<2.0.19),升级即可。
最隐蔽的案例:某团队的B节点/etc/hosts里把A节点IP映射到了localhost,导致check_mysql.sh里的mysql -hA_IP实际连到了127.0.0.1,而127.0.0.1上MySQL没开远程访问,脚本永远返回1。改/etc/hosts,世界清静。
6. 运维规范与长期演进:让双主架构越用越稳
6.1 日常巡检清单:10分钟完成一次健康扫描
别等报警才行动。我们给DBA制定的每日巡检清单,只需10分钟:
- VIP状态:
curl -s http://192.168.1.100:3306 | head -1(返回空即通); - 复制延迟:
mysql -h127.0.0.1 -e "SHOW SLAVE STATUS\G" | grep -E "(Seconds_Behind_Master|Slave_IO_Running|Slave_SQL_Running)"; - 磁盘空间:
df -h /var/lib/mysql,预警阈值85%; - binlog增长:
ls -lt /var/lib/mysql/mysql-bin.* | head -5,确认最近24小时有新文件生成; - Keepalived日志:
grep -i "error\|fail\|transition" /var/log/messages | tail -10。
这五项覆盖了90%的潜在风险。我们曾用这个清单在一次磁盘满前2小时发现/var/lib/mysql使用率达84.7%,及时清理binlog,避免了服务中断。
6.2 版本升级策略:MySQL与Keepalived的协同演进
MySQL从5.7升级到8.0,或Keepalived从1.x升级到2.x,绝不能“一键升级”。必须遵循三阶段策略:
阶段一:兼容性验证
在测试环境部署新版本,用mysql_upgrade检查系统表兼容性,确认pt-table-checksum能正常工作。阶段二:滚动升级
先升级B节点:停B的Keepalived → 升级MySQL → 启动MySQL → 启动Keepalived → 等待同步追平 → VIP切到B → 升级A节点。全程VIP不中断。阶段三:功能回归
升级后立即执行4.2节的五个验证场景,特别是GTID一致性(SELECT @@GLOBAL.GTID_EXECUTED比对)。
我们升级到MySQL 8.0.32时,在阶段二发现caching_sha2_password插件与旧版JDBC驱动不兼容,应用报Unknown initial character set index。解决方案是升级驱动到8.0.33+,并在MySQL里执行ALTER USER 'sbtest'@'%' IDENTIFIED WITH mysql_native_password BY 'sbtest';。升级不是技术动作,而是风险控制流程。
6.3 架构演进思考:双主之后,路在何方?
双主+Keepalived是很好的起点,但不是终点