<返回更多

记一次处理Dubbo多网卡注册IP错误问题

2022-12-02    Java热点
加入收藏

场景介绍

今天在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. 为什么服务C调用不了服务A?
  2. 172.18.0.3这个ip是什么东东,哪里来的。

针对问题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;
}
复制代码

这块代码的整体逻辑还是很好理解的:

  1. 查找所有的网卡
  2. 校验网卡对应的ip是否合适
  3. 如果找不到合适的ip,则设置ip为127.0.0.1

再看下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;
}
复制代码

这方法也分为几步,也是很好理解的:

  1. 查找所有合适的网卡
  2. 在查找的网卡里遍历一遍,看是否有优先设置的
  3. 如果没有,选择一个网速100毫秒响应的
  4. 如果还没有,则返回第一个网卡

第一步,查找所有合适的网卡,怎么算合适的呢?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等。这里就不展开说明了,有兴趣的小伙伴可以自己试一试。

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>