編寫網絡利用程序時,我們1般都是在網絡狀態良好的局域網乃至是本機內進行測試調試。有無辦法在網絡狀態良好的內網環境中,在不改動程序本身代碼的條件下,為利用程序摹擬復雜的外網環境――特別是網絡延遲呢?這是我在學校寫網絡程序時就有過的想法,只是1直沒認真研究,直到最近在公司編寫跨服代碼。
跨服觸及多臺服務器之間,還有服務器與客戶端之間的通訊,流程很復雜,其中每步都要正確處理網絡異常延遲與斷開的情況。測試人員通過改代碼或下斷點的方式來測試網絡延遲是極麻煩的,而且能摹擬的延遲用例也很有限。因此如果有1個第3方工具為利用程序使用的某個socket(IP端口)摹擬網絡延遲,那測試人員應當會非常喜歡的。
最初找到的工具有Linux自帶的tc命令(需要配合tc自帶的模塊netem)和1個第3方工具dummynet。前者概念很復雜,命令行參數也很復雜。后者跨平臺,在Windows上也可用;但在Linux上安裝非常麻煩,為了編譯dummynet提供的內核模塊,需要編譯正確版本的Linux內核源代碼――我在這1步卡了很久,1直沒弄定。終究還是決定用tc。計劃的方案是用tc為服務端端口分別設置收包和發包的網絡延遲,這樣可解決tc只能工作在Linux中的問題。tc手冊和網上很多文章都提到tc只能設置發包延遲,而沒法設置收包延遲。但只要配合Linux自帶的ifb(Intermediate Functional Block device)內核模塊和1點小技能,tc就能夠設置收包延遲。
其實tc可以很簡單地為1塊網卡設置網絡延遲:
# 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
--⑴92.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中的3個有點復雜的概念:qdisc(排隊規則)、class(類)和filter(過濾器)。我花了很多天才基本理解它們是如何組合在1起工作的。這里我不打算詳細解釋這些概念(想詳細了解的可查看文末列出的參考資料),只寫下我是怎樣做的。
假定現在本機上有兩個相互通訊的利用程序在運行,程序A在端口14100監聽,程序B和A的14100端口之間建立了TCP連接。我想在B到A的通訊方向上設置延遲,方法是在本地環回網卡的發送端設置qdisc與filter,過濾所有發給本地14100端口的包,并給這些包設置延遲。
首先在本地環回網卡lo添加1條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添加1個5秒延遲的qdisc:
# tc qdisc add dev lo parent 1:4 handle 40: netem delay 5s
給root qdisc添加1個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會在系統中開辟出1塊虛擬網卡。如果我們將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
接下來像之前1樣,在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
大功告成!從頭到尾全部進程都沒有對利用程序本身做任何修改,也沒有改變網絡協議的行動,也沒有影響機器上其它正在運行的程序。
不過這些tc命令對測試人員來講依然太復雜了,畢竟tc的目標用戶似乎是專業網管和系統管理員。本來只想簡單地摹擬網絡延遲,卻沒想到最后發現這觸及1個很大的課題――流量控制Orz。屆時我還要把它們封裝成簡單的命令才行。
Network emulation (by Wikipedia)
man tc
man netem
netem(by Linux Foundation)。這篇文章講到了如作甚指定的IP設置網絡延遲,和如何用ifb設置收包延遲。
Linux Advanced Routing & Traffic Control HOWTO。比較相干的是第3章Introduction to iproute2、第9章Queueing Disciplines for Bandwidth Management(其中講到的IMQ就是ifb的前身),和第12章Advanced filters for (re-)classifying packets。
Deleting filters in tc
Linux TC的ifb原理和ingress流控