资讯动态

Docker部署Mycat实现MySQL主从读写分离实战指南

发布时间:2026/10/15 6:56:21 来源:尧图企业网站定制
数据库一旦出现慢查询、连接数告警很多人第一反应是加配置、加缓存但久而久之会发现读多写少的场景下最省心的还是把读和写拆到不同的库上。这个系列第二篇就来聊一个被讨论了无数遍但依然不过时的组合用Docker安装Mycat把MySQL主从复制变成对应用透明的读写分离。这篇内容适合刚接触Mycat、主从复制也只在文档里见过的朋友。我会把主从环境、Mycat配置、验证方法还有那些真正会绊倒人的坑全部写出来保证跟着做能跑通。先说明一下本文的目录和配置全部来自我实际部署过的方案版本上我选Mycat 1.6系和MySQL 5.7这两个版本搭配兼容性最稳生产环境如果版本不同思路照样可以参考。1. 为什么用Docker装Mycat读写分离的基础认知与选型这一章先不讲命令先把逻辑理清楚。很多人拿到工具就喜欢开跑但Mycat配置一旦出错排错比安装痛苦得多。知道它在整个架构里处于什么位置后面调起参来才有方向。1.1 读写分离解决什么问题读写分离的本质是把数据库的读压力和写压力拆到不同的物理节点上。大量业务属于典型的读多写少订单查询、商品浏览、报表统计读请求动不动占80%、90%而写请求只是其中一小撮。如果把读全部压在一个库里哪怕这个库配置再高连接数和CPU也会先扛不住。读写分离后写库只处理写读库承担读主库的压力一下就释放了。但注意读写分离不是银弹。它解决的是并发读压力不解决单表数据量暴增的问题。数据量大到一定程度还得上分库分表。这也是为什么我在这篇里只谈读写分离不硬塞分片规则——先把一个环节做扎实再谈下一步。你要是看到一篇文章里既讲了分片又讲了读写分离结果两头都没说透那大概率是写给你收藏用的不是写给你照着做的。1.2 为什么选择Mycat而非自研或使用云RDS有两条替代路径一是应用层自己搞双数据源二是直接买云厂商的读写分离地址。我自己不排斥这两条路但它们各有各的适用边界。应用层双数据源要求团队代码规范极强所有读操作都得走专门的读库接口漏一个就是事故。而且一旦要从读写分离切到分库分表改动量等于重写数据访问层。云RDS自带的只读实例则要看你用的数据库版本和云厂商限制对于已经在自建机房或者本地开发环境的场景很难直接套用。Mycat走的是中间件层透明代理的路线。它伪装成一个MySQL服务器应用只知道自己连了一个“假MySQL”库名、表名、SQL写法基本不用变。它内部再把SQL解析、路由到真正的MySQL节点。这样一来代码层几乎不需要感知读库写库的存在后续哪怕调整主从拓扑也只是改配置而不是改代码。1.3 Docker部署的收益与成本你可能想问Mycat不是解压就能跑的吗为什么非要Docker我第一次搭的时候也是直接下载tar包跑后来发现有两个痛点一是本机环境可能被改坏装了一堆依赖二是不够干净换机器很难复现。用Docker之后Mycat的Java环境、运行依赖、目录结构全被镜像封住我只需要关心conf和logs这两个挂载出来的目录。成本自然也有。Docker是Linux容器网络模式带来的端口转发、文件挂载权限、时区不一致都有可能成为新手陷阱。但总体利大于弊尤其适合本地实验和快速交付。下面所有步骤都基于Docker来写遇到坑的时候我也会点出对应解法。2. 准备阶段基于Docker搭建MySQL主从复制环境Mycat自身不存储数据它只是路由。所以必须先有一套MySQL主从复制环境。如果你的生产环境已经有主从了这一章可以跳过直接看第三章如果是像我一样从空环境开始就按下面的流程来。2.1 主从库容器规划与配置文件我习惯用MySQL 5.7来做演示理由很简单5.7是Mycat生态里被验证最多的版本8.0虽然也能连但认证插件和JDBC参数会多一些小坑。生产环境用什么版本请以你的现状为准思路是通用的。先规划两个容器名字用mysql-m和mysql-s。端口方面主库映射到3306从库映射到3307避免和本机已有MySQL端口冲突。先建配置目录mkdir -p /opt/mysql-m/conf /opt/mysql-s/conf主库配置文件/opt/mysql-m/conf/my.cnf[mysqld] server-id1 log-binmysql-bin binlog_formatROW binlog_row_imagefull sync_binlog1从库配置文件/opt/mysql-s/conf/my.cnf[mysqld] server-id2 log-binmysql-bin binlog_formatROW read_only1 super_read_only1主从配置有两点需要特别强调。第一server-id必须全局唯一不能为0否则复制链路会出诡异问题。第二从库一定要开read_only读写分离场景下从库只允许接收主库复制过来的写操作任何业务写入都必须被数据库自身挡住。就算Mycat配置错误把写路由到从库从库也会直接拒绝这等于给系统加了一道保险。然后启动两个MySQL容器docker run -d --name mysql-m \ -p 3306:3306 \ -v /opt/mysql-m/conf/my.cnf:/etc/mysql/mysql.conf.d/mysqld.cnf \ -e MYSQL_ROOT_PASSWORDRoot123 \ mysql:5.7 docker run -d --name mysql-s \ -p 3307:3306 \ -v /opt/mysql-s/conf/my.cnf:/etc/mysql/mysql.conf.d/mysqld.cnf \ -e MYSQL_ROOT_PASSWORDRoot123 \ mysql:5.7注意镜像内部的配置文件位置mysql:5.7镜像实际加载的是/etc/mysql/mysql.conf.d/mysqld.cnf如果你用8.0镜像路径和配置风格可能有差异。挂载单文件前先在宿主机把文件建好否则Docker会自动创建一个目录导致挂载失败。这是个特别低级的坑但我见过不止一个人踩进去。2.2 初始化复制账号并建立主从链路容器起来后先看一下主库状态docker exec -it mysql-m mysql -uroot -pRoot123 -e SHOW MASTER STATUS;这个操作要在从库建立主从链接之前做因为返回值里的File和Position是复制链路的起点。接着在主库创建复制专用账号权限只需要REPLICATION SLAVE千万不能给得太宽CREATE USER repl% IDENTIFIED BY Repl123; GRANT REPLICATION SLAVE ON *.* TO repl%;然后在从库执行CHANGE MASTER并启动CHANGE MASTER TO MASTER_HOST192.168.31.100, MASTER_PORT3306, MASTER_USERrepl, MASTER_PASSWORDRepl123, MASTER_LOG_FILEmysql-bin.000003, MASTER_LOG_POS154; START SLAVE;这里的192.168.31.100是宿主机IP两个容器是隔离的从库访问主库只能通过宿主机IP加映射端口不能写localhost。如果你把主从放进同一个Docker网桥网络也可以直接写容器名mysql-m但要确保在同一个network里这里为了简单直接用宿主机IP。查看复制状态docker exec -it mysql-s mysql -uroot -pRoot123 -e SHOW SLAVE STATUS\G重点关注Slave_IO_Running和Slave_SQL_Running是否都是Yes。如果说NO八成是账号权限、防火墙、或者MASTER_LOG_FILE和POS不对。把报错信息仔细读一遍这一章的错误多半是信息不匹配造成的。2.3 准备业务库表并为Mycat创建专用账号主从打通后在主库建一个业务库shop建一张t_order基础表CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4; USE shop; CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL, amount DECIMAL(10,2) NOT NULL, create_time DATETIME NOT NULL ) ENGINEInnoDB;从库会自动把库表结构复制过去。然后创建一个Mycat用来连接物理MySQL的业务账号。注意这个账号是Mycat跟后端MySQL通信用的不是Mycat对外给应用用的账号不要混淆CREATE USER mycat_user% IDENTIFIED BY Mycat123; GRANT ALL PRIVILEGES ON shop.* TO mycat_user%;主库和从库都要执行这条授权因为Mycat会分别用这个账号连接主从。如果你想让Mycat能操作多个库授权范围可以适当放大但安全起见最小权限原则别丢。到这里数据层的准备就结束了。先别着急往下走确认一下两台MySQL能正常互访在从库容器里执行mysql -h宿主机IP -P3306 -umycat_user -pMycat123 -e SELECT 1;能通过再继续排错成本会小很多。3. Mycat容器启动的两种做法现成镜像与自建镜像Mycat本身是Java写的官方提供的部署方式很简单解压启动就行。但用Docker部署时镜像来源是个问题。我见过不少同学一上来就docker run一个第三方的mycat镜像然后因为容器里配置路径不对、时区不对、缺少JVM参数而卡住。所以这一章把两种方式都讲清楚你按自己的偏好选。3.1 目录规划conf和logs挂载出来无论用哪种镜像这一步一定要做。Mycat的核心配置在conf目录运行日志在logs目录把这两个目录从容器里挂载到宿主机才能在不重新构建镜像的前提下改配置、看日志。这也是Docker部署相对tar包部署的核心优势。我习惯这样规划mkdir -p /opt/mycat/conf /opt/mycat/logs后面容器启动时把这两个目录映射进去。需要注意如果你的镜像没有/usr/local/mycat/conf这个路径而是别的路径比如/mycat/conf一定要先查清楚否则挂载后容器可能起不来。3.2 方式一拉取社区镜像直接启动Docker Hub上搜索mycat能找到不少社区维护的镜像大多基于Mycat 1.6.7.6打包。直接用镜像的问题是你无法确定镜像里的目录结构、启动脚本、以及是否内置了额外的东西。如果只是本地验证选一个更新时间近、下载量高的镜像即可。启动命令大致如下docker run -d --name mycat \ -p 8066:8066 -p 9066:9066 \ -v /opt/mycat/conf:/usr/local/mycat/conf \ -v /opt/mycat/logs:/usr/local/mycat/logs \ 你选择的镜像名:1.6.7.68066是Mycat对外提供MySQL协议服务的端口应用连接就是这个端口9066是管理端口可以用来查看Mycat的运行状态。两个端口都要映射出来。这种方式的优点是真的快适合环境里的快速验证。缺点也很明显万一镜像维护者做了自定义改动你的排错思路就得跟着变。我建议至少把镜像里的conf目录导出来跟官方tar包里的conf比对一下确认没有预置奇怪的配置。3.3 方式二基于Dockerfile自建镜像更可控如果用现成镜像踩了几次坑或者你想把Mycat镜像纳入自己的镜像仓库管理我推荐用Dockerfile自己构建。步骤不复杂核心就三步拉一个带JRE的基础镜像把Mycat发布的tar包解压进去覆盖启动命令。我用的Dockerfile长这样FROM openjdk:8u212-jre-alpine RUN apk add --no-cache bash wget \ wget -O /tmp/mycat.tar.gz http://dl.mycat.org.cn/1.6.7.6/Mycat-server-1.6.7.6-release-20190627191047-linux.tar.gz \ mkdir -p /usr/local/mycat \ tar -zxvf /tmp/mycat.tar.gz -C /usr/local/ \ mv /usr/local/Mycat-server* /usr/local/mycat WORKDIR /usr/local/mycat CMD [/usr/local/mycat/bin/mycat, console]注意dl.mycat.org.cn的下载链接有时候会调整如果你发现404就去官网找到最新版tar包地址替换一下。基础镜像我选的是openjdk:8u212-jre-alpine因为Mycat 1.6依托JDK8且alpine体积小。如果运行时报缺libc之类的错换成openjdk:8u212-jre-slim或多装几个apk包。构建并启动docker build -t mycat:1.6.7.6 . docker run -d --name mycat \ -p 8066:8066 -p 9066:9066 \ -v /opt/mycat/conf:/usr/local/mycat/conf \ -v /opt/mycat/logs:/usr/local/mycat/logs \ mycat:1.6.7.6启动后检查日志docker logs -f mycat如果看到Mycat Server startup successfully之类的字样说明容器起来了。期间可以在宿主机测试mysql -h127.0.0.1 -P8066 -uroot -p123456 -e SHOW DATABASES;这一步没通过之前不要急着改配置先确认容器和网络是通的。4. schema.xml/server.xml配置核心让Mycat认识主从拓扑Mycat启动后默认是一套空逻辑真正决定读写分离能否生效的是conf目录下的schema.xml、server.xml和rule.xml三个文件。这一章不碰分库分表只把读写分离的最小配置讲透。4.1 schema与dataNode的逻辑映射从应用视角看Mycat是一个MySQL服务器它里面有一个“逻辑库”比如shopdb逻辑库里有一张“逻辑表”t_order。应用发的SQL都是对逻辑库的。Mycat拿到SQL后要根据逻辑表找到它对应的物理库。完成这个映射的就是schema和dataNode。在schema.xml里我这样定义mycat:schema xmlns:mycathttp://io.mycat/ schema nameshopdb checkSQLschematrue sqlMaxLimit100 table namet_order dataNodedn_order / /schema dataNode namedn_order dataHostdh_mysql databaseshop / /mycat:schema解释一下schema nameshopdb定义了一个逻辑库逻辑库对应用开放table namet_order把逻辑表t_order映射到名为dn_order的数据节点dataNode里的databaseshop指物理MySQL里的真实数据库名。这里有一个细节如果你什么都不配置tableMycat也可以把整个库的所有表都代理过去。但实际使用时显式声明table更稳妥尤其是后面要做分片或读写分离精细化控制时。如果只有一张表rule规则可以不写不会影响读写分离。4.2 dataHost里writeHost/readHost与balance属性的作用接下来是最关键的dataHost配置。它是Mycat和物理MySQL之间的连接池配置同时也定义了主从拓扑。我的写法是dataHost namedh_mysql maxCon1000 minCon10 balance1 writeType0 dbTypemysql dbDriverjdbc switchType-1 slaveThreshold100 heartbeatselect user()/heartbeat writeHost hosthostM urljdbc:mysql://192.168.31.100:3306/shop?useSSLfalseserverTimezoneAsia/Shanghai usermycat_user passwordMycat123 readHost hosthostS urljdbc:mysql://192.168.31.100:3307/shop?useSSLfalseserverTimezoneAsia/Shanghai usermycat_user passwordMycat123 weight1 / /writeHost /dataHost这里有几个属性每个都要说清楚balance读写分离的开关和模式。0表示不开启读写分离所有请求都去writeHost1表示读请求在writeHost和readHost之间按负载均衡分发2表示读请求只发给readHostwriteHost不承担读。读写分离最常用的是1。如果你的主库和从库性能差距很大建议用2把读压力全部甩给从库主库专心写。writeType写请求的路由方式。0表示只发往第一个writeHost1表示轮询所有writeHost。我们在实例里只有一个writeHost所以填0。switchType主从切换策略。-1表示不自动切换主库挂了读写分离直接不可用1表示自动切换到下一个writeHost。如果暂时没有搭建高可用先用-1避免误切换造成脑裂。heartbeatMycat用来探测后端MySQL存活状态的SQL。select user()是个轻量查询够用。特别强调一下读写分离的内部机制Mycat在这层配置中将所有写SQL、显式事务内的SQL、以及带FOR UPDATE等锁语义的SQL都强制路由到writeHost。普通SELECT则根据balance策略分发到写或读Host。实际效果上应用层只感知到一个逻辑库但SQL已经在背后被拆开了。4.3 server.xml用户权限与访问控制server.xml里主要配置Mycat对外暴露的用户。它是独立于物理MySQL账号的另一层账号应用用的是这个账号连接Mycat。mycat:server xmlns:mycathttp://io.mycat/ user nameroot defaultAccounttrue property namepassword123456/property property nameschemasshopdb/property property namereadOnlyfalse/property /user /mycat:server关键属性schemas允许该用户访问的逻辑库多个库用逗号分隔。readOnly设为false表示这个用户可以在逻辑库上执行写操作。如果设置为true那么所有SQL都会被Mycat强制当作只读处理应用拿到的任何写操作都会失败。我见过有人把readOnly配置成true然后怎么也写不进去查半天才看到是这里的问题。如果你只是想让部分账号只读可以单独再配一个user。改完配置记得重启Mycat容器或者用9066管理端口执行reload config生产环境我建议通过管理端口热加载避免连接中断。热加载命令mysql -h127.0.0.1 -P9066 -uroot -p123456 -e reload config;注意9066管理端口用户也是server.xml里定义的用户名密码一样。如果reload返回成功但实际配置未生效建议直接重启容器保证干净状态。5. 读写分离验证三板斧server_id、日志、故障模拟配置完成后最重要的一步不是看它能连上而是证明它真的把读和写分发到了不同的物理MySQL上。很多人的读写分离是“配了但没完全配”所以这一章我会给三个验证角度。5.1 通过select server_id观察路由分布这是最直观、最推荐的方法。主库的server-id是1从库的server-id是2。如果用Mycat连接多次执行SELECT server_id;返回的结果如果会随机出现1和2说明读请求被分发了如果永远是1说明读全部走了主库没有分离成功。for i in {1..10}; do mysql -h127.0.0.1 -P8066 -uroot -p123456 -e SELECT server_id; 2/dev/null | grep -v server_id; done我当时跑出来的输出类似1 2 1 2 2 1这基本可以确认读写分离生效了。注意server_id是会话变量返回的必然是当前物理连接所在的MySQL的server-id所以这个方法准确可靠。5.2 从日志维度确认读写链路如果你不想在shell里写循环也可以看MySQL和Mycat的日志。在主库执行SHOW PROCESSLIST;观察连接来源在Mycat容器里看logs/mycat.log找到SQL解析和路由记录。Mycat默认会打印路由表信息例如route result里会标出执行节点dn_order对应的host。多执行几次不同SQL对比可以发现SELECT被路由到readHostINSERT/UPDATE被路由到writeHost。我实际使用中更喜欢用MySQL的general_log来做端到端验证但生产环境开general_log会有性能问题建议只在排错时临时开。验证完记得关掉别让它一直开着拖累性能。5.3 暂停从库复制模拟延迟验证读不会击穿主库更严苛的验证方式是模拟从库延迟甚至不可用。在从库上执行STOP SLAVE SQL_THREAD;然后通过Mycat执行普通SELECT。如果配置的是balance2读请求只会发往从库。此时从库虽然不执行复制SQL但读取线程并不受影响所以你依然能查到旧数据不会报错如果执行INSERT则会成功写入主库。这时候再用SELECT COUNT(*)会发现数据并没有新增——因为从库复制线程停了读到的还是旧数据。这恰恰说明读写分离在发挥作用。验证完别忘了把从库复制线程恢复START SLAVE SQL_THREAD;这里有一个常见误区有人用STOP SLAVE后执行普通SELECT发现还是能读到数据就以为读写分离没生效。不是的从库停止的是复制SQL线程读取线程不受影响。如果你想验证“读请求是否被发送到从库”可以在从库上临时把连接数拉满或者直接断掉然后用Mycat执行SELECT观察是否报错或超时。生产环境不要这么干但在本地实验环境很有说服力。6. 我会遇到的坑时间、权限、事务路由与后续扩展最后这一章集中写一些实战相关的坑和扩展思路。这些东西不是从官方文档里抄来的是我自己搭的时候真正被绊倒过的地方。6.1 主从同步失败排查清单主从同步失败是这整套方案里最影响体验的问题。最常见的几个原因我列一张表给你参考现象可能原因排查思路Slave_IO_Running: Connecting连不上主库检查主机IP、端口映射、防火墙Slave_IO_Running: No复制账号权限不对重新GRANT并确认host是%Slave_SQL_Running: Nobinlog位置或复制冲突看Last_SQL_Error信息必要时重置链路数据不一致从库被写入过开启read_only并比对关键表数据我遇到最多的是第一次搭的时候从库MASTER_LOG_FILE和POS复制了SHOW MASTER STATUS前一刻的值导致重复执行后报错。后来我习惯先把主库的写操作停一下再取binlog位置。另外Docker容器重启后MySQL会自动恢复主从链路但如果你使用--restartalways容器启动顺序不当时从库可能在主库还没就绪时就开始连接导致IO线程反复重试。这种情况一般会自动恢复不必恐慌观察几分钟再查状态。6.2 容器时区与JDBC连接参数Docker容器默认时区是UTC而MySQL默认也是。如果你在业务SQL里用了NOW()写入的时间可能和本地时间差8小时。我一般启动MySQL容器时加环境变量-e TZAsia/ShanghaiMycat容器也一样加。另外在dataHost的JDBC URL里我习惯带上serverTimezoneAsia/Shanghai否则Mycat在解析DATETIME时可能报错或偏移。JDBC URL里我还建议加上useSSLfalserewriteBatchedStatementstrue。前者避免本地环境证书警告干扰后者能大幅度提高批量INSERT的写入性能。如果你用的是MySQL 8.0还要考虑加allowPublicKeyRetrievaltrue否则连接握手阶段可能失败。6.3 事务、带锁查询与强制走主库的场景这里要重点提醒事务内的SELECT默认会被路由到主库。因为事务一旦开启后续读必须看到之前写的结果如果读被分发到从库而主从复制还没跟上就可能读到旧数据。这是数据库一致性的基本要求不是Mycat的问题。类似的还有SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE以及写操作里的子查询Mycat都会尽可能路由到主库。如果你的业务对一致性要求极高可以把这类查询也设计成走主库或者在SQL前加Mycat注释强制路由。Mycat支持在SQL前加/*!mycat:db_typemaster*/这样的注释但不同版本语法有些差异使用前先查对应版本文档。我自己在项目里的经验是读写分离只适合容忍轻度主从延迟的业务。像金融对账、库存扣减这种场景要么全部走主库要么引入分布式事务中间件不能无脑依赖读写分离。6.4 后续扩展思路分库分表、监控与高可用读写分离跑通之后很自然的下一步就是分库分表。Mycat的核心价值本来就在于分片读写分离只是它顺手能做好的事。如果你后面要把t_order按id取模分到多个库只需要在schema.xml里配置rule并增加多个dataNode。监控方面建议把Mycat的9066管理端口纳入监控系统定期采集show status;里的连接数、QPS、后端MySQL心跳状态。这个命令信息量很大比看日志直观。高可用方面如果希望主库挂掉时Mycat能自动把读写到从库可以把switchType改成1并配置多个writeHost。但这就要面对一致性和脑裂问题需要引入更完整的HA方案。我的建议是先把读写分离跑稳再考虑自动切换不要一上来就上大而全的架构。最后再分享一个小技巧。如果你用Docker部署后改了schema.xml但刷配置生效后Mycat还是报找不到表多半是宿主机挂载目录的配置文件和容器里路径不对。我建议在启动容器前先执行docker run --rm --entrypoint cat 镜像名 /usr/local/mycat/conf/schema.xml把镜像内默认配置打出来和挂载目录里的对照一下再决定文件放哪。这个动作能帮你省至少半小时的排查时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