我们以常见的微服务脚手架RuoYi-Cloud-Plus 为例

场景

服务网关-》业务服务不通

在微服务场景中,网关会从注册中心获取服务的列表,这些数据以键值对的形式存储在本地缓存中,key是服务的名称,value是服务的IP地址和端口号,通过服务名称查找IP地址和端口号,来明确要访问的服务,但是有些情况服务启动成功后发现有时候确无法调用成功。

记一次nacos注册服务IP错误的解决方案_spring cloud


没怎么了解网络的同学看这种类型的Ip会很疑惑,明明自己服务器/电脑根本不是这个开头的

调用后一般会出现这种错误

org.springframework.cloud.gateway.support.NotFoundException: 503 SERVICE_UNAVAILABLE "Unable to find instance for ruoyi-auth"
	at org.springframework.cloud.gateway.support.NotFoundException.create(NotFoundException.java:45)

服务与服务之间调用不通

dubbo 调用最常见的一种网络错误

No provider available for the service xxxxxxx from registry xxxxxx:8848 on the consumer xxxxx using the dubbo version 3.1.11. 
 Please check if the providers have been started and registered.

其中针对这种类似问题dubbo专门开了个排查清单感兴趣的同学可以看看

dubbo地址找不到异常

分析

在你检查了一遍后,你发现了nacos里面注册的IP不对并不是服务启动机器的IP。

那为什么会出现这种情况呢?

Spring Cloud服务注册到注册中心(如Nacos、Eureka等)时,服务实例IP地址的获取通常涉及到Spring Cloud框架下特定客户端库的源代码。对于不同的注册中心,具体的实现类会有所不同。这里以Spring Cloud Alibaba Nacos Discovery为例,简述相关源码位置:

记一次nacos注册服务IP错误的解决方案_IP_02


在**NacosDiscoveryProperties#init()**方法中进行了服务IP的初始化行为

在这里会先判断两个配置
1:spring.cloud.nacos.discovery.ip: 指定服务实例在注册到 Nacos 时使用的 IP 地址

2:spring.cloud.nacos.discovery.networkInterface: 指定服务实例用来检测 IP 地址的网络接口

如果没有设置IP,且没有设置网卡的情况下会调用InetUtils#findFirstNonLoopbackHostInfo()

记一次nacos注册服务IP错误的解决方案_网络_03


核心代码如下

