实时音视频这种实时业务一般用udp传输数据,其对网络性能是非常敏感的,在实战中,经常需要测试当前端到端或端到云的网络性能。在这里我们讨论一下网络性能测试中所涉及到指标,技术和相关工具,以及如何编写自己的网络性能测试工具。
性能指标
先给出几个比较重要的指标的定义以及它们的意义。
带宽(吞吐量)
单位时间内传输的数据量,单位通常是每秒比特数,记作bps;
带宽反映了网络的传输能力,越大越好;丢包
数据包丢失个数,等于“发送数据包数” - “接受数据包数”;
丢包反映了网络可靠性,越小越好;时延
数据包从发送开始到接收到该数据所耗费的时间,单位通常是毫秒;
时延反映了网络的速度,越小越好;抖动
指时延的变化,即两个数据包时延的差值;
抖动反映了网络的稳定性,越小越好;乱序
指接收到的数据包顺序和发送顺序不一致的次数;
乱序反映了网络的稳定性,越小越好;
当乱序比较严重时,丢包也会比较严重,所以一般都以丢包指标为主,忽略乱序指标;
测试工具
网上有很多测试网络性能的工具,如果它们能满足需求的话,就不用自己再造轮子了。
ping
ping是最常见的,几乎在所有的OS上都有它的存在。 其工作原理如图
Local发送的数据包,Remote收到数据包后原样发回来;
数据包里包含有序号和时间戳信息;
序号用于判断是否丢包;
时间戳用于计算来回时延(图中蓝色部分),它等于接收时间减去数据包时间戳;
不同OS的ping命令选项可能会略有差别,以Mac OSX的ping为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | $ping -s 1024 192.168.1.100 PING www.microsoft.com (23.42.217.205): 1024 data bytes 1032 bytes from 23.42.217.205: icmp_seq=0 ttl=49 time=83.883 ms 1032 bytes from 23.42.217.205: icmp_seq=1 ttl=49 time=77.958 ms 1032 bytes from 23.42.217.205: icmp_seq=2 ttl=49 time=80.053 ms 1032 bytes from 23.42.217.205: icmp_seq=3 ttl=49 time=78.244 ms 1032 bytes from 23.42.217.205: icmp_seq=4 ttl=49 time=77.937 ms ... ^C --- 192.168.1.100 ping statistics --- 30 packets transmitted, 29 packets received, 3.3% packet loss round-trip min/avg/max/stddev = 77.843/95.375/141.314/19.167 ms |
其中 -s 1024
指示包的大小为1024字节;从ping结果可以看出,发送了30个包,收到29个包,3.3%的丢包率,最小时延77.843毫秒,最大时延
141.314毫秒,平均时延95.375毫秒,时延的标准差19.167。另外,ping用的是ICMP协议,网络对ICMP协议处理性能,可能跟
UDP或TCP是不一样的,所以测试结果只能做为参考。
小结:ping的优点是简单便捷,可以测试时延和丢包,缺点是无法测试带宽。
iperf
iperf功能功能强一些,可以测带宽,丢包,抖动, 但是测不了时延。它的工作原理如图:
从图中可以看出iperf是单向发数据包,并不会像ping一样接收方把数据包发回给发送方,所以它是测不了时延,但能测试抖动。抖动等于接收时间间隔(绿色长度)减去发送时间间隔(蓝色长度,即timestamp2-timestamp1)。
下面是一个例子。
服务端:
1 2 3 4 5 6 7 8 9 10 | $iperf -u -s -p 12345 -i 1 -w 1000000------------------------------------------------------------ Server listening on UDP port 12345 Receiving 1470 byte datagrams UDP buffer size: 977 KByte ------------------------------------------------------------ |
客户端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | $ iperf -u -c 127.0.0.1 -p 12345 -i 1 -t 5 -b 16K -l 62 ------------------------------------------------------------ Client connecting to 127.0.0.1, UDP port 12345 Sending 62 byte datagrams UDP buffer size: 9.00 KByte (default) ------------------------------------------------------------ [ 4] local 127.0.0.1 port 59805 connected with 127.0.0.1 port 12345 [ ID] Interval Transfer Bandwidth [ 4] 0.0- 1.0 sec 2.00 KBytes 16.4 Kbits/sec [ 4] 1.0- 2.0 sec 1.94 KBytes 15.9 Kbits/sec [ 4] 2.0- 3.0 sec 1.94 KBytes 15.9 Kbits/sec [ 4] 3.0- 4.0 sec 1.94 KBytes 15.9 Kbits/sec [ 4] 4.0- 5.0 sec 2.00 KBytes 16.4 Kbits/sec [ 4] 0.0- 5.1 sec 9.87 KBytes 16.0 Kbits/sec [ 4] Sent 163 datagrams [ 4] Server Report: [ 4] 0.0- 5.1 sec 9.87 KBytes 16.0 Kbits/sec 0.046 ms 0/ 163 (0%) |
其中 -b 16K 指定了带宽参数。测试结果为丢包0个,平均抖动为0.046毫秒。
自己开发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | or(unsigned second = 0; second < test_seconds; second++) { for(unsigned ui = 0; ui < 8; ui++) { sendto 1024 bytes; } msleep(1000); } |
从上面可以看出,ping和iperf各有优缺点,通常需要两者组合才能满足我们的需求。有时候现有工具不能满足实际应用的需求,比如说完全模拟实
际业务环境或者在产品里集成测试功能,这时候就需要发挥“自己动手,丰衣足食”的精神,造出一个适合自己用的轮子来。我们这里只讨论关键点之一:如何匀速发送数据。
我们以设定发送包长为1024字节,带宽为64kbps为例子,讨论发送数据的实现方案。
发送数据最简单的方法就是,起一个线程,每秒直接发送完当前秒的数据,然后sleep一秒,再继续发送,如下:
这种方法比较简单,但是因为发送数据是需要花费时间的,假如发送64Kbit花费了5毫秒,实际发送码率(带宽)为64/1005≈63.68Kbps,比设定值低一些。把发送时间考虑在内,第2个改进后的代码版本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | for(unsigned second = 0; second < test_seconds; second++) { unsigned ts_start = gettimestamp(); for(unsigned ui = 0; ui < 8; ui++) { sendto 1024 bytes; } unsigned elapsed = gettimestamp() - ts_start; msleep(1000-elapsed); } |
从大尺度上看,这个版本确实会按设定带宽发送数据,但从小的的时间片上看,其瞬时发送速率是非常高的。假如发送64Kbit花费了5毫秒,则瞬时速 率为 64*1000/5=12800Kbps,是设定值的20倍。这种瞬时高发送速率可能会导致网络中某些路由器或交换机来不及处理而大量丢包。所以我们继续 改进,在每发送一个包时check是否发送太快,如果发送太快的话就sleep一下缓一缓。改进后的第三个版本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | uint64_t sent_bytes = 0; unsigned kick_time = gettimestamp(); for(unsigned second = 0; second < test_seconds; second++) { sendto 1024 bytes; sent_bytes += 1024; unsigned elapsed = gettimestamp() - kick_time; unsigned normal = sent_bytes * 1000 * 8 / (64*1000); if(normal > elapsed) { msleep(normal-elapsed); } } |
这个版本基本能够按照设定值匀速发送数据了。当然,它还不是最完美的,当设定带宽很高而包长很小时,会导致太多的check,占用太多CPU。这里就不继续改进了,有兴趣的看官可以自己实现之。