上篇聊了Mycat的基本定位,这次直接上干货:用Docker把Mycat跑起来,后端接上一套现成的MySQL主从环境,把读写分离真正落到业务上。整个过程我会按照自己实操的顺序来讲,配置文件、启动命令、验证方法全部贴出来,照着做就能复现。适合谁看?已经搭过MySQL主从、但一直没搞明白中间件怎么接入的人,或者正在选型读写分离方案、想快速验证Mycat实际效果的人。如果你对Mycat还完全没有概念,建议先花十分钟搞清楚“逻辑库”“数据节点”这两个词的含义,再来读这篇,不然配置阶段会有点晕。
1. 动手前的思路拆解:读写分离为什么交给Mycat
1.1 读写分离要解决的实际问题
先想清楚一个事情:既然MySQL本身就支持主从复制,为什么还要在中间加一层Mycat?答案是——MySQL只负责把数据从主库同步到从库,但它不负责决定一条SQL到底该发给哪个库。
主从搭好之后,两个库同时在线,主库接受写入,从库接受读取,这是我们最想要的形态。但是如果没有一层东西来做流量分发,应用层就得自己维护两套数据源,而且还得时刻关注主从状态,写代码的成本一下就上去了。更麻烦的是,一旦主库切换、从库扩缩容,应用层代码也要跟着改。
Mycat就是用来解决这个“SQL往哪发”的问题的。应用只需要连接一个Mycat的逻辑地址,Mycat解析你发过来的SQL,根据语句类型自动路由:INSERT/UPDATE/DELETE走主库,SELECT走从库。应用感知不到后端到底有几台MySQL,也感知不到主从链路修没修复,只要Mycat活着,业务就能跑。
1.2 为什么用Docker装,而不是直接下载安装包
如果只是在自己的开发机上临时体验,直接下载Mycat压缩包、配好JDK环境变量也能跑。但我的建议是用Docker,原因有三个:
第一,Mycat是Java写的,直接装需要先搞定JDK版本,然后配置环境变量,再把解压后的目录结构和权限理顺,整个过程大约要花二十分钟。而Docker镜像把这些都打包好了,docker run一条命令完事,省下的时间可以用来排查后面真正容易出错的配置文件。
第二,Docker容器对宿主机是隔离的,Mycat改配置改崩了、端口被占用了,直接删掉容器重建就行,不会污染系统环境。这对反复测试的场景特别友好。
第三,部署形态更贴近生产环境。生产上Mycat多半也是跑在容器里或者独立服务器上,跟应用不在同一台机器,容器化的方式能让你提前适应这种访问模型:通过IP加端口连接,而不是通过localhost。
1.3 环境清单与版本选择
我这次用的环境如下:
| 组件 | 版本/配置 | 说明 |
|---|---|---|
| 宿主机 | CentOS 7 / Docker 20.10 | 生产环境常见组合 |
| Mycat镜像 | docker.io/mycat/mycat:1.6-RELEASE | 最稳定、资料最多的版本 |
| MySQL主库 | 192.168.1.101:3306(MySQL 5.7) | 写库 |
| MySQL从库 | 192.168.1.102:3306(MySQL 5.7) | 读库,已配置主从同步 |
| 测试表 | testdb.user | 一个简单的用户表 |
这里有个版本提醒:Mycat 1.6对MySQL 8.x的兼容性不太好,因为Mycat 1.6内部集成的JDBC驱动版本较老,默认的密码认证方式跟MySQL 8对不上。如果你后端跑的是MySQL 8,要么给Mycat建一个使用mysql_native_password的专用账号,要么直接用MySQL 5.7省心。后面常见问题部分我还会单独说这个坑。
2. 前置准备:把MySQL主从复制先跑通
2.1 主库配置要点
Mycat本身不帮你做数据同步,数据在主库和从库之间的一致性完全依赖MySQL原生复制能力。所以前置条件是主从已经搭好,并且数据是实时同步的。
主库的配置文件/etc/my.cnf至少要有这些内容:
[mysqld] server-id=1 log-bin=mysql-bin binlog-do-db=testdbserver-id必须是全局唯一的值,从库不能和主库相同,否则复制链路会报错。log-bin开启二进制日志,这是主从复制的基础,没有binlog从库根本无从同步。binlog-do-db的意思是只记录testdb这个库的变更,如果你有多余的库不想同步到从库,就靠这个参数来限定。
改完配置后重启主库,然后执行:
SHOW MASTER STATUS;记录下返回的File和Position两列的值,例如mysql-bin.000001和154。这两个值在后面配置从库时会用到。注意,主库一旦有新的写操作,Position会不断变化,所以在配从库的窗口期内,尽量不要对主库做数据变更。
2.2 从库配置要点与复制链路校验
从库的/etc/my.cnf:
[mysqld] server-id=2 read_only=1read_only=1是生产环境必须开的。如果不开启这个参数,从库在复制间隙也能接受外部写入,一旦有人直接把数据写进从库,主从数据就对不上了,Mycat的读写分离也就失去意义。这个参数对普通账号生效,用Mycat连接的账号如果你不额外给SUPER权限,也会被限制住,这其实是好事,下文验证部分会体现出来。
从库重启后,执行下面的SQL建立复制关系:
CHANGE MASTER TO MASTER_HOST='192.168.1.101', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='repl_password', MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=154; START SLAVE;然后一定要执行一遍:
SHOW SLAVE STATUS\G;看到Slave_IO_Running: Yes和Slave_SQL_Running: Yes同时为Yes,主从才算真正跑通。只要有一个不是Yes,后面Mycat配了也是白配,读从库读到的永远是旧数据。
2.3 Mycat专用账号的权限设计
这里要单独强调一个点:Mycat连后端MySQL时,需要一个专用的连接账号,这个账号不能乱用root,一是安全问题,二是权限太大会掩盖很多配置错误。
我在主库和从库上分别执行了:
CREATE USER 'mycat_user'@'%' IDENTIFIED BY 'mycat_pass'; GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO 'mycat_user'@'%'; FLUSH PRIVILEGES;因为只是做读写分离验证,给增删改查就够了。注意主从两个库都要建这个账号,否则Mycat连完主库没问题,切到从库时发现账号不存在,会直接导致读操作报错。从库数据同步是MySQL复制进程做的,跟Mycat账号没关系,但Mycat作为客户端连接从库执行SELECT时,必须要有对应账号。
3. Docker部署Mycat的完整过程
3.1 镜像选择和启动命令
Mycat官方在Docker Hub上的镜像仓库是docker.io/mycat/mycat,我用的标签是1.6-RELEASE。这个镜像虽然不是很活跃,但胜在Mycat 1.6下载量最大、网上踩坑资料最多,出问题能搜到答案。
拉取并启动:
docker pull docker.io/mycat/mycat:1.6-RELEASE docker run -d \ --name mycat \ -p 8066:8066 \ -p 9066:9066 \ -e MYCAT_PORT=8066 \ -v /opt/mycat/conf:/usr/local/mycat/conf \ docker.io/mycat/mycat:1.6-RELEASE端口说明:8066是Mycat的业务端口,应用和mysql客户端连的是这个口;9066是管理端口,用来查看Mycat的数据源状态、连接数等监控信息。两个都必须映射出来。
-v /opt/mycat/conf:/usr/local/mycat/conf这行是目录挂载,把宿主机/opt/mycat/conf目录映射到容器里的配置目录。因为Mycat的三个核心配置文件(server.xml、schema.xml、rule.xml)都在conf目录下,不挂载的话,每次改配置都要先进容器,非常麻烦。
3.2 配置目录挂载与容器内路径
宿主机上的/opt/mycat/conf目录需要提前创建好,并把Mycat默认的配置文件先放进去。最简单的办法是:先不挂载启动一次容器,把容器里的配置目录整个复制出来。
mkdir -p /opt/mycat/conf docker cp mycat:/usr/local/mycat/conf/server.xml /opt/mycat/conf/server.xml docker cp mycat:/usr/local/mycat/conf/schema.xml /opt/mycat/conf/schema.xml docker cp mycat:/usr/local/mycat/conf/rule.xml /opt/mycat/conf/rule.xml或者更省事:直接把一个挂载了conf的空目录的容器跑起来,用docker cp把模板文件拷进去。不过注意顺序——docker cp只复制文件,如果目标文件已存在会覆盖。这么做的好处是,你拿到的文件就是该镜像自带的原版配置,不容易出现版本不匹配的问题。
我用的顺序是:先跑一个临时的无名容器,把三个文件复制到宿主机,然后删除临时容器,再用挂载方式正式启动。这样能确保第一次启动时,conf目录里就有一套可用的配置。
3.3 启动后的第一件事:看日志
不管启动命令是不是报成功,都要看日志确认:
docker logs -f mycat正常的日志末尾会出现Mycat启动成功的标记,日志里能看到类似Server startup successfully的关键字。如果日志里报Schema error或者connect refused,说明配置里有问题,先去改配置文件再重启容器。
重启容器用:
docker restart mycat配置文件改了之后,不需要重新创建容器,重启即可生效。这一点在调试阶段非常方便。
4. 核心配置解析:读写分离全靠schema.xml
4.1 server.xml:逻辑库和用户入口
server.xml是整个Mycat的入口配置,定义了两样东西:Mycat对外暴露的用户账号,以及这些账号能访问的逻辑库。
我配的server.xml简化后如下:
<mycat:server xmlns:mycat="http://io.mycat/"> <system> <property name="defaultSqlParser">druidparser</property> </system> <user name="root"> <property name="password">123456</property> <property name="schemas">TESTDB</property> </user> </mycat:server>用户名为root,密码123456,这个账号是客户端连接Mycat时用的,不是后端MySQL的账号。schemas指向逻辑库TESTDB,逻辑库的概念不用想得太复杂,你可以把它理解成Mycat对外提供的一个虚拟数据库。客户端连上Mycat后,看到的库是TESTDB,但数据实际存放在后端的物理MySQL里。
如果还需要一个只读账号,可以再加上一个用户,把readOnly属性设为true:
<user name="readonly_user"> <property name="password">123456</property> <property name="schemas">TESTDB</property> <property name="readOnly">true</property> </user>4.2 schema.xml:dataHost中的balance才是灵魂
schema.xml是读写分离配置的核心文件,90%的工作量都花在这里。文件里主要有三块:schema定义逻辑库和表、dataNode定义数据节点、dataHost定义后端真实MySQL主机。
我的配置如下:
<?xml version="1.0"?> <!DOCTYPE mycat:schema SYSTEM "schema.dtd"> <mycat:schema xmlns:mycat="http://io.mycat/"> <schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100"> <table name="user" dataNode="dn1" /> </schema> <dataNode name="dn1" dataHost="dtHost1" database="testdb" /> <dataHost name="dtHost1" maxCon="20" minCon="5" balance="1" writeType="0" dbType="mysql" dbDriver="native" switchType="1"> <heartbeat>select user()</heartbeat> <writeHost host="192.168.1.101" url="192.168.1.101:3306" user="mycat_user" password="mycat_pass"> <readHost host="192.168.1.102" url="192.168.1.102:3306" user="mycat_user" password="mycat_pass" /> </writeHost> </dataHost> </mycat:schema>逐行解释一下关键部分:
schema标签:
name="TESTDB"是逻辑库名,必须跟server.xml里用户配置的schemas一致。checkSQLschema="true"表示即使客户端写了TESTDB.user这样的带库名SQL,Mycat也会自动去掉TESTDB.前缀再路由。sqlMaxLimit="100"是安全限制,防止有人一条SELECT *把全表数据拉爆,Mycat会自动在尾部追加LIMIT 100。table标签:
name="user"声明这个表在逻辑库内。dataNode="dn1"表示这个表的数据存储在dn1节点上。注意,这里有一个很容易踩的坑:schema.xml里没声明的表,Mycat默认是拒绝访问的。也就是说,你在TESTDB库里创建的任何表,如果不在这里手动登记,查询时Mycat会直接报错。这个特性在公司多人协作时尤其容易踩雷,后面我会细说。dataNode标签:
name="dn1"定义节点名字,dataHost="dtHost1"指向后端主机组,database="testdb"指定物理库名。也就是说,客户端访问TESTDB.user,实际落到的物理位置是192.168.1.101/102这两台MySQL上的testdb库的user表。dataHost标签:这里是最关键的。
balance参数决定读写分离行为,它的取值含义如下:
| balance值 | 行为 |
|---|---|
| 0 | 不开启读写分离,所有读操作发往writeHost(主库) |
| 1 | 所有读操作发往readHost(从库),writeHost完全不做读 |
| 2 | 读操作在主库和从库之间随机分发 |
| 3 | 所有读操作发往readHost,从库多台时轮询分发 |
一主一从的标准场景,直接用balance="1",意思简单粗暴:写全走主,读全走从。如果你的架构是一主多从,可以改成3,让Mycat自动在多个从库之间轮询。
writeType="0"表示所有写操作只发往第一个writeHost。writeType="1"是把写随机分发到所有writeHost,这个模式已经不被官方推荐,在1.6版本里建议保持0。
switchType="1"表示主库挂了自动切换。默认值是1,开着就好。配了切换之后,主库故障时Mycat会把写操作自动切到从库上,等主库恢复再切回来,这个在高可用场景下很有用。
- heartbeat心跳:Mycat每隔一段时间会向后端MySQL发起
select user()这个查询,用来确认后端数据库是否存活。如果心跳失败,Mycat会把这个数据源标记为不可用,读写连接就会被摘除。
4.3 不改rule.xml行不行
很多人配置读写分离时纠结rule.xml的事,担心不分片就用不了Mycat。其实rule.xml是分片规则文件,在纯读写分离的场景下,如果表没有配置分片规则,rule.xml根本不会被触发。
但文件本身必须存在,因为Mycat启动时会加载并解析这个文件,缺少了它或者格式错误,启动直接失败。我的做法是:完全不修改rule.xml,用镜像自带的默认文件,只要它合法就行。以后真的要做分片了,再回头补配置。
4.4 配置改完怎么快速校验
配置文件改完,重新启动容器后,先别急着连业务端口,先看日志有没有报错。
然后做一个最基础的连通性测试:
docker exec -it mycat mysql -uroot -p123456 -h127.0.0.1 -P8066注意这里的mysql命令是容器镜像自带的客户端。如果容器里没有mysql客户端,就直接在宿主机上用系统自带的客户端连:
mysql -uroot -p123456 -h127.0.0.1 -P8066 -DTESTDB连上之后执行:
SHOW DATABASES; USE TESTDB; SELECT * FROM user LIMIT 1;能查出数据,说明Mycat到后端MySQL的通路已经打通,读写分离的骨架已经立起来了。
5. 读写分离的实测验证
5.1 用general_log抓真实SQL走向
配置配好了,怎么证明读走了从库、写走了主库?靠猜不行,要在数据库侧抓实际SQL。MySQL的通用日志可以做到这一点。
在主库和从库上分别开启:
SET GLOBAL general_log = ON; SET GLOBAL log_output = 'TABLE';开启之后,执行过的SQL会记录到mysql.general_log表里。然后通过Mycat执行一组操作:
# 通过Mycat登录 mysql -uroot -p123456 -h127.0.0.1 -P8066 -DTESTDB # 执行写操作 INSERT INTO user(name, age) VALUES('zhangsan', 22); # 执行读操作 SELECT * FROM user;然后回到主库查日志:
SELECT * FROM mysql.general_log WHERE command_type='Query';正常情况下,主库日志里能看到INSERT INTO user...,但看不到SELECT * FROM user。再到从库查同样的日志,结果正好相反:能看到刚才那条SELECT,看不到INSERT。
需要注意,general_log里会混入Mycat的心跳语句select user()以及一些连接初始化的日志,别被干扰,只看自己执行的业务SQL就行。
5.2 通过管理端口观察数据源连接
除了看SQL日志,还有一个更直接的方式:连Mycat的管理端口,查看数据源状态。
mysql -uroot -p123456 -h127.0.0.1 -P9066执行:
show @@datasource;输出结果里会列出Mycat后端每个数据源的信息,包括:
SERVICE STATUS:数据源状态,1表示正常MAX CON/CUR CON:最大连接数和当前连接数R/W:表示当前数据源是写库还是读库
再执行:
show @@connection;可以看到当前活跃的会话,以及每个会话连接的物理数据源。如果读会话都挂在从库对应的连接上,写会话都挂主库上,说明Mycat的路由策略完全正常。
5.3 写故障场景:从库只读保护的作用
这里我想多聊一个真实场景。我在搭好环境后做了一次故意破坏测试:临时把从库的read_only=1参数保留,然后尝试通过Mycat往从库写入数据。
确切地说,我并没有直接指定从库执行写入,而是在测试Mycat路由时,把schema.xml临时改成了balance="0"(不开启读写分离),同时保留从库的read_only。这种情况下所有写操作会走主库,所以不会出问题。更值得测试的是另一面——如果某天配置出错,把写请求发到了从库,从库的read_only就是最后一道防线,它会直接拒绝这条SQL,返回The MySQL server is running with the --read-only option的错误。这个报错表面看是故障,其实是保护机制在起作用,它能帮你快速定位“写被路由到从库”这类配置错误。
生产环境里,从库一律保持只读,是必须坚持的基线。
5.4 同步延迟和数据一致性观察
读写分离生效之后,主从之间的延迟就成了需要关注的点。MySQL默认的主从复制是异步的,主库提交事务,从库基于binlog回放,天然存在一个极小的时间窗口。
验证方法是:通过Mycat在主库插入数据后,立刻通过Mycat查询。如果从库复制延迟很小,能查到我刚插入的数据;如果延迟大,就会查不到。Mycat不会帮你做数据合并,它只负责路由,读到的就是从库那一刻的真实数据。
对这种延迟,常规做法是:
- 业务上对强一致要求高的场景,比如刚写完立刻要读的订单支付结果,走主库读,或者强制主库读。
- Mycat里可以通过在SQL前加注解强制走主库,例如:
/*!mycat: db_type=master */ SELECT * FROM user;这个注解会告诉Mycat忽略读写分离规则,强制把这条SELECT路由到主库。适合支付回调、刚下完单的详情展示这种场景。实际项目中,我会在涉及资金、库存扣减后的即时查询里,统一加上这个注解,避免读从库读到旧数据。
6. 常见问题排查实录
6.1 连不上Mycat的8066端口
症状:mysql -h127.0.0.1 -P8066连接超时或拒绝连接。
排查顺序:
- 确认容器是up状态:
docker ps -a。 - 确认8066已映射到宿主机:
docker port mycat。 - 确认Mycat日志没有报错:
docker logs mycat | tail -50。 - 如果日志里有
Schema error或者connect refused,多半是schema.xml里的物理库名不对,或者Mycat连接MySQL的账号没建好。
我遇到的比较隐蔽的情况是:宿主机SELinux开着,端口映射后外部访问不通,临时用--privileged启动容器验证过一次。如果你的环境是云服务器,还要确认安全组放行了8066和9066两个端口。
6.2 Mycat配MySQL 8的密码插件坑
前面提到过Mycat 1.6连MySQL 8会失败,症状是Mycat启动时后端连接报错,日志里出现Public Key Retrieval is not allowed或者Unable to load authentication plugin 'caching_sha2_password'。
原因是MySQL 8默认的密码认证插件是caching_sha2_password,Mycat 1.6集成的旧版JDBC驱动只认mysql_native_password。
解决方法有两种:
- 给Mycat建专用账号时,强制指定密码插件:
CREATE USER 'mycat_user'@'%' IDENTIFIED WITH mysql_native_password BY 'mycat_pass';- 如果账号已经建好,直接改插件类型:
ALTER USER 'mycat_user'@'%' IDENTIFIED WITH mysql_native_password BY 'mycat_pass';如果后端是MySQL 5.7,完全不存在这个问题,方法直接略过。
6.3 心跳失败导致读写连接全部掉线
症状:Mycat能启动,但执行SQL时报错,管理端口查show @@datasource发现所有数据源状态异常。
一般就是schema.xml里heartbeat语句执行不了。默认心跳是select user(),这个语句任何MySQL用户都能执行,但如果用户本身连不上,心跳自然失败。排查方法是手动用mycat_user账号连接一下后端MySQL,确认账号密码、端口、远程访问权限都没问题:
mysql -umycat_user -pmycat_pass -h192.168.1.101 -P3306 -e "select user();"能返回结果,心跳就没问题。这里还要注意,如果后端MySQL的url写的是localhost,而Mycat在容器里,它访问的localhost是容器自己,根本连不到宿主机上的MySQL。这个坑在Docker部署场景下特别容易踩,url一定要写宿主机IP或后端MySQL的容器IP。
6.4 读操作明显走了主库的排查
症状:从库上没有任何SELECT日志,所有读操作都堆积在主库。
这种情况先检查schema.xml的balance值。如果设成了0,读写分离自然不生效。如果确认是1,再检查readHost的配置是否真的正确——包括IP、端口、账号,任何一个参数错了,Mycat在初始化时会把读连接标记为不可用,然后自动把读流量回落到writeHost上。
这里有个经验:Mycat在readHost不可用时不会报错,它会静默地把读请求转发到主库,表面的表现就是“功能正常,但读压力全在主库”。所以配置完之后一定要通过管理端口查一遍数据源状态,确认读连接是正常的,别只看业务能跑就认为万事大吉。
6.5 逻辑库里的表访问报错
症状:通过Mycat执行SELECT * FROM some_table时报错,提示表不存在,但物理库明明有这个表。
原因就是我前面提过的:schema.xml的schema标签下没有声明这张表。Mycat不认识它,自然不允许访问。
解决方法是,在schema标签下补上table配置:
<schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100"> <table name="user" dataNode="dn1" /> <table name="order" dataNode="dn1" /> </schema>每次新增业务表,都需要同步修改schema.xml并重启。生产上如果表非常多,这个步骤确实繁琐,但也正因为Mycat强制声明表结构,才能更精确地控制分片策略——哪些表分片、哪些表不分片、哪些表只走某个数据节点,都是靠这个文件表达出来的。这个特性对开发来说像个紧箍咒,对运维来说反而是清晰的管控入口。
最后再分享一个我个人的体会:Docker部署Mycat这件事,难点从来不在Docker命令本身,而在配置文件的理解和校验。一条docker run五分钟就写完了,但schema.xml里每个参数为什么这么配、balance为什么取1、心跳为什么用select user(),这些才是真正要花时间琢磨的。建议你配好第一遍之后,故意把balance改回0跑一次,再用general_log观察一下SQL走向,亲自感受一下读写分离“开启”和“关闭”的区别。这样踩过一轮之后,对Mycat的路由机制就会有很直观的记忆,后面去配置分片、配置高可用,都建立在这个基础上。我在实际项目里帮同事排查过几次“读写不分离”的问题,八成以上都是配置导致的读写连接可用性问题,只要按照上面的日志、管理端口、general_log三步逐层排查,基本都能快速定位。