1 Dubbo 是怎么做的
Dubbo 获取网卡地址的逻辑在各个版本中也是千回百转,走过弯路,也做过优化,我们用最新的 2.7.2-SNAPSHOT 版本来介绍,在看以下源码时,大家可以怀着质疑的心态去阅读,在 dubbo github 的 master 分支可以获取源码。获取 localhost 的逻辑位于
org.apache.dubbo.common.utils.NetUtils#getLocalAddress0() 之中
private static InetAddress getLocalAddress0() {
InetAddress localAddress = null;
// 首先尝试获取 /etc/hosts 中 hostname 对应的 IP
localAddress = InetAddress.getLocalHost();
Optional
addressOp = toValidAddress(localAddress); if (addressOp.isPresent()) {
return addressOp.get();
}
// 没有找到适合注册的 IP,则开始轮询网卡
Enumeration
interfaces = NetworkInterface.getNetworkInterfaces(); if (null == interfaces) {
return localAddress;
}
while (interfaces.hasMoreElements()) {
NetworkInterface network = interfaces.nextElement();
Enumeration
addresses = network.getInetAddresses(); while (addresses.hasMoreElements()) {
// 返回第一个匹配的适合注册的 IP
Optional
addressOp = toValidAddress(addresses.nextElement()); if (addressOp.isPresent()) {
return addressOp.get();
}
}
}
return localAddress;
}
Dubbo 这段选取本地地址的逻辑大致分成了两步
先去 /etc/hosts 文件中找 hostname 对应的 IP 地址,找到则返回;找不到则转 2
轮询网卡,寻找合适的 IP 地址,找到则返回;找不到返回 null,在 getLocalAddress0 外侧还有一段逻辑,如果返回 null,则注册 127.0.0.1 这个本地回环地址
首先强调下,这段逻辑并没有太大的问题,先别急着挑刺,让我们来分析下其中的一些细节,并进行验证。
Dubbo 首先选取的是 hostname 对应的 IP,在源码中对应的 InetAddress.getLocalHost();
在 *nix
系统实际部署 Dubbo 应用时,可以首先使用 hostname
命令获取主机名
紧接着在 /etc/hosts
配置 IP 映射,为了验证 Dubbo 的机制,我们随意为 hostname 配置一个 IP 地址
127.0.0.1 localhost 1.2.3.4 coreygdeMacBook-Pro.local
接着调用 NetUtils.getLocalAddress0()
进行验证,控制台打印如下:
在 toValidAddress 逻辑中,Dubbo 存在以下逻辑判定一个 IP 地址是否有效
private static Optional
toValidAddress(InetAddress address) { if (address instanceof Inet6Address) {
Inet6Address v6Address = (Inet6Address) address;
if (isValidV6Address(v6Address)) {
return Optional.ofNullable(normalizeV6Address(v6Address));
}
}
if (isValidV4Address(address)) {
return Optional.of(address);
}
return Optional.empty();
}
依次校验其符合 Ipv6 或者 Ipv4 的 IP 规范,对于 Ipv6 的地址,见如下代码:
static boolean isValidV6Address(Inet6Address address) {
boolean preferIpv6 = Boolean.getBoolean("java.net.preferIPv6Addresses");
if (!preferIpv6) {
return false;
}
try {
return address.isReachable(100);
} catch (IOException e) {
// ignore
}
return false;
}
首先获取 java.net.preferIPv6Addresses
参数,其默认值为 false,鉴于大多数应用并没有使用 Ipv6 地址作为理想的注册 IP,这问题不大,紧接着通过 isReachable 判断网卡的连通性。例如一些网卡可能是 VPN/虚拟网卡的地址,如果没有配置路由表,往往无法连通,可以将之过滤。
对于 Ipv4 的地址,见如下代码:
static boolean isValidV4Address(InetAddress address) {
if (address == null || address.isLoopbackAddress()) {
return false;
}
String name = address.getHostAddress();
boolean result = (name != null
&& IP_PATTERN.matcher(name).matches()
&& !Constants.ANYHOST_VALUE.equals(name)
&& !Constants.LOCALHOST_VALUE.equals(name));
return result;
}
对比 Ipv6 的判断,这里我们已经发现前后不对称的情况了
Ipv4 相比 Ipv6 的逻辑多了 Ipv4 格式的正则校验、本地回环地址校验、ANYHOST 校验
pv4 相比 Ipv6 的逻辑少了网卡连通性的校验
大家都知道,Ipv4 将 127.0.0.1 定为本地回环地址, Ipv6 也存在回环地址:0:0:0:0:0:0:0:1 或者表示为 ::1。改进建议也很明显,我们放到文末统一总结。
如果上述地址获取为 null 则进入轮询网卡的逻辑(例如 hosts 未指定 hostname 的映射或者 hostname 配置成了 127.0.0.1 之类的地址便会导致获取到空的网卡地址),轮询网卡对应的源码是 NetworkInterface.getNetworkInterfaces()
,这里面涉及的知识点就比较多了,支撑起了我写这篇文章的素材,Dubbo 的逻辑并不复杂,进行简单的校验,返回第一个可用的 IP 即可。
性子急的读者可能忍不住了,多网卡!合适的网卡可能不止一个,Dubbo 怎么应对呢?按道理说,我们也替 Dubbo 说句公道话,客官要不你自己指定下?我们首先得对多网卡的场景达成一致看法,才能继续把这篇文章完成下去:我们只能尽可能过滤那些“不对”的网卡。Dubbo 看样子对所有网卡是一视同仁了,那么是不是可以尝试优化一下其中的逻辑呢?
许多开源的服务治理框架在 stackoverflow 或者其 issue 中,注册错 IP 相关的问题都十分高频,大多数都是轮询网卡出了问题。既然事情发展到这儿,势必需要了解一些网络、网卡的知识,我们才能过滤掉那些明显不适合 RPC 服务注册的 IP 地址了。
我并没有想要让大家对后续的内容望而却步,特地选择了这个大家最熟悉的 Linux 命令!对于那些吐槽:“天呐,都 2020 年了,你怎么还在用 net-tools/ifconfig,iproute2/ip 了解一下”的言论,请大家视而不见。无论你使用的是 mac,还是 linux,都可以使用它去 CRUD 你的网卡配置。
2.1 常用指令
启动关闭指定网卡:
ifconfig eth0 up
ifconfig eth0 down
ifconfig eth0 up
为启动网卡 eth0,ifconfig eth0 down
为关闭网卡 eth0。ssh 登陆 linux 服务器操作的用户要小心执行这个操作了,千万不要蠢哭自己。不然你下一步就需要去 google:“禁用 eth0 网卡后如何远程连接 Linux 服务器” 了。
为网卡配置和删除IPv6地址:
ifconfig eth0 add 33ffe:3240:800:1005::2/64 #为网卡eth0配置IPv6地址
ifconfig eth0 del 33ffe:3240:800:1005::2/64 #为网卡eth0删除IPv6地址
用 ifconfig 修改 MAC 地址:
ifconfig eth0 hw ether 00:AA:BB:CC:dd:EE
配置 IP 地址:
[root@localhost ~]# ifconfig eth0 192.168.2.10
[root@localhost ~]# ifconfig eth0 192.168.2.10 netmask 255.255.255.0
[root@localhost ~]# ifconfig eth0 192.168.2.10 netmask 255.255.255.0 broadcast 192.16
启用和关闭arp协议:
ifconfig eth0 mtu 1500 #设置能通过的最大数据包大小为 1500 bytes
在一台 centos 上执行 ifconfig -a
eth0 Link encap:Ethernet HWaddr 52:54:00:a9:5f:ae
inet addr:10.154.30.130 Bcast:10.154.63.255 Mask:255.255.192.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:149673 errors:0 dropped:0 overruns:0 frame:0
TX packets:152271 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:15205083 (15.2 MB) TX bytes:21386362 (21.3 MB)
lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
docker0 Link encap:Ethernet HWaddr 02:42:58:45:c1:15
inet addr:172.17.0.1 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
tun0 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
UP POINTOPOINT NOARP MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:100
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
eth0 表示第一块网卡, 其中 HWaddr 表示网卡的物理地址,可以看到目前这个网卡的物理地址(MAC 地址)是 02:42:38:52:70:54
inet addr 用来表示网卡的 IP 地址,此网卡的 IP 地址是 10.154.30.130,广播地址, Bcast: 172.18.255.255,掩码地址 Mask:255.255.0.0
lo 是表示主机的回环地址,这个一般是用来测试一个网络程序,但又不想让局域网或外网的用户能够查看,只能在此台主机上运行和查看所用的网络接口。比如把 HTTPD 服务器的指定到回坏地址,在浏览器输入 127.0.0.1 就能看到你所架构的 WEB 网站了。但只有你能看得到,局域网的其它主机或用户则无从知晓。
第一行:连接类型:Ethernet(以太网)HWaddr(硬件mac地址)
第二行:网卡的IP地址、子网、掩码
第三行:UP(代表网卡开启状态)RUNNING(代表网卡的网线被接上)MULTICAST(支持组播)MTU:1500(最大传输单元):1500字节(ifconfig 不加 -a 则无法看到 DOWN 的网卡)
第四、五行:接收、发送数据包情况统计
第七行:接收、发送数据字节数统计信息。
紧接着的两个网卡 docker0,tun0 是怎么出来的呢?我在我的 centos7 上装了 docker 和 open。这两个东西应该是日常干扰我们做服务注册时的罪魁祸首了,当然,也有可能存在 eth1 这样的第二块网卡。ifconfig -a 看到的东西就对应了 JDK 的 api :NetworkInterface.getNetworkInterfaces()
。我们简单做个总结,大致有三个干扰因素
以 docker 网桥为首的虚拟网卡地址,毕竟这东西这么火,怎么也得单独列出来吧?
以 TUN/TAP 为代表的虚拟网卡地址,多为 VPN 场景
以 eth1 为代表的多网卡场景,有钱就可以装多网卡了!
干扰因素二:多网卡
coreydeMacBook-Pro:dubbo-in-action corey$ ifconfig -a
lo0: flags=8049
mtu 16384 options=1203
inet 127.0.0.1 netmask 0xff000000
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
nd6 options=201
gif0: flags=8010
mtu 1280 stf0: flags=0<> mtu 1280
XHC0: flags=0<> mtu 0
XHC20: flags=0<> mtu 0
en0: flags=8863
mtu 1500 ether 88:e9:fe:88:a0:76
inet6 fe80::1cab:f689:60d1:bacb%en0 prefixlen 64 secured scopeid 0x6
inet 30.130.11.242 netmask 0xffffff80 broadcast 30.130.11.255
nd6 options=201
media: autoselect
status: active
p2p0: flags=8843
mtu 2304 ether 0a:e9:fe:88:a0:76
media: autoselect
status: inactive
awdl0: flags=8943
mtu 1484 ether 66:d2:8c:8c:dd:85
inet6 fe80::64d2:8cff:fe8c:dd85%awdl0 prefixlen 64 scopeid 0x8
nd6 options=201
media: autoselect
status: active
en1: flags=8963
mtu 1500 options=60
ether aa:00:d0:13:0e:01
media: autoselect
status: inactive
en2: flags=8963
mtu 1500 options=60
ether aa:00:d0:13:0e:00
media: autoselect
status: inactive
bridge0: flags=8863
mtu 1500 options=63
ether aa:00:d0:13:0e:01
Configuration:
id 0:0:0:0:0:0 priority 0 hellotime 0 fwddelay 0
maxage 0 holdcnt 0 proto stp maxaddr 100 timeout 1200
root id 0:0:0:0:0:0 priority 0 ifcost 0 port 0
ipfilter disabled flags 0x2
member: en1 flags=3
ifmaxaddr 0 port 9 priority 0 path cost 0
member: en2 flags=3
ifmaxaddr 0 port 10 priority 0 path cost 0
nd6 options=201
media:
status: inactive
utun0: flags=8051
mtu 2000 inet6 fe80::3fe0:3e8b:384:9968%utun0 prefixlen 64 scopeid 0xc
nd6 options=201
utun1: flags=8051
mtu 1380 inet6 fe80::7894:3abc:5abd:457d%utun1 prefixlen 64 scopeid 0xd
nd6 options=201
内容很多,我挑几点差异简述下
内容展示形式不一样,没有 Linux 下的接收、发送数据字节数等统计信息
真实网卡的命名不一样:eth0 -> en0
虚拟网卡的命名格式不一样:tun/tap -> utun
我们进行了以上探索,算是对网卡有一点了解了。回过头来看看 Dubbo 获取网卡的逻辑,是否可以做出改进呢?
Dubbo Action 1:
保持 Ipv4 和 Ipv6 的一致性校验。为 Ipv4 增加连通性校验;为 Ipv6 增加 LoopBack 和 ANYHOST 等校验。
Dubbo Action 2:
NetworkInterface network = interfaces.nextElement();
if (network.isLoopback() || network.isVirtual() || !network.isUp()) {
continue;
}
JDK 提供了以上的 API,我们可以利用起来,过滤一部分一定不正确的网卡。
对于真实多网卡、内外网 IP 共存的场景,不能仅仅是框架侧在做努力,用户也需要做一些事,就像爱情一样,我可以主动一点,但你也得反馈,才能发展出故事
Dubbo User Action 1:
可以配置 /etc/hosts
文件,将 hostname 对应的 IP 显式配置进去。
Dubbo User Action 2:
可以使用启动参数去显式指定注册的 IP:
-DDUBBO_IP_TO_REGISTRY=1.2.3.4
也可以指定 Dubbo 服务绑定在哪块网卡上:
-DDUBBO_IP_TO_BIND=1.2.3.4
参考:
https://www.jianshu.com/p/09f9375b7fa7
https://stackoverflow.com/questions/29958143/what-are-en0-en1-p2p-and-so-on-that-are-displayed-after-executing-ifconfig
http://dubbo.apache.org/zh-cn/docs/user/quick-start.html