MyCAT分片实战:从踩坑到稳定落地的完整手记
1. 这不是“分库分表”的说明书而是一线团队踩坑三年后整理的分片落地手记你看到标题里“数据分片概述、部署MyCAT服务、测试配置案例”这三段式结构别急着划走——这不是教科书目录也不是PPT提纲。这是某中型互联网公司数据库组在2021年Q3到2023年Q2之间为支撑日均订单量从8万跃升至42万、单表峰值写入达每秒1700条记录的实战路径缩影。他们没用云厂商封装好的分布式数据库中间件而是选了MyCAT——不是因为情怀是因为当时自研中间件尚未通过灰度验证而商业产品license成本在预算红线之上。我参与过其中三个核心业务线的迁移改造从最初把MyCAT当“高级代理”用到后来能精准控制SQL路由、规避跨分片JOIN、预判主从延迟引发的读写不一致整个过程没有一份文档能覆盖真实场景里的90%问题。关键词里没写“MySQL”但所有操作都基于MySQL 5.7.28InnoDB引擎没提“高并发”但每个配置项背后都对应着压测时TPS掉点的具体曲线所谓“测试配置案例”其实是指在支付对账、用户行为日志、商品库存三个差异极大的业务模型上如何让同一套MyCAT集群稳定扛住不同读写特征的流量。它解决的从来不是“能不能分”而是“分得稳不稳、查得准不准、扩得快不快”。如果你正面临单库CPU持续高于75%、慢查询日志里大量出现“Sending data”状态、或者DBA开始反复提醒“这张表再不拆就要锁表维护”——那你不是在学一个中间件而是在接手一个必须今天就给出方案的生产事故。这篇文章不讲CAP理论推导不画抽象架构图只告诉你分片键怎么选才不会让80%的查询落到同一个节点、schema.xml里 标签的interval值设成30还是60实测下来差的是3.7秒故障感知延迟、为什么用catlet做跨库更新比用ER分片更可控、以及那个被官方文档轻描淡写带过的balance3参数在双主架构下实际会引发主键冲突的底层机制。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这种字段虽然查询频繁但每天变更数万次会导致跨分片UPDATEMyCAT无法保证事务原子性最终数据不一致。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改造对窗口函数、CTEWITH语句、JSON_EXTRACT等MySQL 5.7新特性支持不完整。上线前必须用实际业务SQL做全量兼容性测试不能只依赖单元测试用例。2.3 分片策略选择取模、范围、哈希、日期没有银弹只有权衡策略类型适用场景扩容痛点数据倾斜风险MyCAT配置示例取模mod-long用户ID、订单ID等数值型主键ID生成均匀需停机重分布扩容成本高低ID连续递增时可能集中function namehash classio.mycat.route.function.PartitionByModproperty namecount4/property/function一致性哈希murmur租户ID、设备ID等字符串主键需动态扩容节点增减仅影响邻近节点中哈希环分布不均function namesharding-by-murmur classio.mycat.route.function.PartitionByMurmurHashproperty nameseed0/propertyproperty namecount8/property/function范围分片auto-sharding-long时间戳、金额区间等有序字段新增范围需人工维护易产生热点高如所有新订单集中在最新分片function namerang-long classio.mycat.route.function.AutoPartitionByLongproperty namemapFileautopartition-long.txt/property/function日期分片sharding-by-date日志、监控等时序数据按月/周归档归档策略复杂冷热数据分离难极低时间天然均匀function namesharding-by-date classio.mycat.route.function.PartitionByDateproperty namedateFormatyyyy-MM-dd/propertyproperty namesBeginDate2023-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位机器ID12位序列号确保全局单调递增且无中心节点。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:MaxGCPauseMillis200 -XX:G1HeapRegionSize4M将GC停顿稳定在150ms内。操作系统层面必须关闭transparent_hugepageTHP。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 namehost1 maxCon1000 minCon10 balance3 writeType0 dbTypemysql dbDrivernative这里的balance3表示“读操作随机分发到所有writeHost和readHost”但前提是writeHost和readHost必须显式声明。我们曾误将从库配置在writeHost标签下导致MyCAT认为所有节点都是可写节点读请求也发往主库主库CPU瞬间拉满。正确做法是writeHost hostmaster1 url192.168.1.10:3306 usermycat password123456 readHost hostslave1 url192.168.1.11:3306 usermycat password123456/ readHost hostslave2 url192.168.1.12:3306 usermycat password123456/ /writeHost第二 的心跳检测机制直接影响故障转移速度heartbeatselect user()/heartbeat看似简单但select user()在MySQL 5.7中执行耗时约8ms而MyCAT默认心跳间隔为10秒interval10000。这意味着主库宕机后MyCAT最多需10秒才发现期间所有写请求失败。我们将interval改为30003秒同时将SQL优化为select 1实测故障感知时间压缩至3.2秒。但要注意过于频繁的心跳会增加DB负载我们通过监控发现当interval 2000时从库IOPS上升12%故最终定为3000。第三 的rule属性必须与分片函数严格匹配table namet_order dataNodedn1,dn2,dn3,dn4 rulesharding-by-murmur /这里rulesharding-by-murmur必须与function标签中的name属性完全一致且大小写敏感。曾有同事将函数名写成sharding-by-murmur小写而配置中写成ShardingByMurmur驼峰导致MyCAT启动时报Cant 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 namebindIp10.0.0.100/property限制仅监听业务服务器所在网段禁用默认用户删除user nameadmin区块新增user namemycat_app并设置强密码至少12位含大小写字母数字符号关闭管理端口若无需实时监控注释掉property namemanagerPort9066/property彻底关闭9066端口。提示MyCAT的SQL防火墙功能sqlExecuteTimeout、sqlRecordCount在1.6.x版本存在性能缺陷开启后QPS下降35%。我们改用前置Nginx做连接数限制limit_conn_zone $binary_remote_addr zoneaddr:10m; limit_conn addr 100;效果更稳定。4. 测试不是跑通SELECT而是模拟线上每一处断裂点4.1 基础连通性测试用最笨的方法验证最核心链路不要一上来就跑JMeter压测。先做三件事直连MyCAT验证协议兼容性mysql -h10.0.0.100 -P8066 -umycat_app -pYourStrongPass123! -e SELECT VERSION();如果返回5.6.29-mycat-1.6.7.5说明协议层正常若报Unknown MySQL server host检查MyCAT是否真正监听8066端口netstat -tuln | grep 8066。验证分片路由准确性在MyCAT客户端执行/*#mycat:datanodedn1*/ SELECT 1;这条注释指令强制SQL发往dn1节点。然后登录dn1对应的物理MySQL查show processlist确认有来自MyCAT IP的连接。这是验证dataNode映射正确的黄金标准。检查心跳状态登录MyCAT管理端口mysql -h10.0.0.100 -P9066 -umycat_app -pYourStrongPass123! -e show heartbeat;正常应显示所有dataHost状态为idleR/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个节点需验证分片函数分布跨分片JOINSELECT o.*, u.username FROM t_order o JOIN t_user u ON o.user_id u.user_id;MyCAT报错cant find table未配置ER分片改用/*!mycat:catletio.mycat.catlets.ShareJoin */强制走ShareJoin全局聚合SELECT COUNT(*) FROM t_order WHERE status 1;MyCAT合并各节点COUNT结果当某节点超时MyCAT返回部分结果需配置property namesqlExecuteTimeout300/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必须拆分为本地事务最终一致性发MQDML跨分片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 nameuseHandshakeV10true/property它会在SQL解析阶段校验WHERE条件是否包含分片键不满足则直接拒绝执行。4.3 故障注入测试主动制造崩溃才能信任稳定性真正的高可用不是“不出问题”而是“出问题时有预案”。我们定期做三类故障演练1. 主库宕机模拟手动kill主库MySQL进程观察MyCAT日志3秒内应出现[INFO][$_NIOREACTOR-0-RW] (MySQLConnection.java:392) - connection closed by master10秒内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配置的switchType1自动切换未生效需检查writeHost内readHost的weight参数是否为0。3. 连接池耗尽攻击用ab工具发起1000并发连接ab -n 10000 -c 1000 http://test-api/order/list?uid1001观察MyCAT连接数show connection应显示活跃连接数接近1000但不超过maxCon1000设定值超出连接数的请求应快速失败响应码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连接池但未配置removeAbandonedOnBorrowtrue导致连接泄露。MyCAT的maxCon是硬上限超出的请求被丢弃而应用层重试机制又不断新建连接形成雪崩。解决方案应用层配置Druiddruid.removeAbandonedOnBorrowtrue druid.removeAbandonedTimeoutMillis60000 druid.logAbandonedtrueMyCAT层增加熔断在server.xml中添加property nameprocessors4/property property nameprocessorBufferPoolType0/property property nameprocessorBufferChunk4096/property限制单个处理器内存占用避免OOM。5.2 查询结果不一致不是数据不同步是读写分离延迟现象用户下单后立即查询订单列表新订单不显示但1秒后刷新又出现了。根因MyCAT将查询路由到从库而主从同步存在延迟SHOW SLAVE STATUS中Seconds_Behind_Master1.2。解决方案分三级业务层对强一致性场景如支付结果页在URL中添加?consistencystrongMyCAT拦截该参数强制路由到主库中间件层配置property nameslaveThreshold100/property单位毫秒当从库延迟超过100ms时自动降级到主库DB层在从库执行STOP SLAVE; START SLAVE;但这是治标不治本需优化主库大事务。5.3 分片键变更不是改配置就行是数据迁移工程需求原用user_id分片现需改为tenant_id租户ID因SaaS化改造。误区直接修改schema.xml中的rule属性重启MyCAT。后果历史数据无法定位新数据写入新分片系统陷入半瘫痪。正确流程双写阶段应用同时写t_order_olduser_id分片和t_order_newtenant_id分片MyCAT配置两个逻辑表数据迁移用Spark读取t_order_old全量数据按tenant_id重新分片写入t_order_new迁移期间保持双写流量切换灰度放开1%流量到t_order_new监控错误率停写旧表确认数据一致后停止双写下线t_order_old。整个过程耗时17天而非配置修改的17秒。5.4 日志爆炸不是磁盘不够是日志级别没调现象MyCAT日志目录每天增长50GBlogs/mycat.log充满DEBUG [$_NIOREACTOR-1-RW] (MySQLConnection.java:222) - write to backend。根因log4j2.xml中Logger nameio.mycat leveldebug additivityfalse未注释。解决方案生产环境必须设为levelinfo关键模块单独调低Logger nameio.mycat.route.RouteStrategy levelwarn/ Logger nameio.mycat.backend.mysql.nio.handler levelerror/这样路由日志只报WARN以上后端处理只报ERROR日志体积减少92%。最后分享一个小技巧MyCAT的show sql.execute命令能实时查看最近100条执行SQL及其耗时但默认关闭。在server.xml中添加property namesqlExecuteLogtrue/property property namesqlExecuteLogLimit100/property开启后运维同学不用登录每台MySQL就能快速定位慢查询源头——是MyCAT路由错了还是物理SQL本身有问题。这个功能救过我们三次P0级故障。