可能相当一部分人还不知道我这篇文章到底要讲什么,我说个场景,大家应该就明晰了。在分布式服务调用过程中,以 Dubbo 为例,服务提供者往往需要将自身的 IP 地址上报给注册中心,供消费者去发现。在大多数情况下 Dubbo 都可以正常工作,但如果你留意过 Dubbo 的 github issue,其实有不少人反馈:Dubbo Provider 注册了错误的 IP。如果你能立刻联想到:多网卡、内外网地址共存、VPN、虚拟网卡等关键词,那我建议你一定要继续将本文看下去,因为我也想到了这些,它们都是本文所要探讨的东西!那么“如何选择合适的网卡地址”呢,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 这段选取本地地址的逻辑大致分成了两步
首先强调下,这段逻辑并没有太大的问题,先别急着挑刺,让我们来分析下其中的一些细节,并进行验证。
Dubbo 首先选取的是 hostname 对应的 IP,在源码中对应的 InetAddress.getLocalHost();
在 *nix
系统实际部署 Dubbo 应用时,可以首先使用 hostname
命令获取主机名
xujingfengdeMacBook-Pro:~ xujingfeng$ hostname
xujingfengdeMacBook-Pro.local
紧接着在 /etc/hosts
配置 IP 映射,为了验证 Dubbo 的机制,我们随意为 hostname 配置一个 IP 地址
127.0.0.1 localhost
1.2.3.4 xujingfengdeMacBook-Pro.local
接着调用 NetUtils.getLocalAddress0()
进行验证,控制台打印如下:
xujingfengdeMacBook-Pro.local/1.2.3.4
在 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 将 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 命令!对于那些吐槽:“天呐,都 2019 年了,你怎么还在用 net-tools/ifconfig,iproute2/ip 了解一下”的言论,请大家视而不见。无论你使用的是 mac,还是 linux,都可以使用它去 CRUD 你的网卡配置。
启动关闭指定网卡:
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.168.2.255
启用和关闭arp协议:
ifconfig eth0 arp #开启网卡eth0 的arp协议
ifconfig eth0 -arp #关闭网卡eth0 的arp协议
设置最大传输单元:
ifconfig eth0 mtu 1500 #设置能通过的最大数据包大小为 1500 bytes
在一台 ubuntu 上执行 ifconfig -a
ubuntu@VM-30-130-ubuntu:~$ 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)
为了防止黑客对我的 Linux 发起攻击,我还是偷偷对 IP 做了一点“改造”,请不要为难一个趁着打折+组团购买廉价云服务器的小伙子。对于部分网卡的详细解读:
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 是怎么出来的呢?我在我的 ubuntu 上装了 docker 和 open。这两个东西应该是日常干扰我们做服务注册时的罪魁祸首了,当然,也有可能存在 eth1 这样的第二块网卡。ifconfig -a 看到的东西就对应了 JDK 的 api :NetworkInterface.getNetworkInterfaces()
。我们简单做个总结,大致有三个干扰因素
我们后续的篇幅将针对这些场景做分别的介绍,力求让大家没吃过猪肉,起码看下猪怎么跑的。
熟悉 docker 的朋友应该知道 docker 会默认创建一个 docker0 的网桥,供容器实例连接。如果嫌默认的网桥不够直观,我们可以使用 bridge 模式自定义创建一个新的网桥:
ubuntu@VM-30-130-ubuntu:~$ docker network create kirito-bridge
a38696dbbe58aa916894c674052c4aa6ab32266dcf6d8111fb794b8a344aa0d9
ubuntu@VM-30-130-ubuntu:~$ ifconfig -a
br-a38696dbbe58 Link encap:Ethernet HWaddr 02:42:6e:aa:fd:0c
inet addr:172.19.0.1 Bcast:172.19.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)
使用 docker network 指令创建网桥之后,自动创建了对应的网卡,我只给出了 ifconfig -a
的增量返回部分,可以看出多了一个 br-a38696dbbe58 的网卡。
我有意区分了“网桥”和“网卡”,可以使用 bridge-utils/brctl 来查看网桥信息:
ubuntu@VM-30-130-ubuntu:~$ sudo brctl show
bridge name bridge id STP enabled interfaces
br-a38696dbbe58 8000.02426eaafd0c no
docker0 8000.02425845c215 no
网桥是一个虚拟设备,这个设备只有 brctl show 能看到,网桥创建之后,会自动创建一个同名的网卡,并将这个网卡加入网桥。
平时我们所说的虚拟网卡、虚拟机,大致都跟 TUN/TAP 有关。我的读者大多数是 Java 从业者,相信我下面的内容并没有太超纲,不要被陌生的名词唬住。对于被唬住的读者,也可以直接跳过 5.1~5.3,直接看 5.4 的实战。
上图中的 eth0 表示我们主机已有的真实的网卡接口 (interface)。
网卡接口 eth0 所代表的真实网卡通过网线(wire)和外部网络相连,该物理网卡收到的数据包会经由接口eth0 传递给内核的网络协议栈(Network Stack)。然后协议栈对这些数据包进行进一步的处理。
对于一些错误的数据包,协议栈可以选择丢弃;对于不属于本机的数据包,协议栈可以选择转发;而对于确实是传递给本机的数据包,而且该数据包确实被上层的应用所需要,协议栈会通过 Socket API 告知上层正在等待的应用程序。
我们知道,普通的网卡是通过网线来收发数据包的话,而 TUN 设备比较特殊,它通过一个文件收发数据包。
如上图所示,tunX 和上面的 eth0 在逻辑上面是等价的, tunX 也代表了一个网络接口,虽然这个接口是系统通过软件所模拟出来的.
网卡接口 tunX 所代表的虚拟网卡通过文件 /dev/tunX 与我们的应用程序(App)相连,应用程序每次使用write 之类的系统调用将数据写入该文件,这些数据会以网络层数据包的形式,通过该虚拟网卡,经由网络接口 tunX 传递给网络协议栈,同时该应用程序也可以通过 read 之类的系统调用,经由文件/dev/tunX 读取到协议栈向 tunX 传递的所有数据包。
此外,协议栈可以像操纵普通网卡一样来操纵 tunX 所代表的虚拟网卡。比如说,给 tunX 设定 IP 地址,设置路由,总之,在协议栈看来,tunX 所代表的网卡和其他普通的网卡区别不大,当然,硬要说区别,那还是有的,那就是 tunX 设备不存在 MAC 地址,这个很好理解,tunX 只模拟到了网络层,要 MAC地址没有任何意义。当然,如果是 tapX 的话,在协议栈的眼中,tapX 和真实网卡没有任何区别。
是不是有些懵了?我是谁,为什么我要在这篇文章里面学习 TUN!因为我们常用的 VPN 基本就是基于 TUN/TAP 搭建的,如果我们使用 TUN 设备搭建一个基于 UDP 的 VPN ,那么整个处理过程可能是这幅样子:
TAP 设备与 TUN 设备工作方式完全相同,区别在于:
ifconfig
之类的命令给该设备设定 IP 地址。ifconfig
之类的命令给该设备设定 IP 地址,你如果愿意,我们可以给它设定 MAC 地址。关于文章中出现的二层,三层,我这里说明一下,第一层是物理层,第二层是数据链路层,第三层是网络层,第四层是传输层。
open 是 Linux 上一款开源的 工具,我们通过它来复现出影响我们做网卡选择的场景。
安装 open
sudo apt-get install open
安装一个 TUN 设备:
ubuntu@VM-30-130-ubuntu:~$ sudo open --mktun --dev tun0
Mon Apr 29 22:23:31 2019 TUN/TAP device tun0 opened
Mon Apr 29 22:23:31 2019 Persist state set to: ON
安装一个 TAP 设备:
ubuntu@VM-30-130-ubuntu:~$ sudo open --mktun --dev tap0
Mon Apr 29 22:24:36 2019 TUN/TAP device tap0 opened
Mon Apr 29 22:24:36 2019 Persist state set to: ON
执行 ifconfig -a
查看网卡,只给出增量的部分:
tap0 Link encap:Ethernet HWaddr 7a:a2:a8:f1:6b:df
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:100
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
inet addr:10.154.30.131 P-t-P:10.154.30.131 Mask:255.255.255.255
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)
这样就解释了文章一开始为什么会有 tun0 这样的网卡了。这里读者可能会有疑惑,使用 ifconfig 不是也可以创建 tap 和 tun 网卡吗?当然啦,open 是一个 工具,只能创建名为 tunX/tapX 的网卡,其遵守着一定的规范,ifconfig 可以随意创建,但没人认那些随意创建的网卡。
这个没有太多好说的,有多张真实的网卡,从普哥那儿搞到如上的 IP 信息。
虽然 ifconfig 等指令是 *nux
通用的,但是其展示信息,网卡相关的属性和命名都有较大的差异。例如这是我 MAC 下执行 ifconfig -a
的返回:
xujingfengdeMacBook-Pro:dubbo-in-action xujingfeng$ 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
对于这些常见网卡命名的解读,我摘抄一部分来自 stackoverflow 的回答:
In arbitrary order of my familarity / widespread relevance:
lo0
is loopback.
en0
at one point "ethernet", now is WiFi (and I have no idea what extraen1
oren2
are used for).
fw0
is the FireWire network interface.
stf0
is an IPv6 to IPv4 tunnel interface to support the transition from IPv4 to the IPv6 standard.
gif0
is a more generic tunneling interface [46]-to-[46].
awdl0
is Apple Wireless Direct Link
p2p0
is related to AWDL features. Either as an old version, or virtual interface with different semantics thanawdl
.the "Network" panel in System Preferences to see what network devices "exist" or "can exist" with current configuration.
many VPNs will add additional devices, often "utun#" or "utap#" following TUN/TAP (L3/L2)virtual networking devices.
use
netstat -nr
to see how traffic is currently routed via network devices according to destination.interface naming conventions started in BSD were retained in OS X / macOS, and now there also additions.
我们进行了以上探索,算是对网卡有一点了解了。回过头来看看 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,我们可以利用起来,过滤一部分一定不正确的网卡。
Dubbo Action 3:
我们本文花了较多的篇幅介绍了 docker 和 TUN/TAP 两种场景导致的虚拟网卡的问题,算是较为常见的一个影响因素,虽然他们的命名具有固定性,如 docker0、tunX、tapX,但我觉得通过网卡名称的判断方式去过滤注册 IP 有一些 hack,所以不建议 dubbo contributor 提出相应的 pr 去增加这些 hack 判断,尽管可能会对判断有所帮助。
对于真实多网卡、内外网 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
TUN/TAP 设备浅析
what-are-en0-en1-p2p-and-so-on-that-are-displayed-after-executing-ifconfig
原文:http://dubbo.apache.org/zh-cn/blog/dubbo-network-interfaces.html