现阶段由我来发掘这功能﹐委实是折杀小弟了。Linux 2.2 本身就自带有许多频宽管理办法﹐其实已够得上哪些高级专属的频宽管理系统了。
Linux 所提供的﹐甚至比 Frame 及 ATM 还要更多。
流量控管的两个基本单元是过滤器(filters) 和伫列(queues)。前者将流量安排至后者﹐后者则收集流量然后决定哪些要先送﹑哪些要晚送﹑或是丢弃掉(drop)。而两者都各有其不同系列。
最常见的过滤器是 fwmark 和 u32﹐前者让您可以使用 Linux netfilter 程序选择流量﹔而后者则允许您依据任何标头(header)选择流量。至于最常听到的伫列演算法就是 Class Based Queue 了。CBQ 可以说是一个 super-queue﹐在其里面包含了其它伫列 (甚至其它 CBQs)。
或许﹐关于伫列演算法(queueing)在频宽管理上的运用﹐一时间尚难一窥全豹﹐不过﹐它还真的不负众望就是了。
作为我们的参考资料框架﹐我把目前这个章节套用在一个 ISP 模式之上﹐也就是我偷师学艺之所﹐暂且称之为 Casema Internet in The Netherlands 吧。Casema﹐事实上是一家 cable 公司﹐他们的客户和他们自己的办公室都有 internet 的需求。大多数公司电脑都有 internet 的连线。实际上﹐他们有大把金钱可以花而不是用 Linux 来做频宽管理的。
那让我们看看我们的 ISP 是怎样用 Linux 来管理其频宽吧。
我们用伫列演算法来判定数据 被传送 的顺序。有一个很重要的概念我们要知道﹐就是我们只能处理哪些我们要传送的数据。那它又是如何影响传送速度之判定顺序呢﹖您可以想像为一位收款员每一分钟可以处理 3 个客人。
然后要付款的客人就要跑到队列的 '尾巴' 去排队。这是所谓的 'FIFO quequeing' (First IN, First Out --- 先入先出)。不过﹐假设我们让某些客人插入队列中间﹐而不是排在最后﹐然后这些客人就可以花更少时间在队列中﹐因而也可以更快的购物。
在 inetnet 的环境中﹐我们没办法直接控制别人要送什么东西过来。这有点像您家中的(实体)信箱﹐您没办法影响全世界去修改他们要送信给您的数量﹐除非您有能力通知所有人。
然而﹐internet 赖以为继的 TCP/IP﹐却有某些功能可以协助我们的。TCP/IP 本身是没办法知道两台主机之间的网路容量的﹐所以它会越来越快('刚开始很慢')的将数据送出去﹐而当封包开始丢失的时候﹐因为没有空间将他们送出去了﹐然后就会开始减慢下来。
这情形﹐就有点像您还没读完一半信件的时候﹐会希望别人不要再寄信给您一样。不同之处﹐这里是在 internet 上面而已。
FIXME﹕解释拥塞视窗 (congestion windows)
[The Internet] ---<E3, T3, whatever>--- [Linux router] --- [Office+ISP]
eth1 eth0
现在﹐我的 Linux router 有两张界面﹐eth0 和 eth1。eth1 用来连接我们的 router﹐它负责将封包送至光纤线路以及将封包接收进来。
eth0 则用来连接另外一个 subnet﹐内有公司防火墙及我们的网路前端﹐我们透过它来连接客户。
由于我们仅能限制送出的部份﹐我们需要两套独立但非常近似的规则。透过修改 eth0 的伫列﹐我们可以判定信息要多快送至我们客户那边﹐因而要分配多少下传(downstream) 频宽给他们﹐所谓的?下载速度'是也。
在 eth1 上面﹐我们判定数据要多快送给 Internet﹑多快给我们的使用者﹐而且公司内部和业务方面均需要上传数据。
CBQ 可以让我们产生好些不同类别(classes)﹐甚至类别中的类别。至于更大的切割﹐或许可以称为 '代理(agencies)'。而在这些类别里面﹐或者可以找到诸如 'bulk' 或 'interactive' 之类的名称。
例如﹐我们或有一条 10 megabit 的 'internet' 连线﹐以分享给我们的客户及公司之需。但我们不能让办公室里的少数同仁盗取大量的本来要卖给客户的频宽。
在另一边﹐比方说我们的客户﹐也不能占用本来给我们的门市通往客户资料库的频宽。
在过去﹐用来解决的办法﹐莫过于使用 Frame Relay/ATM 或建立虚拟电路(Virtual Circuits)。这的确能解决问题﹐只不过 frame 并不容易十分细致调整﹐而 ATM 在携带 IP 流量上的效率也非常差强人意﹐而且﹐两者都未有标准法则以产生不同类型的流量进入不同的 VCs 。
不过﹐假如您真的使用 ATM 的话﹐Linux 同样能架轻就熟的为您表演高难度的流量分类技巧。而另一方法是牵两条独立的线路﹐但似乎不怎么实用﹐也不十分完善﹐且也不见得能完全解决您的所有问题。
这时候﹐您就要求助于 CBQ 这尊菩萨了。
显而易见﹐我们这里主要有两个类别﹕ 'ISP' 和 'Office' 。刚开始的时候﹐我们真的不必十分讲究他们频宽的切割﹐所以我们就不再于其类别下细分了。
我们决定客户的下传流量必须保证在 8 megabits 的范围﹐而我们的办公室只有 2 megabits。
可以用 iproute2 之 tc
工具来设定起流量控管。
# tc qdisc add dev eth0 root handle 10: cbq bandwidth 10Mbit avpkt 1000
好了﹐这里有一堆数字。究竟如何呢﹖我们已经设定起 eth0 的 '伫列戒律 (queueing discipline)' 了。我们以 'root' 来宣告这是顶层(root) discipline。我们还将其 handle 设为 '10'。因为我们这里要做 CBQ﹐所以我们也同时在命令行中指明。我们告诉核心﹐它可以支配 10M bit ﹐同时平均封包体积大约为 1000 个 octet。
好﹐现在我们就产生我们的顶层类别﹐它凌架于其它的所有类别之上。
# tc class add dev eth0 parent 10:0 classid 10:1 cbq bandwidth 10Mbit rate \
10Mbit allot 1514 weight 1Mbit prio 8 maxburst 20 avpkt 1000
哇﹐这里的数字更多﹗Linux 的 CBQ 在实作上其实蛮具通用性的。我们用 'parent 10:0' 来指定此类别是源自我们刚才产生的 qdisc handel '10:' 这个顶层类别而来的。而 'classid 10:1' 呢﹐则是我们授予这个类别的名称。
我们这里无须告诉核心更多信息﹐单纯产生一个类别以满足可用设备就是了。我们同时还指定出 MTU(外加一些 overhead) 为 1514 个 octet。而且﹐我们量定此类别的 '比重(weight) ' 为 1Mbit﹐此为一个微调参数而已。
现在﹐再让我们产生 ISP 类别吧﹕
# tc class add dev eth0 parent 10:1 classid 10:100 cbq bandwidth 10Mbit rate \
8Mbit allot 1514 weight 800Kbit prio 5 maxburst 20 avpkt 1000 \
bounded
我们先拨出 8Mbit﹐同时用 'bounded' 参数来指定此类别一定不能超过此限制。否则此类别或会从其它类别那里借用频宽﹐我们后面将会讨论到。
还是让我们产生Office 之顶层类别﹐赶快收锣吧﹕
# tc class add dev eth0 parent 10:1 classid 10:200 cbq bandwidth 10Mbit rate \
2Mbit allot 1514 weight 200Kbit prio 5 maxburst 20 avpkt 1000 \
bounded
为了帮助我们更好的理解﹐下面的图示列出了所有的类别﹕
+-------------[10: 10Mbit]-------------------------+
|+-------------[10:1 root 10Mbit]-----------------+|
|| ||
|| +-----[10:100 8Mbit]---------+ [10:200 2Mbit] ||
|| | | | | ||
|| | ISP | | Office | ||
|| | | | | ||
|| +----------------------------+ +------------+ ||
|| ||
|+------------------------------------------------+|
+--------------------------------------------------+
好了﹐现在我们已经告诉核心有什么类别存在﹐但还没说要怎样去管理伫列。我们不如马上起而行﹐一次搞定两个类别吧。
# tc qdisc add dev eth0 parent 10:100 sfq quantum 1514b perturb 15
# tc qdisc add dev eth0 parent 10:200 sfq quantum 1514b perturb 15
目前的范例中﹐我们安装的是 Stochastic Fairness Queueing discipline (sfq)﹐虽然不是很贴切﹐不过它却可以在不耗费更多 CPU 运转之下﹐很好的处理大量频宽。而我们通常使用的会是 The Token Bucket Filter。
到目前为止﹐我们仅剩一件事情要做而已﹐就是向核心解释什么样的封包属于什么样的类别。刚开始﹐我们纯粹用 iproute2 来做就好﹐但是﹐配合 netfilter 双剑合璧的话﹐更是如虎添翼。
# tc filter add dev eth0 parent 10:0 protocol ip prio 100 u32 match ip dst \
150.151.23.24 flowid 10:200
# tc filter add dev eth0 parent 10:0 protocol ip prio 25 u32 match ip dst \
150.151.0.0/16 flowid 10:100
这里﹐我们假设办公室网路躲在位址为 150.151.23.24 的防火墙之后﹐而我们其它 IP 位址则属于 ISP 的。
使用 u32 比对(match) 更是容易﹐而用 netfilter 来标识封包也更能够设定出精密的比对规则﹐然后我们可以在 tc 里进行比对。
好了﹐现在我们已经将下传频宽切割完毕﹐同样的﹐我们在上传频宽上面依样画葫芦。为求简捷﹐这次让我们一鼓作气﹕
# tc qdisc add dev eth1 root handle 20: cbq bandwidth 10Mbit avpkt 1000
# tc class add dev eth1 parent 20:0 classid 20:1 cbq bandwidth 10Mbit rate \
10Mbit allot 1514 weight 1Mbit prio 8 maxburst 20 avpkt 1000
# tc class add dev eth1 parent 20:1 classid 20:100 cbq bandwidth 10Mbit rate \
8Mbit allot 1514 weight 800Kbit prio 5 maxburst 20 avpkt 1000 \
bounded
# tc class add dev eth1 parent 20:1 classid 20:200 cbq bandwidth 10Mbit rate \
2Mbit allot 1514 weight 200Kbit prio 5 maxburst 20 avpkt 1000 \
bounded
# tc qdisc add dev eth1 parent 20:100 sfq quantum 1514b perturb 15
# tc qdisc add dev eth1 parent 20:200 sfq quantum 1514b perturb 15
# tc filter add dev eth1 parent 20:0 protocol ip prio 100 u32 match ip src \
150.151.23.24 flowid 20:200
# tc filter add dev eth1 parent 20:0 protocol ip prio 25 u32 match ip src \
150.151.0.0/16 flowid 20:100
在我们这个虚构范例中﹐我们发现一个现象﹐就是在 ISP 客户大都离线的时候(比方说﹐早上 8 点)﹐我们的办公室却只有 2Mbit﹐显然是极其浪费的。
如果将 'bounded' 叙述拿掉﹐那么类别就能够借取其它类别的频宽来用。
而有些类别或许不愿意将他们的频宽外借﹐例如两个租用同一线路的敌对 ISP﹐是绝对不会向对方进贡的。在那样的状况下﹐您可以在 'tc class add' 的句子后端﹐加上一个关键词 'isolated' 即可。
FIXME﹕此构思并没经过测试﹗小心尝试﹗
我们这里还可精益求精。如果所有办公室员工同时开启他们的股票程序﹐还是有可能把资料库的频宽给吃掉的。所以﹐我们可以再建两个子类别﹕'Human' 和 'Database'。
我们的资料库永远需要 500Kbit﹐所以就剩下 1.5M 给我们的员工来花费。
我们要在 Office 这个类别里面再建两个类别﹕
# tc class add dev eth0 parent 10:200 classid 10:250 cbq bandwidth 10Mbit rate \
500Kbit allot 1514 weight 50Kbit prio 5 maxburst 20 avpkt 1000 \
bounded
# tc class add dev eth0 parent 10:200 classid 10:251 cbq bandwidth 10Mbit rate \
1500Kbit allot 1514 weight 150Kbit prio 5 maxburst 20 avpkt 1000 \
bounded
FIXME﹕尚需范例﹗
有好些方法都可以做到这点。其中最简单明了就是 'TEQL' --- "True" (or "trivial") link equalizer。和大多数用来做伫列的办法一样﹐负载分流也是双向的。线路的两端都需要参与﹐才能获得完整的效果。
发挥一下想像力吧﹕
+-------+ eth1 +-------+
| |==========| |
'network 1' ----| A | | B |---- 'network 2'
| |==========| |
+-------+ eth2 +-------+
A 和 B 都是路由器﹐而且﹐我们目前假设它们都是跑 Linux 的。如果流量从 network 1 送到 network 2 那边﹐那么 router A 就需要将封包分送至 B 的两条线路去。而 router B 则需要设定为能够接受这样的安排。调过来也一样﹐当封包从 network 2 流向 network 1﹐router B 也需要将封包分送至 eth1 和 eth2 去。
负责分送的部份﹐就是由 'TEQL' 设备来做的﹐参考如下剑诀(易如削蜡)﹕
# tc qdisc add dev eth1 root teql0
# tc qdisc add dev eth2 root teql0
这需要在两边的机器上做啦。那个 teql0 的设备﹐基本上以 roundrobin 的方式向 eth1 和 eth2 进行分送﹐来发送封包。数据并不是从 teql 设备进入的﹐仅仅以 'raw' 格式出现在 eth1 和 eth2 上面。
然而﹐我们现在只是把设备弄起来而已﹐我们还需要适当的路由。方法之一是用一个 /31 的网路给这两条线路﹐另外一个 /31 网路给 teql0 设备﹕
FIXME: 不知道是否需要某些诸如 'nobroadcast' 的设定呢﹖用一个 /31 同时包含网路位址和广播位址似乎太小了 --- 如果这个设计不可行﹐那就试用 /30 吧﹐然后相应的调整 IP 位址就是了。您或许甚至连 eth1 和 eth2 的 IP 位址也不想给呢﹗
On router A:
# ip addr add dev eth1 10.0.0.0/31
# ip addr add dev eth2 10.0.0.2/31
# ip addr add dev teql0 10.0.0.4/31
On router B:
# ip addr add dev eth1 10.0.0.1/31
# ip addr add dev eth2 10.0.0.3/31
# ip addr add dev teql0 10.0.0.5/31
现在﹐router A 应该可以在这 2 条真实线路和 1 个均衡设备上 ping 10.0.0.1﹑10.0.0.3﹑和 10.0.0.5 了。而 router B 也应该可以透过线路 ping 10.0.0.0﹑10.0.0.2﹑和 10.0.0.4 。
如果上面的都成功了﹐router A 要将 10.0.0.5 设定为连接 network 2 的路由﹐同时 router B 则要将 10.0.0.4 设为连接 network 1 的路由。在一些特殊环境中﹐例如 network 1 是您家中的网路﹐而 network 2 是 internet﹐那么 router A 就要将 10.0.0.5 设定为预设网关了。
任何事情都是知易行难。在 router A 和 router B 双方﹐它们的 eth1 和 eth2 都必须将返回路径的过滤关闭﹐否则它们会将哪些不是以它们本身为目标 IP 位址的封包丢弃掉﹕
# echo 0 > /proc/net/ipv4/conf/eth1/rp_filter
# echo 0 > /proc/net/ipv4/conf/eth2/rp_filter
然后﹐就是讨厌的封包重排序的问题了。比方说﹐有 6 个封包需要从 A 送到 B 去 --- eth1 或会得到 1﹑3﹑和 5 ﹔而 eth2 或会得到 2﹑4﹑和 6 。假如一切理想﹐router B 会收到这样的顺序﹕1﹑2﹑3﹑4﹑5﹑6。不过﹐现实中有很大机会将会是﹐核心按这样的顺序接收﹕2﹑1﹑4﹑3﹑6﹑5。问题是这样会让 TCP/IP 感到困扰。透过线路携带多个不同的 TCP/IP 连线并不至于有什么问题﹐然而您却不能合并多条线路让下载单一文档的 FTP 变得大幅加快﹐除非您用来传送和接收的作业系统都是 Linux﹐因为在某些简单的重排序中﹐连线的交握并不容易处理。
不过﹐对许多应用程序而言﹐线路的负载分流的确是一个非常厉害的武器。