前段时间,我负责的远程控制手机的功能新加了动态修改手机端网络延迟的功能(网络测试),所以当时 Infra 团队为我们构建了一个 WiFi 网络环境,并通过两台 Linux 主机进行实际的网络控制。不过后来我们对其软件解决方案进行了改善,使其不仅能够修改网络的延迟,还能够修改网络的带宽和丢包情况,本文就介绍了这整个实现方案。
网络结构
上图是整个 WiFi 网络环境的结构的简化图。
同一 WiFi 由多个 AP 负责,它们使用同一个 SSID,密码,但是工作在不同的信道上。当任一 AP 故障时,连在 L3 交换机上的无线网络控制器,将负责故障的无缝转移。
整个网络链路中,从 AP 开始到 Linux Server 上,均采用了链路聚合技术,而且呈网状连接,来达到容灾和负载均衡的功效。
因为和 Linux Server 直接相连的的是一个基于 VRRP 的虚拟冗余路由,而且 Linux Server 上也使用了动态链路聚合技术,所以每台 Linux Server 上都有 4 个网络接口,每一个 L3 交换机会使用到其中的两个接口,这两个接口会聚合在一起。 在每台 Linux Server 上有四个物理网卡,它们两两组合。通过 Linux 提供的网卡 bonding 功能聚合在一起。不过在下面的介绍中,因为我没有可供实验的实体机,所以我将采用 2 对 veth peer 虚拟网卡的方式模拟 Linux Server 和一台 L3 交换机的连接过程。
我这里创建了 2 对 veth peer 网卡,假设 veth1 veth3 是在 L3 交换机上的两个网卡,这样就能模拟出 Linux Server 与 L3 交换机之间通过两条网线连通,Linux Server 和 L3 交换机上各有 2 个网络接口的效果。
# 创建两对 veth peer ip link add veth0 type veth peer name veth1 ip link add veth2 type veth peer name veth3 # 启动四个 veth 接口 ip link set veth0 up ip link set veth2 up ip link set veth1 up ip link set veth3 up # 确认 Linux Server 上的网络接口 ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether fa:16:3f:ad:cd:f8 brd ff:ff:ff:ff:ff:ff inet 10.231.227.166/18 brd 10.231.255.255 scope global dynamic eth0 valid_lft 3492sec preferred_lft 3492sec 3: veth3@veth2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 62:30:f1:52:80:e2 brd ff:ff:ff:ff:ff:ff 4: veth2@veth3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether b2:96:e5:2d:46:db brd ff:ff:ff:ff:ff:ff 5: veth1@veth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 62:b0:ec:1e:03:b6 brd ff:ff:ff:ff:ff:ff 6: veth0@veth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether a6:04:98:44:cd:5f brd ff:ff:ff:ff:ff:ff
接下来,我们创建两个 bond 将 Linux Server 上这两对 veth 网卡聚合在一起,这需要修改 /etc/sysconfig/network-scripts/ 下的网卡配置。
# 重启网络 service NetworkManager restart Redirecting to /bin/systemctl restart NetworkManager.service
service network restart
ip link set bond0 up ip link set bond1 up # 确认当前 Linux Server 网络状况 ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether fa:16:3f:ad:cd:f8 brd ff:ff:ff:ff:ff:ff inet x.x.x.x/18 brd 10.231.255.255 scope global dynamic eth0 valid_lft 3063sec preferred_lft 3063sec 3: veth3@veth2: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc noqueue master bond1 state UP group default qlen 1000 link/ether 62:b0:ec:1e:03:b6 brd ff:ff:ff:ff:ff:ff 4: veth2@veth3: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc noqueue master bond0 state UP group default qlen 1000 link/ether a6:04:98:44:cd:5f brd ff:ff:ff:ff:ff:ff 5: veth1@veth0: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc noqueue master bond1 state UP group default qlen 1000 link/ether 62:b0:ec:1e:03:b6 brd ff:ff:ff:ff:ff:ff 6: veth0@veth1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc noqueue master bond0 state UP group default qlen 1000 link/ether a6:04:98:44:cd:5f brd ff:ff:ff:ff:ff:ff 7: bond1: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 62:b0:ec:1e:03:b6 brd ff:ff:ff:ff:ff:ff 8: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether a6:04:98:44:cd:5f brd ff:ff:ff:ff:ff:ff
此外,我们还可以通过 /proc/net/bonding/bond0 查看当前 bond0 接口的状态,其中我们会看到该接口的 bonding 模式是动态链路聚合,hash 策略是 layer 2+3,还能看到其下的两块虚拟网卡 veth0 和 veth2 的原始 MAC 和使用的 MAC,以及该聚合链路对端的 MAC 地址,也就是 bond1 的 MAC 地址。同理,bond1 的信息也可以通过类似的方式查看。
Bonding Mode: IEEE 802.3ad Dynamic link aggregation Transmit Hash Policy: layer2+3 (2) MII Status: up MII Polling Interval (ms): 100 Up Delay (ms): 0 Down Delay (ms): 0
802.3ad info LACP rate: slow Min links: 0 Aggregator selection policy (ad_select): stable System priority: 65535 System MAC address: a6:04:98:44:cd:5f Active Aggregator Info: Aggregator ID: 1 Number of ports: 2 Actor Key: 15 Partner Key: 15 Partner Mac Address: 62:b0:ec:1e:03:b6
Slave Interface: veth0 MII Status: up Speed: 10000 Mbps Duplex: full Link Failure Count: 0 Permanent HW addr: a6:04:98:44:cd:5f Slave queue ID: 0 Aggregator ID: 1 Actor Churn State: monitoring Partner Churn State: monitoring Actor Churned Count: 0 Partner Churned Count: 0 details actor lacp pdu: system priority: 65535 system mac address: a6:04:98:44:cd:5f port key: 15 port priority: 255 port number: 1 port state: 61 details partner lacp pdu: system priority: 65535 system mac address: 62:b0:ec:1e:03:b6 oper key: 15 port priority: 255 port number: 1 port state: 61
Slave Interface: veth2 MII Status: up Speed: 10000 Mbps Duplex: full Link Failure Count: 0 Permanent HW addr: b2:96:e5:2d:46:db Slave queue ID: 0 Aggregator ID: 1 Actor Churn State: monitoring Partner Churn State: monitoring Actor Churned Count: 0 Partner Churned Count: 0 details actor lacp pdu: system priority: 65535 system mac address: a6:04:98:44:cd:5f port key: 15 port priority: 255 port number: 2 port state: 61 details partner lacp pdu: system priority: 65535 system mac address: 62:b0:ec:1e:03:b6 oper key: 15 port priority: 255 port number: 2 port state: 61
Bonding Mode: IEEE 802.3ad Dynamic link aggregation Transmit Hash Policy: layer2+3 (2) MII Status: up MII Polling Interval (ms): 100 Up Delay (ms): 0 Down Delay (ms): 0
802.3ad info LACP rate: slow Min links: 0 Aggregator selection policy (ad_select): stable System priority: 65535 System MAC address: 62:b0:ec:1e:03:b6 Active Aggregator Info: Aggregator ID: 1 Number of ports: 2 Actor Key: 15 Partner Key: 15 Partner Mac Address: a6:04:98:44:cd:5f
Slave Interface: veth1 MII Status: up Speed: 10000 Mbps Duplex: full Link Failure Count: 0 Permanent HW addr: 62:b0:ec:1e:03:b6 Slave queue ID: 0 Aggregator ID: 1 Actor Churn State: none Partner Churn State: none Actor Churned Count: 0 Partner Churned Count: 0 details actor lacp pdu: system priority: 65535 system mac address: 62:b0:ec:1e:03:b6 port key: 15 port priority: 255 port number: 1 port state: 61 details partner lacp pdu: system priority: 65535 system mac address: a6:04:98:44:cd:5f oper key: 15 port priority: 255 port number: 1 port state: 61
Slave Interface: veth3 MII Status: up Speed: 10000 Mbps Duplex: full Link Failure Count: 0 Permanent HW addr: 62:30:f1:52:80:e2 Slave queue ID: 0 Aggregator ID: 1 Actor Churn State: none Partner Churn State: none Actor Churned Count: 0 Partner Churned Count: 0 details actor lacp pdu: system priority: 65535 system mac address: 62:b0:ec:1e:03:b6 port key: 15 port priority: 255 port number: 2 port state: 61 details partner lacp pdu: system priority: 65535 system mac address: a6:04:98:44:cd:5f oper key: 15 port priority: 255 port number: 2 port state: 61
vi /etc/sysconfig/network-scripts/ifcfg-bond1.91 DEVICE="bond1.91" BOOTPROTO=none ONPARENT=yes IPADDR=192.168.11.2 NETMASK=255.255.255.0 VLAN=yes NM_CONTROLLED=no
# 启动 vlan ifup /etc/sysconfig/network-scripts/ifcfg-bond1.90 ifup /etc/sysconfig/network-scripts/ifcfg-bond1.91 # 查看网络设备 ip addr 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether fa:16:3f:ad:cd:f8 brd ff:ff:ff:ff:ff:ff inet x.x.x.x/18 brd 10.231.255.255 scope global dynamic eth0 valid_lft 2127sec preferred_lft 2127sec 3: veth3@veth2: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc noqueue master bond1 state UP group default qlen 1000 link/ether 62:b0:ec:1e:03:b6 brd ff:ff:ff:ff:ff:ff 4: veth2@veth3: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc noqueue master bond0 state UP group default qlen 1000 link/ether a6:04:98:44:cd:5f brd ff:ff:ff:ff:ff:ff 5: veth1@veth0: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc noqueue master bond1 state UP group default qlen 1000 link/ether 62:b0:ec:1e:03:b6 brd ff:ff:ff:ff:ff:ff 6: veth0@veth1: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 1500 qdisc noqueue master bond0 state UP group default qlen 1000 link/ether a6:04:98:44:cd:5f brd ff:ff:ff:ff:ff:ff 7: bond1: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 62:b0:ec:1e:03:b6 brd ff:ff:ff:ff:ff:ff 8: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether a6:04:98:44:cd:5f brd ff:ff:ff:ff:ff:ff 9: bond0.90@bond0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether a6:04:98:44:cd:5f brd ff:ff:ff:ff:ff:ff inet 192.168.10.1/24 brd 192.168.10.255 scope global bond0.90 valid_lft forever preferred_lft forever 10: bond0.91@bond0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether a6:04:98:44:cd:5f brd ff:ff:ff:ff:ff:ff inet 192.168.11.1/24 brd 192.168.11.255 scope global bond0.91 valid_lft forever preferred_lft forever 13: bond1.90@bond1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 62:b0:ec:1e:03:b6 brd ff:ff:ff:ff:ff:ff inet 192.168.10.2/24 brd 192.168.10.255 scope global bond1.90 valid_lft forever preferred_lft forever 14: bond1.91@bond1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 62:b0:ec:1e:03:b6 brd ff:ff:ff:ff:ff:ff inet 192.168.11.2/24 brd 192.168.11.255 scope global bond1.91 valid_lft forever preferred_lft forever
上述的这套方案就是各个设备之间(Linux、L3 Switch、AP 控制器)连接的建立方式,因为我这里没有可用的物理设备做实验,所以只能以模拟的方式进行,不过即便是模拟的方式也和实际在物理机上进行的方案如出一辙,至少从网络接口的属性上来看和物理机上的状态是类似的。因为 veth 不能跨越多台 Linux 设备,也不能将 bond 设备放入命名空间中(我还尝试过在容器中创建 bond 设备,也不允许),所以接下来的部分我就不能以模拟的方式进行了。相反的,我会更多的以图文的形式介绍整个网络的组件过程。
这里总结一下从 Linux Server 到 L3 Switch 的连接方式,之前说过它们之间的连接建立在双方的 2 条物理连接上(上例中通过 2 对 veth peer 模拟),这两条物理连接构成一个基于 LACP 协议的聚合链路。在物理链路上,我们创建了两个虚拟网络 vlan90 和 vlan91,分别对应了不同的数据流向。这时候,我们只需要配置一下路由表,就能保证 Linux 和 L3 Switch 之间的连通性,如下图所示。不仅 Linux 和 L3 Switch 之间采用了虚拟网络 vlan 的方式进行路由,L3 Switch 和 AP 控制器之间也是通过这种方式连接,我们后面会介绍到它。 这里之所以特意分出两个 vlan 代表不同的传输方向,是想要通过 vlan 网络接口控制网络的带宽,延迟等。因为,tc 只能控制从一个网卡发出的数据带宽和延迟,而不能控制从一个网卡接收到的数据带宽和延迟。所以,你会发现实际物理连接的对应接口 bond0 并没有绑定 IP 地址,IP 地址实际上绑定在了 bond0 下的 vlan 网络接口上,这时候当内核从 bond 0 收到一个数据包时,发现该包实际上对应的网络设备应该是 bond0.91,这时候内核就会将该包转发到 bond0.91 这个网络接口上,这就涉及到了向一个网卡发送数据的过程,也就是说只要将 tc(传输控制)规则加载 bond0.91 这个网卡上,就能达到控制流入数据包的效果。
IP 分配方案
理解了 vlan 的构建方式和 LACP 链路的构建方式之后,我们从更高的视角来看一下整个网络中是如何分配 IP 和路由的。这里我们假设 L3 交换机和 AP 控制器之间也是通过 LACP(L3 交换机使用 bond2 <-> bond3 连通 AP 控制器,L3 交换机使用 bond4 连通交换机,AP 1 使用 bond5,AP 2 使用 bond 6 连通交换机) 和 vlan 进行通讯,这不过这里的每个 vlan 已经不再代表一个方向了,而是一个 vlan 代表一个 AP 的双向数据,这也是很自然的,因为 tc 并不会作用于 AP 控制器和 L3 交换机之间,所以没必要再将每个方向的流量拆分成不同的虚拟网络。这时候路由表中需要新添加几项内容,才能保证 L3 交换机和 AP,AP 控制器之间的连通。 在上图中,L3 交换机与 AP 控制器之间是通过两条物理网线直接连接的,建立在这两条物理网线之上的 LACP 链路(bond2<->bond3)上衍生出了 vlan 30 和 vlan31。而从 L3 交换机到 AP 之间是通过两个 L2 交换机连接的,L2 交换机到 L3 交换机之间的连接构成了一个 LACP 聚合链路(bond4)。各个 AP 又分别通过两个 L2 交换机构成了多条 LACP 链路(bond5 和 bond6)。
最后,手机上的默认网关是 AP 控制器,数据从手机通过 L2 交换机 -> L3 交换机-> AP 控制器,AP 控制器的默认网关是 L3 交换机,数据再流回 L3 交换机,L3 交换机的默认网关是 Linux Server,数据通过 vlan 91 流向 Linux,Linux 再将数据包发给自己的 NAT 网关,通过它发送到互联网中。
而从互联网中流回手机的数据包通过 NAT 网关流入 Linux Server,Linux Server 再通过 vlan90 将数据包传送给 L3 交换机,L3 交换机再将数据发给对应的手机。
IP 划分的方案就已经介绍完了,接下来我们再说一下 IP 地址分配的方式,这里我们在 Linux Server 上使用到了 dnsmasq。dnsmasq 提供 DNS 缓存和 DHCP 服务功能。作为域名解析服务器(DNS),dnsmasq 可以通过缓存 DNS 请求来提高对访问过的网址的连接速度。作为 DHCP 服务器,dnsmasq 可以用于为局域网电脑分配内网 IP 地址和提供路由。DNS 和 DHCP 两个功能可以同时或分别单独实现。dnsmasq 轻量且易配置,适用于个人用户或少于 50 台主机的网络。
DHCP(动态主机配置协议)是一个局域网的网络协议。指的是由服务器控制一段 IP 地址范围,客户机登录服务器时就可以自动获得服务器分配的 IP 地址和子网掩码。它通常被应用在大型的局域网络环境中,主要作用是集中的管理、分配 IP 地址,使网络环境中的主机动态的获得 IP 地址、Gateway 地址、DNS 服务器地址等信息,并能够提升地址的使用率。DHCP 协议采用客户端/服务器模型,主机地址的动态分配任务由网络主机驱动。 当一个主机新加入局域网中时会以广播的形式发出 DHCP Discover 报文,一旦 DHCP Server 接收到 Discover 报文,就会向 DHCP Client 发送一个 Offer 报文,该报文中包含为 Client 分配的 IP 地址,这时候 Client 会选择一个 IP (一般按照 Offer 报文的先后顺序)并广播发送一个 Request 报文,其中包含选中的 DHCP Server IP 和自己准备使用的 IP 地址。如果 DHCP Server 收到该 Request 报文后,如果发现该 IP 就是自己刚才为该 Client 分配的 IP,就会回复一个 ACK 报文。之后 Client 会不断地续期该 IP。当 Client 不再需要该 IP 地址时,会向 DHCP Server 发送释放请求。
理解了 iptables 是什么之后,我们怎么用它才能达到我们的目的呢?我们需要使用 iptables 的 mangle 表,因为它可以对报文进行一些修改,我们要通过 IP 地址筛选出不同的手机,进一步地根据源 IP 地址和目标 IP 地址筛选出不同方向的数据包,然后将它们分到不同的类型中。这里所说的类型大家可能不是特别理解,简单地讲,我们可以将一批类似的数据包(比如:从手机 A 发出的包)分到一类中(类通过数字进行标示),随后我们就可以在传输控制模块 tc 中对各个类型的数据包(根据类型编号)加上传输控制策略(比如带宽限制,延迟限制等)。
这里大家可能会好奇为啥分的类由两部分组成(冒号分割),前面部分都是 1,后半部分各不一样。这部分内容,我们随后介绍 tc 的时候在说,这里我们只要保证各种数据包分配的类型是唯一的就行了,这就需要我们在自己的管理程序中维护当前类型分配的情况(我们是在 Java 中维护的,然后Java 再通过命令行操纵 iptables),在要分配新的类型时选择一个当前未被占用的类型即可。此外值得一提的是,因为手机上的数据包会通过网关进行转发才能到 Linux Server,所以我们只能通过 IP 地址来过滤出特定的手机,而不能使用报文的 MAC 地址过滤,这也就是为什么我们前面采用的是静态 DHCP 分配方案(每个 MAC 地址只会分配一个绑定的 IP 地址),而不是动态分配 IP。
Linux tc 配置
为每个手机的每个方向分配不同的类型之后,我们终于可以加网络限制策略了,这里我们使用到的 tc 命令行工具。它是 Linux 操作系统中的流量控制器 TC(Traffic Control),它利用队列规定建立处理数据包的队列,并定义队列中的数据包被发送的方式,从而实现对流量的控制。
CBQ CBQ 是 Class Based Queueing(基于类别排队)的缩写。 它实现了一个丰富的连接共享类别结构,既有限制(shaping)带宽的能力,也具有带宽优先级管理的能力。 带宽限制是通过计算连接的空闲时间完成的。 空闲时间的计算标准是数据包离队事件的频率和下层连接(数据链路层)的带宽。