分库分表这件事,配置层面本身不难,难的是报错出来之后你根本分不清是配置问题还是环境问题。前阵子我帮某公司处理一个订单库的分库故障,MyCat启动正常,心跳正常,但只要一执行SQL,日志里就是整屏的NoRouteToHostException: 没有到主机的路由。当时团队里好几个人第一反应是"是不是MyCat的schema配错了,分片路由出问题了",结果把rule.xml翻来覆去改了好几遍,一点用没有。后来冷静下来从头排查,才发现根子根本不在分库配置,而在中间件到后端MySQL的网络链路上。这篇就把整个排查过程和分库配置的规范做法一起整理出来,给同样被这个报错坑过的人一个参考。
1. 故障现场:分库配置完,一执行SQL就报错
1.1 报错日志长什么样
先说背景。这套系统的订单表单库数据量已经涨到几亿行,单表查询和写入都在恶化,于是计划用MyCat把订单数据按订单ID哈希分到4个MySQL实例上。MyCat启动过程很顺利,wrapper.log里没有异常,mycat.log里也有正常的启动记录。结果等到应用侧一发起查询,日志就开始刷错误,典型的报错长这样:
ERROR 14:22:31.123 MyCat-Check 09:12:33.123 - connection error java.net.NoRouteToHostException: 没有到主机的路由 at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) at com.mysql.jdbc.StandardSocketFactory.connect(StandardSocketFactory.java:156) ...注意这里的关键词是NoRouteToHostException,中文提示"没有到主机的路由"。这个异常在Java网络编程里是一个非常明确的信号:尝试建立TCP连接时,底层操作系统返回了EHOSTUNREACH错误。也就是说,TCP三次握手的SYN包根本没有抵达目标主机,或者目标主机在协议栈层面直接回应了"主机不可达"。这不是业务逻辑问题,也不是分片规则算错了,是网络层的问题。
很多人在这一步容易犯的错误是盯着StandardSocketFactory.connect和com.mysql.jdbc这些字样,以为是MySQL驱动的问题,甚至怀疑是驱动版本太低。实际上com.mysql.jdbc.StandardSocketFactory只是JDBC驱动创建socket的入口,真正报错的点是底层socket连接失败。报错信息已经把问题定位到网络层了,再往下查就是IP、端口、防火墙、路由这几件事。
1.2 中间件架构决定了"网络报错"不是小事
MyCat的本质是一个数据库代理,它对外伪装成MySQL服务器,业务应用连的是MyCat的8066端口,然后MyCat根据分片规则把SQL拆解后转发给真正的后端MySQL节点。这里存在两层网络连接:应用到MyCat是一层,MyCat到每个后端数据节点又是一层。
分库之后,后端通常有多个数据源节点,这带来的问题是:任何一个节点连不上,整个查询链路就失败。因为MyCat的SQL路由是编译期确定的,它先解析SQL,找到涉及的表对应的分片节点,然后并行或串行地把子SQL发到各个节点。如果一个分片节点不可达,那对应的子查询直接抛异常,最终反映到应用侧就是整个查询失败。
更难排查的是,MyCat的故障表现往往不是"某一张表查不了",而是"几乎所有SQL都不正常"。因为select user()这条心跳语句会尝试连接所有配置的数据主机,一旦某个dataHost连不上,它会被标记为不可用,后续所有路由到这个dataHost的请求都会快速失败。这就会造成一个错觉:明明配置了4个节点,感觉像是全部坏了。
所以遇到这类问题,我个人的习惯是:先不看分片规则,先确认中间件和后端数据库之间的物理链路是否畅通。链路不通,配置再正确也是白搭。
2. 这个报错的真实含义:从TCP/IP开始拆
2.1 NoRouteToHostException 是怎么产生的
要理解这个报错,得从TCP连接的建立过程说起。应用发起new Socket(ip, port)的时候,操作系统会做这几件事:
- 查本地路由表,确定去目标IP应该走哪块网卡、下一跳是谁。
- 如果目标IP在同一局域网,直接通过ARP协议解析目标IP的MAC地址。
- 发出SYN包,等待目标主机的SYN-ACK响应。
NoRouteToHostException对应的系统错误码是EHOSTUNREACH,触发它的常见场景有这么几类:
- 本地路由表中没有到达目标网段的路由条目,或者默认路由缺失。
- 目标IP在同一网段,但ARP解析不到对应的MAC地址,说明目标主机在当前网络中不存在或者网卡没起来。
- 中间设备(路由器、防火墙)返回了ICMP的"目标主机不可达"报文。
- 目标主机上的防火墙规则主动返回了
icmp-host-prohibited或类似的主机不可达消息。 - IPv6/IPv4地址解析混乱,比如DNS解析出的是IPv6地址,但当前网络没有IPv6路由,连接IPv6地址时报NoRouteToHost。
这里要区分几个容易混淆的错误:如果目标端口没开,通常会得到"Connection refused(连接被拒绝)";如果防火墙直接丢弃报文,表现通常是"Connection timed out(连接超时)";而"No route to host"最典型的特征就是目标主机本身不可达,或者被防火墙以"主机不可达"的方式拒绝。
2.2 分库场景里最常见的三种触发原因
结合我实际处理过的MyCat故障,这个报错在分库环境里绝大多数是下面三种原因之一:
第一种,dataHost的URL里IP地址写错了。听起来很蠢,但实际发生的频率非常高。尤其是配置从旧环境拷贝过来、或者服务器扩容后IP变更,但schema.xml里还是旧IP。如果写的是一个当前网络中不存在的地址,连接时就会出现"No route to host"。
第二种,后端MySQL服务器的防火墙规则针对来源IP做了限制。比如MySQL主机上配了iptables白名单,只允许某个旧中间件服务器的IP访问3306端口,新换的MyCat服务器IP不在白名单里。默认规则如果配置的是REJECT --reject-with icmp-host-prohibited,那从新MyCat发来的连接就会收到ICMP主机不可达响应,Java层面直接报NoRouteToHostException。
第三种,MyCat所在主机和后端MySQL所在主机不在同一个网段,中间的网络设备没有配路由,或者云环境的安全组规则不允许两个网段互通。特别是在混合部署场景里,比如MyCat跑在某个容器网络里,后端MySQL在传统虚拟机网络里,中间的网络策略没打通,也会出现这个报错。
2.3 排查原则:先网络后配置,别一上来就改文件
我见过太多人在报错后第一件事就是改配置文件,今天改rule.xml,明天改server.xml,后天升级JDBC驱动,结果问题依旧。这里分享一个我个人非常坚持的排查顺序原则:先证明网络通不通,再谈配置对不对。
原因是网络链路是配置生效的前提。MyCat本质上就是一堆TCP连接的管理器,它不能逾越操作系统网络层的限制。先把"中间件主机能访问后端MySQL主机的3306端口"这个事实确认下来,后面所有的配置排查才成立。否则你在schema.xml里改了一百遍IP,实际上网络根本不通,那和没改一样。
正确的排查顺序是:
- 确认配置中写的IP是不是后端MySQL的真实IP。
- 从MyCat主机ping后端MySQL主机,验证三层连通性。
- 用telnet或nc验证3306端口是否可达。
- 检查两端防火墙、云安全组规则。
- 检查MySQL服务的监听地址和账号授权。
- 全部确认无误后,再回头看MyCat的配置细节。
3. 完整排查实录:从日志到修复的全过程
3.1 第一步:核对我方主机到目标主机的连通性
把这个案例展开讲。当时MyCat部署在一台新服务器上,IP是192.168.1.55,后端MySQL的IP是192.168.1.101。先看schema.xml里的dataHost配置,确认url字段写的是jdbc:mysql://192.168.1.101:3306/order_db,IP没写错。
然后在MyCat主机上直接ping目标IP:
ping -c 4 192.168.1.101结果能通,TTL正常,丢包率0。这说明三层网络基本是通的,至少"目标主机存在、本机有路由到达目标"这个大前提是成立的。
紧接着用ip route确认路由条目:
ip route | grep 192.168.1.0输出显示192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.55,同一个网段直连,路由没有问题。
这一步很多人会跳过,但它的价值在于把"目标不存在"和"目标存在但被防火墙挡住"这两种情况区分开。ping不通,可能是IP错了、网段不通、或者交换机问题;ping得通但TCP连不上,那问题大概率出在端口、防火墙或者MySQL服务本身。
3.2 第二步:确认3306端口是否可达
三层通了,接下来验证四层端口。用nc做一次快速的TCP端口探测:
nc -vz -w 5 192.168.1.101 3306结果出现了很关键的现象:命令挂在那里卡了一会儿,然后报connect to 192.168.1.101 port 3306 (tcp) failed: No route to host。
注意这个细节很有价值。如果端口完全没监听,通常报的是Connection refused;如果是防火墙DROP规则,会卡到超时;而这里明确报的是"No route to host",和MyCat日志里的异常完全一致。这基本可以把问题锁定在:目标主机存在,但它对来自192.168.1.55这个来源的TCP连接,以"主机不可达"的方式进行了拒绝。
为了进一步验证是不是来源IP差异导致的,我在另一台已经被允许访问MySQL的旧服务器上做同样的测试:
nc -vz -w 5 192.168.1.101 3306结果秒开,Connected to 192.168.1.101。到这一步,结论已经呼之欲出了:不是网络拓扑的问题,不是MySQL服务的问题,是这个后端主机针对192.168.1.55这个来源IP做了拒绝。
3.3 第三步:查防火墙和安全组,锁定真凶
既然怀疑是防火墙,那就上MySQL主机看iptables规则:
iptables -L -n --line-numbers看到INPUT链里有这么几条规则:
1 ACCEPT tcp -- 192.168.1.100 0.0.0.0/0 tcp dpt:3306 2 ACCEPT tcp -- 192.168.1.0/24 0.0.0.0/0 tcp dpt:3306 3 REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited规则里放行了旧中间件服务器192.168.1.100,也放行了整个192.168.1.0/24网段,但最后一条默认REJECT规则设置了reject-with icmp-host-prohibited。这里有个知识点:REJECT和DROP不同,DROP是直接把包丢掉,对端表现为超时;而REJECT会主动回一个ICMP错误报文,对端收到icmp-host-prohibited后,内核会返回EHOSTUNREACH,Java就把它翻译成NoRouteToHostException。
那为什么网段白名单里有192.168.1.0/24,MyCat的192.168.1.55还是被拒了?继续看规则才发现,192.168.1.0/24的那条ACCEPT规则被加在REJECT规则之后,而且链上还有一条针对192.168.1.55的单独REJECT规则,优先级更高,排在最前面:
0 REJECT all -- 192.168.1.55 0.0.0.0/0 reject-with icmp-host-prohibited这条规则的来源是旧运维在"临时屏蔽某台问题主机"时加的,后来忘了清理。新MyCat恰好复用了这个IP,于是被一视同仁地拒之门外。这个问题在日志里看起来极其诡异,因为配置、网络、路由全都没问题,但就是连不上。
找到规则后处理就很简单了,先临时移除这条规则验证:
iptables -D INPUT 1再回到MyCat主机上测试端口:
nc -vz -w 5 192.168.1.101 3306这次秒连成功。为了确保重启后规则不会恢复,需要把修正后的iptables规则持久化。不同发行版持久化方式不同,CentOS 6用service iptables save,CentOS 7以上如果用的是firewalld则用firewall-cmd管理,如果仍然用的iptables服务,同样先保存再确认。另外要检查一下是否还有别的工具在开机时加载旧规则,比如/etc/sysconfig/iptables文件里也要同步清理。
3.4 第四步:修复并验证分库读写
防火墙问题解决后,回到MyCat主机上先直接测MySQL连接,确认后端账号本身没问题:
mysql -h 192.168.1.101 -u mycat -p -P 3306 -e "select user()"能正常返回,说明MySQL侧账号授权也没问题。然后重启MyCat,观察日志,心跳信息从失败变为正常,PhysicalDBPool里的节点状态变成运行中。
最后一条SQL彻底验证分库读写:
mysql -h 127.0.0.1 -P 8066 -u mycat -p -e "select count(1) from order_db.orders"能正常返回统计结果。再插入一条测试数据,分别登录4个后端MySQL实例查一下数据落到了哪个分片,确认哈希分片规则按预期工作。到这里,问题才算真正闭环。
4. 分库配置的规范操作:schema.xml和rule.xml怎么填才不出错
4.1 schema.xml 的核心结构
网络问题解决后,我们回到MyCat配置本身。很多人在配置分库时容易忽略一个事实:schema.xml不仅定义了逻辑库和逻辑表,还定义了dataHost到物理MySQL的映射关系。理解这张映射,是配置不出错的根本。
一个最小可用的schema.xml核心结构是这样的:
<mycat:schema xmlns:mycat="http://io.mycat/"> <schema name="order_db" checkSQLschema="false" sqlMaxLimit="100"> <table name="orders" dataNode="dn1,dn2,dn3,dn4" rule="order-hash" /> </schema> <dataNode name="dn1" dataHost="host1" database="order_db" /> <dataNode name="dn2" dataHost="host2" database="order_db" /> <dataNode name="dn3" dataHost="host3" database="order_db" /> <dataNode name="dn4" dataHost="host4" database="order_db" /> <dataHost name="host1" maxCon="100" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1"> <heartbeat>select user()</heartbeat> <writeHost host="mysql1" url="jdbc:mysql://192.168.1.101:3306/order_db" user="mycat" password="xxx" /> </dataHost> <!-- host2 host3 host4 结构相同,url替换为对应IP --> </mycat:schema>逐个解释关键属性:
schema的name是逻辑库名,应用连接MyCat后看到的库就是这个名字,和后端物理库名无关。table标签里的dataNode指定这个逻辑表的数据落在哪几个数据节点上,多个节点用逗号分隔。rule指定分片规则名称,对应rule.xml里定义的tableRule。dataNode里的database是后端物理MySQL上的真实库名,每个dataNode必须保证这个库存在。dataHost里的balance代表读写负载均衡策略,writeType代表写节点选择方式。分库不分读写分离的话,balance="0"表示不启用读写分离,writeType="0"表示所有写操作都发给第一个writeHost。heartbeat是MyCat对后端节点的心跳检测SQL,select user()是MySQL下最简单的存活探测语句。心跳失败会导致dataHost被标记为不可用。
这里有一个新手常踩的坑:dataNode的database和后端实际库名不一致。比如逻辑库叫order_db,后端物理库叫order_db_01,如果dataNode里写了order_db而物理机上没有这个库,MyCat启动时可能不会立刻报错,但运行时会频繁报"Table doesn't exist"或者连接异常。所以配置前先去每台物理机上确认库名完全一致。
4.2 rule.xml 分片规则与数据节点映射
rule.xml是分库的"切分算法"所在,它由tableRule和function两部分组成。tableRule定义哪些表用哪个函数、按哪个字段分片;function定义具体的分片算法实现和参数。
订单分库最常用的哈希分片配置如下:
<tableRule name="order-hash"> <rule> <columns>order_id</columns> <algorithm>my-hash</algorithm> </rule> </tableRule> <function name="my-hash" class="io.mycat.route.function.PartitionByHash"> <property name="partitionCount">4</property> </function>这里partitionCount设置为4,含义是把数据分成4个分片,对应schema.xml里dn1,dn2,dn3,dn4这4个dataNode。要注意的是,PartitionByHash做的是对分片键取hash值再对分片数取模,所以分片数变了,同一个key落在的分片位置就全变了。
这也是分库方案设计中最需要谨慎的一步:分片数一旦确定,后期扩容就要做数据迁移,不是简单改个数字就行。比如订单表按order_id哈希分成4片,当数据量涨到需要扩成8片时,必须重新分布历史数据,这个成本非常高。所以规划阶段建议把未来两到三年的数据增长估算进去,宁可初始分片数多一点,也不要频繁扩容。
还有一个细节值得说:分片字段的选择直接决定查询效率。order_id这种业务主键适合做哈希分片,因为订单查询通常按订单ID精确查询,路由能精准定位到单个分片。如果拿一个不会出现在查询条件里的字段做分片键,那大部分查询都会变成全分片扫描,分库的性能优势就没了。
4.3 一套配置自检清单(可直接抄作业)
配置完之后不要急着联调,我建议按下面的清单过一遍,能挡掉大部分低级问题:
- 所有后端MySQL实例能ping通,3306端口能从MyCat主机访问。
- 每台后端MySQL上已创建对应的物理库,库名和schema.xml里
dataNode的database完全一致。 - 每台后端MySQL上已创建MyCat连接账号,并且授权了对应库的权限。比如执行
CREATE USER 'mycat'@'192.168.1.%' IDENTIFIED BY 'password';和GRANT ALL PRIVILEGES ON order_db.* TO 'mycat'@'192.168.1.%';。注意@后面的host要能覆盖MyCat主机的IP,不能只授权localhost。 - MyCat的
server.xml里已配置应用连接账号,并且指定了能访问的逻辑库schema。 - schema.xml里所有
dataHost的url中的IP、端口、库名正确,账号密码正确。 - rule.xml里的分片数
partitionCount和schema.xml里table标签的dataNode数量一致。 - 配置修改后用
mycat start重启,观察wrapper.log和mycat.log是否出现"binding"或心跳成功的字样。 - 用mysql客户端连一下MyCat 8066端口,对每个逻辑表执行
show create table确认表结构能正常返回。
这套清单是我每次搭分库环境都会走一遍的流程。表面上看都是琐碎检查,但分库拆分的链路比较长,任何一个环节的小错误,最终都会变成线上一个让人抓狂的诡异报错。
5. 常见问题速查表与实操避坑心得
5.1 分库网络报错速查表
这里整理一张速查表,把分库部署阶段容易遇到的报错、可能原因和排查方向列出来。遇到问题先对号入座,能省不少时间。
| 报错特征 | 可能原因 | 排查方向 |
|---|---|---|
| NoRouteToHostException: 没有到主机的路由 | 目标IP不可达/防火墙REJECT/路由缺失 | ping、nc、iptables、安全组 |
| ConnectException: Connection refused | 端口未监听/端口级拒绝 | 查MySQL服务、ss -lntp |
| SocketTimeoutException: Read timed out | 防火墙DROP规则/网络丢包 | 抓包、检查防火墙默认策略 |
| Access denied for user | MySQL账号不存在或host不匹配 | 检查授权host、密码 |
| Unknown database | dataNode里库名错误 | 核对物理库名 |
| Table doesn't exist | 逻辑表未建到所有后端节点 | 在所有物理库建表 |
这张表里我最想强调的是第一行和第三行的区别。很多人分不清"No route to host"和"Read timed out",前者说明对端的拒绝是主动的、快速的,通常是REJECT策略或网络不可达;后者说明包发出去了但石沉大海,通常是DROP策略或网络黑洞。搞清楚这两个区别,排查防火墙规则的时候能省一半力气。
5.2 几条踩坑后总结的心得
第一个心得:分库之后,MyCat主机到后端MySQL的链路健康度,决定了整个系统的可用性。所以在上线前,建议额外写一个定时探测脚本,在MyCat主机上定期检查每个后端节点的3306端口连通性,出现异常及时告警,而不是等业务方发现查询失败了才回头看日志。
第二个心得:打防火墙白名单一定要规范化。这个案例里如果运维当时能按IP段统一管理,并且每个加规则的变更都走工单记录,就不至于留一条私加的REJECT规则在线上一放几个月。不是说防火墙规则不能临时加,而是规则一定要有备注、要有负责人、要有定期清理机制。
第三个心得:数据节点的主从配置里,balance和writeType一定要理解清楚再填。如果后端只有一个MySQL实例,balance="0"没问题。如果后面加了从库但没配好主从同步,又把balance调成"1",MyCat会把读请求分发到从库,主从延迟直接造成脏读,那种问题比连不上更难查。
第四个心得:分库配置改动前,先备份一份正常的schema.xml和rule.xml。这个习惯救过我很多次。改配置之前先把当前文件复制一份带时间戳的备份,出问题了秒级回滚,不用靠记忆把配置改回去。
还有一点是给Java应用侧的:如果应用连MyCat超时,应用侧的连接池参数也要配套调整。MyCat本身只是中间件,connectTimeout、socketTimeout这些参数如果设置得太小,网络抖动时应用会先于MyCat把连接判定为失败。分库环境下节点多,一次查询要跨多个物理连接,网络链路的复杂度本来就高,参数留一点余量是必要的。
以上这些经验,都是在这个分库故障的处理过程中一点点沉淀出来的。问题本身不算复杂,一个防火墙规则就能让整个分库架构瘫痪,这恰恰说明中间件场景下网络基础设施的稳定性有多重要。如果以后再遇到NoRouteToHostException,先别怀疑分片规则,从网络层一步步重新走一遍,你可能会发现,真凶就在你觉得最不可能的那个位置。