前几天一个后端朋友跑来找我说他们的订单库最近扛不住了单台 MySQL 读写混跑查询一多CPU 直接飙到 80%关键业务写操作被拖得特别慢。这个场景太典型了读多写少、数据库连接动不动被慢查询占满。当时我建议他走读写分离把查询流量从主库上拆出去。而实现手段我推荐了 Mycat——这个系列前面已经聊过它的基本定位今天这篇就接着往下走用 Docker 把 Mycat 跑起来配合 MySQL 主从复制把读写分离完整落地。这篇内容适合两类人一类是后端开发想在不改业务代码的情况下给数据库减负另一类是初级 DBA想弄明白 Mycat 到底怎么配置、流量怎么分流的。文章中我会把 MySQL 主从搭建、Mycat 三份核心配置文件、启动验证、常见坑全部过一遍配置基本可以直接抄走用。先提醒一句读写分离不是银弹后面我会讲清楚它解决的问题和它解决不了的问题。1. 先想清楚为什么需要 Mycat 这种中间件来读写分离1.1 单库瓶颈到底出在哪里很多团队对数据库的第一反应是“加配置”“上 SSD”但字节到硬件之后真正的问题往往不在磁盘而在并发。以电商订单库为例一个下单操作可能只写几条记录但商品详情、订单查询、用户历史订单这些接口一次请求可能就要查几十上百条记录。读和写混在同一个实例上时大量 select 会把 CPU、IO 和连接池打满写请求就得排队。这就是“读多写少”的典型场景。读写分离的核心思路说白了就一句话把那些慢查询、查询量大的 select 全部引流到从库主库只保留写操作和一小部分实时性要求极高的读操作。这样主库压力下降从库专门扛读流量整体吞吐能提高不少。但这里有个前提必须说清楚从库的数据是异步同步过来的存在主从延迟。如果业务要求“写入后立刻读到自己写的数据”读写分离就会出问题。所以做任何方案之前先问自己一句这个系统能容忍 1-3 秒的数据不一致吗能容忍才走读写分离这条路。1.2 Mycat 的角色应用无侵入的“数据库路由器”实现读写分离常见有两条路。一条是应用层搞多数据源比如在代码里写两个数据源然后通过 AOP 或注解给读操作指定从库另一条是引入中间件让应用只连一个地址中间件自己完成 SQL 解析和路由。两条路我都走过前者灵活但侵入性强每个项目都要维护一套切库逻辑后者对业务透明改动最小。Mycat 走的就是后者。它在 MySQL 前端伪装成一个数据库服务器应用根本不知道背后连着两台甚至更多台 MySQL。Mycat 收到 SQL 后解析出是 select 还是 insert/update/delete再决定把这包 SQL 扔给哪个后端实例。从应用视角看它只是换了一个“数据库地址”而已。“数据库路由器”这个比喻我个人觉得很贴切。就像快递分拣中心包裹SQL到了之后分拣员看目的地址SQL 类型放到不同线路的车上送到对应片区主库或从库。快递员应用不需要关心线路怎么走他只需要把包裹交给分拣中心。1.3 为什么直接用 Docker 部署我同事第一次部署 Mycat 是在物理机上跑的结果踩了一下午环境坑JDK 版本不匹配、启动脚本路径不对、配置文件少拷贝了一个、端口被别的 Java 进程占掉……Mycat 本质是一个 Java 进程对运行环境有一定要求。用 Docker 之后镜像里把 JDK 和运行环境全部打包好了拉下来直接启动省掉一大半环境问题。另外Mycat 在测试环境和生产环境往往需要不同配置容器化之后配置目录独立挂载改配置、重启容器就能完成环境切换。集群扩展时多启几个容器也能快速横向拉起。当然容器化不是没代价。Mycat 本身不存数据但它依赖配置文件和后端连接所以配置目录必须挂载到宿主机镜像升级时不要覆盖配置容器重启策略也要想好别让容器一崩就“消失”了。下面这张表可以看下容器部署和传统部署的差别对比项物理机/虚拟机部署Docker 部署环境一致性依赖基础环境易受 JDK 版本干扰镜像内容自带环境拉起一致配置管理散落在安装目录迁移易遗漏独立挂载可备份可复用多实例扩展端口、目录冲突风险高容器隔离扩展方便快速恢复需要重新部署一次一条命令重启2. 地基先行用 Docker 搭好 MySQL 主从复制2.1 主从复制的核心链路Mycat 只负责路由真正的数据同步还得靠 MySQL 自己的主从复制。先花 30 秒把原理理清楚后面排查问题会轻松很多。主库上所有数据变更都会按顺序写进 binlog。从库启动两个线程I/O 线程负责连上主库把 binlog 拉到本地存成 relay logSQL 线程负责把 relay log 里的日志逐条回放做到和主库一样的数据变更。说白了主从复制就是“日志传输 日志回放”。要注意的点是这个同步是异步的从库追日志需要时间。正常情况下延迟很小几十毫秒但主库有大事务或者从库配置较弱时延迟会被拉大。后面接 Mycat 时读请求是按批打到从库的如果从库明显落后读出来的数据就是旧数据这一点业务方必须有心理预期。2.2 启动两个 MySQL 容器假设宿主机已经装好 Docker我们先创建目录放主从各自的自定义配置文件。mkdir -p /data/mysql-master/conf /data/mysql-master/data mkdir -p /data/mysql-slave/conf /data/mysql-slave/data主库配置文件/data/mysql-master/conf/my.cnf[mysqld] server-id1 log-binmysql-bin binlog_formatROW从库配置文件/data/mysql-slave/conf/my.cnf[mysqld] server-id2 relay-logmysql-relay-bin read_only1server-id必须唯一等于是实例在集群里的身份证。主库开启log-bin才可能产生 binlog从库设置read_only防止业务误连从库写数据。binlog_format 我用 ROW它比 STATEMENT 在同步准确性上更好虽然日志体积会大一些但对一致性友好。然后启动两个实例端口故意错开docker run -d --name mysql-master \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -v /data/mysql-master/conf:/etc/mysql/conf.d \ -v /data/mysql-master/data:/var/lib/mysql \ mysql:8.0 docker run -d --name mysql-slave \ -p 3307:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -v /data/mysql-slave/conf:/etc/mysql/conf.d \ -v /data/mysql-slave/data:/var/lib/mysql \ mysql:8.0启动后分别确认能登录docker exec -it mysql-master mysql -uroot -proot123 docker exec -it mysql-slave mysql -uroot -proot123两个容器之间能互相访问吗这个先别急着测。目前它们都在同一个默认 bridge 网络里容器名不一定能直接解析。后面 Mycat 容器要访问这两个 MySQL我建议给 Mycat 用宿主机 IP 去连省去网络解析的麻烦。如果跨服务器部署就用对应服务器的 IP。2.3 初始化主从同步在主库执行创建同步专用账号CREATE USER repl% IDENTIFIED BY repl123; GRANT REPLICATION SLAVE ON *.* TO repl%; FLUSH PRIVILEGES;这个账号权限只需要REPLICATION SLAVE不需要给库表读写权限权限最小化。然后查看主库当前 binlog 位点SHOW MASTER STATUS;记录下File和Position比如mysql-bin.000001和157。接着到从库执行CHANGE MASTER TO MASTER_HOST192.168.1.10, MASTER_PORT3306, MASTER_USERrepl, MASTER_PASSWORDrepl123, MASTER_LOG_FILEmysql-bin.000001, MASTER_LOG_POS157; START SLAVE;这里的MASTER_HOST要根据实际环境写容器内可以用宿主 IP。执行完检查状态SHOW SLAVE STATUS\G;重点关注这几个字段Slave_IO_Running: Yes表示能连上主库并拉日志Slave_SQL_Running: Yes表示 relay log 回放正常Seconds_Behind_Master: 0表示当前没有延迟。最后验证一下同步是否真的通了。主库建库建表、插入数据CREATE DATABASE db_test; USE db_test; CREATE TABLE t_user (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50)); INSERT INTO t_user(name) VALUES (zhangsan),(lisi);到从库执行SHOW DATABASES;和SELECT * FROM db_test.t_user;。能看到db_test和两条数据说明主从链路没问题。2.4 这个阶段最容易翻车的三个点第一从库配置了server-id2但auto.cnf里的server-uuid如果跟主库一样复制会直接报错。这种情况常见于用docker cp或者直接复制了主库的数据目录。解决方法很简单删掉从库数据目录下的auto.cnf再重启容器。第二MySQL 8 默认的认证插件是caching_sha2_password老版本 MySQL 客户端和很多中间件解析不了。尽管 Mycat 这一步还没上我建议在主从搭建时就把账号认证方式统一一下。后续 Mycat 要连 MySQL 时用下面这种写法创建账号CREATE USER mycat% IDENTIFIED WITH mysql_native_password BY mycat123; GRANT ALL PRIVILEGES ON db_test.* TO mycat%; FLUSH PRIVILEGES;第三忘记放行防火墙端口。两台机器部署时从库要能访问主库的 3306后面 Mycat 机器要能访问 3306、3307。安全组和防火墙规则提前确认。3. Mycat 配置拆解三份 XML 是全部核心3.1 先建立逻辑库和逻辑表的概念Mycat 配置里经常出现“逻辑库”“逻辑表”这个词。所谓逻辑就是 Mycat 虚拟出来的库表应用只会看到这一层。比如你定义了一个逻辑库叫TESTDB里面的逻辑表叫t_user但背后 Mycat 真正访问的其实是你 MySQL 里的物理库db_test、物理表t_user。可以理解成前台点菜客户应用对着菜单逻辑库表点菜后厨MySQL真正做菜菜单上写的是“鱼香肉丝”后厨做的可能用的是猪肉而不是鱼肉。只要菜品符合预期客户不需要知道后厨细节。Mycat 的核心配置就是三份 XMLserver.xml定义账号和全局参数schema.xml定义逻辑库表和后端数据源的关系rule.xml定义分片规则。读写分离场景下前两份是绝对主角。3.2 server.xml登录账号和全局参数先看一份最小可用的server.xml?xml version1.0 encodingUTF-8? !DOCTYPE mycat:server SYSTEM server.dtd mycat:server xmlns:mycathttp://org.opencloudb/ system property namedefaultSqlParserdruidparser/property /system user namemycat property namepasswordmycat123/property property nameschemasTESTDB/property /user user namemanager property namepasswordmanager123/property /user /mycat:server第一个user是给应用连接 Mycat 用的连接后只能看到TESTDB这个逻辑库。第二个user是管理端口用的登录 Mycat 的 9066 管理端口做运维操作比如查看数据源状态、在线调整配置。这里有个细节容易被忽略Mycat 的前端连接 8066 和管理连接 9066 是同一个进程的两个端口配置里的用户体系是独立的。生产环境里管理账号要单独设置不要共用业务账号。3.3 schema.xml读写分离的真正战场schema.xml是 Mycat 的灵魂。读写分离能不能生效主要看dataHost里的writeHost和readHost配得对不对。下面这份配置可以直接用于我们的主从环境?xml version1.0 encodingUTF-8? !DOCTYPE mycat:schema SYSTEM schema.dtd mycat:schema xmlns:mycathttp://org.opencloudb/ schema nameTESTDB checkSQLschematrue sqlMaxLimit100 table namet_user primaryKeyid dataNodedn1/ /schema dataNode namedn1 dataHostlocalhost1 databasedb_test/ dataHost namelocalhost1 maxCon1000 minCon10 balance1 writeType0 dbTypemysql dbDrivernative switchType1 heartbeatselect user()/heartbeat writeHost hostmaster url192.168.1.10:3306 usermycat passwordmycat123 readHost hostslave url192.168.1.11:3307 usermycat passwordmycat123 weight1/ /writeHost /dataHost /mycat:schema逐个解释关键属性schema标签定义了逻辑库TESTDBtable标签把逻辑表t_user映射到数据节点dn1。dataNode标签说明这个逻辑节点对应的是dataHost上的物理库db_test。如果后续有多个表可以继续在schema里加table标签。dataHost里balance是最关键的参数。balance0表示不开启读写分离所有请求走写节点balance1表示读请求全部走从库节点balance2和3主要用在多分片或多主多从场景。读写分离最常用balance1。writeHost和readHost是嵌套关系readHost必须写在writeHost内部。Mycat 的读写分离不是平行的两台机器而是“写主机下挂读机器”的树形结构。如果读机器和写机器配置在同一层Mycat 会认为它们是两个数据源和预期就偏离了。heartbeat是心跳检测语句Mycat 用它定期探测后端 MySQL 是否存活。探活失败后如果switchType1Mycat 会自动把写流量切换到备用节点。心跳语句不复杂select user()足够用。3.4 rule.xml 和分片的关系如果你只做读写分离不需要配置分片规则。但 Mycat 启动时依然会读取rule.xml所以我建议保留一份默认配置不要删除。里面定义的分片算法、分片函数在schema.xml里没有表指定分片规则时基本不生效可以不用深究。如果硬要精简可以把rule.xml换成最简形式只保留空的mycat:rule标签结构。我这里直接沿用默认配置更稳妥。4. Docker 运行 Mycat 全程实操4.1 准备镜像和配置目录我用的镜像是基于 Mycat 1.6.7.6 版本打的容器镜像镜像里默认路径为/usr/local/mycat/conf。不同镜像的目录可能会变启动之前可以先执行docker run --rm 镜像名 ls /usr/local/mycat看一下结构确认 conf 目录位置。提前把三份配置文件放到宿主机/data/mycat/confmkdir -p /data/mycat/conf # 将 server.xml、schema.xml、rule.xml 放入 /data/mycat/conf然后启动 Mycatdocker run -d --name mycat \ -p 8066:8066 \ -p 9066:9066 \ -v /data/mycat/conf:/usr/local/mycat/conf \ mycat-image:1.6-7.6注意端口映射8066 是应用连接端口9066 是管理端口。两个端口都要映射出来。启动之后看日志确认状态docker logs -f mycat出现类似startup success或Server startup successfully的日志说明 Mycat 已经起来了。如果日志里报读取配置文件失败基本是挂载路径没对或者 XML 语法有问题。4.2 用客户端走一遍读写链路用 MySQL 客户端连接 Mycat 的前端端口mysql -u mycat -pmycat123 -h 127.0.0.1 -P 8066登录后执行SHOW DATABASES; USE TESTDB; SELECT * FROM t_user;能正常执行说明 Mycat 和后端 MySQL 的连接也通了。此时你可能会发现查出来的数据全靠主从复制同步到从库。这个没有任何问题因为 Mycat 已经替应用做好了路由应用只需要关心TESTDB这个逻辑库。4.3 验证读写分离是否真正生效的三种方法很多人的测试停在了“能连上、能查到数据”但这不够。我们要确认 select 是不是真的走了从库。方法一反复执行服务器变量查询在 Mycat 前端连接里执行SELECT server_id;如果每次返回 2说明读请求全部路由到了从库server-id2如果偶尔返回 1说明有部分读请求还留在主库。在balance1配置下稳定返回 2 是符合预期的。方法二开启从库的 general_log 看真实流量最可靠的方法是直接看后端 MySQL 收到的 SQL。在主库执行SET GLOBAL general_log ON; SET GLOBAL log_output TABLE;然后在 Mycat 上执行几条 select。再到主库查SELECT * FROM mysql.general_log WHERE argument LIKE SELECT% ORDER BY event_time DESC LIMIT 5;这个方法能精确看到哪些 SQL 打到了哪个实例。实际操作时我一般是把主库和从库的 general_log 都打开对比同一批 select 分别出现在哪个库的日志里。方法三通过管理端口查看连接分布连接 Mycat 的 9066 管理端口mysql -u manager -pmanager123 -h 127.0.0.1 -P 9066执行SHOW DATASOURCE;输出里能看到每个dataHost的 writeHost 和 readHost 各自有多少连接。如果只看到 writeHost 有连接readHost 一直是 0多半是配置没对或者根本没触发读请求。4.4 验证时顺手做的数据一致性测试验证读写分离的同时我建议顺手测一下主从同步的正常性。在 Mycat 上执行INSERT INTO t_user(name) VALUES (wangwu); SELECT * FROM t_user;接着到从库查SELECT * FROM db_test.t_user;如果能查到wangwu说明写 MySQL 主库后同步到从库的链路也正常。不要只验证select走了从库要验证写入后的数据最终能流到从库这样才算一条完整的闭环。5. 踩坑记录和问题速查5.1 连不上 8066 端口第一个常见现象是mysql -h 127.0.0.1 -P 8066报连接拒绝。我遇到的情况有一半是 Mycat 容器根本没好另一半是端口映射没配。先看docker ps确认容器在跑再看docker logs mycat确认没有报异常。如果容器正常但宿主机端口不通检查防火墙和 SELinux。容器内部端口 8066 是否监听可以用docker exec mycat netstat -tlnp | grep 8066确认。如果容器内部没问题但宿主机访问不到考虑端口映射是不是被其他进程占了。5.2 数据全部打在主库上这种情况最打击信心配置看着没问题但读请求全都走了主库。按优先级排查看schema.xml里dataHost的balance是不是0如果是改1重启检查table标签是否真的把逻辑表映射到了对应dataNode如果表没有指定Mycat 会走默认路由确认readHost的账号密码对不对如果从库连接失败Mycat 可能直接把流量降级到 writeHost最后检查是不是事务内读。Mycat 对事务内 SQL 有特殊处理——事务期间的所有 SQL 都会发给 writeHost保证一致性。业务如果使用了BEGIN之后查数据这些 select 走主库是正常现象不算 bug。一般事务内读会有大量 select 走主库这个可以通过代码层面优化减少长事务。5.3 MySQL 8 认证插件不兼容这个问题在 5.0 和 8.0 混用时特别典型。Mycat 自带的后端驱动可能不支持 MySQL 8 的caching_sha2_password认证表现为日志里报Unknown authentication plugin。解决办法不是升级驱动而是在 MySQL 里给连接账号改认证方式ALTER USER mycat% IDENTIFIED WITH mysql_native_password BY mycat123;改完不用重启 mycat只要后端连接池重建连接就能生效。重建连接最快的方式是重启 mycat 容器。5.4 读写分离期间必须盯的业务细节主从延迟是最容易忽略的。balance1把所有 select 都推到从库后一旦从库延迟超过几秒可能会出现这种场景用户下单成功后查订单详情列表里看不到刚下的单然后工单就来了。经验做法是写一个延迟监控脚本定时从从库执行SHOW SLAVE STATUS把Seconds_Behind_Master打到监控系统超过阈值直接告警。同时业务侧尽量做兜底关键数据写入后立刻读用缓存或者强制走主库普通列表查询可以容忍延迟。还有连接池的问题。Mycat 本身有连接池应用也有连接池。如果应用连接池里的老连接一直没释放Mycat 又发生过后端切换可能会让请求打到旧节点。遇到诡异的数据“看不新鲜”可以先考虑刷新连接池试试。下面整理了一张速查表方便你后面直接对着排查现象可能原因排查命令/动作8066 端口无法连接容器未启动、端口映射错误、防火墙docker logs mycat、netstat -tlnp只有主库有流量balance0、readHost 配错查看 schema.xml、SHOW DATASOURCE报认证失败MySQL8 认证插件不兼容ALTER USER ... mysql_native_password数据查不到最新的主从延迟SHOW SLAVE STATUS看Seconds_Behind_Master从库能连但读写未分离事务内读、长连接已建立检查应用是否BEGIN后再 selectMycat 启动失败XML配置错误、挂载路径错误看容器日志确认 conf 目录挂载位置最后分享个人习惯这套东西我前前后后搭过不少次现在每次做读写分离方案都会固定做三件事一是部署前先单独验证主从同步确保数据能同步才开始接 Mycat否则后面排查问题会多一重变量二是验证阶段一定开 general_log看实际SQL打到了哪个实例而不是只看SELECT server_id——后者能说明大致方向前者才是铁证三是给主从延迟设监控和告警这是读写分离上线后最容易出事的点延迟一大业务反馈立刻就来。如果你接下来要在这个环境上继续扩展可以往两个方向走一个是在 Mycat 上做水平分片把单表数据拆到多个 MySQL 实例另一个是给 writeHost 加一个备用主节点配合switchType做自动故障切换。从读写分离到分片到高可用这些东西都是一层一层叠上去的每一层先跑稳再往上加比一次性搞一个大而全的方案要可靠得多。