ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Mycat分库连不上?NoRouteToHostException路由排查与解决指南

Mycat分库连不上?NoRouteToHostException路由排查与解决指南

1. 问题出现的真实场景:从分库配置到连接失败

先交代一下背景。我们做了一个模拟项目X,数据量涨得比较快,单库单表的压力已经有点明显了,所以想通过中间件把数据拆到多个MySQL实例上。选的方案是mycat,版本是1.6系列。配置分库的时候,逻辑库、逻辑表、dataNode、dataHost都按文档配好了,启动也提示成功,结果一执行查询就抛异常,日志里反复出现这么一行:

java.net.NoRouteToHostException: 没有到主机的路由

当时第一反应是网络不通,但用mysql客户端直接连后端数据库是完全正常的,ping也没问题,这就让人很费解。后来把mycat的日志翻了个底朝天,才发现问题并不在mycat本身,而是它和后端MySQL之间的连接路由走不通。

这里要说明一下,本文所有实例、配置、工程名均采用脱敏虚构处理,只保留技术细节,方便你对照自己的环境排查。

2. 理解mycat分库的连接机制

mycat本身不存数据,它只是一个代理层。你发给它的SQL,它会解析出要访问哪些分片,然后通过它自己的连接池去连后端真实的MySQL实例,拿到结果再返回给你。所以mycat和后端数据库之间的网络连通性,决定了分库能不能用。

这个环节里有两个容易忽略的点:

  • mycat连接后端数据库时的IP,是你配置的dataHost里的ip,不是你在应用里连mycat的那个IP。
  • mycat的heartbeat(心跳)会定期去连后端实例,如果心跳都失败,它会把该dataNode标记为不可用,后续SQL直接拒绝路由。

报"NoRouteToHostException"的时候,通常意味着TCP层已经走到了“路由不可达”的状态。也就是说,网络包发出去了,但在某个网关或主机上被回绝,没有一条可以抵达目标主机的路径。

听上去像是网络问题对不对?但实际情况里,有很大一部分是mycat自身配置或运行环境导致的“假路由不可达”。

3. 第一次排查:网络真的通吗?

我当时的排查步骤是先按传统思路来:

  1. ping后端MySQL主机,能通。
  2. telnet后端MySQL端口,能通。
  3. 在mycat所在主机上,用mysql命令行直接连后端数据库,能连。
  4. mycat启动日志看起来正常。

这就怪了,一切正常,为什么mycat就是连不上。

后来发现,mycat连接后端时使用的地址,并不是我们以为的那个地址。为什么这么说?因为我配置dataHost的时候,写的是内网IP,但mycat所在的主机是一台双网卡机器,默认路由走的另一张网卡,而那张网卡到后端MySQL的网段恰好没有路由。直接ping或telnet的时候,命令本身走了应用层的系统路由表,没走到mysqld那一步,所以看起来是通的。而mycat的Java虚拟机在建立socket时,如果系统路由表里没有那个目的地址的路由项,就会抛NoRouteToHostException,而不是简单的Connection refused。

这台机器的路由表长这样:

default via 10.0.0.1 dev eth0 10.10.0.0/16 dev eth1 scope link

后端MySQL的IP是10.20.0.10,它既不在eth1直连网段,也没有一条路由指向10.20.0.0/16,default路由又只能从eth0出去。而eth0那一边的网络本身到不了10.20.0.10,所以内核在发包时找不到合适的下一跳,直接返回了"No route to host"。

这种问题在物理机上不多见,但在虚拟化环境、容器环境、多网卡服务器上非常常见。你用系统命令去测觉得通,实际程序跑起来就是不通,核心差别就在路由表。

后来还是在mycat所在主机上执行了这句命令才发现的:

ip route get 10.20.0.10

结果输出里明确写着 “network is unreachable”,那一刻就知道问题出在哪了。

4. 根源解析:NoRouteToHostException到底是谁抛出来的

很多人遇到这个异常,第一反应是mycat配置问题,其实这个异常是操作系统底层网络栈返回的。Java的socket connect操作,在操作系统层面如果发现目标网络不可达,会返回ENETUNREACH,Java直接映射为NoRouteToHostException。

这里要补充一个容易混淆的知识点:

  • Connection refused:目标主机的端口没有服务在监听,或者防火墙主动拒绝了,这种是能到主机但到不了端口。
  • No route to host:网络层已经判定目标主机路径不存在,通常是路由配置、网段隔离、或目标主机关机等。

