在Linux上为指定IP端口模拟网络收发包延迟

  编写网络应用程序时,我们一般都是在网络状况良好的局域网甚至是本机内进行测试调试。有没有办法在网络状况良好的内网环境中,在不改动程序自身代码的前提下,为应用程序模拟复杂的外网环境——尤其是网络延迟呢?这是我在学校写网络程序时就有过的想法,只是一直没认真研究,直到最近在公司编写跨服代码。


  跨服涉及多台服务器之间,还有服务器与客户端之间的通讯,流程很复杂,其中每一步都要正确处理网络异常延迟与断开的情况。测试人员通过改代码或下断点的方式来测试网络延迟是极麻烦的,而且能模拟的延迟用例也很有限。因此如果有一个第三方工具为应用程序使用的某个socketIP端口)模拟网络延迟,那测试人员应该会非常喜欢的。


  最初找到的工具有Linux自带的tc命令(需要配合tc自带的模块netem)和一个第三方工具dummynet。前者概念很复杂,命令行参数也很复杂。后者跨平台,在Windows上也可用;但在Linux上安装非常麻烦,为了编译dummynet提供的内核模块,需要编译正确版本的Linux内核源代码——我在这一步卡了很久,一直没搞定。最终还是决定用tc。计划的方案是用tc为服务端端口分别设置收包和发包的网络延迟,这样可解决tc只能工作在Linux中的问题。tc手册和网上很多文章都提到tc只能设置发包延迟,而无法设置收包延迟。但只要配合Linux自带的ifbIntermediateFunctional Block device)内核模块和一点小技巧,tc就可以设置收包延迟。


  其实tc可以很简单地为一块网卡设置网络延迟:


# tc qdisc add dev wlan0 root netem delay 1s


  这条命令给无线网卡wlan0发送的包设置了1秒网络延迟。


  可以通过ping局域网中的其它机器来验证:


# ping -c 4 192.168.1.5

PING192.168.1.5 (192.168.1.5) 56(84) bytes of data.

64bytes from 192.168.1.5: icmp_req=1 ttl=64 time=1002 ms

64bytes from 192.168.1.5: icmp_req=2 ttl=64 time=1001 ms

64bytes from 192.168.1.5: icmp_req=3 ttl=64 time=1002 ms

64bytes from 192.168.1.5: icmp_req=4 ttl=64 time=1004 ms


---192.168.1.5 ping statistics ---

4 packets transmitted, 4 received, 0% packet loss, time 3006ms

rtt  min/avg/max/mdev = 1001.446/1002.830/1004.967/1.642 ms


  但是这样会影响所有通过该网卡发送的包。这不是我想要的。我只想给服务器上指定的端口设置网络延迟,不想影响其它端口,所以还是把这条延迟规则去掉吧:


# tc qdisc del dev wlan0 root


  为了只给指定的IP端口设置延迟,我们需要使用tc中的三个有点复杂的概念:qdisc(排队规则)、class(类)和filter(过滤器)。我花了很多天才基本理解它们是如何组合在一起工作的。这里我不打算详细解释这些概念(想详细了解的可查看文末列出的参考资料),只会写下我是怎么做的。


  假设现在本机上有两个相互通讯应用程序在运行,程序A在端口14100监听,程序BA14100端口之间建立了TCP连接。我想在BA的通讯方向上设置延迟,方法是在本地环回网卡的发送端设置qdiscfilter,过滤所有发给本地14100端口的包,并给这些包设置延迟。


  首先给在本地环回网卡lo添加一条root qdisc


# tc qdisc add dev lo root handle 1: prio bands 4


  这条qdisc下设4classhandleid1:。在没有filter的情况下,tcIP协议层收到的包会根据IP包头的TOSTypeofService)字段进入第1~第3class(与pfifo_fast规则相同),第4class是没用的。现在给第4class添加一个5秒延迟的qdisc


# tc qdisc add dev lo parent 1:4 handle 40: netem delay 5s


  给root qdisc添加一个filter,将发给14100端口的包都送到第4class


# tc filter add dev lo protocol ip parent 1:0 prio 4 u32 \

match ip dport 14100 0xffff flowid 1:4


  这样就可以了。


  如果要撤销网络延迟,可以把filter删掉。先列出filter的信息:


# tc -s filter show dev lo

filter parent 1: protocol ip pref 4 u32

