链路层的网卡聚合-基于Linux bonding

时间:2021-10-09 23:37:25

linux总是可以用一种最简单的方式实现一个很复杂的功能,特别是网络方面的,哪怕这个功能被认为只是在高端设备上才有,linux也可以很容易的实现,以前的文章已经说了不少次了,比如vlan功能,比如高级路由和防火墙功能等等,本文着重说一下linux的bonding,也就是端口聚合的功能模块。不可否认,在网络设备这个层面上上,linux搞出了两个很成功的虚拟设备的概念,一个是tap网卡,另一个就是本文所讲述的bonding,关于tap网卡的内容,请参阅之前关于OpenVPN的文章。
     如果有一个问题摆在眼前,那就是关于linux bonding有什么比较好的资料,答案就是linux内核的文档,该文档在$KERNEL-ROOT/Documentation/networking/bonding.txt,我觉得没有任何资料比这个更权威了。
一、bonding简介
bonding是一个linux kernel的driver,加载了它以后,linux支持将多个物理网卡捆绑成一个虚拟的bond网卡,随着版本的升级,bond驱动可配置的参数越来越多,而且配置本身也越来越方便了。
     我们在很多地方会使用到物理网卡端口汇聚的功能,比如我们想提升网络速率,比如我们想提供热备份,比如我们想把我们的主机配置成一个网桥,并且使之支持802.3ad动态端口聚合协议等等,然而最重要的还是两点,第一点是负载均衡,第二点就是热备份啦。
二、驱动以及Changes介绍
linux的bonding驱动的最初版本仅仅提供了基本的机制,而且需要在加载模块的时候指定配置参数,如果想更改配置参数,那么必须重新加载bonding模块;然后modprobe支持一种rename的机制,也就是在modprobe的时候支持使用-o重新为此模块命名,这样就可以实现一个模块以不同的配置参数加载多次了,起初比如我有4个网口,想把两个配置成负载均衡,两个配置成热备,这只能手工重新将bonding编译成不同的名称来解决,modprobe有了-o选项之后,就可以两次加载相同的驱动了,比如可以使用:
modprobe bonding -o bond0 mode=0
modprobe bonding -o bond1 mode=1

加载两次bonding驱动,用lsmod看一下,结果是bond0和bond1,并没有bonding,这是由于modprobe加载时命名了,然而最终,这个命名机制不再被支持了,因为正如modprobe的man手册所叙述的一样,-o重命名机制主要适用于test。最后,bonding支持了sysfs的配置机制,对/sys/class/net/目录下的文件进行读或者写就可以完成对驱动的配置。
     不管怎样,在sysfs完全支持bonding配置之前,如果想往某一个bonding网卡添加设备或者删除设备的时候,还是要使用经典且传统的ioctl调用,因此必然需要一个用户态程序与之对应,该程序就是ifenslave。
     我想,如果linux的所有关于设备的配置都能统一于sysfs,所有的关于内核和进程配置统一于procfs(内核是所有进程共享的地址空间,也有自己的内核线程以及进程0,因此对内核的配置应该在procfs中),对所有的消息,使用netlink通信,这就太好了,摆脱了命令式的ioctl配置,文件式(netlink使用的sendto之类的系统调用也可以归为文件系统调用相关的)的配置将更加高效,简单以及好玩!