所以如果你看到的是NoRouteToHostException,就不要再纠结端口和防火墙了,先确认路由和网络拓扑。

mycat里的具体表现是:

  • heartbeat日志里持续出现NoRouteToHostException。
  • 原本可以正常查询的SQL开始报“Can't connect to MySQL server on 'xxx' (NoRouteToHostException)”或“Connection refused”。
  • mycat的日志文件里堆了相同的堆栈信息。

很多团队在容器里跑mycat时也会遇到类似问题,因为容器默认的bridge网络和宿主机网络不在同一路由域,mycat容器不能用宿主机IP去连后端数据库,必须保证容器到后端数据库的路由可用。

5. 排查过程复盘:详细操作记录

下面是我当时的完整排查过程,你按这个顺序走一遍基本能定位80%的问题。

5.1 第一步:确认mycat连接的是哪个IP

打开mycat的schema.xml,找到类似这样的配置片段:

<dataHost name="host1" maxCon="10" minCon="1" balance="0" writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1"> <heartbeat>select user()</heartbeat> <writeHost host="node1" url="10.20.0.10:3306" user="mycat_user" password="xxxxxx" /> </dataHost>

注意url里写的是10.20.0.10:3306,这个就是mycat要连接的地址。请确认这个IP和端口确实是后端MySQL的地址,而不是映射后的VIP、漂移IP之类容易被混淆的地址。

5.2 第二步:在mycat所在主机上验证路由

这里不用ssh到mycat容器里去看,直接在宿主机上跑:

ip route get 10.20.0.10

如果结果是:

10.20.0.10 via 10.0.0.1 dev eth0 src 10.0.0.5

说明有路由,问题不一定是路由。如果结果是:

RTNETLINK answers: Network is unreachable

那基本就是路由缺失,尤其你要确认mycat进程实际运行在哪个netns里,别用宿主机的路由去判断容器内的网络。

还可以在mycat主机上执行:

route -n

看有没有到10.20.0.0/24这类的路由条目。比如有:

10.20.0.0/24 10.0.0.1 0.0.0.0 UG 0 0 0 eth0

这就是OK的。没有的话,就需要手动加一条静态路由。

5.3 第三步:检查防火墙和iptables规则

虽然NoRouteToHost不一定和防火墙直接相关,但不能排除某些云环境里的安全组策略强制返回不可达。检查一下本机iptables:

iptables -L -n

尤其留意OUTPUT链和FORWARD链是否有REJECT规则。此外,如果mycat跑在云服务器上,看一下安全组里是否放行了3306端口,以及是否限制了源IP。

5.4 第四步:看mycat日志

mycat日志默认在logs目录下,一般是mycat.log。搜索关键字"route"或者"heartbeat",可以看到类似:

ERROR java.sql.SQLException: NoRouteToHostException: 没有到主机的路由

注意看堆栈里的IP,能直接看出它尝试连接的地址是什么。

5.5 第五步:检查mycat所在环境是否有多网卡或路由策略

这是最常见也最隐蔽的问题。如果mycat跑在一台多网卡的机器上,比如一张网卡走办公网,一张网卡走业务网,而默认网关在办公网,那么mycat连业务网段的数据库时,很有可能从错误的网卡出去,导致路由不可达。

解决办法是添加一条更精确的静态路由,指定目标网段走业务网卡:

ip route add 10.20.0.0/24 via 10.0.0.1 dev eth0

或者针对单个IP添加:

ip route add 10.20.0.10/32 via 10.0.0.1 dev eth0

这里需要注意,这条命令只是临时生效,重启网络后会消失。要持久化,在CentOS环境可以写到/etc/sysconfig/network-scripts/route-eth0里,内容格式为:

10.20.0.0/24 via 10.0.0.1 dev eth0

在Ubuntu环境,写到/etc/netplan/下的yaml配置文件里,或者在/etc/network/interfaces里追加up命令。

提示:容器环境里的情况更特殊,宿主机加路由不一定能进容器,要在容器所在network namespace内操作,或者用docker run的--network参数指定host网络模式。

6. 验证与后续优化:让mycat稳定运行

路由改好之后,先在mycat所在主机上重新验证:

ping 10.20.0.10 telnet 10.20.0.10 3306 ip route get 10.20.0.10

这三步都能通过后,重启mycat服务,观察logs/mycat.log。

