今天在Docker上部署服务时,启动都很成功,但是访问时却访问失败。之前在本地启动、在测试服k8s上启动都很正常,为什么同样的代码、同样的docker镜像在docker上却有问题呢?真让人摸不着头脑。
服务是部署在docker集群上,因为是部署测试,就准备了3台机器,由3台机器组成的一个集群。服务是按业务划分,分别写在不同的docker-compose.yml文件中,最后通过docker stack 启动。
dubbo的注册中心使用的是nacos。
因为在本地测试正常,而且也在k8s上部署且访问正常,所以在docker环境启动也很顺利。唯一不行的就是docker环境部署后部分接口访问报错。
在接口访问失败后,立刻查看了服务日志,报错是dubbo接口调用超时。错误如下:
org.Apache.dubbo.rpc.RpcException:
FAIled to invoke the method getExportedURLs in the service org.apache.dubbo.rpc.service.GenericService.
Tried 1 times of the providers [172.18.0.3:20881] (1/1) from the registry 172.10.36.101:8848 on the consumer 172.19.0.7 using the dubbo version 2.7.17.
。。。。。省略一大堆。。。。。
error message is:Host is unreachable: /172.18.0.3:20881
复制代码
从报错日志来看,有个明显的错误是Host is unreachable。现在是服务C调用服务A的dubbo接口调用不到,用我蹩脚的英文理解刚才错误信息是主机不可达。
此时,有两个疑问出现在容量不够大的脑子里:
针对问题1,首先想到的是服务A没有注册到nacos里去,打开nacos控制台发现服务A是已经注册上去了的,并且发现注册的ip是172.18.0.3,刚好和问题2里的ip一致。
既然服务已经正常注册,那就剩下172.18.0.3这个ip是哪里来的了。通过docker exec命令进入服务A容器,使用命名ifconfig看下服务A的ip信息:
找到了,是服务A容器的一个网卡地址,不过这个容器怎么网卡?难道是因为多个网卡导致的吗?那为什么部署在k8s容器里没有问题?难道k8s里面没有多个网卡吗?又接着一连串的问号在脑海里出现了?
先看看,k8s里的ip信息吧。通过kubectl exec登录容器, 查看ip信息ip addr。嗯?只有两个,比docker里少了好多。
看来是,dubbo注册的时候,选择网卡的时候,是有一定的机制的,选择的不是我想要的。看看源码吧,到底是怎么选择的。
Dubbo获取网卡地址的逻辑是在
org.apache.dubbo.common.utils.NETUtils类中getLocalAddress0方法。
private static InetAddress getLocalAddress0() {
InetAddress localAddress = null;
// @since 2.7.6, choose the {@link NetworkInterface} first
try {
NetworkInterface networkInterface = findNetworkInterface();
Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
Optional<InetAddress> addressOp = toValidAddress(addresses.nextElement());
if (addressOp.isPresent()) {
try {
if (addressOp.get().isReachable(100)) {
return addressOp.get();
}
} catch (IOException e) {
// ignore
}
}
}
} catch (Throwable e) {
logger.warn(e);
}
try {
localAddress = InetAddress.getLocalHost();
Optional<InetAddress> addressOp = toValidAddress(localAddress);
if (addressOp.isPresent()) {
return addressOp.get();
}
} catch (Throwable e) {
logger.warn(e);
}
return localAddress;
}
复制代码
这块代码的整体逻辑还是很好理解的:
再看下Dubbo是如何校验ip是否合适的呢?对应方法为toValidAddress
private static Optional<InetAddress> toValidAddress(InetAddress address) {
if (address instanceof Inet6Address) {
Inet6Address v6Address = (Inet6Address) address;
if (isPreferIPV6Address()) {
return Optional.ofNullable(normalizeV6Address(v6Address));
}
}
if (isValidV4Address(address)) {
return Optional.of(address);
}
return Optional.empty();
}
复制代码
先判断拿到的地址是否为ipv6,再看是否设置了优先选择ipv6,即有没有配置
JAVA.net.preferIPv6Addresses=true,我们项目配置的是java.net.preferIPv4Stack=true,所以走的是下面的逻辑。再检测是否是合法的ipv4地址,拿到ip后,检查ip的网速,如果响应时间为100ms内,则把这个ip作为注册ip。
知道了Dubbo是如何选择网卡的了,但是好像对我们没有太大帮助,我总不能限制网卡的网速去吧?
最好的办法还是,我是否可以设置?再看一遍源码是不是遗漏了什么。看下是如何选择网卡的:
public static NetworkInterface findNetworkInterface() {
List<NetworkInterface> validNetworkInterfaces = emptyList();
try {
// 寻找合适的网卡
validNetworkInterfaces = getValidNetworkInterfaces();
} catch (Throwable e) {
logger.warn(e);
}
NetworkInterface result = null;
// Try to find the preferred one
for (NetworkInterface networkInterface : validNetworkInterfaces) {
// 是否为优选的网卡
if (isPreferredNetworkInterface(networkInterface)) {
result = networkInterface;
break;
}
}
if (result == null) { // If not found, try to get the first one
for (NetworkInterface networkInterface : validNetworkInterfaces) {
Enumeration<InetAddress> addresses = networkInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
Optional<InetAddress> addressOp = toValidAddress(addresses.nextElement());
if (addressOp.isPresent()) {
try {
if (addressOp.get().isReachable(100)) {
return networkInterface;
}
} catch (IOException e) {
// ignore
}
}
}
}
}
if (result == null) {
result = first(validNetworkInterfaces);
}
return result;
}
复制代码
这方法也分为几步,也是很好理解的:
第一步,查找所有合适的网卡,怎么算合适的呢?getValidNetworkInterfaces方法里判断只要不是被忽略的网卡就是合适的,里面的代码就不细看了,大致逻辑是,通过参数
dubbo.network.interface.ignored可以设置哪些网卡被忽略,如果忽略多个可以用逗号拼接。例如dubbo.network.interface.ignored=eth0,eth1。这个参数好像可以满足我们的需求。
第二步,查找网卡是否有被我们优先设置的。
isPreferredNetworkInterface方法我们看下:
public static boolean isPreferredNetworkInterface(NetworkInterface networkInterface) {
// dubbo.network.interface.preferred
String preferredNetworkInterface = System.getProperty(DUBBO_PREFERRED_NETWORK_INTERFACE);
return Objects.equals(networkInterface.getDisplayName(), preferredNetworkInterface);
}
复制代码
可以看到,我们可以通过
DUBBO_PREFERRED_NETWORK_INTERFACE这个参数,也就dubbo.network.interface.preferred来指定网卡。例如:dubbo.network.interface.preferred=eth0。
看到这里,我们就明白了,至少我们可以通过排除网卡或者设置网卡来让Dubbo选择合适的ip去注册。因为docker容器里,不一定有多少个确定的网卡,还是指定网卡比较保险。
好了,知道如何让Dubbo来选择网卡了,我们只要找到各个容器里在同一网段的网卡就好了。
于是,登录到各个docker容器里,查看ip信息。对比了一下,发现eth1这个ip的网段都是10.10.x.x网段的。
配置环境变量信息
dubbo.network.interface.preferred=eth1,重启服务,然后访问接口,果然通了。
大功告成!
其实,还有其他办法来解决问题,比如:protocol配置host信息、设置 DUBBO_IP_TO_REGISTRY和DUBBO_IP_TO_BIND等。这里就不展开说明了,有兴趣的小伙伴可以自己试一试。