研究网卡地址注册时的一点思考
我曾经写过一篇和本文标题类似的文章《研究优雅停机时的一点思考》,上文和本文都有一个共同点:网卡地址注册和优雅停机都是一个很小的知识点,但是背后牵扯到的知识点却是庞大的体系,我在写这类文章前基本也和大多数读者一样,处于“知道有这么个东西,但不了解细节”的阶段,但一旦深挖,会感受到其中的奇妙,并有机会接触到很多平时不太关注的知识点。
另外,我还想介绍一个叫做”元阅读“的技巧,可能这个词是我自己造的,也有人称之为”超视角阅读“。其内涵指的是,普通读者从我的文章中学到的是某个知识点,而元阅读者从我的文章中可能会额外关注,我是如何掌握某个知识点的,在一个知识点的学习过程中我关注了哪些相关的知识点,又是如何将它们联系在一起,最终形成一个体系的。这篇文章就是一个典型的例子,我会对一些点进行发散,大家可以尝试着跟我一起来思考”网卡地址注册“这个问题。
1 如何选择合适的网卡地址
可能相当一部分人还不知道我这篇文章到底要讲什么,我说个场景,大家应该就明晰了。在分布式服务调用过程中,以 Dubbo 为例,服务提供者往往需要将自身的 IP 地址上报给注册中心,供消费者去发现。在大多数情况下 Dubbo 都可以正常工作,但如果你留意过 Dubbo 的 github issue,其实有不少人反馈:Dubbo Provider 注册了错误的 IP。如果你能立刻联想到:多网卡、内外网地址共存、VPN、虚拟网卡等关键词,那我建议你一定要继续将本文看下去,因为我也想到了这些,它们都是本文所要探讨的东西!那么“如何选择合适的网卡地址”呢,Dubbo 现有的逻辑到底算不算完备?我们不急着回答它,而是带着这些问题一起进行研究,相信到文末,其中答案,各位看官自有评说。
2 Dubbo 是怎么做的
Dubbo 获取网卡地址的逻辑在各个版本中也是千回百转,走过弯路,也做过优化,我们用最新的 2.7.2-SNAPSHOT 版本来介绍,在看以下源码时,大家可以怀着质疑的心态去阅读,在 dubbo github 的 master 分支可以获取源码。获取 localhost 的逻辑位于 org.apache.dubbo.common.utils.NetUtils#getLocalAddress0()
之中
1 | private static InetAddress getLocalAddress0() { |
Dubbo 这段选取本地地址的逻辑大致分成了两步
- 先去 /etc/hosts 文件中找 hostname 对应的 IP 地址,找到则返回;找不到则转 2
- 轮询网卡,寻找合适的 IP 地址,找到则返回;找不到返回 null,再 getLocalAddress0 外侧还有一段逻辑,如果返回 null,则注册 127.0.0.1 这个本地回环地址
首先强调下,这段逻辑并没有太大的问题,先别急着挑刺,让我们来分析下其中的一些细节,并进行验证。
2.1 尝试获取 hostname 映射 IP
Dubbo 首先选取的是 hostname 对应的 IP,在源码中对应的 InetAddress.getLocalHost();
在 *nix
系统实际部署 Dubbo 应用时,可以首先使用 hostname
命令获取主机名
1 | xujingfengdeMacBook-Pro:~ xujingfeng$ hostname |
紧接着在 /etc/hosts
配置 IP 映射,为了验证 Dubbo 的机制,我们随意为 hostname 配置一个 IP 地址
1 | 127.0.0.1 localhost |
接着调用 NetUtils.getLocalAddress0()
进行验证,控制台打印如下:
1 | xujingfengdeMacBook-Pro.local/1.2.3.4 |
2.2 判定有效的 IP 地址
在 toValidAddress 逻辑中,Dubbo 存在以下逻辑判定一个 IP 地址是否有效
1 | private static Optional<InetAddress> toValidAddress(InetAddress address) { |
依次校验其符合 Ipv6 或者 Ipv4 的 IP 规范,对于 Ipv6 的地址,见如下代码:
1 | static boolean isValidV6Address(Inet6Address address) { |
首先获取 java.net.preferIPv6Addresses
参数,其默认值为 false,鉴于大多数应用并没有使用 Ipv6 地址作为理想的注册 IP,这问题不大,紧接着通过 isReachable 判断网卡的连通性。例如一些网卡可能是 VPN/ 虚拟网卡的地址,如果没有配置路由表,往往无法连通,可以将之过滤。
对于 Ipv4 的地址,见如下代码:
1 | static boolean isValidV4Address(InetAddress address) { |
对比 Ipv6 的判断,这里我们已经发现前后不对称的情况了
- Ipv4 相比 Ipv6 的逻辑多了 Ipv4 格式的正则校验、本地回环地址校验、ANYHOST 校验
- Ipv4 相比 Ipv6 的逻辑少了网卡连通性的校验
大家都知道,Ipv4 将 127.0.0.1 定为本地回环地址, Ipv6 也存在回环地址:0:0:0:0:0:0:0:1 或者表示为 ::1。改进建议也很明显,我们放到文末统一总结。
2.3 轮询网卡
如果上述地址获取为 null 则进入轮询网卡的逻辑(例如 hosts 未指定 hostname 的映射或者 hostname 配置成了 127.0.0.1 之类的地址便会导致获取到空的网卡地址),轮询网卡对应的源码是 NetworkInterface.getNetworkInterfaces()
,这里面涉及的知识点就比较多了,支撑起了我写这篇文章的素材,Dubbo 的逻辑并不复杂,进行简单的校验,返回第一个可用的 IP 即可。
性子急的读者可能忍不住了,多网卡!合适的网卡可能不止一个,Dubbo 怎么应对呢?按道理说,我们也替 Dubbo 说句公道话,客官要不你自己指定下?我们首先得对多网卡的场景达成一致看法,才能继续把这篇文章完成下去:我们只能 ** 尽可能 ** 过滤那些“** 不对 **”的网卡。Dubbo 看样子对所有网卡是一视同仁了,那么是不是可以尝试优化一下其中的逻辑呢?
许多开源的服务治理框架在 stackoverflow 或者其 issue 中,注册错 IP 相关的问题都十分高频,大多数都是轮询网卡出了问题。既然事情发展到这儿,势必需要了解一些网络、网卡的知识,我们才能过滤掉那些明显不适合 RPC 服务注册的 IP 地址了。
3 Ifconfig 介绍
我并没有想要让大家对后续的内容望而却步,特地选择了这个大家最熟悉的 Linux 命令!对于那些吐槽:“天呐,都 2019 年了,你怎么还在用 net-tools/ifconfig,iproute2/ip 了解一下”的言论,请大家视而不见。无论你使用的是 mac,还是 linux,都可以使用它去 CRUD 你的网卡配置。
3.1 常用指令
** 启动关闭指定网卡:**
1 | ifconfig eth0 up |
ifconfig eth0 up
为启动网卡 eth0,ifconfig eth0 down
为关闭网卡 eth0。ssh 登陆 linux 服务器操作的用户要小心执行这个操作了,千万不要蠢哭自己。不然你下一步就需要去 google:“禁用 eth0 网卡后如何远程连接 Linux 服务器” 了。
** 为网卡配置和删除 IPv6 地址:**
1 | ifconfig eth0 add 33ffe:3240:800:1005::2/64 #为网卡 eth0 配置 IPv6 地址 |
** 用 ifconfig 修改 MAC 地址:**
1 | ifconfig eth0 hw ether 00:AA:BB:CC:dd:EE |
** 配置 IP 地址:**
1 | [root@localhost ~]# ifconfig eth0 192.168.2.10 |
** 启用和关闭 arp 协议:**
1 | ifconfig eth0 arp #开启网卡 eth0 的 arp 协议 |
** 设置最大传输单元:**
1 | ifconfig eth0 mtu 1500 #设置能通过的最大数据包大小为 1500 bytes |
3.2 查看网卡信息
在一台 ubuntu 上执行 ifconfig -a
1 | ubuntu@VM-30-130-ubuntu:~$ ifconfig -a |
为了防止黑客对我的 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 字节(ipconfig 不加 -a 则无法看到 DOWN 的网卡)
第四、五行:接收、发送数据包情况统计
第七行:接收、发送数据字节数统计信息。
紧接着的两个网卡 docker0,tun0 是怎么出来的呢?我在我的 ubuntu 上装了 docker 和 openvpn。这两个东西应该是日常干扰我们做服务注册时的罪魁祸首了,当然,也有可能存在 eth1 这样的第二块网卡。ifconfig -a 看到的东西就对应了 JDK 的 api :NetworkInterface.getNetworkInterfaces()
。我们简单做个总结,大致有三个干扰因素
- 以 docker 网桥为首的虚拟网卡地址,毕竟这东西这么火,怎么也得单独列出来吧?
- 以 TUN/TAP 为代表的虚拟网卡地址,多为 VPN 场景
- 以 eth1 为代表的多网卡场景,有钱就可以装多网卡了!
我们后续的篇幅将针对这些场景做分别的介绍,力求让大家没吃过猪肉,起码看下猪怎么跑的。
4 干扰因素一:Docker 网桥
熟悉 docker 的朋友应该知道 docker 会默认创建一个 docker0 的网桥,供容器实例连接。如果嫌默认的网桥不够直观,我们可以使用 bridge 模式自定义创建一个新的网桥:
1 | ubuntu@VM-30-130-ubuntu:~$ docker network create kirito-bridge |
使用 docker network 指令创建网桥之后,自动创建了对应的网卡,我只给出了 ifconfig -a
的增量返回部分,可以看出多了一个 br-a38696dbbe58 的网卡。
我有意区分了“网桥”和“网卡”,可以使用 bridge-utils/brctl 来查看网桥信息:
1 | ubuntu@VM-30-130-ubuntu:~$ sudo brctl show |
网桥是一个虚拟设备,这个设备只有 brctl show 能看到,网桥创建之后,会自动创建一个同名的网卡,并将这个网卡加入网桥。
5 干扰因素二:TUN/TAP 虚拟网络设备
平时我们所说的虚拟网卡、虚拟机,大致都跟 TUN/TAP 有关。我的读者大多数是 Java 从业者,相信我下面的内容并没有太超纲,不要被陌生的名词唬住。对于被唬住的读者,也可以直接跳过 5.1~5.3,直接看 5.4 的实战。
5.1 真实网卡工作原理
上图中的 eth0 表示我们主机已有的真实的网卡接口 (interface)。
网卡接口 eth0 所代表的真实网卡通过网线 (wire) 和外部网络相连,该物理网卡收到的数据包会经由接口 eth0 传递给内核的网络协议栈(Network Stack)。然后协议栈对这些数据包进行进一步的处理。
对于一些错误的数据包, 协议栈可以选择丢弃;对于不属于本机的数据包,协议栈可以选择转发;而对于确实是传递给本机的数据包, 而且该数据包确实被上层的应用所需要,协议栈会通过 Socket API 告知上层正在等待的应用程序。
5.2 TUN 工作原理
我们知道,普通的网卡是通过网线来收发数据包的话,而 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 ,那么整个处理过程可能是这幅样子:
5.3 TAP 工作原理
TAP 设备与 TUN 设备工作方式完全相同,区别在于:
- TUN 设备是一个三层设备,它只模拟到了 IP 层,即网络层 我们可以通过 /dev/tunX 文件收发 IP 层数据包,它无法与物理网卡做 bridge,但是可以通过三层交换(如 ip_forward)与物理网卡连通。可以使用
ifconfig
之类的命令给该设备设定 IP 地址。 - TAP 设备是一个二层设备,它比 TUN 更加深入,通过 /dev/tapX 文件可以收发 MAC 层数据包,即数据链路层,拥有 MAC 层功能,可以与物理网卡做 bridge,支持 MAC 层广播。同样的,我们也可以通过
ifconfig
之类的命令给该设备设定 IP 地址,你如果愿意,我们可以给它设定 MAC 地址。
关于文章中出现的二层,三层,我这里说明一下,第一层是物理层,第二层是数据链路层,第三层是网络层,第四层是传输层。
5.4 openvpn 实战
openvpn 是 Linux 上一款开源的 vpn 工具,我们通过它来复现出影响我们做网卡选择的场景。
安装 openvpn
1 | sudo apt-get install openvpn |
安装一个 TUN 设备:
1 | ubuntu@VM-30-130-ubuntu:~$ sudo openvpn --mktun --dev tun0 |
安装一个 TAP 设备:
1 | ubuntu@VM-30-130-ubuntu:~$ sudo openvpn --mktun --dev tap0 |
执行 ifconfig -a
查看网卡,只给出增量的部分:
1 | tap0 Link encap:Ethernet HWaddr 7a:a2:a8:f1:6b:df |
这样就解释了文章一开始为什么会有 tun0 这样的网卡了。这里读者可能会有疑惑,使用 ifconfig 不是也可以创建 tap 和 tun 网卡吗?当然啦,openvpn 是一个 vpn 工具,只能创建名为 tunX/tapX 的网卡,其遵守着一定的规范,ifconfig 可以随意创建,但没人认那些随意创建的网卡。
6 干扰因素三:多网卡
这个没有太多好说的,有多张真实的网卡,从普哥那儿搞到如上的 IP 信息。
7 MAC 下的差异
虽然 ifconfig 等指令是 *nux
通用的,但是其展示信息,网卡相关的属性和命名都有较大的差异。例如这是我 MAC 下执行 ifconfig -a
的返回:
1 | xujingfengdeMacBook-Pro:dubbo-in-action xujingfeng$ ifconfig -a |
内容很多,我挑几点差异简述下:
内容展示形式不一样,没有 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.
8 Dubbo 改进建议
我们进行了以上探索,算是对网卡有一点了解了。回过头来看看 Dubbo 获取网卡的逻辑,是否可以做出改进呢?
Dubbo Action 1:
保持 Ipv4 和 Ipv6 的一致性校验。为 Ipv4 增加连通性校验;为 Ipv6 增加 LoopBack 和 ANYHOST 等校验。
Dubbo Action 2:
1 | NetworkInterface network = interfaces.nextElement(); |
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:
1 | -DDUBBO_IP_TO_REGISTRY=1.2.3.4 |
也可以指定 Dubbo 服务绑定在哪块网卡上:
1 | -DDUBBO_IP_TO_BIND=1.2.3.4 |
9 参考文章
what-are-en0-en1-p2p-and-so-on-that-are-displayed-after-executing-ifconfig
** 欢迎关注我的微信公众号:「Kirito 的技术分享」,关于文章的任何疑问都会得到回复,带来更多 Java 相关的技术分享。**
研究网卡地址注册时的一点思考