双主热备这个词我第一次认真啃透是在做数据库高可用改造的时候。当时线上一个订单库还是单节点半夜磁盘直接报了故障业务停了好几个小时第二天复盘会上领导只问了一句能不能让两台机器都干活一台挂了另一台立刻顶上。这句话就是双主热备架构最朴素的原点。我准备从四个角度把双主热备讲透它到底解决什么问题、核心原理是什么、数据库层和负载均衡层怎么落地、以及实际排障中踩过的那些坑。这篇文章适合正在被高可用问题困扰的运维、DBA、后端开发也适合正准备做双机改造的团队作为参考。文章里的配置和步骤都是我实际用过的方案不是教科书式的理论推演你可以直接拿去改一改用。1. 双主热备到底解决什么问题1.1 从单机说起高可用基本盘先聊个常识单机系统再怎么稳定也逃不过硬件损坏、进程崩溃、磁盘写满、内核卡死这些意外。你无法预测哪天凌晨磁盘会出问题只能提前把架构做成“坏了也能继续跑”的样子。高可用本质上围绕三件事展开冗余、检测、切换。冗余解决的是“有备用资源”检测解决的是“知道主节点坏了”切换解决的是“流量能快速换到备用资源上”。双主热备就是这三件事的落地组合两台机器都部署同样的服务平时各干各的或者一台承载主要流量一台待命一旦其中一台异常另一台在秒级内接手所有业务。很多团队容易犯一个错误只买了双机却没有配套的探测和切换机制。结果某台服务器宕机了业务照样停。因为那台机器上跑着的服务根本不会自己切到另一台去。所以判断一套双主热备做得好不好不是看买了多少台机器而是看故障发生后流量能不能自动过去。1.2 和主备、主从的区别三种架构一定要分清我刚接触高可用时也把主备、主从、双主搞混过。后来自己搭了几次环境才彻底把边界理清楚。三者的差异主要在于“平时怎么干活”和“挂了怎么切换”。架构模式平时状态故障切换硬件利用率典型场景主备备机纯空闲随时等待接管靠脚本或人工拉起备机可能需要几分钟完成接管低数据库、虚拟机、基础网络设备主从从库同步数据可承担读流量不能接管写从库需要提升为主库切换逻辑复杂中读写分离、备份、数据分析双主热备两台都在运行VIP各自分散部署互为备份VIP秒级漂移另一台同时接管高数据库、Nginx、HAProxy等入口层我用生活化一点的说法来解释主备就像配了一个专职司机平时司机不开车专职保姆式待命主从像领导和副手平时副手帮着处理一部分杂活读请求但拍板签字的还是领导双主热备更像是两个都能独立接活的师傅平时各自带一摊事谁临时缺了席另一个马上把活全接过来。这样对比下来你会发现双主热备最核心的爽点是解决“备机闲得慌”的问题。尤其在负载均衡层和数据库层两台机器同时跑资源利用率比传统主备高一倍。1.3 什么时候选双主热备什么时候别选双主热备听着热闹但它不是万能药。根据我这些年做架构选型的经验适合上双主热备的业务有几个共同特征。适合的场景单机写性能还有明显余量也就是说当前这台机器的CPU、磁盘、内存并没有被压到天花板业务对切换时间有比较明确的要求比如希望30秒以内恢复团队愿意为高可用付出运维复杂度能接受每天巡检复制状态和VIP状态。不适合的场景大致有三类。第一类是单机写能力已经到瓶颈双主热备不但解决不了扩展问题反而会因为复制开销和双写冲突把情况搞得更糟第二类是业务要求两边同时高强度写入且没有做分片和冲突避让的设计这种诉求更适合分布式数据库比如TiDB、OceanBase这类架构或者做基于业务键的路由分片第三类是团队没有专职的DBA或运维搭好之后没人持续盯那还不如用云厂商托管的高可用方案。还有一个容易被忽略的前提双主热备解决的是高可用问题不是数据备份问题。很多人以为两台机器互相复制数据备份就可以省了这是大忌。误删数据的UPDATE和DELETE会原封不动地在两条链路上跑一遍备份才是最后一道保险。2. 双主热备的核心原理节点、VIP、复制和脑裂2.1 “热备”靠什么实现VIP漂移与健康检查双主热备里最常听到的词是VIP。VIP是虚拟IP客户端只通过这个IP访问服务物理机上哪个节点持有VIP请求就打到哪台机器。Keepalived通过VRRP协议完成VIP的漂移。两台机器之间周期性发送心跳报文正常情况下VIP1挂在节点AVIP2挂在节点B。一旦节点A的心跳消失节点B会在几个报文周期内宣布接管VIP1把流量补位过来。这个“几秒”其实就是高可用切换的窗口。但VRRP只能感知“Keepalived进程活着”不能感知“业务活着”。所以生产环境一定要配健康检查脚本。我见过无数案例是Keepalived没死但后端的Nginx或者MySQL早就挂了VIP还牢牢挂在那台死掉的机器上。正确做法是在track_script里写检测逻辑业务进程挂了就立刻停掉本机的keepalived让VIP漂移到对端。检测脚本的粒度也很重要。你可以检测TCP端口比如nc -z 127.0.0.1 3306也可以做业务级探测比如用mysqladmin ping带密码检查数据库是否真的能响应。检测越贴近业务真实状态切换判断就越准。成本当然也高一点但这点成本完全值得。2.2 数据库双主的数据通道主主复制的机理数据库层的双主热备核心就是两套MySQL互相把对方当成自己的从库。MySQL主从复制的基础流程是主库把变更写入binlog从库的IO线程从主库拉取binlog事件先落到本地relay log再由SQL线程串行重放。双主场景下A把变更写入自己的binlogB拉过来重放B的变更也会写进自己的binlogA再拉过去重放。两台机器互为对方的中继形成一个环状复制。这里有个关键参数log_slave_updatesON。如果不开B从A拉取并重放的数据不会记到B自己的binlog里后续A也就无法再从B那吃到这条变更双主链路直接断了一半。还有一个所有新手都会担心的点循环复制会不会无限来回传MySQL内部其实做了一个保护机制binlog事件里会携带源server_id如果当前节点发现某个事件的server_id和自己的server_id相同默认就不会再重放。所以只要两台机器的server_id不同复制链路是稳定的不会产生乒乓效应。但双主复制有一个绕不开的痛点两边同时写同一行数据必然冲突。比如A和B同时执行一条UPDATE user SET balancebalance-100MySQL无法保证最终结果是哪个状态。所以严谨的落地方式是双主复制解决了容灾接管但业务写入流量在任意时刻只打到其中一个VIP上另一台承载读流量或纯待命。真正要双边同时写必须引入自定义的路由和冲突规避机制比如根据业务键哈希分片而不是在MySQL层裸奔。2.3 脑裂与仲裁双主系统绕不开的槛脑裂是分布式系统里最危险的故障没有之一。双主场景下的脑裂简单说就是两台机器之间的心跳断了双方都认为对方死了于是同时抢占同一个VIP同时以主节点身份提供写服务。想象一下这个画面两个客服同时接到一张工单都以为对方请了假于是都开始处理同一个客户的需求最后对客户给出的答复完全不一样。数据库那边的后果比这更严重A写了一条记录B也写了一条冲突记录两边数据开始分叉。防脑裂最常用的手段是仲裁和隔离。仲裁指的是引入一个第三方节点投票心跳断了以后必须问仲裁节点“我到底该不该接管”隔离Fencing则是在确认节点故障后主动把它踢出集群比如Webhook远程关机、开关IPMI、通过交换机禁用端口。Keepalived本身只依靠VRRP报文确认对方状态在没有仲裁机制的环境里严格来说存在双主同时持VIP的风险。我实际部署时的额外保险是加心跳检测脚本不仅检测本机业务还会尝试探测对端业务和网关连通性。如果网络分区导致两边都活着脚本配合防火墙规则或者仲裁脚本把其中一台的VIP摘掉。对于MySQL双主倒是有一个好消息如果两边同时持有VIP但只有一边在写入另一边只做查询那数据是不会分叉的。所以我一直强调生产环境要让双主始终处于“一写一读等待”的运行时状态真正把双写能力备而不用。这能在不引入大规模改造的前提下极大降低脑裂的损失面。3. 实操记录MySQL双主热备从0到1这章我用两台CentOS上的MySQL 8.0作为示例环境和配置都是我在生产环境验证过的。整体步骤是初始化参数、备份数据、建立双主复制链路、开启半同步、验证切换回切。3.1 服务器规划与关键参数初始化两台机器的角色我定义为db1和db2业务VIP1挂在db1VIP2挂在db2。MySQL复制用户单独创建不跟业务账号混用。第一步是修改my.cnf两台机器的server-id必须不同这是整个复制体系的身份证。先看db1的my.cnf核心配置[mysqld] server-id 1 log-bin mysql-bin binlog-format ROW gtid_mode ON enforce-gtid-consistency ON log_slave_updates ON relay-log relay-log auto_increment_increment 2 auto_increment_offset 1 sync_binlog 1 innodb_flush_log_at_trx_commit 1db2的参数只有两处不同server-id 2auto_increment_offset 2。auto_increment_increment2表示每次自增的步长是2offset分别是1和2效果是db1的自增主键永远是奇数db2的永远是偶数从源头基本避免两边同时插入时主键撞车。sync_binlog1和innodb_flush_log_at_trx_commit1这两个参数强烈建议开着它们保证每次事务提交时binlog和redo log都刷到磁盘不会在掉电时出现日志丢失。代价是性能有一些下降但对高可用场景来说数据安全优先。GTID模式强烈建议开启。GTID给每个事务分配一个全局唯一ID主从识别事务进度靠ID不靠文件名和偏移量后续切换和回切都很省事。enforce-gtid-consistencyON是为了防止执行GTID不支持的语句。3.2 初始化数据双主复制必须先保证两台机器初始数据一致。如果数据量小mysqldump可以应付数据量大或者业务不能停太久的场景我用的是Percona XtraBackup做物理备份恢复速度比逻辑备份快一个量级。以数据量300G为例子用mysqldump会非常痛苦XtraBackup的操作是先全量备份db1再恢复到db2xtrabackup --backup --target-dir/backup/mysql_backup --host127.0.0.1 --userbackup_user --passwordyour_pass xtrabackup --prepare --target-dir/backup/mysql_backup xtrabackup --move-back --target-dir/backup/mysql_backup --datadir/data/mysql chown -R mysql:mysql /data/mysql--prepare这一步很重要它是把备份期间产生的redo日志回放让备份数据达到一致性状态。很多第一次用xtrabackup的人会跳过这步直接move-back结果MySQL起不来报各种数据不一致错误。如果数据量在几十G量级也可以直接用mysqldump导出再导入。操作的时候注意加上--single-transaction --set-gtid-purgedON确保导出的数据带有一致的GTID集合方便后续change master直接用GTID自动定位。3.3 建立双主复制链路先在db1上创建复制账号CREATE USER repl% IDENTIFIED BY StrongPass123; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO repl%; FLUSH PRIVILEGES;db2上执行相同创建只是host、密码要对应好。然后分别在对方上配置change masterCHANGE MASTER TO MASTER_HOSTdb1_ip, MASTER_USERrepl, MASTER_PASSWORDStrongPass123, MASTER_AUTO_POSITION 1; START SLAVE;db1上反向把db2指定为master执行同样的操作。配置完成后用SHOW SLAVE STATUS\G检查状态重点看Slave_IO_Running和Slave_SQL_Running是不是都显示Yes。两条链路都起来后可以做一次最基础的数据验证在db1建一张测试表并插入一条记录去db2查询能看到再在db2插入一条回db1查询也能看到。看到这条链路真正闭环双主复制的骨架就算立起来了。3.4 半同步与切换演练普通异步复制存在一个致命窗口主库提交事务后从库还没来得及拉取主库就崩溃了这条事务会丢。为了把丢数据的窗口压到接近零我会启用半同步复制。MySQL 8.0的启用方式INSTALL PLUGIN rpl_semi_sync_master SONAME semisync_master.so; INSTALL PLUGIN rpl_semi_sync_slave SONAME semisync_slave.so; SET GLOBAL rpl_semi_sync_master_enabled 1; SET GLOBAL rpl_semi_sync_slave_enabled 1;这里有个细节双主下两台机器既是master又是slave所以两个插件和两个开关都要在db1、db2上同时设置。半同步的原理是主库提交事务时必须等待一台从库确认收到binlog确认方式通过网络ACK完成。如果从库长时间不响应半同步会自动降级为异步保证主库不卡死。切换演练是检验架构的唯一标准。正常状态下VIP1在db1、VIP2在db2。演练第一步模拟db1宕机停掉db1的MySQL和keepalived观察VIP1是否在几秒内出现在db2上业务连接VIP1是否一切正常第二步修复db1启动MySQL后先不要切回VIP让它作为从库追平db2的GTID集合确认Seconds_Behind_Master0之后再让VIP1回到db1。回切这一步是事故高发区很多人贪快MySQL刚起来就急着切VIP结果db1的GTID落后db2几条事务读到的是旧数据。正确的回切顺序永远是先追平再切流量。4. 实操记录Keepalived双VIP双主高可用数据库之外双主热备的另一大主战场是接入层。以Nginx、HAProxy这类负载均衡组件为主两台机器同时承载流量通过双VIP互备解决单点问题。4.1 设计思路为什么要双VIP而不是单VIP很多人做Nginx高可用只做一套VIP挂一台机器另一台纯备机。这种方式传统、可靠但备机真的在浪费机器。双VIP思路是VIP1平时挂在节点AVIP2平时挂在节点BA挂了VIP1漂到BB挂了VIP2漂到A任意一台宕机都不会整体失去服务能力。这套方案的优势很直接两台机器的硬件资源都被用到业务可以通过DNS轮询或者前端入口把流量分散到两个VIP上。即使某个VIP漂移了另一台机器也只是临时多扛一倍流量。前提是你得对机器的容量做预算别让单台机器平时就跑到80%以上。Keepalived实现双VIP的方式是配置两个VRRP实例。每个实例用不同的virtual_router_id绑定一个VIP各自设置主备关系和抢占策略。两个实例互相错开形成你中有我的互备拓扑。4.2 Keepalived配置示例以Nginx双机为例节点A的keepalived.conf关键配置global_defs { router_id nginx_a } vrrp_script chk_nginx { script /etc/keepalived/chk_nginx.sh interval 2 fall 2 rise 2 } vrrp_instance VI_1 { state MASTER interface eth0 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 192.168.10.100/24 } track_script { chk_nginx } } vrrp_instance VI_2 { state BACKUP interface eth0 virtual_router_id 52 priority 90 advert_int 1 authentication { auth_type PASS auth_pass 2222 } virtual_ipaddress { 192.168.10.101/24 } track_script { chk_nginx } }节点B的配置要特别注意两点把VI_1的state改成BACKUP、优先级改成90把VI_2的state改成MASTER、优先级改成100。这样默认情况下VIP1落在AVIP2落在B。此时还有一个策略问题需要考虑A恢复后要不要自动抢占VIP1我强烈建议在故障恢复率高的场景里关掉抢占。方法是把实例的state都配成BACKUP并加上nopreempt参数。这样A宕机后VIP1漂到BA恢复后不会自动抢VIP1避免白天上班时间段因为自动回切引发一次新的抖动。等运维确认数据一致后再手动漂移VIP回A。健康检查脚本chk_nginx.sh的内容非常简单我实际用的是#!/bin/bash if ! [ -f /var/run/nginx.pid ] || ! pgrep nginx /dev/null 21; then systemctl stop keepalived fi核心逻辑是如果Nginx进程不在就停掉本机keepalived让VIP自动漂走到对端。这个脚本的触发间隔、失败重试次数都可以按业务容忍度调整一般2-3秒检查一次比较合适。4.3 配合Nginx和HAProxy的落地两台Nginx都正常启动虚拟IP通过keepalived绑定到网卡上。Nginx的server监听地址可以写成listen 0.0.0.0:80这样VIP漂移到哪台流量就由哪台Nginx接管两台机器的nginx.conf保持一致。如果要挡后端业务的高可用Nginx里通常配置一组upstream后端应用节点upstream app_servers { server 192.168.10.10:8080 max_fails3 fail_timeout10s; server 192.168.10.11:8080 max_fails3 fail_timeout10s; keepalive 32; } server { listen 0.0.0.0:80; location / { proxy_pass http://app_servers; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这套方案的真正常见组合其实是“HAProxy Keepalived双VIP”。HAProxy负责四层和七层负载均衡Keepalived负责VIP漂移。两个VIP平时都可以对外提供入口配合DNS轮询把流量分散到两台故障时单台也能撑住全部入口流量。4.4 故障模拟与验证配置完成后需要做几组验证才算真正上线。第一步静态检查在A上执行ip addr show | grep 192.168.10.100确认VIP在预期节点在B上执行同样的命令看到192.168.10.101。第二步动态检查从客户端持续访问A上的VIP然后执行systemctl stop keepalived观察访问中断时间是否只有几秒同时确认VIP1已经出现在B上。第三步恢复验证重新启动A的keepalived观察VIP是否按你配置的抢占策略回到A或者保持不变。第四步也是最容易被忽略的一步拔网线模拟心跳中断检查是否出现两台机器同时持有同一个VIP的脑裂情况。应对的办法是依赖上一章提到的track_script和外部防火墙隔离有条件的话上仲裁节点。5. 常见问题与排查技巧实录5.1 MySQL双主复制的典型故障排查实际运维里最常遇到的MySQL复制故障是IO线程报错。Slave_IO_Running: Connecting基本跑不出三个原因网络不通、账号权限不对、主库的binlog位置或GTID集合对不上。GTID模式下如果发现备机一直报Last_IO_Errno 1236说明备机请求的GTID事务在主库binlog里已经找不到了通常是binlog过期清理导致。千万别想着手工改gtid_purged蒙混过关最稳妥的方案是重新做一次全量备份恢复再重新change master。这个教训我踩过一次手工拼接GTID集合看起来追上来了实际数据分析时发现漏了事务后来回滚数据花费了很长时间。SQL线程报错1062主键冲突或者1032找不到记录时第一反应应该是去看后端业务有没有两边写同一条数据而不是想着通过跳过事务来“修复”。跳过事务在GTID模式下不是一个好操作它会留下空洞后续切换主库时可能带来二次问题。正确做法是比对双方数据找出冲突源头修改业务路由再重建复制链路。复制延迟也需要盯。Seconds_Behind_Master如果持续上涨优先检查是否有大事务比如一次性UPDATE几十万行、DDL操作还有主库的慢SQL。MySQL 8.0可以开启并行复制改善重放速度但根本问题还是大事务要拆分、慢SQL要优化。5.2 Keepalived双主的常见故障排查Keepalived最常见的问题是“VIP不漂移”。95%的情况是健康检查脚本没配好或者脚本权限不对。脚本要能被keepalived进程读取执行权限至少要是755。另一个隐蔽问题是脚本里用了MySQL客户端但MySQL客户端不在keepalived进程的环境变量PATH里导致脚本每次执行都报command not found健康检查形同虚设。我建议脚本里一律写命令绝对路径比如/usr/local/mysql/bin/mysqladmin。第二个常见问题是“两个节点同时持有同一个VIP”。这是严重事故处理方法是立刻通过防火墙或交换机强制隔离其中一台的流量然后排查心跳网络。生产环境一定要把心跳网络单独用一条直连或者独立VLAN不要跟业务网共用否则业务流量一大就会导致心跳报文延迟丢失诱发脑裂。第三个问题是“VIP频繁来回抖动”。如果把A和B都配成了MASTER或者没有配置nopreempt故障恢复后VIP会在A和B之间来回抢占业务连接反复断。解决方法是明确抢占策略生产环境我通常配BACKUP加nopreempt把回切动作留给人工验证后再操作。5.3 常见问题速查表故障现象可能原因处理建议Slave_IO_Running为Connecting网络不通、账号错误、GTID集合不匹配排查网络连通性检查复制账号权限必要时重建链路Last_IO_Errno 1236binlog已被清理重新全量初始化数据再change masterSQL线程1062或1032双写冲突或数据不一致先停写比对两边数据修复业务路由不要盲目跳事务复制延迟持续上涨大事务、慢SQL、主库压力过大拆分大事务、优化慢SQL、考虑并行复制两个节点同时持有同一VIP心跳失败脑裂立即做网络隔离排查心跳网络补充仲裁机制VIP不漂移健康检查脚本无效或权限错误检查track_script配置、脚本路径、执行权限故障恢复后VIP来回抖动抢占策略配置不当配置BACKUPnopreempt手动控制回切双主两台都能写入但数据开始分叉业务没有做写路由避让改成单一VIP写入另一台只读或待命5.4 几条刻进骨子里的经验教训第一写流量不要在双主两边同时打。我给一个内部系统搭过双主复制业务方为了“充分利用资源”把一部分写请求路由到了备VIP结果第二天就出现了唯一键冲突日志刷出来的全是报错。双主热备的定位是故障接管不是双主双写。真想双写至少要做分片和冲突避让。第二回切比故障切换更容易出事。故障发生的时候大家精神高度紧张反而会谨慎操作恢复之后容易掉以轻心MySQL刚起来就把VIP切回去结果因为GTID落后导致数据读到旧值。所有回切动作之前都必须确认备份节点已经完全追平主节点Seconds_Behind_Master0GTID集合一致。这个检查没有例外。第三永远保留独立备份。两台机器尽管互相同步但误删数据会同时影响两边。我见过一个团队没有做任何备份靠双主复制当备份手动执行了一条没有WHERE条件的DELETE两边一起删光。从那以后我无论怎么推崇双主热备最后都会补一句一定要有独立的定时备份且备份要定期恢复演练。我个人的体会是双主热备是一个性价比很高的高可用方案但它不是一套“配上就完事”的静态架构而是一套需要长期维护、定期演练的动态系统。每一次故障切换、每一次回切都会暴露新的细节问题。把这些细节一个个补齐你的双主热备才真正值得被信任。后面有机会的话我再把同城双活、跨机房多活的路线单独整理出来那又是一套不一样的逻辑。
阅读完成 · 觉得有帮助?