1. 这不是“分库分表”的说明书,而是一线团队踩坑三年后整理的分片落地手记
你看到标题里“数据分片概述、部署MyCAT服务、测试配置案例”这三段式结构,别急着划走——这不是教科书目录,也不是PPT提纲。这是某中型互联网公司数据库组在2021年Q3到2023年Q2之间,为支撑日均订单量从8万跃升至42万、单表峰值写入达每秒1700+条记录的实战路径缩影。他们没用云厂商封装好的分布式数据库中间件,而是选了MyCAT——不是因为情怀,是因为当时自研中间件尚未通过灰度验证,而商业产品license成本在预算红线之上。我参与过其中三个核心业务线的迁移改造,从最初把MyCAT当“高级代理”用,到后来能精准控制SQL路由、规避跨分片JOIN、预判主从延迟引发的读写不一致,整个过程没有一份文档能覆盖真实场景里的90%问题。
关键词里没写“MySQL”,但所有操作都基于MySQL 5.7.28+InnoDB引擎;没提“高并发”,但每个配置项背后都对应着压测时TPS掉点的具体曲线;所谓“测试配置案例”,其实是指在支付对账、用户行为日志、商品库存三个差异极大的业务模型上,如何让同一套MyCAT集群稳定扛住不同读写特征的流量。它解决的从来不是“能不能分”,而是“分得稳不稳、查得准不准、扩得快不快”。如果你正面临单库CPU持续高于75%、慢查询日志里大量出现“Sending data”状态、或者DBA开始反复提醒“这张表再不拆就要锁表维护”——那你不是在学一个中间件,而是在接手一个必须今天就给出方案的生产事故。这篇文章不讲CAP理论推导,不画抽象架构图,只告诉你:分片键怎么选才不会让80%的查询落到同一个节点、schema.xml里 标签的interval值设成30还是60,实测下来差的是3.7秒故障感知延迟、为什么用catlet做跨库更新比用ER分片更可控、以及那个被官方文档轻描淡写带过的balance=3参数,在双主架构下实际会引发主键冲突的底层机制。
2. 分片设计不是技术决策,而是业务妥协的艺术
2.1 数据分片的本质:用空间换时间,用复杂度换扩展性
很多人一上来就问“该用垂直分片还是水平分片”,这个问题本身就有陷阱。垂直分片(按业务模块拆库)解决的是耦合问题,水平分片(按数据特征拆表)解决的是容量瓶颈。但在真实业务里,90%的系统需要的是混合策略:用户中心库做垂直拆分(user_info、user_address、user_wallet分离),而订单库必须水平分片(按order_id取模或按user_id哈希)。关键在于识别出那个“不可拆”的核心实体——对电商是订单,对社交是消息,对SaaS是租户。这个实体的主键或强关联字段,就是天然的分片键候选。
我见过最典型的错误,是把create_time作为分片键。理由很朴素:“新数据总在最新分片,查询热点集中,缓存命中率高”。但现实是:运营要查“上周所有未支付订单”,财务要跑“上月各区域销售额”,这类范围查询会触发全节点扫描,MyCAT会把SQL发给所有dataNode,结果是10个节点各自执行WHERE create_time BETWEEN '2024-04-01' AND '2024-04-07',再把结果集合并。实测下来,这种查询耗时是单库的3.2倍——网络传输开销、结果集序列化反序列化、内存Merge排序全部叠加。而用user_id哈希分片,同样查询只需定位到2~3个节点,耗时反而降低18%。
提示:分片键必须满足两个刚性条件——高频等值查询(WHERE user_id = ?)和低变更率(user_id一旦生成永不修改)。像status这种字段,虽然查询频繁,但每天变更数万次,会导致跨分片UPDATE,MyCAT无法保证事务原子性,最终数据不一致。
2.2 MyCAT不是透明代理,它的路由规则有明确边界
MyCAT的定位是“数据库中间件”,不是“数据库协议网关”。这意味着它只解析MySQL协议中的特定报文类型,对存储过程、用户自定义函数、部分hint语法支持有限。我们曾在线上环境遇到一个致命问题:某个报表SQL用了/*+ USE_INDEX(t1, idx_status) */强制索引,MyCAT在解析时把整个hint当作文本透传,导致目标MySQL节点因找不到对应索引而报错。排查三天才发现MyCAT 1.6.7.5版本对复杂hint的兼容性存在缺陷,降级到1.6.5.1才解决。
更隐蔽的是SQL改写规则。比如你写SELECT * FROM order WHERE order_id IN (1,2,3,4,5),MyCAT默认会将其拆成5条单值查询分发到对应节点。但如果IN列表超过1000项,它会自动转为临时表JOIN方式——先在某个节点创建临时表插入ID列表,再与其他节点JOIN。这个行为由server.xml里的 true 和 false 共同控制,但官方文档从未说明阈值逻辑。我们通过Wireshark抓包+源码调试才确认:IN子句长度超过999时触发临时表模式,而临时表创建失败会导致整个查询返回空结果,且无任何错误日志。
注意:MyCAT的SQL解析器基于JSqlParser改造,对窗口函数、CTE(WITH语句)、JSON_EXTRACT等MySQL 5.7+新特性支持不完整。上线前必须用实际业务SQL做全量兼容性测试,不能只依赖单元测试用例。
2.3 分片策略选择:取模、范围、哈希、日期,没有银弹只有权衡
| 策略类型 | 适用场景 | 扩容痛点 | 数据倾斜风险 | MyCAT配置示例 |
|---|---|---|---|---|
| 取模(mod-long) | 用户ID、订单ID等数值型主键,ID生成均匀 | 需停机重分布,扩容成本高 | 低(ID连续递增时可能集中) | <function name="hash" class="io.mycat.route.function.PartitionByMod"><property name="count">4</property></function> |
| 一致性哈希(murmur) | 租户ID、设备ID等字符串主键,需动态扩容 | 节点增减仅影响邻近节点 | 中(哈希环分布不均) | <function name="sharding-by-murmur" class="io.mycat.route.function.PartitionByMurmurHash"><property name="seed">0</property><property name="count">8</property></function> |
| 范围分片(auto-sharding-long) | 时间戳、金额区间等有序字段 | 新增范围需人工维护,易产生热点 | 高(如所有新订单集中在最新分片) | <function name="rang-long" class="io.mycat.route.function.AutoPartitionByLong"><property name="mapFile">autopartition-long.txt</property></function> |
| 日期分片(sharding-by-date) | 日志、监控等时序数据,按月/周归档 | 归档策略复杂,冷热数据分离难 | 极低(时间天然均匀) | <function name="sharding-by-date" class="io.mycat.route.function.PartitionByDate"><property name="dateFormat">yyyy-MM-dd</property><property name="sBeginDate">2023-01-01</property></function> |
我们最终在订单库采用“用户ID一致性哈希 + 订单ID取模”二级分片:先用user_id哈希定位到4个逻辑库(db0~db3),再在每个库内用order_id % 8分16张物理表(t_order_00~t_order_15)。这样既避免单库压力过大(单库最大承载2000QPS),又保证同一用户的订单物理聚集,提升关联查询效率。但代价是全局唯一order_id生成必须跨库协调——我们弃用MySQL自增,改用Snowflake算法生成64位ID,高位41位时间戳+10位机器ID+12位序列号,确保全局单调递增且无中心节点。
3. MyCAT部署不是复制粘贴,每个配置项都在生产环境里流过血
3.1 环境准备:别让JDK版本成为第一个背锅侠
MyCAT 1.6.x系列要求JDK 1.7+,但实测发现OpenJDK 1.8.0_292存在GC停顿异常问题:在高并发INSERT场景下,Young GC频率从每分钟3次飙升至每秒2次,Full GC间隔从4小时缩短至18分钟。根本原因是G1垃圾收集器在该版本对大对象分配(MyCAT内部大量使用ByteBuffer)存在bug。解决方案不是升级JDK,而是切换到ZGC——但ZGC要求JDK 11+,而MyCAT 1.6.7.5不兼容JDK 11。最终我们锁定JDK 1.8.0_242,并在启动脚本中添加-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:G1HeapRegionSize=4M,将GC停顿稳定在150ms内。
操作系统层面,必须关闭transparent_hugepage(THP)。CentOS 7默认开启THP,会导致MyCAT进程内存分配出现“大页抖动”,表现为top命令中RES内存值剧烈波动(±2GB),并伴随大量minor page fault。执行echo never > /sys/kernel/mm/transparent_hugepage/enabled后,内存稳定性提升47%,连接池超时率下降至0.03%。
实操心得:MyCAT安装包自带JRE,但生产环境严禁使用。必须独立安装JDK并配置JAVA_HOME,否则升级JDK时需重新打包MyCAT,且无法复用现有JVM监控体系(如Prometheus JMX Exporter)。
3.2 核心配置文件深度解析:schema.xml不是模板,是运行契约
schema.xml定义了MyCAT的逻辑视图与物理映射关系。新手常犯的错误是直接拷贝官网示例,却忽略三个致命细节:
第一, 的balance属性决定读写分离策略
<dataHost name="host1" maxCon="1000" minCon="10" balance="3" writeType="0" dbType="mysql" dbDriver="native">这里的balance="3"表示“读操作随机分发到所有writeHost和readHost”,但前提是<writeHost>和<readHost>必须显式声明。我们曾误将从库配置在<writeHost>标签下,导致MyCAT认为所有节点都是可写节点,读请求也发往主库,主库CPU瞬间拉满。正确做法是:
<writeHost host="master1" url="192.168.1.10:3306" user="mycat" password="123456"> <readHost host="slave1" url="192.168.1.11:3306" user="mycat" password="123456"/> <readHost host="slave2" url="192.168.1.12:3306" user="mycat" password="123456"/> </writeHost>第二, 的心跳检测机制直接影响故障转移速度
<heartbeat>select user()</heartbeat>看似简单,但select user()在MySQL 5.7中执行耗时约8ms,而MyCAT默认心跳间隔为10秒(interval="10000")。这意味着主库宕机后,MyCAT最多需10秒才发现,期间所有写请求失败。我们将interval改为3000(3秒),同时将SQL优化为select 1,实测故障感知时间压缩至3.2秒。但要注意:过于频繁的心跳会增加DB负载,我们通过监控发现,当interval < 2000时,从库IOPS上升12%,故最终定为3000。
第三, 的rule属性必须与分片函数严格匹配
<table name="t_order" dataNode="dn1,dn2,dn3,dn4" rule="sharding-by-murmur" />这里rule="sharding-by-murmur"必须与<function>标签中的name属性完全一致,且大小写敏感。曾有同事将函数名写成sharding-by-murmur(小写),而配置中写成ShardingByMurmur(驼峰),导致MyCAT启动时报Can't find function,但日志只显示ERROR [WrapperSimpleAppMain] (StartupListener.java:49) - startup error,无具体函数名提示,排查耗时6小时。
3.3 server.xml安全加固:别让默认配置成为攻击入口
MyCAT默认监听0.0.0.0:8066,且admin用户密码为空。在内网环境这很危险——我们曾遭遇一次内部扫描事件:某开发用个人笔记本连内网,笔记本中病毒后反向扫描10.0.0.0/16网段,发现MyCAT管理端口开放,尝试admin空口令登录成功,执行reload @@config_all导致配置重载失败,整个集群连接中断12分钟。
必须修改的三项:
- 绑定IP:在
<system>节点下添加<property name="bindIp">10.0.0.100</property>,限制仅监听业务服务器所在网段; - 禁用默认用户:删除
<user name="admin">区块,新增<user name="mycat_app">并设置强密码(至少12位,含大小写字母+数字+符号); - 关闭管理端口:若无需实时监控,注释掉
<property name="managerPort">9066</property>,彻底关闭9066端口。
提示:MyCAT的SQL防火墙功能(sqlExecuteTimeout、sqlRecordCount)在1.6.x版本存在性能缺陷,开启后QPS下降35%。我们改用前置Nginx做连接数限制(limit_conn_zone $binary_remote_addr zone=addr:10m; limit_conn addr 100;),效果更稳定。
4. 测试不是跑通SELECT,而是模拟线上每一处断裂点
4.1 基础连通性测试:用最笨的方法验证最核心链路
不要一上来就跑JMeter压测。先做三件事:
直连MyCAT验证协议兼容性:
mysql -h10.0.0.100 -P8066 -umycat_app -p'YourStrongPass123!' -e "SELECT VERSION();"如果返回
5.6.29-mycat-1.6.7.5,说明协议层正常;若报Unknown MySQL server host,检查MyCAT是否真正监听8066端口(netstat -tuln | grep 8066)。验证分片路由准确性:
在MyCAT客户端执行:/*#mycat:datanode=dn1*/ SELECT 1;这条注释指令强制SQL发往dn1节点。然后登录dn1对应的物理MySQL,查
show processlist,确认有来自MyCAT IP的连接。这是验证dataNode映射正确的黄金标准。检查心跳状态:
登录MyCAT管理端口:mysql -h10.0.0.100 -P9066 -umycat_app -p'YourStrongPass123!' -e "show @@heartbeat;"正常应显示所有dataHost状态为
idle,R/W列为W/R(主库可写从库可读)。若出现down,立即检查对应MySQL节点网络连通性及账号权限。
4.2 分片逻辑测试:用真实业务SQL击穿所有边界条件
我们设计了七类必测SQL,覆盖99%的线上场景:
| 测试类型 | 示例SQL | 预期行为 | 实际问题案例 |
|---|---|---|---|
| 单分片等值查询 | SELECT * FROM t_order WHERE order_id = 123456789; | 仅访问1个dataNode | 无 |
| 多分片IN查询 | SELECT * FROM t_order WHERE order_id IN (1,2,3,4,5); | 访问5个dataNode(若ID分散) | ID连续时可能集中到1个节点,需验证分片函数分布 |
| 跨分片JOIN | SELECT o.*, u.username FROM t_order o JOIN t_user u ON o.user_id = u.user_id; | MyCAT报错can't find table(未配置ER分片) | 改用/*!mycat:catlet=io.mycat.catlets.ShareJoin */强制走ShareJoin |
| 全局聚合 | SELECT COUNT(*) FROM t_order WHERE status = 1; | MyCAT合并各节点COUNT结果 | 当某节点超时,MyCAT返回部分结果,需配置<property name="sqlExecuteTimeout">300</property> |
| 分页查询 | SELECT * FROM t_order ORDER BY create_time DESC LIMIT 20,10; | MyCAT下发LIMIT 30到各节点,合并后取第20~30条 | 数据量大时内存溢出,需改用游标分页 |
| 跨库事务 | BEGIN; INSERT INTO t_order ...; INSERT INTO t_order_log ...; COMMIT; | MyCAT报错not support multi node transaction | 必须拆分为本地事务+最终一致性(发MQ) |
| DML跨分片 | UPDATE t_order SET status = 2 WHERE user_id = 1001 AND status = 1; | MyCAT将SQL广播到所有dataNode | 若user_id哈希后落在多个节点,会产生重复更新,必须加AND order_id IN (...)限定 |
最关键的测试是跨分片UPDATE。我们曾在线上执行UPDATE t_order SET pay_time = NOW() WHERE user_id = 1001,由于user_id哈希函数配置错误,该user_id被映射到dn1和dn2两个节点,结果两条订单记录pay_time都被更新,造成资损。因此,所有UPDATE/DELETE必须包含分片键的等值条件,MyCAT配置中应启用<property name="useHandshakeV10">true</property>,它会在SQL解析阶段校验WHERE条件是否包含分片键,不满足则直接拒绝执行。
4.3 故障注入测试:主动制造崩溃,才能信任稳定性
真正的高可用不是“不出问题”,而是“出问题时有预案”。我们定期做三类故障演练:
1. 主库宕机模拟
手动kill主库MySQL进程,观察MyCAT日志:
- 3秒内应出现
[INFO][$_NIOREACTOR-0-RW] (MySQLConnection.java:392) - connection closed by master - 10秒内
show @@heartbeat应显示对应dataHost状态变为down - 新写请求应自动路由到新的writeHost(需提前配置多主)
2. 网络分区测试
用iptables阻断MyCAT到某从库的3306端口:
iptables -A OUTPUT -d 192.168.1.11 -p tcp --dport 3306 -j DROP预期:读请求自动避开该从库,错误日志中出现connection refused但不影响整体服务。若MyCAT卡死,则说明<heartbeat>配置的switchType="1"(自动切换)未生效,需检查<writeHost>内<readHost>的weight参数是否为0。
3. 连接池耗尽攻击
用ab工具发起1000并发连接:
ab -n 10000 -c 1000 'http://test-api/order/list?uid=1001'观察MyCAT连接数:
show @@connection应显示活跃连接数接近1000,但不超过maxCon="1000"设定值- 超出连接数的请求应快速失败(响应码503),而非排队等待
实操心得:MyCAT的连接池是“懒加载”模式,即首次请求时才创建连接。压测前必须先用
mysql -h... -e "SELECT 1;"预热,否则首波请求会因连接创建耗时导致毛刺。我们编写了一个Python脚本,在每次部署后自动执行100次预热查询。
5. 常见问题与排查技巧实录:那些文档里不会写的真相
5.1 连接数爆满:不是MyCAT配置错了,是应用没关连接
现象:MyCAT监控显示PROCESSLIST中连接数持续增长,show @@connection返回2000+连接,但netstat -an | grep :8066 | wc -l只有300左右。
根因:Java应用使用Druid连接池,但未配置removeAbandonedOnBorrow=true,导致连接泄露。MyCAT的maxCon是硬上限,超出的请求被丢弃,而应用层重试机制又不断新建连接,形成雪崩。
解决方案:
- 应用层配置Druid:
druid.removeAbandonedOnBorrow=true druid.removeAbandonedTimeoutMillis=60000 druid.logAbandoned=true - MyCAT层增加熔断:在
server.xml中添加
限制单个处理器内存占用,避免OOM。<property name="processors">4</property> <property name="processorBufferPoolType">0</property> <property name="processorBufferChunk">4096</property>
5.2 查询结果不一致:不是数据不同步,是读写分离延迟
现象:用户下单后立即查询订单列表,新订单不显示;但1秒后刷新又出现了。
根因:MyCAT将查询路由到从库,而主从同步存在延迟(SHOW SLAVE STATUS中Seconds_Behind_Master=1.2)。
解决方案分三级:
- 业务层:对强一致性场景(如支付结果页),在URL中添加
?consistency=strong,MyCAT拦截该参数,强制路由到主库; - 中间件层:配置
<property name="slaveThreshold">100</property>(单位毫秒),当从库延迟超过100ms时自动降级到主库; - DB层:在从库执行
STOP SLAVE; START SLAVE;,但这是治标不治本,需优化主库大事务。
5.3 分片键变更:不是改配置就行,是数据迁移工程
需求:原用user_id分片,现需改为tenant_id(租户ID),因SaaS化改造。
误区:直接修改schema.xml中的rule属性,重启MyCAT。
后果:历史数据无法定位,新数据写入新分片,系统陷入半瘫痪。
正确流程:
- 双写阶段:应用同时写t_order_old(user_id分片)和t_order_new(tenant_id分片),MyCAT配置两个逻辑表;
- 数据迁移:用Spark读取t_order_old全量数据,按tenant_id重新分片写入t_order_new,迁移期间保持双写;
- 流量切换:灰度放开1%流量到t_order_new,监控错误率;
- 停写旧表:确认数据一致后,停止双写,下线t_order_old。
整个过程耗时17天,而非配置修改的17秒。
5.4 日志爆炸:不是磁盘不够,是日志级别没调
现象:MyCAT日志目录每天增长50GB,logs/mycat.log充满DEBUG [$_NIOREACTOR-1-RW] (MySQLConnection.java:222) - write to backend。
根因:log4j2.xml中<Logger name="io.mycat" level="debug" additivity="false">未注释。
解决方案:
- 生产环境必须设为
level="info"; - 关键模块单独调低:
这样路由日志只报WARN以上,后端处理只报ERROR,日志体积减少92%。<Logger name="io.mycat.route.RouteStrategy" level="warn"/> <Logger name="io.mycat.backend.mysql.nio.handler" level="error"/>
最后分享一个小技巧:MyCAT的
show @@sql.execute命令能实时查看最近100条执行SQL及其耗时,但默认关闭。在server.xml中添加:<property name="sqlExecuteLog">true</property> <property name="sqlExecuteLogLimit">100</property>开启后,运维同学不用登录每台MySQL,就能快速定位慢查询源头——是MyCAT路由错了,还是物理SQL本身有问题。这个功能救过我们三次P0级故障。