三、bonding配置参数
在内核文档中,列举了许多bonding驱动的参数,然后本文不是文档的翻译,因此不再翻译文档和介绍和主题无关的参数,仅对比较重要的参数进行介绍,并且这些介绍也不是翻译,而是一些建议或者心得。
ad_select:802.3ad相关。如果不明白这个,那不要紧,抛开Linux的bonding驱动,直接去看802.3ad的规范就可以了。列举这个选项说明linux bonding驱动完全支持了动态端口聚合协议。
arp_interval和arp_ip_target:以一个固定的间隔向某些固定的地址发送arp,以监控链路。有些配置下,需要使用arp来监控链路,因为这是一种三层的链路监控,使用网卡状态或者链路层pdu监控只能监控到双绞线两端的接口的健康情况,而监控不到到下一条路由器或者目的主机之间的全部链路的健康状况。
primary:表示优先权,顺序排列,当出现某种选择事件时,按照从前到后的顺序选择网口,比如802.3ad协议中的选择行为。
fail_over_mac:对于热备模式是否使用同一个mac地址,如果不使用一个mac的话,就要完全依赖免费arp机制更新其它机器的arp缓存了。比如,两个有网卡,网卡1和网卡2处于热备模式,网卡1的mac是mac1,网卡2的mac是mac2,网卡1一直是master,但是网卡1突然down掉了,此时需要网卡2接替,然而网卡2的mac地址与之前的网卡1不同,别的主机回复数据包的时候还是使用网卡1的mac地址来回复的,由于mac1已经不在网络上了,这就会导致数据包将不会被任何网卡接收。因此网卡2接替了master的角色之后,最好有一个回调事件,处理这个事件的时候,进行一次免费的arp广播,广播自己更换了mac地址。
lacp_rate:发送802.3ad的LACPDU,以便对端设备自动获取链路聚合的信息。
max_bonds:初始时创建bond设备接口的数量,默认值是1。但是这个参数并不影响可以创建的最大的bond设备数量。
use_carrier:使用MII的ioctl还是使用驱动获取保持的状态,如果是前者的话需要自己调用mii的接口进行硬件检测,而后者则是驱动自动进行硬件检测(使用watchdog或者定时器),bonding驱动只是获取结果,然而这依赖网卡驱动必须支持状态检测,如果不支持的话,网卡的状态将一直是on。
mode:这个参数最重要,配置以什么模式运行,这个参数在bond设备up状态下是不能更改的,必须先down设备(使用ifconfig bondX down)才可以配置,主要的有以下几个:
1.balance-rr or 0:轮转方式的负载均衡模式,流量轮流在各个bondX的真实设备之间分发。注意,一定要用状态检测机制,否则如果一个设备down掉以后,由于没有状态检测,该设备将一直是up状态,仍然接受发送任务,这将会出现丢包。
2.active-backup or 1:热备模式。在比较高的版本中,免费arp会在切换时自动发送,避免一些故障,比如fail_over_mac参数描述的故障。
3.balance-xor or 2:我不知道既然bonding有了xmit_hash_policy这个参数,为何还要将之单独设置成一种模式,在这个模式中,流量也是分发的,和轮转负载不同的是,它使用源/目的mac地址为自变量通过xor|mod函数计算出到底将数据包分发到哪一个口。
4.broadcast or 3:向所有的口广播数据,这个模式很XX,但是容错性很强大。
5.802.3ad or 4:这个就不多说了,就是以802.3ad的方式运行。
...
xmit_hash_policy:这个参数的重要性我认为仅次于mode参数,mode参数定义了分发模式,而这个参数定义了分发策略,文档上说这个参数用于mode2和mode4,我觉得还可以定义更为复杂的策略呢。
1.layer2:使用二层帧头作为计算分发出口的参数,这导致通过同一个网关的数据流将完全从一个端口发送,为了更加细化分发策略,必须使用一些三层信息,然而却增加了计算开销,天啊,一切都要权衡!
2.layer2+3:在1的基础上增加了三层的ip报头信息,计算量增加了,然而负载却更加均衡了,一个个主机到主机的数据流形成并且同一个流被分发到同一个端口,根据这个思想,如果要使负载更加均衡,我们在继续增加代价的前提下可以拿到4层的信息。
3.layer3+4:这个还用多说吗?可以形成一个个端口到端口的流,负载更加均衡。然而且慢!事情还没有结束,虽然策略上我们不想将同一个tcp流的传输处理并行化以避免re-order或者re-transmit,因为tcp本身就是一个串行协议,比如Intel的8257X系列网卡芯片都在尽量减少将一个tcp流的包分发到不同的cpu,同样,端口聚合的环境下,同一个tcp流也应该使用本policy使用同一个端口发送,但是不要忘记,tcp要经过ip,而ip是可能要分段的,分了段的ip数据报中直到其被重组(到达对端或者到达一个使用nat的设备)都再也不能将之划为某个tcp流了。ip是一个完全无连接的协议,它只关心按照本地的mtu进行分段而不管别的,这就导致很多时候我们使用layer3+4策略不会得到完全满意的结果。可是事情又不是那么严重,因为ip只是依照本地的mtu进行分段,而tcp是端到端的,它可以使用诸如mss以及mtu发现之类的机制配合滑动窗口机制最大限度减少ip分段,因此layer3+4策略,很OK!
miimon和arp:使用miimon仅能检测链路层的状态,也就是链路层的端到端连接(即交换机某个口和与之直连的本地网卡口),然而交换机的上行口如果down掉了还是无法检测到,因此必然需要网络层的状态检测,最简单也是最直接的方式就是arp了,可以直接arp网关,如果定时器到期网关还没有回复arp reply,则认为链路不通了。
四、我该如何配置呢
1.首先,传统的方式肯定不妥,内核文档上写的有,大家参考便是,记住,首先要装一个ifenslave
2.最新的sysfs配置方式
首先确认你的系统上有sys这个目录,并且mount于它的文件系统是sysfs类型的。然后就是下面的步骤了,很简单:
第零步:加载模块
root@zyxx:modprobe bonding
第一步:进入相应目录
root@zyxx:cd /sys/class/net/
第二步:查看一下文件,熟悉一下地形(该步骤可省略)
root@zyxx:/sys/class/net# ls
bond0 bonding_masters  eth0  eth1  eth2  eth3  eth4  eth5  lo

第三步:看看当前有哪些bond设备
root@zyxx:/sys/class/net# cat bonding_masters
bond0

