编写网络应用程序时,我们一般都是在网络状况良好的局域网甚至是本机内进行测试调试。有没有办法在网络状况良好的内网环境中,在不改动程序自身代码的前提下,为应用程序模拟复杂的外网环境——尤其是网络延迟呢?这是我在学校写网络程序时就有过的想法,只是一直没认真研究,直到最近在公司编写跨服代码。
跨服涉及多台服务器之间,还有服务器与客户端之间的通讯,流程很复杂,其中每一步都要正确处理网络异常延迟与断开的情况。测试人员通过改代码或下断点的方式来测试网络延迟是极麻烦的,而且能模拟的延迟用例也很有限。因此如果有一个第三方工具为应用程序使用的某个socket(IP端口)模拟网络延迟,那测试人员应该会非常喜欢的。
最初找到的工具有Linux自带的tc命令(需要配合tc自带的模块netem)和一个第三方工具dummynet。前者概念很复杂,命令行参数也很复杂。后者跨平台,在Windows上也可用;但在Linux上安装非常麻烦,为了编译dummynet提供的内核模块,需要编译正确版本的Linux内核源代码——我在这一步卡了很久,一直没搞定。最终还是决定用tc。计划的方案是用tc为服务端端口分别设置收包和发包的网络延迟,这样可解决tc只能工作在Linux中的问题。tc手册和网上很多文章都提到tc只能设置发包延迟,而无法设置收包延迟。但只要配合Linux自带的ifb(Intermediate Functional Block device)内核模块和一点小技巧,tc就可以设置收包延迟。
其实tc可以很简单地为一块网卡设置网络延迟:
# tc qdisc add dev wlan0 root netem delay 1s
这条命令给无线网卡wlan0发送的包设置了1秒网络延迟。
可以通过ping局域网中的其它机器来验证:
# ping -c 4 192.168.1.5
PING 192.168.1.5 (192.168.1.5) 56(84) bytes of data.
64 bytes from 192.168.1.5: icmp_req=1 ttl=64 time=1002 ms
64 bytes from 192.168.1.5: icmp_req=2 ttl=64 time=1001 ms
64 bytes from 192.168.1.5: icmp_req=3 ttl=64 time=1002 ms
64 bytes 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 3006 ms
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监听,程序B和A的14100端口之间建立了TCP连接。我想在B到A的通讯方向上设置延迟,方法是在本地环回网卡的发送端设置qdisc与filter,过滤所有发给本地14100端口的包,并给这些包设置延迟。
首先在本地环回网卡lo添加一条root qdisc:
# tc qdisc add dev lo root handle 1: prio bands 4
这条qdisc下设4个class,handle id为1:。在没有filter的情况下,tc从IP协议层收到的包会根据IP包头的TOS(Type of Service)字段进入第1~第3个class(与pfifo_fast规则相同),第4个class是没用的。现在给第4个class添加一个5秒延迟的qdisc:
# tc qdisc add dev lo parent 1:4 handle 40: netem delay 5s
给root qdisc添加一个filter,将发给14100端口的包都送到第4个class:
# 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 flowid 1: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(实际网卡)收到的包重定向到ifb,ifb就会将收到的包又发回给wlan0,最后仍然通过wlan0送给IP层,上层协议毫不知情。因此通过设置ifb的发包延迟就可以实现wlan0的收包延迟。
为了使用ifb,首先需要载入ifb内核模块,这个模块在Debian 7中是自带的:
# modprobe ifb
通过ip命令可看到系统中多出了ifb0和ifb1两块网卡:
# 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的发送端设置qdisc和filter,为发送到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
大功告成!从头到尾整个过程都没有对应用程序本身做任何修改,也没有改变网络协议的行为,也没有影响机器上其它正在运行的程序。