做后端或者DBA的同学多少都听过MyCat。它本身是一个数据库中间件前面挡着应用后面管理一堆MySQL实例对外伪装成一个逻辑库让业务层只跟一个“假数据库”打交道。这篇文章主要聊双主双从高可用读写分离架构属于实战系列的第二章。第一章通常已经把MyCat基础环境装好、把逻辑库和物理节点打通了这一章要把它升级成能扛故障的架构两台主库互相同步各自再带一台从库既能做读写分离又能在主库故障时自动切换写节点。这套方案在中小型电商、运营后台、SAAS系统里非常常见也比较适合已经会简单读写分离、但还没上过高可用方案的团队。全文我会把架构思路、MySQL双主双从配置、MyCat核心参数、故障切换细节、以及我实际踩过的坑都拆开讲尽量做到可以直接照着抄。1. 架构设计与核心思路1.1 双主双从到底是个什么拓扑双主双从字面意思就是两个主库加两个从库但真正落地的时候它不是简单的四台MySQL而是一套对称的复制关系M1 和 M2 互为主从M1 的写入会同步到 M2M2 的写入也会同步到 M1。S1 作为 M1 的从库专门承接读流量。S2 作为 M2 的从库专门承接读流量。应用只连接 MyCat由 MyCat 在四个节点之间做路由。反过来看这就是两组“一主一从”通过双主复制串联起来。M1 挂了M2 还能顶写S1 挂了读流量还可以打到 S2如果 MyCat 配置得当M1 挂了之后所有读写都能继续不会因为单台数据库宕机导致业务中断。不过要强调一点双主不是让两个库并发写同一份数据。实际生产里绝大多数时间是 M1 承担所有写流量M2 作为热备写节点只有 M1 故障或主动切换时才接管写。M2 的写能力是“被保留下来的”不是“随时都在用的”。否则双主同时写同一张表主键冲突和数据错乱会很快找上门。1.2 为什么高可用架构常选双主而不是单主单主加多从是很多人最开始接触的高可用形态一台主库多台从库主挂了可以由脚本或MHA之类工具提升某台从库为主。问题是提升过程涉及修改应用连接、处理复制日志补传、更新VIP切换如果这套动作没有预演很容易在故障现场手忙脚乱。而 MyCat 这种中间件做高可用天然依赖后端连接池里有多个可用的 writeHost。如果只有一个主库MyCat 想切也没地方切。双主架构的意义就是给 MyCat 提供两个可以写的候选节点MyCat 一旦探测到当前写节点心跳失败可以把写流量切到另一台主库。这个动作对应用是透明的应用看到的还是同一个逻辑库。另外双主在数据新鲜度上也比“从库提升为主库”更平滑。从库提升为主库时通常需要等待从库把所有relay log应用完如果主库故障瞬间丢失一部分最新数据还要做数据补偿。双主之间是实时同步故障切换后数据缺失窗口要小很多。当然这不是零丢失真要彻底解决丢数据还得上半同步复制或其它方案后面我会单独讲。1.3 读写分离之后流量怎么走在 MyCat 的配置里读流量和写流量的路由逻辑不一样。我们先把双主双从的流量路径理清楚写SQLMyCat 根据 dataHost 里 writeHost 的优先级默认选第一个可用 writeHost也就是 M1。M1 正常时M2 虽然也是 writeHost但不承担写流量。读SQLMyCat 根据 balance 参数决定分发策略。配置成只读从库后普通查询会从 S1 和 S2 两个从库中选一个执行。事务内的读MyCat 为了保证同一事务的读己之写事务内的普通查询会强制走当前事务绑定的写节点不会跨节点。所以最终看到的效果就是一条查询进来很可能落到 S1 或 S2 上而一条插入或更新通常落在 M1 上。如果 M1 故障写入切换到 M2S2 继续承担读S1 如果不做处理则可能暂离读池。2. 环境准备与基础配置2.1 节点规划与版本选型先讲一个我在实际项目里验证过的节点规划大家可以根据自己的机房条件换IP但结构可以不变。我建议用四台MySQL实例加一台MyCat节点。MySQL用5.7版本原因有两点一是MyCat 1.6系列对MySQL 5.7的兼容性非常成熟二是5.7的并行复制在从库延迟控制上表现比较稳。如果你想用MySQL 8.0也不是不行但要注意MyCat连接MySQL 8.0时的认证插件兼容问题需要额外调整。角色节点名IP:端口说明主库1M1192.168.10.11:3306主写节点日常承担所有写入主库2M2192.168.10.12:3306备写节点M1故障时接管写入从库1S1192.168.10.13:3306M1的从库承担读流量从库2S2192.168.10.14:3306M2的从库承担读流量中间件MyCat192.168.10.10:8066应用连接入口四台MySQL需要提前装好并确认3306端口可访问。MyCat用户建议单独创建权限不要给太高能执行业务所需DML即可。复制账号也不要直接拿业务账号复用权限尽量收敛。2.2 四台MySQL的my.cnf参数双主双从最核心的配置文件就是MySQL的my.cnf。重点不是把每一项都写满而是把复制相关的参数设置正确。下面是我常用的基础模板[mysqld] server-id101 log-binmysql-bin binlog_formatrow sync_binlog1 log_slave_updatesON relay_logrelay-bin gtid_modeON enforce_gtid_consistencyON节点之间唯一的区别是 server-id。M1 用 101M2 用 102S1 用 103S2 用 104确保全局唯一。GTID模式顺手开着后面做故障恢复和切换时能省很多事不用到处找binlog文件和Position。如果业务不想用GTID也可以退回到传统的MASTER_LOG_FILE和MASTER_LOG_POS方式。但从维护角度看GTID更省心我建议第一次搭建就直接用GTID。还有两个参数需要重点说自增值的偏移量。双主架构里如果两台主库都允许写必须避免自增主键冲突。推荐在M1和M2上分别设置M1auto_increment_offset1 auto_increment_increment2M2auto_increment_offset2 auto_increment_increment2这样M1生成的自增ID是1、3、5、7M2生成的是2、4、6、8双主同时写入也不会撞主键。虽然前面说平时M2不写但故障切换时M2会接管这个配置能在切换后继续写入而不冲突。S1和S2作为从库建议额外开启read_onlyON并且只对复制账号的写入放行。M1和M2不建议开启read_only否则MyCat自动切换后M2会拒绝写SQL。2.3 MyCat相关配置文件准备MyCat安装好之后核心配置都在conf目录下。第一次部署时我习惯先备份原始文件然后再改。主要关注这三个文件server.xml定义MyCat对外暴露的用户、密码、逻辑库名。schema.xml定义逻辑库映射到哪些物理数据节点以及读写分离和故障切换规则。rule.xml定义分片规则但做双主双从读写分离时不涉及分片可以先不关心。server.xml里的用户配置大概长这样user nameapp_user property namepasswordApp2024/property property nameschemasapp_db/property property namereadOnlyfalse/property /user这个用户是应用连MyCat用的跟后端的MySQL账号不是一回事。MyCat会用这个用户把SQL转发到后端的真实MySQL所以后端也要创建对应的账号。3. 双主双从复制配置实操3.1 双主互为主从的搭建首先在M1和M2上创建复制账号。MySQL 5.7示例GRANT REPLICATION SLAVE ON *.* TO repl192.168.10.% IDENTIFIED BY Repl2024; FLUSH PRIVILEGES;然后在M1上执行CHANGE MASTER TO MASTER_HOST192.168.10.12, MASTER_PORT3306, MASTER_USERrepl, MASTER_PASSWORDRepl2024, MASTER_AUTO_POSITION1; START SLAVE;M2上执行CHANGE MASTER TO MASTER_HOST192.168.10.11, MASTER_PORT3306, MASTER_USERrepl, MASTER_PASSWORDRepl2024, MASTER_AUTO_POSITION1; START SLAVE;用GTID时要注意MASTER_AUTO_POSITION1意味着两端会自动从已有的GTID位置开始同步不需要手动找binlog文件和POS比传统方式方便得多。配置完分别在两边查看复制状态SHOW SLAVE STATUS\G重点关注三个字段Slave_IO_Running 必须为 Yes。Slave_SQL_Running 必须为 Yes。Seconds_Behind_Master 为 0。如果IO线程没起来先看网络和复制账号权限尤其是MySQL 8.0的认证插件可能影响复制连接。如果SQL线程报错通常是有重复主键或SQL执行失败这个后面在排查部分详细讲。3.2 从库挂到各自的主库M1和M2互相同步完成之后把S1挂到M1下把S2挂到M2下。逻辑跟双主复制的后半段一样只是方向变简单了。在S1上CHANGE MASTER TO MASTER_HOST192.168.10.11, MASTER_PORT3306, MASTER_USERrepl, MASTER_PASSWORDRepl2024, MASTER_AUTO_POSITION1; START SLAVE;在S2上把MASTER_HOST换成192.168.10.12即可。有一点容易被忽略S1和S2其实可以额外开启log_bin和log_slave_updates。如果你希望S1将来能晋升为主库或者想从从库再挂一个低层级的从库这两个参数必须开。如果只是纯读库开不开影响不大但开了之后监控和数据补偿也更灵活。全部配置完成后总共是四条复制链路M1 - M2M2 - M1M1 - S1M2 - S2这是一张典型的双向复制加单向复制的拓扑。只要server-id不重复MySQL不会在这四条链路里产生无限循环。3.3 验证复制数据一致性别以为复制线程都是Yes就万事大吉。双主架构里最怕的是数据已经不一致了复制线程还傻乎乎的显示Yes。所以必须做一个基础验证。在M1上创建一个测试库和测试表CREATE DATABASE IF NOT EXISTS app_db; USE app_db; CREATE TABLE IF NOT EXISTS t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO t_user(name) VALUES(test1),(test2),(test3);然后分别去M2、S1、S2上查询这张表确保都能看到同样的三条数据。再在M2上插入一条数据回M1查一下确认反向复制也没问题。有条件的话用pt-table-checksum做一次全量校验它能对比主库和从库的每张表数据是否一致发现差异后可以用pt-table-sync修复。后两个工具虽然有点老但在MySQL 5.7场景下非常稳。3.4 复制状态监控脚本复制链路一多靠人肉去查就不可靠了。我一般会在每台MySQL服务器上放一个最简单的脚本定时检查复制状态#!/bin/bash mysql -uroot -pYourPass -e SHOW SLAVE STATUS\G | grep -E Slave_IO_Running|Slave_SQL_Running|Seconds_Behind_Master然后配合crontab每分钟执行把结果写到日志文件里或者直接接到告警平台。集中的监控方案可以用Prometheus加mysqld_exporter但基础的脚本至少能保证故障发生第一时间被感知而不是等业务反馈才知道。4. 读写分离与高可用实现细节4.1 schema.xml双主双从核心配置下面是一份可用的schema.xml核心片段我这版配置的精髓在dataHost内部先看代码?xml version1.0? !DOCTYPE mycat:schema SYSTEM schema.dtd mycat:schema xmlns:mycathttp://io.mycat/ schema nameapp_db checkSQLschemafalse sqlMaxLimit100 table namet_user dataNodedn1 / /schema dataNode namedn1 dataHostdtHost databaseapp_db / dataHost namedtHost maxCon500 minCon10 balance3 writeType0 dbTypemysql dbDrivernative switchType1 heartbeatselect user()/heartbeat writeHost hostM1 url192.168.10.11:3306 usermycat passwordMycat2024 readHost hostS1 url192.168.10.13:3306 usermycat passwordMycat2024 weight1 / /writeHost writeHost hostM2 url192.168.10.12:3306 usermycat passwordMycat2024 readHost hostS2 url192.168.10.14:3306 usermycat passwordMycat2024 weight1 / /writeHost /dataHost /mycat:schema这份配置逻辑上很清楚M1 和 S1 绑在一起M2 和 S2 绑在一起。每个from start writeHost都可以挂多个readHost。当MyCat需要读流量时会根据balance参数选择后端节点。4.2 balance和writeType参数逐项解读参数选型是双主双从架构的重头戏很多人在这上面栽过跟头。balance决定读流量的分发方式balance0不开启读写分离所有读操作都发给当前可用的 writeHost。这样从库就完全吃不到读流量了。balance1读请求在 writeHost 和 readHost 之间分发但会避开故障节点。balance2所有读请求随机分发到 writeHost 和 readHost 上主库和从库都承担读。balance3所有读请求只发到 readHost也就是只发到从库主库不承担读。双主双从的标准形态下我更推荐 balance3。因为读流量应该尽可能从从库走把主库的IO能力省下来承担写入和复制。当然balance3也有代价如果 S1 和 S2 同时挂了读请求会全部失败而写节点仍然健康。这种情况需要有预案比如临时把balance改成0或者尽快恢复从库。writeType则负责写流量的分配writeType0只把写请求发到第一个 writeHost也就是M1。M1故障后自动切换到M2。writeType1随机把写请求发到所有 writeHost也就是M1和M2都会承担写。这种模式一般不建议除非你有非常完善的数据冲突处理方案。双主双从高可用场景下writeType保持0是最稳的让M1享受“写全部”的待遇M2就是备用写节点。4.3 自动故障切换的实际过程MyCat的故障切换依赖心跳机制。dataHost里配置的heartbeatselect user()/heartbeat会被MyCat周期性地发给每个writeHost。一旦M1连续多次心跳失败MyCat会把M1标记为不可用并把写请求自动转移到下一个可用的writeHost也就是M2。这个切换过程是秒级的但注意一点MyCat只负责切换连接路由不会去改MySQL的复制关系和read_only状态。如果M2被设置成了read_only哪怕MyCat把写流量切过去M2也会拒绝执行写入。所以双主节点必须保证可写最好在账号权限层限制应用直连而不是靠read_only做保护。切换完成后应用侧已经建立的连接可能会收到“通信链路故障”之类的异常。这是正常的因为旧连接指向的M1可能已经不可达。应用连接池需要配置连接重试和可用性检查比如Druid连接池开启testOnBorrowtrue、testWhileIdletrue。HikariCP把connection-test-query设置为SELECT 1并调低max-lifetime到适当值。这样一来故障切换时应用只会出现短暂的报错和重试而不是长时间卡死。4.4 事务内读的一致性处理读写分离架构天然会面临一个问题用户刚写入一条记录紧接着去查结果查询落到了从库而从库复制还没跟上导致读不到刚插入的数据。MyCat对这种情况有应对事务内的读会走当前事务已经绑定的连接。也就是说只要你在同一个事务里先写了后查了查询就会落到写节点上保证读己之写。但如果你不在事务里普通查询默认走从库就可能有主从延迟问题。对于必须强一致的查询可以用MyCat的强制主库路由注解让这条SQL走写节点绕过从库延迟。以1.6系列为例常用写法是/*!mycat:db_typemaster*/ SELECT * FROM t_user WHERE id 100;当然不同MyCat版本的注解格式略有差异具体以你安装版本支持的语法为准。在写业务代码时建议只对确实不能接受延迟的少量查询加这个注解不要全表都加否则读写分离就失去意义了。5. 常见问题与排查技巧实录5.1 主键冲突与自增偏移设置双主架构最常见的问题就是主键冲突。我在第一次搭双主时因为没改auto_increment参数结果M1和M2同时在业务低峰期被双写很快就出现Duplicate entry报错。解决办法就是前面讲的M1: auto_increment_offset1, auto_increment_increment2M2: auto_increment_offset2, auto_increment_increment2这个方案只能保证MySQL自动生成的主键不冲突前提是两个主库都生成不同奇偶性的ID。如果你用的是应用层分布式ID或者UUID主键也可以不需要这套配置。另外双主同时写同一张表仍然可能因为ID相同但数据不同而相互覆盖所以业务上还是要避免同一行数据在两个主库并发更新。5.2 复制中断Got fatal error 1236 / 1062双主双从复制链路一旦中断第一件事不是急着修而是看Last_SQL_Errno和Last_IO_Errno。最常见的是1062主键冲突通常是从库已经有相同主键的数据复制过程中又插入了重复记录。解决思路是先停slave。在从库上分析冲突记录确认主库数据为准还是从库数据为准。用pt-table-sync或者手动删除冲突记录。再启动slave。如果数据差异比较大与其一遍遍清理不如直接重建从库。重建的方法很简单从主库先做一次全量备份恢复到从库然后再重新change master。双主架构下重建从库比传统架构还要小心因为主库本身可能也在通过复制接收数据备份前最好确认没有瞬时的写入。另外一个常见错误是1236表示从库找不到主库发送的binlog。这种情况大多是因为binlog被purge了或者从库停太久主库binlog已经清理掉。如果是GTID模式可以用RETURNED_GTID_SET做补拉但最稳妥的也是重建从库。5.3 自动切换后应用连接失效问题M1故障后MyCat把写流量切到M2但应用层可能瞬间冒出大量连接异常。这不是MyCat的错而是应用连接池还在使用与旧节点的连接。处理办法有几层应用连接池要开启空闲连接检测避免拿到已经失效的连接。数据库账号权限要允许MyCat快速重新创建连接不能因为连接数限制导致切换后无法新建连接。应用层要做好重试机制遇到连接异常就重试一两次而不是直接把错误抛给用户。我见过有团队把高可用完全交给中间件应用侧不做任何容错结果切换时SQL大量失败最后花了一个多小时才恢复。高可用不是一个节点的事情而是要应用、中间件、数据库三层配合。5.4 主从延迟导致读脏数据即使复制链路正常从库也可能因为业务高峰期写入量过大出现延迟。延迟期间从库上的数据不是最新查询就会读到旧值。要降低这个问题的影响可以从几个方向入手从库调大并行复制参数MySQL 5.7里可以设置slave_parallel_workers4并配合slave_parallel_typeLOGICAL_CLOCK。主从之间使用半同步复制至少保证一个从库收到binlog后才提交事务能显著降低故障切换时的数据丢失风险也能让从库延迟更可控。MyCat的 heartbeat 语句改成show slave status并配置延迟阈值参数当从库延迟超过阈值时暂时不把读请求分给它。业务层对极端要求一致性的查询直接强制走主库。生产环境里没有绝对的实时只有可接受的延迟窗口。把主从延迟从秒级压到毫秒级再配合强制读主基本能覆盖99%的场景。6. 压测与长期稳定运行的经验6.1 读写分离效果压测配置完成后我建议先用压测工具验证整个链路确实在按预期工作。可以用sysbench它的oltp_read_write脚本刚好模拟读写混合场景sysbench /usr/share/sysbench/oltp_read_write.lua \ --mysql-host192.168.10.10 \ --mysql-port8066 \ --mysql-userapp_user \ --mysql-passwordApp2024 \ --mysql-dbapp_db \ --tables8 \ --table_size100000 \ --threads32 \ --report-interval5 \ --time300 run压测过程中分别登录M1、M2、S1、S2执行SHOW PROCESSLIST;观察查询和写连接到底落在哪些节点。如果配置正确M1上应该有写和部分事务内读S1和S2上应该有大量普通查询。如果所有查询都压在M1上说明balance参数配成了0或者SQL没有走MyCat。6.2 配套监控和切换演练双主双从不是配完就结束的架构后续运维比配置更重要。我强烈建议至少在监控里覆盖以下几项四个节点的复制状态包括IO线程、SQL线程、延迟秒数。MyCat的运行日志和连接池状态。MySQL的关键指标QPS、TPS、连接数、临时表、慢查询。应用侧的读写耗时和错误率。监控收集到数据只是第一步更重要的是定期的切换演练。我每季度会在测试环境模拟一次主库宕机直接kill -9M1的mysqld进程然后看MyCat是否在预期时间内把写流量切到M2。演练时还要检查M2上的binlog是否完整S2能不能正常承接读流量应用侧连接池是否能自动恢复。这个过程能帮团队把故障切换的肌肉记忆练出来真出事时不用临时翻文档。另外要明确一点双主双从不等于备份。复制只能保证数据有副本如果一条误删除的SQL在主库上执行了复制也会把这条删除同步到所有节点。所以每天的全量备份和binlog增量备份仍然必须有恢复流程也要定期验证。6.3 什么时候要考虑演进成分片架构双主双从适合单库容量可控、写并发不算特别高的场景。如果以后业务增长出现以下信号就要考虑进一步演进单库存储快达到磁盘瓶颈。单个表的写入QPS长期超过几千M1的IO已经顶不住。单条SQL查询因为数据量过大越来越慢即使加了索引也无法改善。这时候可以引入MyCat的分片能力把数据按某一列拆到多个数据节点。也可以考虑换用更成熟的分布式数据库。双主双从可以看作是从单库到分库分表的中间站它先让你解决可用性再遇到容量瓶颈时再去解决扩展性。就我个人来说这套架构最值钱的地方不是省了几台服务器而是让业务团队理解了一个道理数据库高可用不是一个中间件的事而是复制链路、连接池、监控、演练、恢复流程一起构成的完整体系。只要把后面这些运维动作做到位双主双从在绝大多数场景下都能稳定跑很多年。