心跳日志如果变成:

INFO heartbeat for host1 is healthy

那就说明已经正常了。接着在mycat命令行下执行:

show @@datasource;

看一下dataHost的状态字段,如果是1或2,表示SYN或IDLE,如果是-1之类的负数,还是有问题。

我当时修复完路由后,还顺手做了几个优化,这里一并分享:

  • 将mycat的JDBC连接池的initialSize和minCon调小,避免启动时大量并发连接导致部分连接因网络抖动失败。
  • 将heartbeat的间隔适当调大,减少因网络瞬断导致频繁切换。
  • 在schema.xml里给dataHost加上了connectionInitSql,确保连接建立后先执行一次set names utf8mb4,避免字符集问题干扰排查。
  • 把后端MySQL的max_connections调大,并开启skip-name-resolve,减少反向DNS解析造成的连接超时。

这些优化不一定每个环境都适用,但遇到连接类报错时,确实能少踩很多坑。

7. 避坑心得:同样的报错,完全不同的原因

这里再补充一个容易误判的真实案例。我的另一个项目里,mycat后端数据库的地址其实是通着的,但依然报NoRouteToHostException。后来发现,这台机器上配置了多块网卡,而Java进程绑定了其中一块网卡的IP去连接数据库。也就是说,connect的时候指定了local bind address,而这个本地地址对应的网卡无法路由到目标网络。

这个情况在代码里一般是这样:

Socket socket = new Socket(); socket.bind(new InetSocketAddress("192.168.1.10", 0)); socket.connect(new InetSocketAddress("10.20.0.10", 3306), 3000);

如果把bind的地址改成可达的网卡IP,或者干脆不bind,让内核自动选择,问题就消失了。mycat本身没有暴露这个参数,但如果你在mycat的JVM启动参数里加了类似-Djava.net.preferIPv4Stack=true这种,也可能影响网络栈行为。

再有一个容易被忽略的点:某些云平台的安全组会丢弃特定源IP的包,并附带ICMP unreachable消息,内核收到后也会报NoRouteToHostException。这种时候,单纯看路由表是看不出来的,需要在云控制台检查安全组双向规则,以及在源主机用tcpdump抓包确认是否有ICMP错误回包。

比如:

tcpdump -i eth0 host 10.20.0.10 and port 3306

如果看到大量“icmp host unreachable”的输出,基本可以确认对端或中间设备在主动拒绝,而不是本地路由缺失。

8. 常用排查命令速查

把上面所有排查手段整理成一个表,方便你直接复制使用。

排查目的命令或操作预期结果
查看目标路由ip route get 10.20.0.10显示via某网关
查看全部路由route -n能看到目标网段条目
排端口连通性telnet 10.20.0.10 3306能连接成功
抓包确认ICMPtcpdump -i eth0 icmp and host 10.20.0.10无大量unreachable
查看mycat数据源状态show @@datasource;状态为正数
查看监听端口netstat -tlnp能看到mysqld监听3306
查看JVM网络栈jstack -l mycat进程号看是否卡在Socket.connect

当你的环境里有防火墙策略,尽量用telnet或nc而不是ping来判断。很多场景里ICMP被禁但TCP通,或反过来,单一命令验证不全面。

注意:改完路由后,一定要重启mycat,不要只reload配置。某些连接异常状态在运行时不会被自动清理,必须重启进程才能恢复。

9. 从一次报错到一套方法论

这次问题虽然定位到根因只花了半天时间,但刚开始也走了一些弯路。复盘下来,有一个很大的体会:遇到NoRouteToHostException这类底层网络异常,不要立刻去翻mycat的官方文档,也不要怀疑分库分表逻辑有问题。先把网络栈的链路理清楚,确认从进程所在的网络命名空间到目标IP是否存在一条可用的路径,再回到应用层排查。

现在我在配置mycat或者任何代理中间件时,都会先做三件事:

  1. 确认进程实际运行环境的IP、路由、网关是什么。
  2. 用和程序相同的方式(不是命令行简单ping)去测试目标地址连通性。
  3. 把mycat的dataHost配置和实际网络拓扑画出来,对照检查是否有跨网段、跨安全域的情况。

这个方法不止适用于mycat,也适用于所有依赖JDBC连接后端数据库的中间件,比如各种数据库代理、数据同步工具等。希望这篇复盘能帮你在遇到同类报错时少走弯路,用最短时间定位到问题真正所在。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进