filter parent 1: protocol ip pref 4 u32 fh 800: ht divisor 1

filter parent 1: protocol ip pref 4 u32 fh 800::800 order 2048 key ht 800bkt 0 flowid1:4 (rule hit 672 success 76)

match 00003714/0000ffff at 20 (success 76 )


  上面的信息显示用76个包被filter过滤了出来,这些包都是由本地环回网卡发给14100端口的。现在删除filter


# tc filter del dev lo pref 4


  不过,上面的情景是两个应用程序都在本地,因此可以通过设置环回网卡的发送端来变相控制14100端口(在环回网卡上)的收包速度。如果程序B在另一台机器上,那就需要ifb的配合了。ifb会在系统中开辟出一块虚拟网卡。如果我们将wlan0(实际网卡)收到的包重定向到ifbifb就会将收到的包又发回给wlan0,通过wlan0送给IP层。因此通过设置ifb的发包延迟就可以实现wlan0的收包延迟。


  为了使用ifb,首先需要载入ifb内核模块,这个模块在Debian7中是自带的:


# modprobe ifb


  通过ip命令可看到系统中多出了ifb0ifb1两块网卡:


# ip link list

1:lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc prio state UNKNOWN mode DEFAULT

link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

2:eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT qlen 1000

link/ether 60:eb:69:99:66:54 brd ff:ff:ff:ff:ff:ff

3:wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mqstate UP mode DORMANT qlen 1000

link/ether 1c:65:9d:a9:db:01 brd ff:ff:ff:ff:ff:ff

10:ifb0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 32

link/ether 7a:2a:33:6b:e7:f7 brd ff:ff:ff:ff:ff:ff

11:ifb1: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 32

link/ether ce:ba:f4:38:df:6c brd ff:ff:ff:ff:ff:ff


  启动ifb0网卡:


#ip link set ifb0 up


  确认ifb0网卡已启动:


# ip link list

1:lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc prio state UNKNOWN mode DEFAULT

link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

2:eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT qlen 1000

link/ether 60:eb:69:99:66:54 brd ff:ff:ff:ff:ff:ff

3:wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP mode DORMANT qlen 1000

link/ether 1c:65:9d:a9:db:01 brd ff:ff:ff:ff:ff:ff

10:ifb0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN mode DEFAULT qlen 32

link/ether 7a:2a:33:6b:e7:f7 brd ff:ff:ff:ff:ff:ff

11:ifb1: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 32

link/ether ce:ba:f4:38:df:6c brd ff:ff:ff:ff:ff:ff


  在wlan0添加ingress qdisc,即收包的排队规则:


# tc qdisc add dev wlan0 ingress


  将wlan0收到的包重定向到ifb0


# tc filter add dev wlan0 parent ffff: \

protocol ip u32 match u32 0 0 flowid 1:1 action mirred egress redirect dev ifb0


  接下来像之前一样,在ifb0的发送端设置qdiscfilter,为发送到14100端口的包设置5秒延迟:


# tc qdisc add dev ifb0 root handle 1: prio bands 4

# tc qdisc add dev ifb0 parent 1:4 handle 40: netem delay 5s

# tc filter add dev ifb0 protocol ip parent 1:0 prio 4 u32 \

match ip dport 14100 0xffff flowid 1:4


  大功告成!从头到尾整个过程都没有对应用程序本身做任何修改,也没有改变网络协议的行为,也没有影响机器上其它正在运行的程序。


  不过这些tc命令对于测试人员来说仍然太复杂了,毕竟tc的目标用户似乎是专业网管和系统管理员。本来只想简单地模拟网络延迟,却没想到最后发现这涉及一个很大的课题——流量控制Orz。届时我还要把它们封装成简单的命令才行。


参考资料


  1. Network emulation (by Wikipedia)

  2. man tc

  3. man netem

  4. netem(by Linux Foundation)。这篇文章讲到了如何为指定的IP设置网络延迟,以及如何用ifb设置收包延迟。

  5. Linux Advanced Routing & Traffic Control HOWTO。比较相关的是第3Introduction to iproute2、第9Queueing Disciplines for Bandwidth Management(其中讲到的IMQ就是ifb的前身),以及第12Advanced filters for (re-)classifying packets

  6. Deleting filters in tc

  7. LinuxTCifb原理以及ingress流控

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。