iperf工具研究
图片待处理
1 简介
1.1 编写目的
本文为xxxx性能测试工具iperf的研究文档,重点介绍iperf的使用方法与调优,以测试出产品网络带宽极限。同时也望为后续性能测试提供参考。
1.2 背景介绍
在进行xxxx产品预研时,使用iperf进行网络性能的测试,发现出现网络性能较低的现象,同时其他产品也有类似情况。
1.3 研究目标
深入分析iperf实现原理与工作流程,摸索提高测试性能的方法,为后续其他产品预研网络性能提供测试用例。
2 iperf介绍
2.1 概述
iperf是一种用于测量IP网络最大带宽的工具。它支持调整定时、协议和缓冲等参数。它会针对每个测试输出带宽、损耗以及其他参数。
2.2 版本与支持的平台
目前iperf有两个大版本:iperf3.x,目前官方还在不停迭代,最新版本为3.12;iperf2.x,目前官方已经停止维护。需要注意的是,iperf3不兼容iperf2,所在当测试两端使用的大版本不同时,服务端会报错:不兼容的命令
iperf3主要在Ubuntu Linux、FreeBSD以及MacOS上进行开发,也是目前官方所支持系统平台,但是也能将iperf移植到Windows或者嵌入式平台。
2.3 iperf的安装
iperf官方只提供源码,不提供iperf二进制文件,所以需要下载源码自己构建,对于Windows平台来说已有三方组织进行发布二进制文件。对于嵌入式平台通过源码自行构建。
Windows平台的iperf已有第三方的组织进行移植,可以直接下载使用:https://files.budman.pw/、https://iperf.fr/;
嵌入式平台需要根据芯片平台与工具链进行配置编译。以arm平台为例,编译命令如下:
./configure --host=arm-linux CXX=arm-buildroot-linux-gnueabi-g++ CC=arm-buildroot-linux-gnueabi-gcc --enable-shared=no --disable-profiling --without-openssl;make
2.4 iperf常用链接
iperf官网:https://software.es.net/iperf/。
iperf提供的一个网络性能调优的指南:https://fasterdata.es.net/。
iperf源码仓库:https://github.com/esnet/iperf;在GitHub上可以查看iperf的issues,一些常见问题可以在这里得到解答。
三方组织移植的iperf:https://files.budman.pw/,该网站移植的版本较新;https://iperf.fr/,该网站版本较老,但是包含了一些iperf的使用示例。
3 配置命令解析
本章节命令全部是基于iperf3版本进行说明。使用方法阅读典型测试用例章节。
3.1 标准操作指令
标准操作 | |
---|---|
命令行操作 | 描述 |
-p,–port n | 服务端监听与客户端连接的端口。这个参数服务端与客户端应当相同。默认为5201。 |
–cport n | 用于指定客户端的端口 |
-f, --format [km/KM] | 指定带宽在命令行中显示的格式。 支持:’k’ = Kbits/sec ‘K’ = Kbytes/sec ‘m’ = Mbits/sec ‘M’ = Mbytes/sec |
-i,–interval n | 设置每次报告间隔的秒数。如果不为0,就间隔设定值的秒数来报告测试结果。如果为0,就每秒报告一次。 |
-F,–filename | 客户端:从文件读取数据朝网络发送,代替随机数据 服务端:从网络中读取数据朝文件写,代替随机数据 |
-A, --affinity n/n,m-F | iperf3绑定cpu核进行运行 |
-B,–bind host | 绑定host的IP地址。对于客户端是设置出站接口。针对服务端是设置进站接口。只对多个网卡的主机有用。 |
-V, --verbose | 输出详细信息 |
-J,–json | 按json格式输出 |
–logfile file | 将输出log写入logfile |
-d,–debug | 发出调试信息 |
-v,–version | 输出版本信息 |
-h,–help | 输出帮助信息 |
3.2 服务端操作指令
服务端操作指令 | |
---|---|
命令行操作 | 描述 |
-s,–server | 以服务端模式运行iperf(只允许同一时间内一个iperf连接) |
-D,daemon | 以守护进程运行iperf |
-I,–pidfile | 在使用守护进程运行时,建议使用pid,写入文件 |
3.3 客户端操作指令
客户端操作指令 | |
---|---|
命令行操作 | 描述 |
-c,–client host | 以客户端模式运行iperf,host为server端IP |
–sctp | 使用sctp协议 |
-u,–udp | 使用udp协议 |
-b,–bandwidth n[KM] | 设置目标带宽(默认udp:1Mb/s,tcp无限制)。如果为多个流测试,带宽限制应用于每个io流。还可以在带宽后面添加/数字,这表示突发模式,将不暂停的发送给定数字的数据包,即使速率超过了设置的带宽。 |
-t,–time n | iperf测试时间。默认测试10s。 |
-n,–num n[KM] | 要发送缓存区的数量。这个选项会覆盖掉-t选项。 |
-k,–blockcount n[KM] | 要发送的块的数量。 |
-l,–length n[KM] | 发送或接收的缓冲区长度。tcp默认128KB,udp默认8KB |
-P,–parallel n | 线程数量,默认为1 |
-R,–reverse | 交换发送接收方向(服务端发送,客户端接收) |
-w,–window n[KM] | 设置socket缓冲区。Tcp:使用tcp window的大小。(这将发送到服务端,以供服务端使用) |
-M,–set-mss n | 设置tcp MSS的大小,MSS通常是TCP/IP报头的MTU-40字节。对于以太网来说,MSS是1460字节(MTU 1500字节) |
-N,–no-delay | 设置TCP无等待操作,禁用Nagle算法。通常只对交互式应用(telnet)禁用。 |
-4,–version4 | 只使用IPv4 |
-6,–version6 | 只是用IPv6 |
-S,–tos n | 设置数据包类型。 IPTOS_LOWDELAY 最小化延迟 0x10 IPTOS_THROUGHPUT 最大化吞吐量 0x08 IPTOS_RELIABILITY 最大可靠性 0x04 IPTOS_LOWCOST 最小花费 0x02 |
-L,–flowlabel n | 设置IPv6流标签(只支持linux) |
-Z,–zerocopy | 使用零拷贝方法发送数据 |
-O,–omit n | 忽略前n个测试结果,跳过TCP慢启动周期。 |
-T,–titile str | 在每个输出前加上这个字符串 |
-C,–linux-congestion algo | 设置阻塞控制算法(Linux只支持iperf3.0,Linux和FreeBSD支持iperf3.1) |
4 典型测试用例
4.1 常用测试命令
服务端的启动参数较少,参数之间可以进行组合:
iperf –s 启动服务端
iperf –s -p port 启动服务端,端口为port
iperf –s –D 以守护进程启动服务端
iperf –s –D –I pidfile 以守护进程启动服务端,并将进程PID写入到pidfile里边方便结束
客户端参数较多,不同参数可以组合:
针对TCP测试,-O
参数用于应对TCP慢启动,在进行TCP测试时偶尔会遇到;-w
参数配置socket缓冲区大小,最佳大小buffer size=带宽*延时;-l
参数指定包长,在iperf的源码中,设置的TCP包长最大1MB,可修改源码更改最大包长。
iperf –c server_ip –t时间 –l包长 –w缓冲区大小 –O5/10
example:
iperf –c server_ip –t120 –l64 –w128K –O10
iperf –c server_ip –t120 –l128 –w128K –O10
iperf –c server_ip –t120 –l256 –w128K –O10
iperf –c server_ip –t120 –l512 –w128K –O10
iperf –c server_ip –t120 –l1024 –w128K –O10
...
iperf –c server_ip –t120 –l64k –w128K –O10
iperf –c server_ip –t120 –l128k –w128K –O10
iperf –c server_ip –t120 –l256k –w128K –O10
...
iperf –c server_ip –t120 –l1024k –w128K –O10
针对UDP测试,“-b”参数指定目标带宽,因为iperf在UDP测试时默认使用1M的带宽进行测试;“-u”参数即指定UDP测试;“-l”参数指定包长,在UDP中,UDP数据报最大承载65507字节的用户数据。
iperf –c server_ip –t时间 –l包长 –w缓冲区大小 –b目标带宽 –u
example:
iperf –c server_ip –t120 –l64 –w128K –u
iperf –c server_ip –t120 –l128 –w128K –u
iperf –c server_ip –t120 –l256 –w128K –u
iperf –c server_ip –t120 –l512 –w128K –u
iperf –c server_ip –t120 –l1024 –w128K –u
...
iperf –c server_ip –t120 –l65507 –w128K –u
4.2 多进程打流
iperf3不支持多线程,所以对于多核CPU,如果一个进程不能测试出最大速率,可以启动多个iperf3进程进行打流。
如果使用的是Windows系统,可以打开多个命令行窗口进行多进程打流:
服务端:
iperf –s –p 5201;
iperf –s –p 5202;
客户端
iperf –c server_ip –p 5201;
iperf –c server_ip –p 5202;
Linux端,如果是嵌入式设备通常只有一个串口终端,可以让服务端以守护进程的方式启动,客户端就使用“&”将进程放入后台运行,并且将log输出到指定文件中,这样就可以在打流的同时查看CPU或者系统占用率:
服务端:
iperf –s –p 5201 –D; 服务端无log
iperf –s –p 5202 –D;
iperf –s –p 5201 –logfile filename &; 服务端将log写入filename中
iperf –s –p 5201 –logfile filename &;
客户端:
iperf –c server_ip –p 5201 –-logfile filename –l包长 –w缓冲区 &
iperf –c server_ip –p 5202 –-logfile filename –l包长 –w缓冲区 &
4.3 测试常见问题
问题1:
在iperf3.1.3版本,Windows环境下测试时发现,UDP测试完成后,客户端正常报告出测试结果,服务端报告的测试结果却不正常,形如图1、图2。这个问题在Linux端不存在,考虑是因为iperf3在Windows使用cygwin进行构建,不能完全兼容。
针对这种情况,可以尝试在客户端增加“-R”参数,这样服务端可以正常报告测试结果,或者将iperf版本更新到3.12后,也可以解决该问题。
iperf –c server_ip –t30 –O10 –b目标带宽 –l包长 –w缓冲区 –u -R
5 工具实现原理分析
5.1 iperf原理概述
iperf工具是典型的C/S架构,用在特定的局域网络下,在使用iperf进行网络测试时需要一端启动server,一端启动client来进行网络打流。
目前iperf支持TCP、UDP的网络性能测试,所以重点分析一下TCP和UDP的测试原理。
TCP的带宽测试原理就是服务端和客户端在建立起连接以后,客户端向服务端发送一定大小的数据包,并记下发送时间,或者客户端在一定时间内向服务端发送数据,记录发送数据的总大小,最后数据总量除以设定的时间即为测试带宽。服务端方面,在建立连接后,接收数据的总量除以接收的时间,即为服务端的测试带宽。
UDP的测试原理类似,不过增加了网络抖动、丢包率的计算。用户在客户端指定以什么速率进行发送数据,客户端在发送时会根据用户指定的速率计算数据发送的时延。在发送时,iperf会为数据包做特殊处理:在数据包中添加ID、发送数据包时的时间。
ID值用于计算丢包率:在接收端首先将数据包的ID值读出来,再将本次读取的ID与上次读取的ID值做比较,差值就是丢失的数据包的个数,最后将丢包个数除以本次读取到的ID值得出丢包率。举个例子:本次收包的ID为20,上次收包的ID为10,所以中间丢失了11~19共9个包,传输的总包数20,最后丢包率就是9/20。
发送数据包的时间用于计算数据包传输抖动(Jitter)。该项测试由服务端完成,客户发送的报文数据包含有发送数据包的时间,服务器端根据该时间信息和接收到报文的时间戳来计算传输抖动,由于它是一个相对值,所以并不需要客户端和服务器端时间同步。传输抖动反映传输过程中是否平滑,如果抖动较大,在视频传输中就表现为帧率变化、有卡顿感,在音频传输中就表现为时快时慢。
iperf中计算传输抖动的方法来自于《RFC 1889》。计算过程如下:
首先,在第i个包发送时记录下发送的时间Si,在第i个包接收时记录下接收的时间Ri;同理在发送第j个包时,也是同样的操作记录下Sj、Rj。
然后计算两个包时延之间的差值:
D
=
∣
(
R
j
−
R
i
)
−
(
S
j
−
S
i
)
∣
=
∣
(
R
j
−
S
j
)
−
(
R
j
−
R
i
)
∣
,
D=|(Rj-Ri)-(Sj-Si)|= |(Rj-Sj)-(Rj-Ri)|,
D=∣(Rj−Ri)−(Sj−Si)∣=∣(Rj−Sj)−(Rj−Ri)∣,
这里差值取绝对值。
随后,计算Jitter:
J
i
t
t
e
r
(
i
)
=
J
i
t
t
e
r
(
i
−
1
)
+
(
D
(
i
−
1
,
i
)
−
J
i
t
t
e
r
(
i
−
1
)
)
/
16
Jitter(i) = Jitter(i-1) + (D(i-1,i)-Jitter(i-1))/16
Jitter(i)=Jitter(i−1)+(D(i−1,i)−Jitter(i−1))/16
这里乘以增益参数1/16是为了降低噪声的影响。
5.2 iperf工程结构
5.2.1 iperf目录结构
主要关注src目录下边的代码实现与docs目录下边的文档,其他目录了解即可。
目录 | 描述 |
---|---|
config | 初始化脚本目录。 |
contrib | 输出结果图片绘制、json化工具目录。 |
docs | 文档。常见问题、编译、更新项。 |
examples | 使用iperf api编写的客户端和服务端测试例子。 |
src | 源码目录。 |
5.2.2 iperf源码文件
src目录下文件如下:
文件 | 描述 | |
---|---|---|
cjson.c | cjson.h | json格式实现 |
dscp.c | dscp.h | |
iperf_api.c | iperf_api.h | iperf测试接口,server和client共用 |
iperf_auth.c | iperf_auth.h | SSL安全方面接口 |
iperf_client_api.c | iperf_client_api.h | iperf客户端接口 |
iperf_server_api.c | iperf_server_api.h | iperf服务端接口 |
iperf_error.c | iperf_error.h | iperf错误信息打印接口 |
iperf_locale.c | iperf_locale.h | iperf帮助信息数组 |
iperf_sctp.c | iperf_sctp.h | sctp socket操作接口实现 |
iperf_tcp.c | iperf_tcp.h | tcp socket操作接口实现 |
iperf_udp.c | iperf_udp.h | udp socket操作接口实现 |
iperf_time.c | ieprf_time.h | |
iperf_util.c | iperf_util.h | |
net.c | net.h | socket函数封装(read、write等) |
timer.c | timer.h | 定时器实现 |
units.c | units.h | B/KB/MB/GB和b/Kb/Mb/Gb单位换算接口 |
version.h | iperf版本信息 | |
flowlabel.h | IPv6流标志 | |
iperf.h | 一些选项的默认值,测试结果结构体 | |
queue.h | iperf常用数据结构实现(链表,队列,环形队列) | |
main.c | 主函数 | |
tcp_info.c | 获取tcp连接状态 | |
t_api.c | 接口测试demo | |
t_timer.c | ||
t_units.c | ||
t_uuid.c |
5.3 iperf工作流程
iperf的设计虽然是分为客户端和服务端,但是它们的源码是同一份,仅通过用户传入参数来判断该进程是启动服务端还是客户端,所以在正式启动服务端或者客户端之前,iperf有一段解析参数的流程,该流程如下,iperf_default函数首先设置服务端或者客户端的一些默认参数,随后调用iperf_parse_argument函数解析用户传入的参数,用户设置的参数会覆盖掉iperf_default函数里边配置的参数。
run函数中需要处理前面解析完成的参数,最终决定是启动server还是client,是前台进程还是守护进程。除此以外,为了方便的退出测试,run函数也会处理中断信号。
static int run(struct iperf_test *test)
{
iperf_catch_sigend(sigend_handler);
if (setjmp(sigend_jmp_buf))
iperf_got_sigend(test);
signal(SIGPIPE, SIG_IGN);
switch (test->role)
{
case 's': // 服务端模式
if (test->daemon)
{
daemon(0, 0);
} // 以守护进程方式启动
if (iperf_create_pidfile(test) < 0)
{
error handing
}
for (;;)
{
iperf_run_server(test); // 启动服务端
iperf_reset_test(test); // 将上次测试参数、结果清零
iperf_get_test_one_off(test);
}
iperf_delete_pidfile(test);
break;
case 'c': // 客户端模式
iperf_run_client(test); // 启动客户端
break;
default:
usage();
break;
}
iperf_catch_sigend(SIG_DFL); //捕捉信号
signal(SIGPIPE, SIG_DFL);
return 0;
}
5.3.1 服务端流程
5.3.1.1 服务端工作流程概述
iperf将socket、bind、listen这些套接字接口封装成了一个iperf_server_client一个函数。setsockopt函数配置地址复用,避免主动关闭服务端导致“Address already is use”的错误。
iperf使用了select模型实现io多路复用。这里猜测是为了可移植性而放弃性能更好的poll、epoll。
select函数的返回值有三种情况:在等于0时,说明已经超时,没有关心的事情发生(读写事件);在返回值大于0时,说明有读/写事件发生;在小于0时,说明发生错误。
在iperf中,select的timeout参数传入的NULL,意味着select将一直等待下去,直到有一个i/o准备好才返回。小于0的情况,各种原因都有,需要自己查一下。
5.3.1.2 iperf中select返回值大于0的四种情况
- 第一种情况:listenfd上有连接请求。
一个最简单的socket的服务器,在listen系统调用之后会调用accept阻塞等待客户端连接。在使用select之后,就需要等待select函数返回之后,调用FD_ISSET判断listenfd上是否有读事件发生。iperf中对listenfd的处理如下,iperf_accept函数调用accept系统调用连接客户端并将connfd放入到集合中,此外还通过mode参数来判断i/o流的数量和方向。
int iperf_run_server(struct iperf_test *test)
{
...
while(state != IPERF_DONE)
{
...
if (FD_ISSET(test->listener, &read_set))
{
if (test->state != CREATE_STREAMS)
{
if (iperf_accept(test) < 0)// accept错误, 退出
{
cleanup_server(test);
return -1;
}
FD_CLR(test->listener, &read_set);
// Set streams number
// io流发送方向、和数量
if (test->mode == BIDIRECTIONAL) // 双向
{
streams_to_send = test->num_streams;
streams_to_rec = test->num_streams;
}
else if (test->mode == RECEIVER) // 接收
{
streams_to_rec = test->num_streams;
streams_to_send = 0;
}
else // 发送
{
streams_to_send = test->num_streams;
streams_to_rec = 0;
}
}
}
...
}
...
}
- 第二种情况:connfd上有读请求,这种情况为控制流,控制服务端的各种行为。
除了处理连接请求,还会处理connfd的读请求,在这种情况下,服务端会主动的去读取客户端此时的状态,根据状态来判断服务端接下来的实现的动作。代码如下,iperf_handle_message_server函数就是一个小型状态机。
int iperf_run_server(struct iperf_test *test)
{
...
while(state != IPERF_DONE)
{
...
if (FD_ISSET(test->ctrl_sck, &read_set))
{
printf("FD_ISSET(test->ctrl_sck, &read_set)\n");
if (iperf_handle_message_server(test) < 0) // 状态变化
{
cleanup_server(test);
return -1;
}
FD_CLR(test->ctrl_sck, &read_set);
}
...
}
...
}
int iperf_handle_message_server(struct iperf_test *test)
{
int rval;
struct iperf_stream *sp;
if ((rval = Nread(test->ctrl_sck, (char *)&test->state, sizeof(signed char), Ptcp)) <= 0)
error handle
switch (test->state)
{
case TEST_START:
break;
case TEST_END:
发送状态、报告测试结果
break;
case IPERF_DONE:
break;
case CLIENT_TERMINATE:
客户端主动关闭、报告测试结果
break;
default:
i_errno = IEMESSAGE;
return -1;
}
return 0;
}
-
第三种情况:需要创建i/o流:这个i/o流是真正的数据流,用于测试时客户端与服务端的数据交换。
-
第四种情况:测试进行中。
这种情况,就是服务端不停的接收客户端发送过来的数据包,并且记录接收数据包的字节数与数据包的数量,以便结果的展示。
int iperf_run_server(struct iperf_test *test)
{
...
while(state != IPERF_DONE)
{
...
if (test->state == TEST_RUNNING) // 测试进行中状态
{
if (test->mode == BIDIRECTIONAL)
{
if (iperf_recv(test, &read_set) < 0)
{
cleanup_server(test);
return -1;
}
if (iperf_send(test, &write_set) < 0)
{
cleanup_server(test);
return -1;
}
}
else if (test->mode == SENDER)
{
// Reverse mode. Server sends.
if (iperf_send(test, &write_set) < 0)
{
cleanup_server(test);
return -1;
}
}
else // 服务端来说,通常仅接收就行了
{
// Regular mode. Server receives.
if (iperf_recv(test, &read_set) < 0)
{
cleanup_server(test);
return -1;
}
}
}
...
}
...
}
5.3.1.3 连接创建的流程
在iperf server端启动时,调用socket函数创建了一个tcp的套接字,然后调用select函数阻塞等待,在select
函数返回值的第一种返回情况中,会调用iperf_accept
接收连接,为客户端创建一个连接套接字。到这里,不管是server端,还是client端,创建的套接字都是tcp的,但是我们在使用时也会用到udp,这个是怎么实现的呢?
阅读iperf_accept
函数源码,在①处调用accept
系统调用,连接了一个客户端套接字s
,随后s
在②处赋值给test->ctrl_sck
(ctrl_sck
初始化为-1);③处为服务端与客户端进行参数交换。
int iperf_accept(struct iperf_test *test)
{
int s;
signed char rbuf = ACCESS_DENIED;
socklen_t len;
struct sockaddr_storage addr;
len = sizeof(addr);
if ((s = accept(test->listener, (struct sockaddr *)&addr, &len)) < 0) // 1
{
i_errno = IEACCEPT;
return -1;
}
if (test->ctrl_sck == -1)
{
/* Server free, accept new client */
test->ctrl_sck = s;
if (Nread(test->ctrl_sck, test->cookie, COOKIE_SIZE, Ptcp) < 0) // 2
{
i_errno = IERECVCOOKIE;
return -1;
}
FD_SET(test->ctrl_sck, &test->read_set);
if (test->ctrl_sck > test->max_fd)
test->max_fd = test->ctrl_sck;
if (iperf_set_send_state(test, PARAM_EXCHANGE) != 0)
return -1;
if (iperf_exchange_parameters(test) < 0) // 3
return -1;
if (test->server_affinity != -1)
if (iperf_setaffinity(test, test->server_affinity) != 0)
return -1;
if (test->on_connect)
test->on_connect(test);
}
else
{
/*
* Don't try to read from the socket. It could block an ongoing test.
* Just send ACCESS_DENIED.
*/
if (Nwrite(s, (char *)&rbuf, sizeof(rbuf), Ptcp) < 0)
{
i_errno = IESENDMESSAGE;
close(s);
return -1;
}
close(s);
}
return 0;
}
在iperf_exchange_parameters
中,对于服务端来说只需要获取到客户端在启动时配置的参数,再将读取的参数解析后配置到test指针中(客户端与服务端的数据交换通过json格式)(①处),随后调用对应协议的listen
函数(tcp、sctp协议),创建正确参数的套接字(②处)。以tcp协议的iperf_tcp_listen
为例,如果用户配置额外的参数,首先会将参数交换之前创建的套接字给关闭,然后再次调用socket函数创建一个新的套接字,并且把该套接字作为创建数据流使用的套接字。
int iperf_exchange_parameters(struct iperf_test *test)
{
int s;
int32_t err;
if (test->role == 'c') // 1
{
if (send_parameters(test) < 0)
return -1;
}
else
{
if (get_parameters(test) < 0) // 2
return -1;
if ((s = test->protocol->listen(test)) < 0) // 3
{
if (iperf_set_send_state(test, SERVER_ERROR) != 0)
return -1;
err = htonl(i_errno);
if (Nwrite(test->ctrl_sck, (char *)&err, sizeof(err), Ptcp) < 0)
{
i_errno = IECTRLWRITE;
return -1;
}
err = htonl(errno);
if (Nwrite(test->ctrl_sck, (char *)&err, sizeof(err), Ptcp) < 0)
{
i_errno = IECTRLWRITE;
return -1;
}
return -1;
}
FD_SET(s, &test->read_set);
test->max_fd = (s > test->max_fd) ? s : test->max_fd;
test->prot_listener = s;
if (iperf_set_send_state(test, CREATE_STREAMS) != 0)
return -1;
}
return 0;
}
创建连接的流程如下:
5.3.1.4 测试结果报告流程
iperf报告测试结果有两种形式:一是间隔一定时间报告一次,二是在测试完成时报告总的测试结果。
下图为TCP测试结果报告,包含测试时间、传输的数据、带宽;对于UDP来说还会有丢包率计算的结果。
测试结果中,最重要的是一定间隔时间内传输的数据量的计算以及时间间隔是如何确定的。
首先是报告时间间隔的确定。在iperf启动时可以指定参数,通过设置“-i”参数可以修改报告时间,默认情况是一秒报告一次。
//默认情况下
int iperf_defaults(struct iperf_test *testp)
{
...
testp->stats_interval = testp->reporter_interval = 1;
...
}
//指定了“-i”参数
int iperf_parse_arguments
(struct iperf_test *test, int argc, char **argv)
{
...
switch (flag)
{
case 'i':
test->stats_interval =
test->reporter_interval = atof(optarg);
if ((test->stats_interval < MIN_INTERVAL ||
test->stats_interval > MAX_INTERVAL) &&
test->stats_interval != 0)
{
i_errno = IEINTERVAL;
return -1;
}
break;
}
...
}
iperf中timer
的实现是基于Linux C的函数clock_gettime
,使用此函数可以方便的获取秒数以及纳秒。iperf将此函数封装了一层,并将纳秒级的精度转换为微秒:
int
iperf_time_now(struct iperf_time *time1)
{
struct timespec ts;
int result;
result = clock_gettime(CLOCK_MONOTONIC, &ts);
if (result == 0) {
time1->secs = (uint32_t) ts.tv_sec;
time1->usecs = (uint32_t) ts.tv_nsec / 1000;
}
return result;
}
iperf_time.c
中还提供了时间比较函数iperf_time_compare
、两个时间点之间的间隔函数iperf_time_diff
用于判断时间是否达到报告时间以及测试总时间。
以上为iperf定时器方面的实现,下边回到测试结果报告流程。
在iperf server端启动时,会根据select函数的返回值来执行相应动作,其中在返回值大于0时,并且此时服务器状态处于创建I/O流的状态,那么服务端就会创建I/O进行数据传输,同时也会创建服务端需要的定时器:
int iperf_run_server(struct iperf_test *test)
{
...
while(state != IPERF_DONE)
{
...
if (test->state == CREATE_STREAMS)
{
...
if (rec_streams_accepted == streams_to_rec
&& send_streams_accepted == streams_to_send)
{
...
if (create_server_timers(test) < 0) // 定时器
{
cleanup_server(test);
return -1;
}
if (create_server_omit_timer(test) < 0) // 忽略结果定时器
{
cleanup_server(test);
return -1;
}
...
}
...
}
}
...
}
在create_server_timers
中创建了三个定时器:server_timer_proc
用于达到测试总时间时退出、server_stats_timer_proc
用于测试期间收集统计数据、server_reporter_timer_proc
用于报告测试结果,包括最终结果与每间隔时间结果:
static int
create_server_timers(struct iperf_test *test)
{
struct iperf_time now;
TimerClientData cd;
int max_rtt = 4; /* seconds */
int state_transitions = 10; /* number of state transitions in iperf3 */
int grace_period = max_rtt * state_transitions;
if (iperf_time_now(&now) < 0)
{
i_errno = IEINITTEST;
return -1;
}
cd.p = test;
test->timer = test->stats_timer = test->reporter_timer = NULL;
if (test->duration != 0)
{
test->done = 0;
test->timer = tmr_create(&now, server_timer_proc, cd, // 达到测试总时间时退出
(test->duration + test->omit + grace_period) * SEC_TO_US, 0);
if (test->timer == NULL)
{
i_errno = IEINITTEST;
return -1;
}
}
test->stats_timer = test->reporter_timer = NULL;
if (test->stats_interval != 0)
{
test->stats_timer = tmr_create(&now, server_stats_timer_proc, // 测试期间收集统计数据
cd, test->stats_interval * SEC_TO_US, 1);
if (test->stats_timer == NULL)
{
i_errno = IEINITTEST;
return -1;
}
}
if (test->reporter_interval != 0)
{
test->reporter_timer = tmr_create(&now, server_reporter_timer_proc, // 报告测试结果
cd, test->reporter_interval * SEC_TO_US, 1);
if (test->reporter_timer == NULL)
{
i_errno = IEINITTEST;
return -1;
}
}
return 0;
}
在创建这些定时器时,也会将所有定时器添加到一个定时器链表中,在服务端的主循环里边对这个链表进行遍历,到达规定时间的定时器进行相应操作:
//创建时添加到定时器链表
Timer*
tmr_create(
struct iperf_time* nowP, TimerProc* timer_proc, TimerClientData client_data,
int64_t usecs, int periodic )
{
struct iperf_time now;
Timer* t;
getnow( nowP, &now );
if ( free_timers != NULL ) {
t = free_timers;
free_timers = t->next;
} else {
t = (Timer*) malloc( sizeof(Timer) );
if ( t == NULL )
return NULL;
}
t->timer_proc = timer_proc;
t->client_data = client_data;
t->usecs = usecs;
t->periodic = periodic;
t->time = now;
iperf_time_add_usecs(&t->time, usecs);
/* Add the new timer to the active list. */
list_add( t );
return t;
}
//在服务端的主循环中遍历
int iperf_run_server(struct iperf_test *test)
{
while (test->state != IPERF_DONE)
{
if (result == 0 ||
(timeout != NULL && timeout->tv_sec == 0 && timeout->tv_usec == 0))
{
/* Run the timers. */
iperf_time_now(&now);
tmr_run(&now);
}
}
}
server_reporter_timer_proc作为测试结果报告定时器,会报告每间隔时间内的测试结果与最终测试结果。
在创建该定时器时,会配置超时时间,一旦到达设置的超时时间,就会执行测试结果报告的回调,在回调函数中判断服务端目前状态,处于完成状态会打印测试的总体信息,形如下图;处于运行状态,就会每间隔时间打印一次带宽测试的结果。
以上主要是间隔时间方面的实现,还有一个传输的字节数计算:在服务端主要是作为数据的接收方,所以在接收时记录接收到的字节数,在打印测试结果时用于计算带宽。
int iperf_recv(struct iperf_test *test, fd_set *read_setP)
{
int r;
struct iperf_stream *sp;
SLIST_FOREACH(sp, &test->streams, streams)
{
if (FD_ISSET(sp->socket, read_setP) && !sp->sender)
{
if ((r = sp->rcv(sp)) < 0)
{
i_errno = IESTREAMREAD;
return r;
}
test->bytes_received += r; // 传输的字节数, 每次累加
++test->blocks_received;// 传输的block数, 每次累加
FD_CLR(sp->socket, read_setP);
}
}
return 0;
}
5.3.2 客户端工作流程
客户端工作流程和服务端差不多,都是创建socket套接字,然后调用select等待连接,最后进行数据传输完成测试结果的计算、报告。
客户端的工作流程如上图所示,在main.c中通过参数解析后启动调用iperf_run_client函数,随后调用iperf_connect函数,在该函数中调用socket等函数完成客户端与服务端的连接,同时将客户端sockfd添加到select数组监听中,并且设置好MSS。
客户端也使用了select,在select返回值大于0,且在读集合中有fd产生读事件,客户端就会调用iperf_handle_message_client函数,该函数相当于一个状态机,根据测试状态进行不同处理。
在客户端处于测试运行状态(running state)时,就是客户端朝服务端一直发送数据。此时,客户端会启动定时器来记录时间。
5.3.2.1 客户端不同状态的处理
在iperf中,服务端和客户端都有一个handle_message的函数,用于处理不同状态下客户端或服务端的行为。为方便说明,这里将客户端的状态变化与服务端联系起来分析。
下图所示的状态流转,是在客户端调用connect,服务端调用accept,客户端和服务端建立起连接后,通过read、write函数把状态互相发送给对方,然后通过handle_message函数进行对应状态的操作。
首先,客户端与服务端建立连接后,服务端会向客户端发送PARAM_EXCHANGE状态,客户端获取到状态后会将用户配置的参数,如“-i、-u、-b”等发送到服务端,服务端根据这些参数重新创建一个套接字;
之后,服务端再向客户端发送CREATE_STREAMS状态,客户端将调用iperf_create_stream函数进行stream的创建,同时服务端也进入创建stream的流程;
stream创建完成后,服务端发送TEST_START到客户端,客户端创建需要的定时器;最后服务端与客户端都进入TEST_RUNNING状态,此状态下,主要进行数据交换。
在测试的结束阶段,主要是交换测试结果、测试结果显示的操作。
以下为客户端的handle_message函数,可以看到该函数先从socket中读取目前的状态值,随后根据状态值进行处理:
int iperf_handle_message_client(struct iperf_test *test)
{
int rval;
int32_t err;
if ((rval = read(test->ctrl_sck, (char *)&test->state, sizeof(signed char))) <= 0)
{
if (rval == 0)
{
i_errno = IECTRLCLOSE;
return -1;
}
else
{
i_errno = IERECVMESSAGE;
return -1;
}
}
switch (test->state) // 状态机状态变化
{
case PARAM_EXCHANGE: // 参数交换状态
if (iperf_exchange_parameters(test) < 0)
return -1;
if (test->on_connect)
test->on_connect(test);
break;
case CREATE_STREAMS: // 创建流状态
if (test->mode == BIDIRECTIONAL)
{
if (iperf_create_streams(test, 1) < 0)
return -1;
if (iperf_create_streams(test, 0) < 0)
return -1;
}
else if (iperf_create_streams(test, test->mode) < 0)
return -1;
break;
case TEST_START: // 测试开始状态
if (iperf_init_test(test) < 0)
return -1;
if (create_client_timers(test) < 0)
return -1;
if (create_client_omit_timer(test) < 0)
return -1;
if (test->mode)
if (iperf_create_send_timers(test) < 0)
return -1;
break;
case TEST_RUNNING: // 测试运行中状态
break;
case EXCHANGE_RESULTS: // 测试结果交换状态
if (iperf_exchange_results(test) < 0)
return -1;
break;
case DISPLAY_RESULTS: // 结果展示状态
if (test->on_test_finish)
test->on_test_finish(test);
iperf_client_end(test);
break;
case IPERF_DONE: //
break;
case SERVER_TERMINATE: // 服务端终止状态
i_errno = IESERVERTERM;
/*
* Temporarily be in DISPLAY_RESULTS phase so we can get
* ending summary statistics.
*/
signed char oldstate = test->state;
cpu_util(test->cpu_util);
test->state = DISPLAY_RESULTS;
test->reporter_callback(test);
test->state = oldstate;
return -1;
case ACCESS_DENIED:
i_errno = IEACCESSDENIED;
return -1;
case SERVER_ERROR:
if (Nread(test->ctrl_sck, (char *)&err, sizeof(err), Ptcp) < 0)
{
i_errno = IECTRLREAD;
return -1;
}
i_errno = ntohl(err);
if (Nread(test->ctrl_sck, (char *)&err, sizeof(err), Ptcp) < 0)
{
i_errno = IECTRLREAD;
return -1;
}
errno = ntohl(err);
return -1;
default:
i_errno = IEMESSAGE;
return -1;
}
return 0;
}
5.3.2.2 客户端发送的什么数据
在iperf中不是直接使用read、write等io函数进行数据发送,而是将其封装再使用,例如在客户端的主循环里,客户端位于TEST_RUNNING状态就会调用iperf_send函数发送数据:
int iperf_run_server(struct iperf_test *test)
{
...
while(state != IPERF_DONE)
{
...
if (test->state == TEST_RUNNING)
{
...
if (test->mode == BIDIRECTIONAL)
{
if (iperf_send(test, &write_set) < 0)
return -1;
if (iperf_recv(test, &read_set) < 0)
return -1;
}
else if (test->mode == SENDER) // Client 发送数据
{
// Regular mode. Client sends.
if (iperf_send(test, &write_set) < 0)
return -1;
}
else
{
// Reverse mode. Client receives.
if (iperf_recv(test, &read_set) < 0)
return -1;
}
}
...
}
...
}
下面主要看iperf_send
函数所做的工作,首先是multisend
,当未设置-b
参数时,multisend
默认为1,代表只发送一个数据包;如果设置-b50M
,代表按照50M的带宽发送,一次发送数据报为10个,10个为默认值;如果设置-b50M/20
,代表按照50M的带宽发送,一次发送数据报为20个。
在发送数据包时,实际使用的是sp->snd(sp)
,snd
这个发送函数根据测试使用的协议确定,tcp协议为iperf_tcp_send
,udp协议为iperf_udp_send
;在发送完所有io流的所有数据包以后,iperf_send
函数需要将描述符集合清零。
int iperf_send(struct iperf_test *test, fd_set *write_setP)
{
register int multisend, r, streams_active;
register struct iperf_stream *sp;
struct iperf_time now;
if (test->settings->burst != 0) // -b参数的作用
multisend = test->settings->burst;
else if (test->settings->rate == 0)
multisend = test->multisend;
else
multisend = 1; /* nope */
for (; multisend > 0; --multisend)
{
if (test->settings->rate != 0 &&
test->settings->burst == 0)
iperf_time_now(&now);
streams_active = 0;
SLIST_FOREACH(sp, &test->streams, streams)
{
if ((sp->green_light && sp->sender &&
(write_setP == NULL ||
FD_ISSET(sp->socket, write_setP))))
{
if ((r = sp->snd(sp)) < 0) // 实际调用的发送函数
{
if (r == NET_SOFTERROR)
break;
i_errno = IESTREAMWRITE;
return r;
}
streams_active = 1;
test->bytes_sent += r;
++test->blocks_sent;
if (test->settings->rate != 0
&& test->settings->burst == 0)
iperf_check_throttle(sp, &now);
if (multisend > 1 && test->settings->bytes != 0
&& test->bytes_sent >= test->settings->bytes)
break;
if (multisend > 1 && test->settings->blocks != 0
&& test->blocks_sent >= test->settings->blocks)
break;
}
}
if (!streams_active)
break;
}
if (test->settings->burst != 0)
{
iperf_time_now(&now);
SLIST_FOREACH(sp, &test->streams, streams)
iperf_check_throttle(sp, &now);
}
if (write_setP != NULL)
SLIST_FOREACH(sp, &test->streams, streams)
if (FD_ISSET(sp->socket, write_setP))
FD_CLR(sp->socket, write_setP);
return 0;
}
以tcp的iperf_tcp_send为例,分析数据包的来源。可以看到,iperf_tcp_send为实际发送数据包的函数,如果系统支持并且设置了零拷贝,那么会调用Nsendfile,其他情况就使用Nwrite。sp->buffer就是客户端朝服务端发送的数据包,在这里是直接使用了sp->buffer,这个buffer另有来源。
int iperf_tcp_send(struct iperf_stream *sp)
{
int r;
if (sp->test->zerocopy)
r = Nsendfile(sp->buffer_fd, sp->socket,
sp->buffer, sp->settings->blksize);
else
r = Nwrite(sp->socket, sp->buffer, sp->settings->blksize, Ptcp); // 直接将sp->buffer发送出去
if (r < 0)
return r;
sp->result->bytes_sent += r;
sp->result->bytes_sent_this_interval += r;
if (sp->test->debug)
printf("sent %d bytes of %d, total %" PRIu64 "\n", r,
sp->settings->blksize, sp->result->bytes_sent);
return r;
}
在客户端进行发送数据之前,客户端与服务端都处于CREATE_STREAM状态,在该状态下,客户端调用iperf_create_stream函数,最终调用iperf_new_stream函数。在iperf_new_stream中,对 sp->buffer进行了数据填充:使用的是mmap函数。
struct iperf_stream *
iperf_new_stream(struct iperf_test *test, int s, int sender)
{
...
sp->buffer_fd = mkstemp(template); // 创建临时文件
if (sp->buffer_fd == -1){}
if (unlink(template) < 0){} // 使临时文件在进程退出时自动删除
if (ftruncate(sp->buffer_fd, test->settings->blksize) < 0){} // 空字符串填充
sp->buffer = (char *)mmap(NULL, test->settings->blksize, // 将文件中的数据填充到sp->buffer中
PROT_READ | PROT_WRITE, MAP_PRIVATE, sp->buffer_fd, 0);
if (sp->buffer == MAP_FAILED)
{
i_errno = IECREATESTREAM;
free(sp->result);
free(sp);
return NULL;
}
...
iperf_add_stream(test, sp);
return sp;
}
主要关注mkstemp、unlink、ftruncate、mmap四个函数。mkstemp函数的作用为创建一个临时文件;unlink的作用:由于临时文件不能自动删除,避免临时文件占用空间,所以调用unlink删除临时文件目录的入口,但是程序可以通过描述符访问临时文件,在进程退出时删除临时文件;ftruncate的作用为:将文件的长度按照传入参数length来填充空字符。所以在iperf中发送的数据包都为空字符。
#include <stdlib.h>
int mkstemp(char *template);
#include <unistd.h>
#include <sys/types.h>
int ftruncate(int fd, off_t length);
#include <unistd.h>
int unlink(const char* pathname);
#include <sys/mman.h>
void* mmap(void* addr, size_t len, int prot,
int flag, int fd, off_t offset);
mmap函数能将一个磁盘文件映射到存储空间中的一个缓冲区上。addr参数用于指定映射存储区的起始地址,通常设置为0,表示由系统选择该映射区的起始地址;len参数是要映射的字节数,iperf中tcp默认128KB,udp默认1460字节;fd参数指定要被映射文件的描述符;offset是要映射字节在文件中的其实偏移量。
prot参数指定了映射存储区的保护要求,iperf中使用PROT_READ | PROT_WRITE,支持可读可写。
prot | 说明 |
---|---|
PROT_READ | 映射区可读 |
PROT_WRITE | 映射区可写 |
PROT_EXEC | 映射区可执行 |
PROT_NONE | 映射区不可访问 |
flag参数影响映射存储区的多种属性,在iperf中使用MAP_PRIVATE,所有对该映射区的引用都是引用副本,所有读写操作只影响副本,不影响原文件。
总结来说,iperf中发送的数据都是在创建stream的时候,新建了一个临时文件,这个临时文件用空字符来填充,文件构造完毕后调用mmap函数把文件内容映射到sp->buffer中,最终调用write系统调用将数据发送出去。
6 问题深入分析
6.1 tcp模式下单线程和多线程区别,速率差异原因
这个问题主要是针对iperf的“-P”参数进行分析。需要明确的是,iperf3的网络模型是单线程多路I/O,而iperf2的网络模型是多线程。
iperf3版本,-P
参数的作用是,申请多路I/O进行打流,但是其实际还是单线程的,所以打流的速率不会有多大变化。针对多核CPU而言,要想达到最大带宽,可以启动多个iperf服务端进程和客户端进程进行打流。
iperf2版本,-P
参数就是实际的多线程,多个线程可以运行在不同CPU的核心上,所以iperf2的多线程打流可以测出较高的带宽。
iperf3使用单线程多路I/O网络模型,iperf2使用多线程网络模型,iperf3和iperf2的-P
参数的差异实际就是两个网络模型的差异。
下边分析一下单线程多路I/O模型与多线程模型的区别。
单线程多路I/O复用模型结构如下图。
①服务端Main Thread创建ListenFd以后,采用I/O多路复用机制(如select、poll、epoll)进行I/O状态阻塞监听。当Client1调用Connect时,I/O多路复用机制监测到ListenFd触发读事件,则调用Accept进行连接,并将ConnFd1添加到监听集合中,随后Accept返回到I/O复用机制处阻塞等待。
②此时Client1再次进行读/写请求,会触发ConnFd1的读/写事件。
③I/O复用机制检测到ConnFd1的读/写事件阻塞返回,并对读/写事件进行处理,在Main Thread处理ConnFd1的读/写事件时,若有新客户端发起连接,服务端不会马上处理该事件,需要等待服务端处理完ConnFd1的事件后再为新客户端分配ConnFd。
④等到读/写事件处理完成后,就会返回I/O复用机制继续等待,新的客户端将会重复②③过程。
通过以上分析,可以知道,单线程多路I/O复用模型虽然可以处理多个I/O请求,但是同一时间内的并发量还是为1,其他客户端需要等待当前客户端事件处理完成,在进行读/写。
多线程模型如下图。
①服务端在Main Thread中阻塞执行Accept,当有Client有连接请求过来,Main Thread中Accept响应并建立连接。
②连接创建成功后,Main Thread拿到ConnFd后,为Client新建一个Thread用于处理读/写事务,Main Thread随后返回Accept阻塞等待。
③Thread1通过套接字ConnFd1与Client1进行通信。
④服务端在处理Client1的业务时,也可接受新的客户端的连接请求与读/写事务。
经过以上分析,在多线程模型中,服务端和客户端的数量可以认为是1:1的,即一个客户端对应一个服务端线程,在多核CPU中,可以认为是多个客户端同时得到服务。
**综上,**在iperf3中使用的是单线程多路I/O,所以即使我们在启动客户端时使用了“-P”参数,多个I/O之间也不是同时运行的,就导致了测试带宽较低。而在多线程(可以启动若干个iperf进程模拟)测试中,每个iperf客户端可以同时向服务端打流,所以测试出的带宽较高。
6.2 -l参数的含义以及如何控制包长
-l
参数,表示要写入的缓冲区长度(标红部分),还有一个字节的单位换算,使用函数unit_atoi
完成转换,Kk、Mm、Gg表达的意思是一样的,例如10K == 10k == 10240字节。对于TCP测试,默认值为128KB。在UDP的情况下,iperf3将基于MTU动态确定发送大小;如果不能确定,则使用1460字节作为发送大小。对于SCTP测试,默认大小为64KB。
回顾5.3.2.2章节,发送缓冲区的长度由test->settings->blksize确定,该参数有默认值:tcp默认128KB,udp默认1460字节,这一点可以在iperf_parse_arguments
函数中得到印证(标蓝部分),几个宏的值分别为:DEFAULT_SCTP_BLKSIZE 64KB
、DEFAULT_TCP_BLKSIZE 128KB
、DEFAULT_UDP_BLKSIZE 1460
。
int iperf_parse_arguments(struct iperf_test *test, int argc, char **argv)
{
...
switch (flag)
{
case 'l': // 手动标红
blksize = unit_atoi(optarg);
client_flag = 1;
break;
}
...
if (blksize == 0)
{
if (test->protocol->id == Pudp)
blksize = 0;
else if (test->protocol->id == Psctp)
blksize = DEFAULT_SCTP_BLKSIZE; // 手动标蓝
else
blksize = DEFAULT_TCP_BLKSIZE; // 手动标蓝
}
if ((test->protocol->id != Pudp && blksize <= 0) || blksize > MAX_BLOCKSIZE)
{
i_errno = IEBLOCKSIZE;
return -1;
}
if (test->protocol->id == Pudp &&
(blksize > 0 &&
(blksize < MIN_UDP_BLOCKSIZE || blksize > MAX_UDP_BLOCKSIZE)))
{
i_errno = IEUDPBLOCKSIZE;
return -1;
}
test->settings->blksize = blksize; // 手动标红
...
}
iperf中udp默认传输1460字节的数据,以太网的MTU为1500字节,可以计算一下这个默认数据会不会引发分片:udp头部8字节,IPv4头部20字节,1460字节+20字节+8字节=1488字节,所以iperf默认udp传输字节为不会引起IP层的分片操作。
在802.11帧中,最多可传送2304字节的有效载荷;802.2LLC有8字节的标头,所以最大可传送2296字节的数据;不管使用哪个传输载荷,MTU配置为1500都不会进行分片。
实际抓包测试:
测试拓扑如上,AX1800的设备通过有线连接千兆云管交换机,测试机通过网线连接交换机。在测试机上使用启动iperf服务端,并打开wireshark抓包;在AX1800上启动iperf客户端,打tcp流量,包长设置为2920(MSS的两倍),命令如下:iperf3 -c 172.8.192.112 -t2 -l 2920。
AX1800和测试机的log如下。
Wireshark抓包如下,可以看到每个tcp的报文的数据长度都是1460。
使用不同的长度进行抓包测试,图为包长4800时的抓包数据:
iperf调用write函数向服务端写入数据,整个buffer的数据是4800字节,而通过协议栈发现这个buffer的长度大于MSS,所以在TCP层进行了分片操作,所以抓包来看,长度大于1460的都进行了分片。
同理,如果我们的包没超过MSS(1460Byte),tcp由于有发送缓冲区的存在,抓到的包的长度并不总是512字节;而udp就比较明了,全是-l参数指定的包长:
6.3 tcp和udp测试速度差异原因
包长(byte) | 1024 | 1460 | 2920 | 4380 | 5840 | 7300 |
---|---|---|---|---|---|---|
UDP(Mbits/sec) | 359 | 510 | 835 | 953 | 951 | 954 |
TCP(Mbits/sec) | 371 | 586 | 877 | 909 | 942 | 941 |
-N参数 | 404 | 493 | 940 | 942 | 942 | 942 |
上表测试了TCP与UDP在包长不同时的性能表现。测试环境:两台Windows10测试机通过千兆网口直连,iperf版本为最新的3.12,测试结果均为服务端计算的结果。
测试命令如下,-b参数设置了目标带宽,iperf就会尽量朝一个带宽去打流,但是也受限于包长,包长过小也是达不到目标带宽的。
iperf3 -c server_ip -t30 -O10 -b1000M -w128k –l包长 [-u][-N]
从测试结果来看,在发送小数据包时,TCP的表现要优于UDP,当数据包较大时,UDP的性能优于TCP。
首先说一下为什么选取1460倍数来作为测试包长。
针对IPv4来说,所有IP数据包都是由IP头部+IP数据组成,如图9所示,通常IP头部20字节(不带IP选项),IP数据最多为65515字节。
下图为IP协议封装UDP与TCP协议。
在网络层之下,数据链路层规定了携带高层协议PDU的帧大小,以太网通常限制为1500字节,这个数据称为MTU。MTU会对IP层的分片造成影响:IP比较MTU和数据的大小,如果太大就会进行分片操作。以MTU为1500字节为例,分片后的IP数据报总长度为1500字节,对于UDP来说,该数据报包含的UDP数据为:1500字节-IPv4头部(20字节)-UDP头部(8字节)=1472字节;对于TCP来说,数据包内包含的TCP数据:1500字节-IPv4头部(20字节)-TCP头部(20字节)=1460字节。
6.3.1 数据包较小时速率差异原因分析
在表2的测试结果中,当数据包为1024字节时,TCP的测试的带宽比UDP的高。
在1024字节的数据长度下,不会触发IP分片操作。UDP就会直接将数据包发送给接收方;而TCP会因为Nagle算法,不会马上将数据包发送出去,而是会拼接数据包至MSS大小(1460字节)。
虽然Nagle算法会增加一定的网络延时,但是会增加网络带宽的利用率;UDP虽然延时较短,但是丢包率较高。所以在iperf服务端计算的结果就会造成TCP的带宽高于UDP。
比如,下图所示为1024字节包长下,TCP与UDP的测试结果,在相同的测试时间条件下,TCP传输了1.3G的数据,UDP传输了1.46G但是丢了0.21G的数据,就造成服务端计算的带宽仅有359Mbits/s
-N
参数,在iperf中就是关闭Nagle算法的指令,对于TCP来讲,测试时间相同,网络延时更低那么发送数据包就会更多,最后服务端计算出来的带宽当然更大。如下图所示为,TCP协议,1024字节包长下,加上了-N参数后的测试结果,从传输数据总量来看还是少于UDP,但是最后的带宽大于UDP以及开启Nagle算法的TCP。
6.3.2 数据包较大时速率差异原因分析
在传输较大的数据包时,UDP的性能优于TCP;TCP不管是否加上-N参数,对于TCP的速率影响都不大,因为Nagle算法只要是针对小包设计的。
在较大数据包下,目标带宽设置的1000M,TCP与UDP都能测试出900M以上的带宽。
TCP由于协议有应答与超时重传的机制,所以在每个数据包接收到以后都会发送应答;而如果没收到应答,就会触发超时重传,这两种机制会占用一定的网络带宽,所以iperf的TCP测试结果会略低于UDP。
6.3.3 小包和大包速率差异原因分析
iperf的带宽计算方法:测试过程中接收到的数据除以测试时间。这里的数据是去除了所有协议头。
在这里拿UDP传输为例做个测试:
测试拓扑如图14,使用AP向测试机打UDP报文。
测试时间为1s,按照1000Mbps的目标带宽,包长分别设置为655Byte、65507Byte。测试结果如下:包长为655字节的数据在1s内发送了26672个数据报,总数据为16.7MB,而包长为65507字节的数据在1s内发送了979个数据报,总数据为61.2MB。
由抓包数据可以知道,带宽的速率完全就是看单位时间内传输的数据多少。
可以简单计算一下发送第一个数据的瞬间,有效载荷的多少:对于65507的包长来说,首包的有效载荷肯定是1472的,那么加上协议头等数据,以太网帧就是1518字节,1472/1518大约为97%,而对于655字节的来说,仅有94%。这里是仅从链路效率上来说明大小包上的差异。
另外从iperf实现来说,直接调用write、read来获取网络中的数据,众所周知I/O操作是很耗时的,在用户空间,read函数调用的次数越少越好。小包数据报文数量比大包数据报文大接近30倍,调用read、write的次数自然也就更多,所以就导致消耗了部分性能在IO操作上。
7 参考
《UNIX网络编程 卷1:套接字联网API》
《UNIX环境高级编程》(第三版)
《TCP/IP详解 卷1:协议》
《RFFC 1889》https://www.rfc-editor.org/rfc/rfc1889