介绍了Linux系统中等效多径路由(ECMP)及其在网络负载均衡上的应用,给出了OpenWrt下的启用的方法
参考
因为偶然发现了Linux内核中的ECMP这种负载均衡的方法,然后查了些资料,这里整理下(业余水平);文末留下了当时发现ECMP的记录
ECMP在工程方面用的很多,能找到的多是说明文档,介绍特性和成套的解决方案,但是基础的材料并不是那么好找,个人也是刚刚接触到ECMP,水平有限,这里先给出本文的主要参考文献
关于负载均衡,[译] 现代网络负载均衡与代理导论(2017)有一个较为全面的介绍,其中作者提到
关于现代网络负载均衡和代理的入门级教学材料非常稀少(dearth)。我问自己:为什么会这样呢?负载均衡是构建可靠的分布式系统最核心的概念之一。因此网上一定有高质量的相关材料?我做了大量搜索,结果发现信息确实相当稀少。 Wikipedia 上面负载均衡 和代理服务器 词条只介绍了一些概念,但并没有深入 、这些主题,尤其是和当代的微服务架构相关的部分。Google 搜索负载均衡看到的大部分都 是厂商页面,堆砌大量热门的技术词汇,而无实质细节。本文将给读者一个关于现代负载均衡和代理的中等程度介绍,希望以此弥补这一领域的信息缺失。
对此个人在经历一番搜索之后也是感同身受,所以才有了总结的想法;如果觉得上文过于“导论”(和本文的关系不大),华为的文档更贴近本文的主题一些,尽量看英文:Introduction to Load Balancing
Jakub Sitnicki’s Blog的系列博文:对Linux内核中的ECMP的实现深入浅出地做了介绍,如果感兴趣的话可以看看
最后是一篇相对详尽的中文资料,重点是ECMP在内核中的变更历史:ECMP在Linux内核的实现
原理及概念
ECMP也就是下面的路由表的情形,就是到某一个目的地址可以有多个下一跳路径可选
root@K2P:~# ip r
default metric 1
nexthop via 10.170.72.254 dev pppoe-VWAN21 weight 1
nexthop via 10.170.72.254 dev pppoe-VWAN22 weight 1
nexthop via 10.170.72.254 dev pppoe-VWAN31 weight 1
nexthop via 10.170.72.254 dev pppoe-VWAN32 weight 1
root@K2P:~# ip -6 r default
default metric 1
nexthop via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN21 weight 1
nexthop via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN22 weight 1
nexthop via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN31 weight 1
nexthop via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN32 weight 1 pref medium
看起来很简单,要弄清楚这个具体能做到什么程度就需要了解下具体的实现,这里一步一步来看
Packet/Flow
Packet对应的是IP分组(数据报、数据包),是数据在L3上传输的单元
对路由器而言,流(Flow)是共享某些特性的packet序列,比如同一个请求(连接)的packets有相同的路由路径
根据负载均衡的对象分:有Per-Packet和Per-Flow两种方式,文档中的两张图对此有个直观的对比
自然而然的会想到L4 Per-Packet负载均衡:每一个的Flow的Packets会被分流,可以实现多拨对看直播都可以加速的效果,然而本文并不会涉及:
Existing Multipath Routing implementation in Linux is designed to distribute flows of packets over multiple paths, not individual packets. Selecting route in a per-packet manner does not play well with TCP, IP fragments, or Path MTU Discovery.
故默认下文都是Per-Flow负载均衡,这就需要利用Packet的header fields信息把一个个的Packet和Flow联系起来,再进行下一跳的路由选择:
-
IPv4 L3 hash
{ Source Address, Destination Address }
-
IPv4 L4 hash
{ Source Address, Destination Address, Protocol, Source Port, Destination Port }
-
IPv6 L3 hash
{ Source Address, Destination Address, Flow Label, Next Header (protocol) }
IPv6分组因为有Flow Label的存在,IPv6即使只用到L3 Hash也可以实现L4负载均衡
IPv6 Flowlabel IPv6数据报(packet)中有一个20字节的字段:流标号(流标签);进而可以用流标号取代路由表来处理流的路由,加速路由器对分组的处理;在ECMP中却提供了Flow的信息,故IPv6 ECMP是比较容易做的
L3/L4
更关键的还是负载均衡的层级,比如在多拨的情况下使用策略路由也可以做“负载均衡”,下面的命令往往提高BT的速度,这属于L3 Per-Flow负载均衡(左图):只在IP地址层级分流
ip rule add table 916
ip route add 10.173.0.0/16 via 10.170.72.254 dev pppoe-VWAN31 table 916
ip route add 10.170.0.0/16 via 10.170.72.254 dev pppoe-VWAN32 table 916
类似的方法对HTTP多线程下载并没有加速作用,对连接分流,需要L4负载均衡:多线程下载的请求之间source port不同,故L4 hash值基本不同,故会被分到不同的路径上,这样一来L4负载均衡是面向连接的就很好理解了,而内核在IPv4/IPv6上实现到一步有一段很长的历史:
Linux内核中ECMP
Linux内核的Git Commit,对发展历程感兴趣的还可以参考博文Celebrating ECMP in Linux,其中还讨论了设备的出站流量和转发流量的ECMP效果的不同,这里把ECMP的变更历史整理到一张表格中,以及对应时间的OpenWrt的内核版本信息:
Linux Kernel | OpenWrt 内核版本 | ECMP变更 | ECMP形式 |
---|---|---|---|
1997 | 2.1.68 | IPv4 ECMP 加入内核 | L3 Per-packet + route cache -> L3 Per-flow | |
2007 | 2.6.23 | IPv4 multipath cached 移除 | L3 Per-packet + route cache -> L3 Per-flow | |
2012.9 | 3.6 | IPv4 route cache 被移除 | L3 Per-packet -> L3 Per-packet | |
2012.10 | 3.8 | IPv6 ECMP 加入内核 | IPv6 Flowlabel -> IPv6 L4 Per-flow | |
2015.9 | 4.4 | 15.05 | 3.18 | IPv4 ECMP:使用L3 Hash | L3 Per-packet + L3 hash -> L3 Per-flow |
2016.4 | 4.7 | IPv4 ECMP: 增加邻居健康检查 | ||
2017.2 | 17.01 | 4.4 | ||
2017.3 | 4.12 | IPv4 ECMP: 增加 L4 Hash | L3 Per-packet + L3/L4 hash -> L3/L4 Per-flow | |
2017.11 | 4.14 | IPv6 ECMP: ICMPv6 修复 | ||
2018.1 | 4.16 | IPv6 ECMP: 支持指定权重 | ||
2018.4 | 4.17 | IPv6 ECMP: 增加 L4 Hash | ||
2018.7 | 18.06 | 4.9/4.14 | ||
2018.10 | 4.19 | |||
2019.11 | 19.07 | 4.14.151 |
这里可以推出(实际早期版本我也没有测试过),在默认的编译选项和内核版本下,较为实用的L4负载均衡,IPv4需要在18.06及之后的版本
IPv6由于使用了Flowlabel作为Hash的对象,根据表格,OpenWrt在15.05之后的版本就可以启用L4负载均衡
本文考虑更多的还是OpenWrt中的应用,更多信息参考本博客中的Speed_Up
启用ECMP
准备部分是从排查问题的角度来看的
在启用之前可以确认下编译内核时的下面选项,即IP: equal cost multipath,ECMP支持,较新版本的OpenWrt默认是打开的
- CONFIG_IP_ROUTE_MULTIPATH=y
暂时不清楚使用下面的多个nexthop命令添加ECMP路由的依赖,只知道命令ip route
的源于iproute2,如果出现如下报错:
Error: either “to” is a duplicate, or “nexthop” is a garbage.
可以尝试:
- 安装ip-full再尝试,OpenWrt默认是ip-tiny,某些情况下也能使用多个nexthop命令添加ECMP路由
- 使用
route
命令多次添加静态路由,可以启用IPv6的ECMP(详见文末的后记),实测在原版的OpenWrt可行route -A inet6 add 2000::/3 gw fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN2 route -A inet6 add 2000::/3 gw fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN3
添加ECMP路由
这里直接引用iproute2的user Guide的Multipath routing部分:
ip route add ${addresss}/${mask} nexthop via ${gateway 1} weight ${number} nexthop via ${gateway 2} weight ${number}
Multipath routes make the system balance packets across several links according to the weight (higher weight is preferred, so gateway/interface with weight of 2 will get roughly two times more traffic than another one with weight of 1). You can have as many gateways as you want and mix gateway and interface routes, like:
ip route add default nexthop via 192.168.1.1 weight 1 nexthop dev ppp0 weight 10
Warning: the downside of this type of balancing is that packets are not guaranteed to be sent back through the same link they came in. This is called “asymmetric routing”. For routers that simply forward packets and don’t do any local traffic processing such as NAT, this is usually normal, and in some cases even unavoidable.
If your system does anything but forwarding packets between interfaces, this may cause problems with incoming connections and some measures should be taken to prevent it.
可以补充的是如果用的多个等带宽的PPPoE Interface的话,可以使用多个nexthop dev ${pppoe-dev}
修改内核运行参数
仅仅是上一步的话,可以见到在使用P2P下载时已经有负载均衡了,但是对HTTP的多线程下载并没有加速效果,因为一些Linux内核对IPv4的ECMP默认设置到L3 Hash,而且也没有开启邻居健康检查,所以需要修改下内核的运行参数,相关的具体的参数说明如下:
fib_multipath_hash_policy - INTEGER Controls which hash policy to use for multipath routes. Only valid for kernels built with CONFIG_IP_ROUTE_MULTIPATH enabled. Default: 0 (Layer 3) Possible values: 0 - Layer 3 1 - Layer 4 2 - Layer 3 or inner Layer 3 if present
fib_multipath_use_neigh - BOOLEAN Use status of existing neighbor entry when determining nexthop for multipath routes. If disabled, neighbor information is not used and packets could be directed to a failed nexthop. Only valid for kernels built with CONFIG_IP_ROUTE_MULTIPATH enabled. Default: 0 (disabled) Possible values: 0 - disabled 1 - enabled
echo "net.ipv4.fib_multipath_hash_policy=1" >> /etc/sysctl.conf
echo "net.ipv4.fib_multipath_use_neigh=1" >> /etc/sysctl.conf
sysctl -p
其他相关的内核运行参数(IPv6 Flowlabel等)可以参考ip-sysctl.txt
OpenWrt上启用ECMP
熟练的话完全根据上面的方法做了,以下是在校园网PPPoE接入下实践的,如果PPPoE拨号多播数量较多可以参考如何提高网速
准备
- 在启用之前可以确认下编译内核时的选项
CONFIG_IP_ROUTE_MULTIPATH=y
(OpenWrt 18.06之后的版本默认已经开启) - IPv6 ECMP只在IPv6 NAT下测试过,受限于当前OpenWrt正式版的内核版本,未对内核参数做进一步测试
- 加速的前提是下多拨可以加速(ISP限制)
添加虚拟网卡
OpenWrt安装kmod-macvlan
后就可以添加虚拟网卡到不同的VLAN上
opkg update && opkg install macvlan
路由器上的RJ45网口是绑定到VLAN上的,如果是添加虚拟网卡到一个VLAN上就是单线多拨,添加到多个VLAN上就可以做多线多拨了
这里的eth0.2
对应于WAN口(在Interface -> Switch
可以看到WAN绑定在VLAN 2上),不同的设备可能有些许不同,这里在eth0.2
上添加两个虚拟网卡
for i in `seq 1 2`
do
ip link add link eth0.2 name veth$i type macvlan
done
拨号
还是用脚本省事,填上用户名和密码即可,拨数为2,填下账号密码
#!/bin/sh
username=
password=
for i in `seq 1 2`
do
uci set network.VWAN$i=interface
uci set network.VWAN$i.proto='pppoe'
uci set network.VWAN$i.username="$username"
uci set network.VWAN$i.password="$password"
uci set network.VWAN$i.metric="$((i+1))"
uci set network.VWAN$i.ifname="veth$i"
uci set network.VWAN$i.ipv6='1'
done
uci commit network
添加所有PPPoE接口到wan zone(担心影响到其他的网络环境的话手动也可以)
for i in `uci show network | grep pppoe | awk -F. '{print $2}'`
do
uci add_list firewall.@zone[2].network=$i
done
uci commit firewall
添加ECMP路由
只用nexthop dev ${pppoe-dev}
:
#IPv4
ip route replace default metric 1 nexthop dev pppoe-VWAN1 nexthop dev pppoe-VWAN2
#IPv6
ip -6 route replace default metric 1 nexthop dev pppoe-VWAN1 nexthop dev pppoe-VWAN2
做完这一步就可以使用ip r
和ip -6 r
命令检查下路由表
修改内核运行参数
echo "net.ipv4.fib_multipath_hash_policy=1" >> /etc/sysctl.conf
echo "net.ipv4.fib_multipath_use_neigh=1" >> /etc/sysctl.conf
sysctl -p
测试
通过东北大学IPv6测速以及IPv4测速测试负载均衡的速度叠加效果,或者使用iftop命令查看各个接口上路由的实时速率
传入连接的问题
添加ECMP路由之后传入连接偶尔连的上,偶尔连不上,但是PT的上传之类的貌似又没有问题(可能因为上传也可以是主动发出的连接),如果有这方面需求的话需要添加策略路由,为某一接口上的地址指定路由规则,下面是IPv4的,完成之后可以从外部访问之类的,但是对其他的影响就不太清楚了,IPv6部分暂时没有测试条件…
ip rule add perf 20 from IP table 20
ip route add default table 20 dev INTERFACE
此处参考自Linux 平台上之 Multipath Routing 應用,非常难得的详细的文章,还是2001年的
后记:偶然发现的ECMP
这里保留当时的发现过程:(当时发现了这个功能非常开心,现在来看部分内容不准确,但是文末的脚本的效果要好于ECMP默认的方法)
只在K2P OpenWrt 18.06上面实践过,对LEDE 17.01无效
下面是在操作一番之后的路由表,对IPv6的测速显示网速翻了四倍
root@OpenWrt:~# ip -6 route | grep pppoe
default from 2001:250:1006:dff0::/64 via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN4 proto static metric 512 pref medium
2000:::/3 dev pppoe-VWAN4 proto static metric 256 pref medium
nexthop via fe80::96db:daff:fe3e:8fcf dev pppoe-wan weight 1
nexthop via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN2 weight 1
nexthop via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN3 weight 1
nexthop via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN4 weight 1
大致可以看出这已经是做了负载均衡,需要的只是安装好luci-app-mwan3,简单操作一下路由表,这里的网关地址和dev需要看具体情况
2000::/3 就是IPv6的单播(Unicast)地址了,在不做任何操纵的情况下,无PD的路由表(单拨)
default from 2001:250:1006:dff0::/64 via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN3 proto static metric 512 pref medium
2001:250:1006:dff0::/64 dev pppoe-VWAN3 proto static metric 256 pref medium
...
之后通过下面两条常规的添加路由表条目的命令:
route -A inet6 add 2000::/3 gw fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN2
route -A inet6 add 2000::/3 gw fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN3
发现路由表就出现了nexthup和负载均衡中的weight标记
root@OpenWrt:~# ip -6 route | grep pppoe
default from 2001:250:1006:dff0::/64 via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN3 proto static metric 512 pref medium
2000::/3 dev pppoe-VWAN3 proto static metric 256 pref medium
nexthop via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN2 weight 1
nexthop via fe80::96db:daff:fe3e:8fcf dev pppoe-VWAN3 weight 1
...
类似的网关添加过程对IPv4效果并不好,这里稍后再细说
之后还需要修改一下防火墙,这里用的还是NAT6中的那条命令
因为对路由表的修改在路由器重启之后会重置,所以还是要添加到开机的启动脚本或者添加到自定的防火墙规则之中,如果已经在NAT6配置过程中配置好了的话这一步就可以忽略了
ip6tables -t nat -I POSTROUTING -s `uci get network.globals.ula_prefix` -j MASQUERADE
实测的峰值速度可以到达双网口的满速,但是一样的问题,在UT,IDM,Thunder这种多线程下载软件下轻松满速,而在YouTube视频应用下,只有半速(甚至包括用IDM下载的情况下)
目前最新版本的mwan3也开始支持IPv6多拨了,但是个人还是觉得日常使用的话用以上的方法更加快捷,不过需要对网络有一定的了解,下面是一段针对IPv6 NAT的多拨hotplug脚本,用了一些OpenWrt开发时推荐的写法,比较实验性
#!/bin/sh
[ "$ACTION" = ifup ] || exit 0
ifaces=$(ubus call network.interface dump | jsonfilter -e '$.interface[@.proto="dhcpv6" && @.up=true].interface')
for iface_6 in $ifaces
do
[ "$INTERFACE" = $iface_6 ] || continue
devices=$(ubus call network.interface dump | jsonfilter -e '$.interface[@.proto="dhcpv6" && @.up=true].device')
ipv6_gw=$(ifstatus $iface_6 | jsonfilter -e '$.route[1].nexthop')
for ipv6_dev in $devices
do
status=$(route -A inet6 add 2000::/3 gw $ipv6_gw dev $ipv6_dev 2>&1)
logger -t NAT6 "Gateway: $ipv6_dev: Done $status"
done
exit 0
done