第四步:从一个bond设备添加或者删除一个以太网卡设备
root@zyxx:/sys/class/net# echo +(-)X > bonding_masters 
#注释:上一条命令中的+号表示添加设备,而-号表示删除设备,+X中的X表示任意一个你喜欢的名字,-X中的X表示bonding_masters中已经存在的一个名字
第五步:进入新创建的bondMy,并且尽情配置吧
root@zyxx:/sys/class/net/bondMy/bonding# ls
active_slave   ad_num_ports    ad_select      arp_validate   lacp_rate   mode          primary  use_carrier
ad_actor_key   ad_partner_key  arp_interval   downdelay      miimon      num_grat_arp  slaves   xmit_hash_policy
ad_aggregator  ad_partner_mac  arp_ip_target  fail_over_mac  mii_status  num_unsol_na  updelay

1.增加eth2到bondMy
root@zyxx:/sys/class/net/bondMy/bonding# echo +eth2 > slaves
2.设置链路监控间隔
root@zyxx:/sys/class/net/bondMy/bonding# echo 100 > miimon
3.设置mode为热备
root@zyxx:/sys/class/net/bondMy/bonding# echo 1 > mode
...
第七步:感慨
整个配置步骤很简单,模块只需要加载一次,以后动态配置就一切OK了。
五、bonding驱动的实现
在看了精彩的配置并且实际上已经配置出一个很好用的网络后,我肯定会急切的看一下源代码的实现,这也是我喜欢linux的原因,因为它可以让你随意patch。实际上bonding的驱动非常简单,和tap一样的,基本就是三大部分:
第一部分:初始化
这部分很简单,就是初始化一个net_device,然后注册进去,这就不多说了
第二部分:实现用户配置接口
该接口有两种,第一种就是传统的基于ioctl的方式配置,就是实现一个ioctl即可,另一种就是通过sysfs实现,也很简单,实现一些attitude的store/show方法即可,不管采用哪种方式,最终都要调用一个函数,那就是netdev_set_master,该函数中最重要的事就一个,那就是将物理网卡的master设置成我们在第一部分初始化的那个bond设备:
slave->master = master;
第三部分:初始化传输和接收接口
对于传输接口,很简单,和多端口网桥类似。bond设备在初始化时将start_xmit初始化成bond_start_xmit,在bond_start_xmit中有一个switch-case,switch什么呢?当然是bonding的mode了,比如在mode0,也就是轮转负载的mode下,bond_start_xmit调用如下代码段:
switch-case:bond_xmit_roundrobin...
bond_for_each_slave_from(bond, slave, i, start_at) {
    if (IS_UP(slave->dev) && (slave->link == BOND_LINK_UP) && (slave->state == BOND_STATE_ACTIVE)) {
        res = bond_dev_queue_xmit(bond, skb, slave->dev);
        break;
    }
}
对于接收接口,所有的设备都从netif_receive_skb开始数据包的协议分发,在该函数的开始处有下面代码:
if (orig_dev->master) {
    if (skb_bond_should_drop(skb))
        null_or_orig = orig_dev;
    else
        skb->dev = orig_dev->master;
}
orig_dev是物理网卡,如果不出意外,在其拥有master的情况下,也就是使用用户接口将物理网卡绑到一个bond设备上后,物理网卡的master字段将被设置,经过这段代码,skb的dev将会被设置成master,也就是说bond设备。接下来的向上层协议的传输就由bond设备来完成了,因为所有的三层信息完全都在bond设备上,而没有在物理网卡orig_dev上。
六、高级主题
不可否认,本文的解析是极其浅显的,如果想获得更高级的主题和一些很深入细节的主题,或者想了解一些关于交换机的知识以及性能方面的问题,请参考Linux关于bonding的文档,该文档在$KERNEL-ROOT/Documentation/networking/bonding.txt,强烈建议阅读,读了该文档之后,你就是bonding方面的专家啦。
附:谈谈配置方式
linux的sysfs和proc(sysctl等)机制可以实现文件io的方式配置系统参数和设备,回想当初学习思科/华为设备配置时,花了上万元RMB之后,最后的收获就是只会打“?”就OK了,后来随着学习的深入,发现windows的netsh也是这种配置方式,甚至感觉比思科/华为设备的配置方式更加简单,但是归根结底都是命令式的,所谓的命令式其实就是类似英语的,主-谓-宾(定-状-补),主语就是系统的当前user,谓语就是命令,宾语就是目标设备或者系统本身,定状补描述命令参数,这类命令对于习惯于自然语言的人们来讲是很方便的,然而记忆的开销却不小。linux通过文件读写的方式可以实现类似的配置,不知道是好是坏,反正我在初用sysfs配置bonding的时候第一个感觉就是“好极了”,再也不用使用man ifenslave了,再也不用vim文档了,kobject将bonding的相关内容组织在$sysfstoor/class/net/目录下,只要你会访问文件,你就能进行配置,而且bonding的新版本几乎废弃了原来的ifenslave的ioctl方式,任何关于bonding配置都可以用sysfs来进行。
     最重要的是,文件式或者netlink的配置相比ioctl式的经典命令式配置,可以减少设备vfs层的膨胀,我们可以看看ioctl层次的代码,是多么的凌乱不堪,每添加一个新命令就需要修改甚至好几层ioctl的代码,其实就是添加一个case语句,用以分发新添加的命令,这样就会导致ioctl代码虽然分层但是却解决不了设备驱动和接口过度耦合的问题。