public InetAddress findFirstNonLoopbackAddress() {
		InetAddress result = null;
		try {
			int lowest = Integer.MAX_VALUE;
			// 获取网卡集合
			for (Enumeration<NetworkInterface> nics = NetworkInterface
					.getNetworkInterfaces(); nics.hasMoreElements();) {
				NetworkInterface ifc = nics.nextElement();
                 //判断网卡接口是否正在运行
				if (ifc.isUp()) {
					log.trace("Testing interface: " + ifc.getDisplayName());
					if (ifc.getIndex() < lowest || result == null) {
						lowest = ifc.getIndex();
					}
					//判断是否已经获取到了IP地址
					else if (result != null) {
						continue;
					}
                    //是否忽略网卡
                    //如果配置了 spring.cloud.inetutils.ignored-interfaces[0]=eth0
					if (!ignoreInterface(ifc.getDisplayName())) {
						for (Enumeration<InetAddress> addrs = ifc
								.getInetAddresses(); addrs.hasMoreElements();) {
							//开始遍历网卡的接口
							InetAddress address = addrs.nextElement();

							if (address instanceof Inet4Address
									&& !address.isLoopbackAddress()
									&& isPreferredAddress(address)) {
                            //address.isLoopbackAddress()     是否为回环地址
                            //前缀匹配 针对spring.cloud.inetutils.preferred-networks配置项
                            //专门指定某个特定的ip前缀进行IP获取类似于左模糊 比如10.0.0;那么它只会找到这个IP段的进行匹配(前缀匹配或全匹配)
								log.trace("Found non-loopback interface: "
										+ ifc.getDisplayName());
								result = address;
							}
						}
					}
				}
			}
		}

可以看到是默认遍历所有网卡接口依次获取,如果已拿到就会跳过后面的获取
如果你其他网卡的顺序在你本机IP那张网卡之前,那么获取的结果就有可能混乱。
导致服务实例可能注册到一个内网不可达或者非公网IP上,从而影响服务间的正常通信。

再来看一下Dubbo的host获取规则

在 Dubbo 中, Provider 启动时主要做两个事情

一是启动 server

二是向注册中心注册服务。启动 server 时需要绑定 socket
向注册中心注册服务时也需要发送 socket 唯一标识服务地址。

查看代码发现,在 org.apache.dubbo.config.ServiceConfig#findConfigedHosts()
通过 InetAddress.getLocalHost().getHostAddress() 获取默认 host。

先看一下**ServiceConfig#findConfigedHosts()**做了一件什么事情

1:先从环境变量获取DUBBO_IP_TO_BIND

2:如果为空则通过 NetUtils.getLocalHost() 获取默认 host。

3:然后通过**NetUtils.getValidNetworkInterfaces()**拿到系统所有网卡列表并过滤出可用的。

4:接着遍历获取第一个可达的网络接口,如果仍找不到,则返回列表中的第一个网络接口。如果没有任何网络接口可用,则返回null。

5:如果为空则继续从环境变量中获取DUBBO_IP_TO_REGISTRY并返回。

常见的解决办法

SpringCloud

1:手动指定服务实例注册到 Nacos 时的 IP
spring:
     cloud:
       nacos:
         discovery:
           ip: xxx.xxx.xxx.xxx

这样nacos客户端就会使用你指定好的IP进行服务注册

2:设置网络段的优先级

如果你希望服务实例一直使用某个特定的网络段,比如你的内网10.1.xxxx.xxxxx那么可以使用这个配置,配置后spring cloud会优先匹配。这样就能保证你服务实例所在宿主机的ip可以正常注册。

spring:
  cloud:
    inetutils:
      preferred-networks:
        - 10.1
3:手动指定服务实例通过哪个网络接口获取 IP 地址用于注册。

当服务实例启动时,Nacos 客户端会根据这个配置去查询指定网络接口上的 IP 地址,并使用查询到的地址进行服务注册。如果你的服务器有多块网卡,或者希望服务使用特定网卡上的 IP 地址注册,可以通过设置这个属性来控制。配置的值应该是网络接口的名称,如 eth0 或 en0。

spring:
  cloud:
	nacos:
	  discovery:
		network-interface: xxxx

DUBBO

1:通过环境变量指定服务注册的地址

有些部署场景需要动态指定服务注册的地址,如 docker bridge 网络模式下要指定注册宿主机 ip 以实现外网通信。dubbo 提供了两对启动阶段的系统属性,用于设置对外通信的ip、port地址。
环境变量如下
DUBBO_IP_TO_REGISTRY:注册到注册中心的 ip 地址
DUBBO_PORT_TO_REGISTRY:注册到注册中心的 port 端口
DUBBO_IP_TO_BIND:监听 ip 地址
DUBBO_PORT_TO_BIND:监听 port 端口

示例docker脚本

docker run --name my-server -d -p 80:8080 -p 28880:28880 --restart=always \
 # 指定dubbo注册到注册中心的IP地址
 -e DUBBO_IP_TO_REGISTRY=192.168.2.2  \
 -e DUBBO_PORT_TO_REGISTRY=20880  \ 
test-image1111

这里的优先级高于dubbo.protocol或dubbo.provider的host配置

2:手动指定IP
dubbo:
  protocol:
    host: xxx.xxx.xxx.xxx

总结

1:如果您的开发环境中存在多块网卡,如因安装了虚拟机软件(如VMware)、使用了VPN或其他网络工具,可能会出现多个网络接口。
Nacos在默认情况下可能会选择到非预期的网卡IP进行注册,比如虚拟机的网络适配器(如VMware Network Adapter VMnet1)或VPN分配的IP地址。
这会导致注册到Nacos的服务IP与您期望的实际服务运行IP(如本机IP 127.0.0.1或物理网卡的IP)不一致,进而影响服务发现和路由。
2:可以通过手动指定IP,网卡或网络段来依次固定。
3: 通过dubbo.protocol或dubbo.provider的host属性对host进行配置,支持IP地址和域名.但此时注册到注册中心的IP地址和监听IP地址是同一个值
4: dubbo可以通过环境变量分别设置注册到注册中心的IP地址和监听IP地址,其优先级高于dubbo.protocol或dubbo.provider的host配置