引言
本文将记录和解释如何通过虚拟机搭建一个集群,以便进行相应的集群软件工具的原型部署和性能实验。文章将围绕集群网络的设置和搭建展开,虚拟化技术采用 KVM 方案。当然,KVM 虚拟机默认就和 host 主机形成了 NAT 的关系,如果把 host 主机看作集群的 master 节点的话,那么我们就什么都不需要做,网络就能开箱即用了。但是,既然是用虚拟机做实验,怎么可能把风险最大的 master 节点的工作直接用 host 主机来做?因此我们理想的网络拓扑是以一个虚拟机为 master 节点,其他虚拟机为 slave 节点,master 和 slave 之间共享一个子网,master 节点至少有两张网卡,内网外网之间有一个 NAT,同时外网网卡利用 host 主机来进入互联网。本文就记录如何实现这样一个虚拟机网络拓扑。
实验环境:host OS ubuntu 16.04, KVM OS ubuntu server 18.04 (如果 host 主机是 ubuntu desktop,请记住关闭 Network Manager 服务,从而使得传统的 linux 网络管理的这一套可以正常生效。当然如果是 ubuntu 17+,新引入的 netplan 会让本来就一团瞎的 ubuntu 上混乱的网络管理更加混乱。不懂 ubuntu 团队脑子在想什么,原始的 linux 网络管理方式简单明快,在这上面以侵入式的方式反复加入一些新的支离破碎的很难完全关闭的中间抽象层,而最终能实现的功能甚至比以前更受限???用个 yaml 配置就显得高大上,现代化了??? netplan, systemd-networkd, network manager,我……)。实验涉及到的主机名称,host:实体的服务器,remote:通过 ssh 和 vnc 连接到 host 进行管理,是一切操作的输入端。master:在 host 上建立的 KVM 虚拟机,作为虚拟集群的 master 节点。node1:在 host 上建立的 KVM 虚拟机,作为虚拟集群的 slave 节点。
虚拟机创建和默认网络分析
用 virsh 套件创建和管理 KVM 虚拟机
先看一下 host 原始的网络环境,忽略无关的网卡,和外网链接的实体网卡为 eno1。安装 KVM 需要的软件 sudo apt-get install qemu-kvm libvirt-bin virtinst bridge-utils
. 其中 bridge-utils
提供了管理 bridge 的工具(其实应该已经 deprecated 了,事实上直接用 ip
命令设置也可以)。libvirt
提供了虚拟机管理的上层 API 套件。
虚拟机的创建很简单,只需要一行 virt-install
即可: sudo virt-install --name=<vmname> --memory=2048 --vcpus=1 --cdrom=ubuntu-18.04.2-live-server-amd64.iso --disk size=5 --graphics vnc,listen=0.0.0.0,password=<key>
。这样就创建了一个虚拟机,当然还可以有更多的配置参数可以设置,甚至可以做成使用 kickstart(debian 似乎叫做 proceed 文件) 自动安装。这些细节有时间再折腾,这里我们直接从 remote 通过 VNC (mac 上使用系统自带的 screen sharing 软件即可)按照给定的端口和密码连接(VNC 连接的 ip 依然是 host 的外网 ip)过去即可按提示完成操作系统安装。(事实上,也可以利用 Ubuntu 官方提供的原生 qcow2 格式的 cloud image 镜像来安装虚拟机,这样就避开了手动安装的过程,可以直接启动。cloud image 可从 tuna 镜像源下载,具体 provision 虚拟机的过程参考1。)
安装 ubuntu 的一些提示。记得在硬盘选项时,需要选 manual 并且对硬盘使用 make bootable device 的选项,才可以成功完成安装过程。另外就是网络设置使用默认的自动 dhcp 即可,因为虚拟机管理软件默认会在 host 打开一个虚拟机内网的 dhcp 分配可用 ip。之后就可以按照安装时设置的用户密码,通过正常的 ssh 从 host 主机连接,即可进行虚拟机内的操作。
这里有个细节,就是你需要虚拟机的 ip 地址才可完成 ssh 连接和操作。一种方法是在 VNC 远程 ifconfig
获取ip 地址,另一种是在 host 中先通过 virsh net-list
获取虚拟机默认的网络名称(一般就是 default),然后 virsh net-dhcp-leases <name>
即可看到每台虚拟机的 ip。假设我们建立两台虚拟机,一台 master,一台 node1,那么 virsh net-dhcp-leases default
结果为:(注意到这里不同虚拟机 mac 地址自动不同,而并没有由 virt-install --network
参数给定,但有些时候网络 debug 时可能需要注意一下是否会有虚拟网卡的 mac 地址冲突)
Expiry Time MAC address Protocol IP address Hostname Client ID or DUID
-------------------------------------------------------------------------------------------------------------------
2019-04-26 16:35:09 52:54:00:68:f2:82 ipv4 192.168.122.236/24 master ff:b5:5e:67:ff:00:02:00:00:ab:11:23:9a:95:d6:3d:2a:00:7f
2019-04-26 16:39:40 52:54:00:78:70:ab ipv4 192.168.122.120/24 node1 ff:b5:5e:67:ff:00:02:00:00:ab:11:b0:6c:b1:12:aa:2d:69:8a
默认的虚拟机网络拓扑
有了虚拟机 ip 之后,我们就可以从 host 直接愉快的 ssh 到 VM 上玩耍了,在此之前,让我们观察一下 KVM 虚拟机默认创建的网络结构。在 host 主机 ifconfig
,我们发现系统多了几张虚拟网卡,部分信息如下。
virbr0 Link encap:Ethernet HWaddr fe:54:00:68:f2:82
inet addr:192.168.122.1 Bcast:192.168.122.255 Mask:255.255.255.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
vnet0 Link encap:Ethernet HWaddr fe:54:00:68:f2:82
inet6 addr: fe80::fc54:ff:fe68:f282/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
vnet1 Link encap:Ethernet HWaddr fe:54:00:78:70:ab
inet6 addr: fe80::fc54:ff:fe78:70ab/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
其中 vnet0 和 vnet1 相当于虚拟机 master 和 node1 插到 host 的网卡入口。事实上,这两个虚拟网卡的类型是 tun。
$ ip link show type tun
17: vnet0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master virbr0 state UNKNOWN mode DEFAULT group default qlen 1000
link/ether fe:54:00:68:f2:82 brd ff:ff:ff:ff:ff:ff
19: vnet1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast master virbr0 state UNKNOWN mode DEFAULT group default qlen 1000
link/ether fe:54:00:78:70:ab brd ff:ff:ff:ff:ff:ff
tun 这种类型的虚拟网卡,可以由应用层直接写入数据而装成是从外面发来的数据包的样子,恰好适合于这里的虚拟化技术。因为默认配置创建的虚拟机只有一块非 loopback 网卡,对应名称是 ens3。所以上层看,就像 host 和 master 两台独立的主机通过网线直接相连,一段网口的网卡是 ens3,而另一端是 vnet0。这恰好是虚拟机想要实现的效果。而底层实现上看,从 ens3 送出的数据包,被 host 应用层的虚拟机程序接管,并将该包从应用层直接写入 tun 虚拟网卡,由于 tun 类型网卡的特点,对于 host 来说,就好像从外界通过 tun 网卡界面收到了 packet 一样。
由于 tun 网卡被 attach 在了 virbr0 的网桥2上,那么虚拟机发出的 packet 会到达 virbr0,再通过网桥的端口mac地址映射(类似于硬件交换机)送出。网桥除了负责将 packet 转发给其他端口(网卡设备)之外,当网桥发现进来 packet 的目的 mac 是自己的 mac 的时候,就不会转发而是直接把对应的 packet 交给内核网络协议栈处理。事实上可以将带 ip 的网桥视为纯粹的二层交换机和接入该交换机的一个具有网桥 ip 和 mac 地址的正常网卡的结合体。注意到连接外网的 host 网卡 eno1 并没有连接在网桥上,因此截止到现在的网络结构的描述,是不足以使得虚拟机连接外网的,当然虚拟机之间已经可以通过 virbr0 链接。真正使得该 virbr0 网桥和外网链接的,是 iptables 中 nat 表的相关设置,这些设置也在建立虚拟机时被默认写入了。
$ sudo iptables -t nat -vL
Chain PREROUTING (policy ACCEPT 149K packets, 17M bytes)
pkts bytes target prot opt in out source destination
Chain INPUT (policy ACCEPT 129K packets, 15M bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 30762 packets, 2228K bytes)
pkts bytes target prot opt in out source destination
Chain POSTROUTING (policy ACCEPT 30742 packets, 2227K bytes)
pkts bytes target prot opt in out source destination
52 8564 RETURN all -- any any 192.168.122.0/24 base-address.mcast.net/24
0 0 RETURN all -- any any 192.168.122.0/24 255.255.255.255
139 8340 MASQUERADE tcp -- any any 192.168.122.0/24 !192.168.122.0/24 masq ports: 1024-65535
268 19096 MASQUERADE udp -- any any 192.168.122.0/24 !192.168.122.0/24 masq ports: 1024-65535
74 6216 MASQUERADE all -- any any 192.168.122.0/24 !192.168.122.0/24
注意到 nat 表中的 POSTROUTING 链的规则,目标为 MASQUERADE 恰好对应了来自 192.168.122.0/24 这一虚拟机内网的 SNAT。这正说明了 KVM 虚拟机默认的网络环境是 NAT。其对应的效果是虚拟机可以访问外网和虚拟机间互访,但外网不能直接访问到虚拟机。对应的数据流向分析,对于虚拟机向外网发起的访问,由于虚拟机路由的默认网关是 192.168.122.1,此网关的地址绑定在 host 的网桥 virbr0 上。虚拟机通过查询本地的 arp 表,找到 virbr0 对应的 mac 地址,并将包发送出去。virbr0 通过 vnet0 接受到该 packet 后,发现目的 mac 地址与自己的 mac 地址相符,因此将该 packet 上传到内核网络协议栈。内核路由发现该 packet 目的 ip 非本机 ip,因此将该 packet 进行 forward(前提是 host 主机的 ip_forward 功能打开,这一设置应该安装 KVM 之后也是默认的)。经过 forward 链后,在 packet 发出之前,会通过 nat 表的 POSTROUTING 链,由于此时 packet 源 ip 在 192.168.122.0/24 网段,触发了 MASQUERADE 规则,因此源 ip 被修改为 host 外网 ip,顺利完成 SNAT 并将 packet 发送至外网。这一过程可以结合这个神图3来分析。
dhcp 及 dns 的默认处理
上述默认的网络设置可以在 /etc/libvirt/qemu/networks/default.xml
中查看,可以很清楚的看到里面 mode='nat'
。而对应的虚拟机网段的 dhcp 服务器由 dnsmasq 实现,其配置文件位于 /var/lib/libvirt/dnsmasq/default.conf
,从中可以明确看到 dhcp 服务绑定的网卡是 virbr0,也即 interface=virbr0
。注意该配置文件中 dhcp-hostsfile
选项指向了同文件夹下的 default.hostsfile
文件。通过修改该文件(具体语法可以在 dnsmasq 的 manpage 里查找 --dhcp-host
的格式),可以使 dhcp 分配的 ip 和对应虚拟机的 mac 地址绑定(实现静态 ip 分配最好还是直接在 virsh 的层面设置,参考4)。通过 sudo service libvirt-bin status
可以看到其管理的子进程除了 qemu-system-x86_64
运行的两台虚拟机外,还有 /usr/sbin/dnsmasq
,就是这一进程在提供对虚拟机内网的 dhcp 和 ip 分配服务。
至于虚拟机中 dns 的默认设置,可以查看 cat /run/systemd/resolve/resolv.conf
,这里显示了 dhcp 分配的 dns ip,应该默认是 192.168.122.1
,而正是 host 主机中的 dnsmasq 在监听这一端口,从而向外进行 dns 查询。注意到虚拟机环境是 ubuntu 18.04,dns 管理的部分也和传统的 linux 直接查看修改 /etc/resolv.conf
不同,ubuntu 又自作主张地添加了新的抽象层来管理 dns,也即 systemd-resolved 服务。如果直接查看 resolv.conf,只会看到 127.0.0.53 这一 ip,考虑到整个 127.0.0.0/8 网段都是默认 loopback (RFC 3330),因此 dns 请求会回到内核并被 systemd-resolved 服务接受并处理,这才再次向外发送 dns 请求。对于 ubuntu 的网络管理机制的复杂混乱以及前向兼容性,我已经无力吐槽了。强烈建议对网络环境的配置有较复杂要求的服务器慎用 ubuntu 这一 linux 发行版。
集群网络拓扑搭建
二层网络设施实现
上面叙述的,都是 KVM 虚拟机默认的 NAT 网络拓扑,但离我们想实现的,node1 外网通信通过 master NAT 来实现还有点远。考虑到在虚拟机现有的网卡上作修改,可能会让虚拟机“失联”,因此保险起见,我们首先为每个虚拟机,再创建一张虚拟网卡。比如对于 master 节点,在 host 键入的命令为 sudo virsh attach-interface master bridge mybr0
。(如果希望持久化这张网卡信息,需要添加 --persistent
选项,这样就修改了 domain 对应的 xml 文件,关于持久化和运行时的两个不同的 xml 文件,可以参考5) 这一命令将在名为 master 的节点新建一张网卡(默认 down,默认 mac 地址随机,因此几乎不会冲突),这张网卡将通过 host 新增的 vnet2 虚拟网卡和指定的新网桥 mybr0 连接。同理也可在 node1 节点新建网卡。当然这些的前提都是在 host 新增一个网桥 mybr0。新建网桥和启动网桥网卡的命令分别为 sudo ip link add name mybr0 type bridge
,ip link set mybr0 up
。(当然也可以用 brctl
和 ifconfig
达到同样的设置效果,但还是推荐使用统一且较新的 ip
API 来操作。)最后我们需要给网桥一个 ip,sudo ip addr add 192.168.48.1/24 dev mybr0
,注意这里千万不要给成和默认网桥 virbr0 (192.168.122.1/24) 在一个网段。
同样的,我们需要 ssh 到各个虚拟机中,把刚才添加的虚拟网卡启动,并添加 192.168.48.0/24
网段的 ip。比如在 master 中 sudo ip addr add 192.168.48.10/24 dev ens6
,这是因为第二块网卡默认命名是 ens6(可以用 ip link show
查看全部网卡,包括未启动的网卡,而默认的 ifconfig
只能看到启动的网卡的信息)。node1 对应的 ip 为 192.168.48.20。完成这些操作后,已经可以从 host ping 通 48 这一网段的两台机器了,两台虚拟机也可以 ping 通 192.168.48.1 这一在 host 的网桥 mybr0。但是,很有可能 48.10 和 48.20 是无法互相 ping 通的。这一问题的可能原因参考6。简单来讲,虽然 host 主机已经开启了 ip_forward
的内核模块,另一方面 bridge 本来应该是纯二层的行为,但是,somehow bridge 的转发还是会过三层 iptables 的那些表和链,而大多数时候 FORWARD 链的默认 policy 是 DROP,这就导致了两台虚拟机之间无法经过 mybr0 这一网桥转发而 ping 通。解决方案也有很多种,既可以把 FORWARD 链的默认 policy 改为 ACCEPT (可能存在一定的安全隐患),也可以根据网卡出口或者源 ip 在 FORWARD 表补充几条 ACCEPT 的规则(最安全,也是 KVM 和 docker 默认设置采用的方案,可以 iptables -L
进行查看),最省事的方案就是 echo 0 > /proc/sys/net/bridge/bridge-nf-call-iptables
,这样系统 bridge 进行转发时,将不再走 iptables 的链(这种方案既不太安全,也可能和其他较复杂的网络配置有一定冲突,慎用)。
三层网络配置
经过上面的配置,我们相当于完成了节点集群二层网络的建设。通俗点说通过两根新网线 vnet2,vnet3 接在新交换机 mybr0 上,使得虚拟机节点之间可以通过这个新网络通讯了。接下来就要在这个网络上,进行三层基础设施的设置。因为现在通过 48 这一网段,所有的虚拟机都无法连接外网。我们自然可以和 KVM 默认的一样,在 host 添加一些基于源 ip 在 48 网段的 nat iptables 规则。但这样做,就和默认的网络一模一样了。我们希望达到的目的是,slave 节点,也就是这里的 node1,想要连接外网的话,只能通过 master 进行。master 上通过 NAT 的方式使 slave 节点连接外网,同时可以轻松控制 slave 节点可以连接外网的细节。很明显,master 要起一个路由的作用,其外网 ip 可以直接使用 KVM 默认的 192.168.122.236,通过 ens3 网卡以NAT 的形式和 host 外网相连 (所以这里总共有两道 NAT)。而 ens6 对应的 192.168.48.10 则与集群内网进行通讯,同时作为整个集群外网连接的网关。以上分析对应的整个网络拓扑图如下所示。
经过上面的分析,剩下的整个设置就清楚了,大概需要以下几个步骤。1. slave 节点默认路由的更改。2. master 节点路由和 NAT 功能的设置。3. (可选)关闭 slave 节点上 122 网段的默认网卡 ens3,将 host 上对应的 vnet 网卡,从 virbr0 网卡上脱离并关闭。完成这些步骤之后,一个虚拟集群的网络结构就基本完成了。其中步骤1,需要查看 node1 上的路由表 (netstat -nr
或 ip route show
似乎信息更全面)和更改 node1 的默认网关到 master (route add default gw 192.168.48.10
,route delete default gw 192.168.122.1
)。步骤2,为了 master 可以作为路由,需要打开 master 虚拟机上的 ip_forward
功能(echo 1 > /proc/sys/net/ipv4/ip_forward
)。之后需要设置 iptables nat 表的 POSTROUTING 链来实现对于源 ip 在 48网段(也即虚拟集群内网)的 SNAT。具体地,sudo iptables -t nat -A POSTROUTING -m addrtype --src-type LOCAL -j ACCEPT
,sudo iptables -t nat -A POSTROUTING -s 192.168.48.0/24 -j SNAT --to 192.168.122.236
。第一句规则参考了7,这主要是防止 master 本地发出的源 ip 为 192.168.48.10 的 packet 也被 NAT 掉,导致 master 在集群内网“失联”。当然也可以和 KVM 虚拟网络默认的那样,通过区分目的 ip 网段来有选择的 SNAT(只有目的 ip 在非源 ip 的网段才做 NAT)。至此,事实上一个集群网络结构就可以跑起来了,其完全模拟了一个小型真实集群的网络拓扑,且 master 节点也在虚拟机模拟,而非 host 主机。
最后为了网络比较干净,可以把已经不需要的 node1 上的 ens3 网卡关掉,也就是说 node1 已经不需要通过 192.168.122 网段来通信了。现在无论是和 host 的局域网,还是出 host 的外网,node1 都通过 192.168.48 网段来通信。
子节点 dns 和 dhcp 调整
搭建完成网络框架之后,node1 节点的 dns 还是指向的 192.168.122.1,当然即使 node1 上的 ens3 网卡关闭,也不影响该 DNS 查询 (请自行思考此时网络通路情况)。但为了更真实的模拟一个集群的网络情况,我们将在 master 虚拟机上运行 dnsmasq 来为其他子节点提供 dhcp 和 dns 服务,用来取代 KVM 默认的监听在 virbr0 上的 dnsmasq 服务(当然 host 上这一服务依然需要存在,用来为 master 节点服务)。
为了完成这一调整,需要在 master 节点上安装 dnsmasq,sudo apt install dnsmasq
,并修改默认的配置文件,在我的虚拟机上,其对应为 /etc/dnsmasq.d/lxd
。其文件内容修改如下,即可只绑定在 master 的 ens6 网卡上,并通过 48 网段,对内网的其他主机提供 dhcp 和 dns 服务。
interface=ens6
# only listen to the interface given above
bind-interfaces
except-interface=lxdbr0
# the ip range dhcp server can distribute
dhcp-range=192.168.48.11,192.168.48.127,255.255.255.0
dhcp-no-override
# optional, one can add mac addr ip addr binding in the following files
dhcp-hostsfile=/etc/dnsmasq.d/macip.map
如果想要对于每个子节点分配固有 ip 的话,需要一个 hostsfile,其内容如下
dhcp-host=52:54:00:eb:6c:6a,192.168.48.20,infinite
上边 mac 地址即为 node1 上 ens6 网卡对应的 mac 地址。如果有更多的机器可按照每行列出。这里有一个坑,就是 hostsfile 的格式与 dnsmasq 的 manpage 的要求并不相同。manpage 指出文件只需要写等号后边的部分,然而这样 dnsmasq 服务会重启失败。只有每行都添加上 dncp-host=
之后,dnsmasq 服务才能正常重启和符合预期的使用,这一坑可以参见这里8。值得注意的是,按照上边的正确写法,查看 dnsmasq status 时,依旧可以在日志中看到关于该文件格式的报错,不过无视就好,因为 dnsmasq 会按照预期工作,配置文件可以生效。
完成 master 上 dnsmasq 配置文件修改后,重启 dnsmasq sudo service dnsmasq restart
,注意这里仅仅 reload 的话是不够的。之后转到 node1 节点上,删除绑定在 ens6 网卡上的 ip,ip addr del 192.168.48.20 dev ens6
。然后重新通过 dhcp 服务使该网卡获取地址,dhclient ens6
。此时再次查看 ens6 上的网址 ip addr show ens6
,可以发现依旧是 192.168.48.20,这与我们在 master 上 hostsfile 内的设置相一致。由此,子节点 node1 的 dhcp 和 dns 服务已经完全被 master 虚拟机接管,而与 host 最大限度的解耦。
我们总不希望在集群的局域网内总是通过 ip 地址来互相 ssh 或者 ping,人类还是更容易记名字,比如 master 和 node1 之类的,因此我们进一步配置,使得该集群的局域网内的本地域名解析可以生效,实现在集群任意节点可以通过 node1 之类的名字和其他节点进行网络通讯。首先在 master 的 /etc/hosts
中添加进名称与 ip 地址的对应,如 192.168.48.10 master
这种。如果再把该 hosts 文件复制到各 slave 节点虚拟机上,hostname 的配置就完成了。但是我们怎么可能选择这么“不折腾”的方案。一个好的集群,一定是要保持 slave 节点的本地配置尽可能的少,因此我们选择使用 master 虚拟机上的 dnsmasq 提供的 dns 服务来为 slave 节点进行本地地址解析服务。由于 dnsmasq 的默认配置即会读取本机的 /etc/hosts
内容,作为dns查询结果,因此理论上 slave 节点上的本地域名解析应该能开箱即用了。但是在 node1 上 ping master
并不会有反应。这一坑又又又是 ubuntu 奇葩的网络管理造成的,其 systemd-resolve 接管的 dns 服务,会直接在本地丢弃长得不像域名的查询,而不会请求远程 DNS 服务器9。(就这据说还是个 feature 而不是 bug10,我只能当作选择 ubuntu 给我了更多折腾的机会吧。)不过 workaround 也是有的,那就是利用 search domain 机制11。我们在 master 的 dnsmasq 配置文件中添加下面两行。
domain=test.cluster
expand-hosts
这样 dhcp 服务除了自动配置 dns 外,还会自动配置 slave 节点的 search domain。所谓的 search domain,就是当请求的域名长得像 hostname 时(只有单个词),系统会自动用 search domain 补全网址。比如 ping master
系统没有在本地搜到 master 的 host 记录的话,就会直接理解为 ping master.test.cluster
。而 expand-hosts 选项,则将 master 上的 hosts 文件自动补充 domain。也即向 dnsmasq 查询 master.test.cluster
的 ip 时,dns 还是可以顺利识别并返回 hosts 中 master 对应的 ip。因此添加这两行配置后,重启 master 上的 dnsmasq 服务,并且在 node1 上刷新 dhcp leases,dhclient ens6
,此时 node1 上已经获得了 search domain。此时 ping master
已经是通的了。这里还有一个小坑要注意,你会发现此时在 node1 上,dig master
依旧得不到应答。首先排除 dns 缓存,systemd-resolve --flush-cache
,之后依旧没结果。问题出在 dig
的默认参数,不会自动补充 search domain 查询,使用 dig +search master
即可得到正确的结果返回。
因为我们完成了 48 网段的 dhcp 配置,之后再添加新的虚拟机时,就很方便了,不需要经过这么多的配置,直接在以前 virt-install
的基础上添加 --network=bridge:mybr0
这一网络参数即可。新安装的虚拟机则会默认进入 48 网段,并且不再和 122 网段的 default 网络直接沟通。这也是我们模拟集群希望达到的目的。对于新加入节点的 ip,你可以在 master 虚拟机中的 /var/lib/misc/dnsmasq.leases
内查看,里面记录了 dnsmasq 分配的所有主机的 mac 地址和对应的 ip 地址。
最后的一点提示,以上几乎大多数网络相关的参数修改和设置,在网络重启,虚拟机重启,或者 host 主机重启后,都会消失和恢复原设置。因此,如果需要长期稳定使用该虚拟网络,有必要通过脚本和修改相应网络配置文件的形式,使得以上网络设置固定,这方面这里暂且不表。
一些实验观察
下面我们通过 ping 和 tcpdump 做一些实验,更好的了解现有的网络拓扑,和 packet 的流向。请注意下面命令所在的机器。
node1:$ hostname
node1
node1:$ traceroute -n 10.0.0.9
1 192.168.48.10 0.968 ms 1.052 ms 0.810 ms
2 192.168.122.1 6.549 ms 6.276 ms 6.412 ms
3 10.0.0.9 6.499 ms 6.214 ms 6.449 ms
# 可以看出 node1 可以顺利和外网通讯,并且通过 48.10 的网关和 122.1 的 virbr0
node1:$ ip route
default via 192.168.48.10 dev ens6
192.168.48.0/24 dev ens6 proto kernel scope link src 192.168.48.20
# 默认路由是 master 的 48.10
最有趣的是当在 node1 上 ping 10.0.0.9
时的抓包情况。使用 sudo tcpdump -i <nic> icmp
,可以发现在 host 的 eno1,virbr0,mybr0,vnet0,vnet2,vnet3,node1 的 ens6,master 的 ens3,ens6 上都可以看到相应的 icmp request 和 reply 包。能说清楚这个问题的话,相信对这个网络的拓扑就已经有一定的了解了。其基本通讯路径是 node1:ens6 - host: vnet3 - host: mybr0 - host: vnet2 - master: ens6 - master: ens3 - host: vnet0 - host: virbr0 - host: eno1。至于其中每个环节都发生了什么,是哪一层的交流,这里不再赘述了。放一个跟踪路由的 ping 的结果供参考。
node1:$ ping -v -I ens6 -R 10.0.0.9
RR: 192.168.48.20
192.168.122.236
10.0.0.10
10.0.0.9
10.0.0.9
192.168.122.1
192.168.48.10
192.168.48.20
一个悬案
这里简单记录一下这个系统里一个我还没能完全确定原因的现象。以上的叙述和实验都是建立在 node1 上的 ens3 网卡已经关闭,或者至少是 122.1 的默认网关已经取消的基础上的。假设我们只是添加了一个 48.10 作为默认网关,并没有删除 node1 路由表中 122.1 的默认网关。这在正常使用的情况,也没什么问题,因为外网请求还是会默认走 48.10。因为单路由表默认网关只有一个,即使多个,linux 也会根据不同的参数最终确定是哪一个,并且一直固定成那个网关作为默认网关,路由表中多余的默认网关项并没什么作用。
但是,当我们在 node1 ping -I ens3 10.0.0.9
的时候,问题就暴露出来了。通过在不同的网卡上 tcpdump
,可以观察到 icmp request 和 reply 都是正常的,走的路线被改成了从 ens3 出发,也即 node1: ens3 - host: vnet1 - host: virbr0 - host: eno1。这一路的网卡,去包和回包都能看到。但是,ping 并不通。而如果把 48.10 的默认路由删除,以上的 ping 就立刻通了。一个更加强烈的例子是,我们将 10.0.0.0/24 网段的路由交给 192.168.122.1,也即在 node1 上 sudo route add -net 10.0.0.0/24 gw 192.168.122.1
。此时就变成了 ping -I ens3 10.0.0.9
通,而 -I ens6
不通。同样的即使不通的 ens6 ping,在整个预期通路的网卡上都可以顺利抓到毫无问题的去包和回包。该问题暂时没找到理想的答案,我的一个猜想是, somehow 协议栈中的某个环节,发现接受到的 packet 的源 ip 和进入的 nic,在路由层面不符,所以丢包,没有向应用层 socket 提供? 一个更合理的猜想:ping
虽然指定了出口网卡,但是监听的入口依然是默认路由的 ip。因此 icmp reply 的包由于源 ip 不符,而没找到应用层对应的监听,而被丢包。
可以说本来一个真实小型集群的网络是很简单的,一般找一个双网卡的机器做 master,内部一个局域网,master 上一个 NAT,所有网线插在一个交换机上就完事了。但如本文所述,当所有东西都是虚拟的时,不仅机器是虚拟的,网线,交换机也是虚拟的。而其虚拟实现还往往和硬件实现有着这样那样的区别,这就导致了纯虚拟的搭起一个集群网络,反而更麻烦一些,还是稍微需要一点手艺的。