发现问题

最近项目在Eureka注册时,发现一个问题:注册的IP地址不是 192.168.0.XXX 的网络IP,而是另外一个网段的地址,如图

通过 ipconfig 命令查看本机的IP地址发现,该IP是本机虚拟网卡VMnet8的地址。

X:\Users\Keats>ipconfigWindows IP 配置以太网适配器 以太网:连接特定的 DNS 后缀 . . . . . . . :IPv4 地址 . . . . . . . . . . . . : 192.168.0.234子网掩码  . . . . . . . . . . . . : 255.255.255.0默认网关. . . . . . . . . . . . . : 192.168.0.1以太网适配器 VMware Network Adapter VMnet1:连接特定的 DNS 后缀 . . . . . . . :IPv4 地址 . . . . . . . . . . . . : 192.168.87.1子网掩码  . . . . . . . . . . . . : 255.255.255.0默认网关. . . . . . . . . . . . . :以太网适配器 VMware Network Adapter VMnet8:连接特定的 DNS 后缀 . . . . . . . :IPv4 地址 . . . . . . . . . . . . : 192.168.29.1子网掩码  . . . . . . . . . . . . : 255.255.255.0默认网关. . . . . . . . . . . . . :

问题现象

Eureka管理页面注册列表展示的IP地址非局域网IP地址,是虚拟机的虚拟IP地址

可能引起的问题

多人开发时,同事通过Feign调用接口,无法正确匹配IP地址,从而导致接口调用失败。

尝试解决

通过百度查找,提供了该解决方案:在 yml 文件中添加一下的配置,以达到忽略指定网卡的目的

spring:cloud:inetutils:ignored-interfaces: ## 忽略网卡- VMware.*

可是当我添加该配置后,发现仍不起作用!页面上显示的instance-id 还是 192.168.29.1:8083 。这就很难受了

刨根问底

于是我想能不能跑一跑Spring的启动代码,看看他到底是怎么取IP的。首先从Eureka自动配置类EurekaClientAutoConfiguration入手

@Bean
@ConditionalOnMissingBean(value = EurekaInstanceConfig.class, search = SearchStrategy.CURRENT)
public EurekaInstanceConfigBean eurekaInstanceConfigBean(InetUtils inetUtils,ManagementMetadataProvider managementMetadataProvider) {String hostname = getProperty("eureka.instance.hostname");// 是否使用IP地址注册。这里就是从配置文件寻值,没找到就用默认值 falseboolean preferIpAddress = Boolean.parseBoolean(getProperty("eureka.instance.prefer-ip-address"));// 获取配置的IP地址String ipAddress = getProperty("eureka.instance.ip-address");instance.setPreferIpAddress(preferIpAddress);if (StringUtils.hasText(ipAddress)) {instance.setIpAddress(ipAddress);}return instance;
}

这里可以看到,Eureka并没有自己直接去系统获取IP地址,而是通过Spring的InetUtils类的findFirstNonLoopbackHostInfo来设置IP地址

public EurekaInstanceConfigBean(InetUtils inetUtils) {this.inetUtils = inetUtils;this.hostInfo = this.inetUtils.findFirstNonLoopbackHostInfo();this.ipAddress = this.hostInfo.getIpAddress();this.hostname = this.hostInfo.getHostname();
}

接着看看findFirstNonLoopbackHostInfo()方法的代码。我在本地Debug跑的时候,项目启动该类会被调用两次,一次没有读取配置文件,项目启动Banner也没有打印,第二次配置文件已经读取。启动日志也打印了一部分。这里原因留个坑

public InetAddress findFirstNonLoopbackAddress() {InetAddress result = null;try {int lowest = Integer.MAX_VALUE;for (Enumeration<NetworkInterface> nics = NetworkInterface.getNetworkInterfaces(); nics.hasMoreElements();) {NetworkInterface ifc = nics.nextElement();// 该网络接口是否启用并正在运行。调用的是native方法if (ifc.isUp()) {log.trace("Testing interface: " + ifc.getDisplayName());if (ifc.getIndex() < lowest || result == null) {lowest = ifc.getIndex();}else if (result != null) {continue;}// 该网卡名称不在忽略范围内// @formatter:offif (!ignoreInterface(ifc.getDisplayName())) {// 遍历IP地址for (Enumeration<InetAddress> addrs = ifc.getInetAddresses(); addrs.hasMoreElements();) {InetAddress address = addrs.nextElement();// 找到 IPV4 且不是回环地址(127.0.0.1) 且是优先选择的地址if (address instanceof Inet4Address&& !address.isLoopbackAddress()&& isPreferredAddress(address)) {log.trace("Found non-loopback interface: "+ ifc.getDisplayName());result = address;}}}// @formatter:on}}}catch (IOException ex) {log.error("Cannot get first non-loopback address", ex);}if (result != null) {return result;}try {return InetAddress.getLocalHost();}catch (UnknownHostException e) {log.warn("Unable to retrieve localhost");}return null;}

从上面的代码及注释可以看到,SpringCloud选择IP的原则是:选择已启动网卡的第一个不在忽略范围且不是回环地址(127.0.0.1)且是优先选择地址的IPV4地址

那么我们想要重定义其所选的IP地址,就需要从忽略范围 和 是否是优先选择地址来做了。

判断是否在忽略范围的代码

由该段代码知,要忽略的网口集合需要从 IgnoredInterfaces 这个属性中获得,那这个属性的值是什么?怎么配置呢?

/** for testing */ boolean ignoreInterface(String interfaceName) {// 遍历IgnoredInterfaces属性集合,该集合内是忽略的网口名字的正则表达式形式for (String regex : this.properties.getIgnoredInterfaces()) {if (interfaceName.matches(regex)) {log.trace("Ignoring interface: " + interfaceName);return true;}}return false;
}

判断是否是首选地址的代码

/** for testing */ boolean isPreferredAddress(InetAddress address) {// 如果配置了仅使用本地接口,则当该InetAddress是本地站点地址时返回if (this.properties.isUseOnlySiteLocalInterfaces()) {final boolean siteLocalAddress = address.isSiteLocalAddress();if (!siteLocalAddress) {log.trace("Ignoring address: " + address.getHostAddress());}return siteLocalAddress;}// 如果preferredNetworks列表没有配置,则所有地址返回Truefinal List<String> preferredNetworks = this.properties.getPreferredNetworks();if (preferredNetworks.isEmpty()) {return true;}// 如果配置了,则返回符合正则的地址for (String regex : preferredNetworks) {final String hostAddress = address.getHostAddress();if (hostAddress.matches(regex) || hostAddress.startsWith(regex)) {return true;}}log.trace("Ignoring address: " + address.getHostAddress());return false;
}

结论

  1. eureka 显示的 instance-id 有两种值,通过 prefer-ip-address 的值来选择
  • ip:端口 true
  • hostname 主机名 false(默认)
  1. 当 prefer-ip-address 的值为 true 时,eureka 取这个值:eureka.client.instance-id