主要问题:Nacos注册时,客户端多张网卡,IP选择造成的问题。
近期以写代码讨生活的左某写的API在调试时出现访问超时的状况,及时处理掉了问题。但左某因此受到三方联调人员的严重鄙视,为此左某决定记录本次异常状况以防再犯。问题概述: 事情这个样子的,最近左羊在服务一个基于SpringCloud微服务研发的产品,在发布一个服务供给前端人员调试时发生连接超时情况,通过排查F12开发者工具
发现访问我的服务时指向的IP为192.168.142.1
,再通过检测本机网卡列表发现其为VMware Network Adapter VMnet8
虚拟机的IP。最后通过在配置文件指定spring.cloud.nacos.discovery.ip
解决到该问题。以下信息为问题发生时的各种信息及个人粗浅理解nacos客户端注册IP的选择逻辑。
问题发生的情况
postman报错信息
{
"code": 1,
"msg": "finishConnect(..) failed: Connection timed out: /192.168.142.1:3023",
"data": null
}
本机注册信息
idea 控制台输出信息
2023-05-02 13:52:47.375 INFO 17964 --- [ main] c.a.c.n.registry.NacosServiceRegistry : nacos registry, DEFAULT_GROUP cloud-asset-management-biz 192.168.142.1:5017 register finished
本机网卡信息
win + r -- > cmd --> ipconfig
Windows IP 配置
以太网适配器 以太网 2:
媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :
以太网适配器 以太网:
连接特定的 DNS 后缀 . . . . . . . : vlan10
IPv4 地址 . . . . . . . . . . . . : 192.168.100.183
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . : 192.168.10.1
以太网适配器 以太网 3:
媒体状态 . . . . . . . . . . . . : 媒体已断开连接
连接特定的 DNS 后缀 . . . . . . . :
以太网适配器 VMware Network Adapter VMnet1:
连接特定的 DNS 后缀 . . . . . . . :
IPv4 地址 . . . . . . . . . . . . : 192.168.192.1
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . :
以太网适配器 VMware Network Adapter VMnet8:
连接特定的 DNS 后缀 . . . . . . . :
IPv4 地址 . . . . . . . . . . . . : 192.168.142.1
子网掩码 . . . . . . . . . . . . : 255.255.255.0
默认网关. . . . . . . . . . . . . :
个人理解
在Nacos客户端注册服务时,选择IP的逻辑大体分为两类:手动指定IP和自动选择IP。手动指定IP的逻辑非常简单,可以在代码中直接指定服务的IP地址,例如:
namingService.registerInstance(serviceName, "192.168.1.100", port);
在这种情况下,服务会始终使用指定的IP地址进行注册和发现。自动选择IP的逻辑稍微复杂一些。Nacos客户端会选择一个合适的网络接口或IP地址进行注册和发现。选择哪个网络接口或IP地址需要满足以下规则:
- 如果手动指定了IP地址,则使用该地址进行服务注册和服务发现。
- 如果配置文件中指定了IP地址,则使用该地址进行服务注册和服务发现。
- 否则,Nacos客户端会遍历所有的本地IP地址,选择一个可用的地址进行注册和发现。选择的条件包括:
- IP地址必须为IPv4格式;
- IP地址不能是回环地址(127.0.0.1)或本地连接地址(169.254.X.X);
- IP地址必须是可达的(即不在黑名单中)。
在选择IP地址后,Nacos客户端会将该地址作为服务的IP地址进行注册,其他客户端就可以通过该地址进行服务调用。
Nacos客户端IP自动选择
以下是 Nacos 客户端 IP 自动选择的核心代码:
public class UtilsAndCommons {
// 根据 IP 地址排序
@SuppressWarnings("unchecked")
public static List<String> getIPList(String ipList) {
String ipListString = IP_SPLIT_PATTERN.matcher(ipList).replaceAll(",");
List<String> ips = new ArrayList(Arrays.asList(ipListString.split(",")));
Collections.sort(ips, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
String[] arr1 = o1.split("\.");
String[] arr2 = o2.split("\.");
for (int i = 0; i < 4; i++) {
int seg1 = Integer.parseInt(arr1[i]);
int seg2 = Integer.parseInt(arr2[i]);
if (seg1 != seg2) {
return seg1 - seg2;
}
}
return 0;
}
});
return ips;
}
// 获取本地 IP 地址列表
public static List<String> getLocalIPList() {
List<String> result = new ArrayList<String>();
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface ni = interfaces.nextElement();
if (ni.isLoopback()) {
continue;
}
Enumeration<InetAddress> addresses = ni.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if (addr.isLoopbackAddress() || !(addr instanceof Inet4Address)) {
continue;
}
result.add(addr.getHostAddress());
}
}
} catch (SocketException e) {
Loggers.SRV_LOG.error("get local ip list error!" + e.getMessage(), e);
}
return result;
}
// 获取服务器 IP 地址列表
public static List<String> getServerIps() {
List<String> ips = new ArrayList<>();
try {
Enumeration<NetworkInterface> nics = NetworkInterface.getNetworkInterfaces();
LOOP:
while (nics.hasMoreElements()) {
NetworkInterface nic = nics.nextElement();
if (nic.isLoopback() || nic.isVirtual() || !nic.isUp()) {
continue;
}
Enumeration<InetAddress> addresses = nic.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if (addr.isLoopbackAddress() || !(addr instanceof Inet4Address)) {
continue;
}
ips.add(addr.getHostAddress());
if (NetUtils.canResolved(addr.getHostName())) {
ips.add(addr.getHostName());
}
}
}
} catch (SocketException e) {
Loggers.SRV_LOG.warn("failed to retrieve the serverIp! {}", e.getMessage());
}
return ips;
}
// 根据 IP 地址获取网段
public static String getIPSegment(String ipAddr) {
String[] arr = ipAddr.split("\.");
if (arr.length != 4) {
return "";
}
return arr[0] + "." + arr[1] + "." + arr[2];
}
}
核心代码主要包括:
getIPList
方法:根据 IP 地址排序。getLocalIPList
方法:获取本地 IP 地址列表。getServerIps
方法:获取服务器 IP 地址列表。getIPSegment
方法:根据 IP 地址获取网段。
在选择注册 IP 地址时,将优先使用配置的 -Dnacos.bind.ip
参数,如果没有配置该参数,则根据触发条件选择 IP 地址。具体流程如下:
- 如果 nacos-client 与 nacos-server 在同一台机器上运行,则优先选择本地 IP 地址,即从
getLocalIPList
方法返回的 IP 地址列表中选择一个 IP 地址。 - 如果 nacos-client 与 nacos-server 不在同一台机器上运行,则选择非本地 IP 地址。选择的规则为:假设 nacos-client 获取到的本地 IP 地址为 IP1,nacos-server 获取到的本地 IP 地址为 IP2,则选择 IP2 所在的网段中距离 IP1 最近(按照 IP 地址排序)的一个 IP 地址。如果不存在则选择 IP2。
- 如果没有选择到合适的 IP 地址,则会使用 0.0.0.0 地址。
参考文献:
1. Nacos官方文档:https://nacos.io/zh-cn/docs/quick-start.html
2. Nacos源码 https://github.com/alibaba/nacos