tcpreplay 发包速率控制算法研究
一. 序
1.1 tcpreplay历史
Tcpreplay 的作者是Aaron Turner,该项目开始于2000年,早期的功能是对tcpdump等抓包工具生成的网络包(即pcap文件)的回放,并加入了一些控制,比方说控制回放的速率,以及拆分客户端和服务端的流量,控制它们从不同网络接口回放。稍后的版本加入了网络包编辑的功能,允许对pcap文件进行各个协议层的修改然后再发送。
Tcpreplay主要的应用场景是各种设备的测试,用户将某些现实场景或实验室场景下产生的流量抓下来,以pcap文件的形式存储,需要的时候就可以使用tcpreplay重现当时的场景,通使用包编辑功能可以让重现场景的应用范围更广。
截至2011年发布的tcpreplay3.4.4,该项目已历经68个版本。但是,主要算法思想变动不大,在前面10个版本的时候已经定下,后面的版本修改工作主要集中于系统兼容性、算法优化、自动编译工具支持、三方库选择等这些方面。更多内容请见:
官网:http://tcpreplay.synfin.net/
1.2 本文目的
由序可知,tcpreplay的主要算法思想在众多版本中具有稳定性,本文从中挑选了3种算法,通过不同版本对比,结合实际项目使用情况,对其进行研究、归纳、总结。这3个算法分别是:速率控制算法、流量拆分算法、缓存算法
名称说明:tcpdump抓取的网络包统一以 ‘pcap包’或者‘pcap文件’描述,一个pcap文件里包含的小包用‘packet’或者‘包’描述。
一. 速率控制算法
1.1 算法目的
Tcpreplay在最早的版本就加入了包回放速率控制的功能。可以让pcap包以抓取时候速率的特定倍数回放,或者以Mb/秒或packet/秒的速率发送出去。
Tcpreplay3.4.4 已经能支持以下速率控制
-x, --multiplier=str 以抓包速率的一定比率发packet
-p, --pps=num 每秒packet
-M, --mbps=str 每秒兆比特
-t, --topspeed 全速(不做任何时间调整)
-o, --oneatatime 终端点击一次发一个packet
--pps-multi=num 相隔特定时间发多少packet
此外,下面几个命令本质上也是速率控制用的
-T, --timer=str 睡眠函数: select, ioport, rdtsc, gtod, nano, abstime
--sleep-accel=num 睡眠调整参数
--rdtsc-clicks=num Specify the RDTSC clicks/usec 特定调整参数
1.2 算法思想
我们考虑一个问题,如何才能让一个网络流量包以特定速率发送出去?
这个问题可以这么考虑,这里的‘速率’是从用户角度出发考虑的,不是机器真正的速率!机器的发包速率是包的长度除以发包耗费时间,包的长度可以理解为内存的大小,时间可以理解为这块内存的内容写到网络接口的时间。从‘用户角度’而言,内存的大小是不可改的,时间却是可以增加的,可以通过简单的sleep 函数做到这点。
速率控制算法的大体思路就是,通过适当的sleep,增加包发送的时间,从而减小算出来的速率,以达到用户设定的(小于机器最大速率)的某个速率。这个算法关键是两点,一是时间,包括承载时间值的变量,以及在这些变量上的运算,tcpreplay在时间变量的精度和运算上都有一些自己的做法,以保证算出来的速率更符合‘从用户角度出发’这个最终目的,--sleep-accel 参数就是这种作用的一个例子,用于在正常运算之外做微调。二是睡眠,睡眠
的实现有多种,而且不同实现方式跟操作系统有很大关系。用户可以通过 –timer 参数选择具体的睡眠方式。
1.1 算法流程
1.1.1 说明
下面流程主要针对以下模式:
-x, --multiplier=str 以抓包速率的一定比率发包
-p, --pps=num 每秒包
-M, --mbps=str 每秒兆比特
-t, --topspeed 全速(不做任何时间调整)
对于以下模式这里没有描述出来:
-o, --oneatatime 终端点击发一次
--pps-multi=num 相隔特定时间发多少包
1.1.2 流程描述
1.1.1 流程图
1.1 算法实现
1.1.1 数据结构
/* 包回放运行时控制结构*/
struct tcpreplay_opt_s {
char *intf1_name; /*端口1名字*/
char *intf2_name;/*端口2名字*/
sendpacket_t *intf1; /*发包子控制结构*/
sendpacket_t *intf2;
tcpr_speed_t speed;
u_int32_t loop; /*循环次数*/
int sleep_accel; /*睡眠调整函数*/
int stats;
/* tcpprep 缓存数据控制结构*/
COUNTER cache_packets;
char *cachedata;
char *comment; /* tcpprep comment */
/* deal with MTU/packet len issues */
int mtu;
int truncate;
/* 睡眠模式,对应不同的睡眠函数实现*/
int accurate;
#define ACCURATE_NANOSLEEP 0
#define ACCURATE_SELECT 1
#define ACCURATE_RDTSC 2
#define ACCURATE_IOPORT 3
#define ACCURATE_GTOD 4
#define ACCURATE_ABS_TIME 5
char *files[MAX_FILES];
COUNTER limit_send;
/* 文件缓存控制结构 */
int enable_file_cache;
file_cache_t *file_cache; /*文件缓存子数据结构*/
int preload_pcap;
};
typedef struct tcpreplay_opt_s tcpreplay_opt_t;
struct packet_cache_s { /*packet 数据结构*/
struct pcap_pkthdr pkthdr; /*包头*/
u_char *pktdata;/*包身*/
struct packet_cache_s *next;
};
typedef struct packet_cache_s packet_cache_t;
typedef struct {/*文件缓存子数据结构*/
int index;
int cached;
packet_cache_t *packet_cache; /*packet 控制结构指针*/
} file_cache_t;
struct sendpacket_s {/*发包子控制结构*/
tcpr_dir_t cache_dir;
int open;
char device[20];
char errbuf[SENDPACKET_ERRBUF_SIZE];
COUNTER retry_enobufs;/*这几个COUNTER变量都是发包结果统计信息*/
COUNTER retry_eagain;
COUNTER failed;
COUNTER sent;
COUNTER bytes_sent;
COUNTER attempt;
enum sendpacket_type_t handle_type; /*发送包使用的三方库类型*/
union sendpacket_handle handle; /*句柄 */
struct tcpr_ether_addr ether;
};
typedef struct sendpacket_s sendpacket_t;
enum sendpacket_type_t { /*发送包使用的三方库类型*/
SP_TYPE_LIBNET,
SP_TYPE_LIBDNET,
SP_TYPE_LIBPCAP,
SP_TYPE_BPF,
SP_TYPE_PF_PACKET
};
union sendpacket_handle {
pcap_t *pcap;
int fd;
#ifdef HAVE_LIBDNET
eth_t *ldnet;
#endif
};
1.1.1 主要函数
/**
*发包主函数,速率控制部分主要是时间的控制。将与
*速率控制无关的部分代码省去了,用 。。。。。。。。。 表示
*/
void
send_packets(pcap_t *pcap, int cache_file_idx)
{
struct timeval last = { 0, 0 }, last_print_time = { 0, 0 }, print_delta, now;
COUNTER packetnum = 0;
struct pcap_pkthdr pkthdr; /*包头控制结构*/
const u_char *pktdata = NULL;/*包身数据结构*/
sendpacket_t *sp = options.intf1;/* 发包子控制结构*/
u_int32_t pktlen; /*包长度*/
。。。。。。。。。。。。。。。。
delta_t delta_ctx;
init_delta_time(&delta_ctx);/*存放当前时间*/
didsig = 0; /*为ONEATATIME模式注册信号*/
if (options.speed.mode != SPEED_ONEATATIME) {/*注册信号*/
(void)signal(SIGINT, catcher);
} else {
(void)signal(SIGINT, break_now);
}
。。。。。。。。。。。。。。。。。。。
/* 主循环
*/
while ((pktdata = get_next_packet(pcap, &pkthdr, cache_file_idx, prev_packet)) != NULL) {
/*为ONEATATIME模式注册信号*/
if (didsig)
break_now(0);
。。。。。。。。。。。。。。。。
packetnum++;
。。。。。。。。。。。。。。。
if (options.speed.mode != SPEED_TOPSPEED)
do_sleep((struct timeval *)&pkthdr.ts, &last, pktlen, options.accurate, sp, packetnum, &delta_ctx); /*各种速率控制的实现,在这个函数里完成*/
/* 获取当前时间*/
start_delta_time(&delta_ctx);
/*真正的发包在这里,通过调用第三方库实现 */
if (sendpacket(sp, pktdata, pktlen) < (int)pktlen)
warnx("Unable to send packet: %s", sendpacket_geterr(sp));
/*last变量存放上个packet的抓取时间*/
if (timercmp(&last, &pkthdr.ts, <))
memcpy(&last, &pkthdr.ts, sizeof(struct timeval));
pkts_sent ++; /*packets 数目累计*/
bytes_sent += pktlen;/*packets 字节数累计*/
}
从上面的函数看到,各种速率控制模式都是在时间调整函数 dosleep 里边实现。主函数在调整函数运行后才发packet,下面是时间调整函数do_sleep
static void
do_sleep(struct timeval *time, struct timeval *last, int len, int accurate,
sendpacket_t *sp, COUNTER counter, delta_t *delta_ctx)
{
/* 参数说明:
time: 当前packet抓取时的系统时间,与last的差就是前一个packet抓取的使用时间
Last: 前一个packet抓取时的系统时间
Len: 当前packet 的长度
Accurate: 睡眠模式
Sp: 发包子控制结构
Counter:当前packet 的 id
Delta_ctx: 存放系统时间的变量
*/
static struct timeval didsleep = { 0, 0 };
static struct timeval start = { 0, 0 };
struct timespec adjuster = { 0, 0 };
static struct timespec nap = { 0, 0 }, delta_time = {0, 0};
struct timeval nap_for, now, sleep_until;
struct timespec nap_this_time;
/*以上timeval 和 timespec 变量都是时间控制需要的,特别注意的是有些变量是timeval,有些是timespec,也就是精度更高,实际上,在最初的版本,时间控制变量都是timeval类型的,现在的版本部分换成了timespec进行计算以提高精度。同时两种不同精度的时间变量同时存在,导致本算法有一部分专门是用来在两种精度之间做转换和调整的,比如,pps模式下的时间微调,就是这个考虑*/
static int32_t nsec_adjuster = -1, nsec_times = -1;
float n;
static u_int32_t send = 0; /* accellerator. # of packets to send w/o sleeping */
u_int32_t ppnsec; /* packets per usec */
static int first_time = 1; /* need to track the first time through for the pps accelerator */
/*下面这个就是根据用户设置的值设定微调的时间值*/
#ifdef TCPREPLAY
adjuster.tv_nsec = options.sleep_accel * 1000;
#else
adjuster.tv_nsec = 0;
#endif
/* acclerator time? */
if (send > 0) {
send --;
return;
}
*/
/* 下面是第一个packet的处理*/
if (options.speed.mode == SPEED_PACKETRATE && options.speed.pps_multi) {
send = options.speed.pps_multi - 1;
if (first_time) {
first_time = 0;
return;
}
}
if (gettimeofday(&now, NULL) < 0)
/* 下面是第一个packet的时间变量初始化*/
if (pkts_sent == 0 || ((options.speed.mode != SPEED_MBPSRATE) && (counter == 0))) {
start = now;
timerclear(&sleep_until);
timerclear(&didsleep);
}
else { /*如果不是第一个packet,算出前面N-1个包使用的时间*/
timersub(&now, &start, &sleep_until);
}
/*下面根据不同模式算出用户指定速率换算成的时间*/
switch(options.speed.mode) {
case SPEED_MULTIPLIER:
/*以该packet抓取的时间的一定倍数去回放
*/
if (timerisset(last)) {
if (timercmp(time, last, <)) { /*这种情况一般是不可能发生的*/
timesclear(&nap);
} else {
/* time-last 就得到该packet 的抓取时间*/
timersub(time, last, &nap_for);
TIMEVAL_TO_TIMESPEC(&nap_for, &nap);
timesdiv(&nap, options.speed.speed);/*除以倍数,得到需要的速率*/
}
} else { /* last 是空,说明是第一个packet,清空nap就行了*/
timesclear(&nap);
}
break;
case SPEED_MBPSRATE:
/* 以 Mbps 的用户设定速率去发
*/
if (pkts_sent != 0) {
n = (float)len / (options.speed.speed * 1024 * 1024 / 8);
nap.tv_sec = n;
nap.tv_nsec = (n - nap.tv_sec) * 1000000000;
nap.tv_sec, nap.tv_nsec);
}
else { /* pkts_sent 是空,说明是第一个packet,清空nap就行了*/
timesclear(&nap);
}
break;
case SPEED_PACKETRATE:
/* 每秒发多少packet
*/
if (! timesisset(&nap)) {
ppnsec = 1000000000 / options.speed.speed * (options.speed.pps_multi > 0 ? options.speed.pps_multi : 1);
NANOSEC_TO_TIMESPEC(ppnsec, &nap);
}
break;
case SPEED_ONEATATIME:
/* 点击一下终端发送一个 packet
*/
/* do we skip prompting for a key press? */
if (send == 0) {
send = get_user_count(sp, counter);
}
/* decrement our send counter */
send --
return; /* leave do_sleep() */
break;
default: /*不是上面任一模式,报错退出*/
errx(-1, "Unknown/supported speed mode: %d", options.speed.mode);
break;
}
/*下面算 pps 模式下的微调时间,大概思路是将上面算出的睡眠时间变量的nsec 精度级别上进行微调,方法是与一个随机数比较,大于它则 nsec 部分取整并增加一个单位,否则取整*/
/*
* since we apply the adjuster to the sleep time, we can't modify nap
*/
memcpy(&nap_this_time, &nap, sizeof(nap_this_time));
if (accurate != ACCURATE_ABS_TIME) {
switch (options.speed.mode) {
case SPEED_MBPSRATE:
case SPEED_MULTIPLIER:/*这两种模式不微调*/
break;
/* Packets/sec is static, so we weight packets for .1usec accuracy */
case SPEED_PACKETRATE: /*这种模式才进行微调*/
if (nsec_adjuster < 0)
nsec_adjuster = (nap_this_time.tv_nsec % 10000) / 1000;
/* update in the range of 0-9 */
nsec_times = (nsec_times + 1) % 10;
if (nsec_times < nsec_adjuster) {
/* sorta looks like a no-op, but gives us a nice round usec number */
nap_this_time.tv_nsec = (nap_this_time.tv_nsec / 1000 * 1000) + 1000;
} else {
nap_this_time.tv_nsec -= (nap_this_time.tv_nsec % 1000);
}
break;
default:
errx(-1, "Unknown/supported speed mode: %d", options.speed.mode);
}
}
/*下面获取系统在发第N-1个packet的使用时间,并与用户设置速率换算成
的时间对比做差,如果用户速率换算成的时间更大,则它们的差就是需要睡眠的时间*/
get_delta_time(delta_ctx, &delta_time);/*第N-1个包实发时间*/
if (timesisset(&delta_time)) {
if (timescmp(&nap_this_time, &delta_time, >)) {/*比较实发时间和用户设置时间*/
timessub(&nap_this_time, &delta_time, &nap_this_time);
} else {
timesclear(&nap_this_time);
}
}
/*根据用户指定速率算出睡眠时间后,别忘了还需要通过adjuster进行微调*/
if (timesisset(&adjuster)) {
if (timescmp(&nap_this_time, &adjuster, >)) {
timessub(&nap_this_time, &adjuster, &nap_this_time);
} else {
timesclear(&nap_this_time);
}
}
/*下面根据用户参数指定的睡眠模式进行睡眠*/
if (!timesisset(&nap_this_time)) return; /* nap_this_time = {0, 0} 不睡眠,直接返回*/
switch (accurate) { /* 否则,根据accurate 进行睡眠 */
#ifdef HAVE_SELECT
case ACCURATE_SELECT:
select_sleep(nap_this_time);
break;
#endif
#ifdef HAVE_IOPERM
case ACCURATE_IOPORT:
ioport_sleep(nap_this_time);
break;
#endif
#ifdef HAVE_RDTSC
case ACCURATE_RDTSC:
rdtsc_sleep(nap_this_time);
break;
#endif
#ifdef HAVE_ABSOLUTE_TIME
case ACCURATE_ABS_TIME:
absolute_time_sleep(nap_this_time);
break;
#endif
case ACCURATE_GTOD:
gettimeofday_sleep(nap_this_time);
break;
case ACCURATE_NANOSLEEP:
nanosleep_sleep(nap_this_time);
break;
default:
errx(-1, "Unknown timer mode %d", accurate);
}
}
从以上实现可以看出,所谓速率控制,在实现上转换成了时间控制,为了提高时间变量操作的精确度,引入了两种级别的变量 timeval 和 timespec, 并增加了微调机制。此外,提供了多种用于睡眠的函数,以供不同操作系统下使用最合适的睡眠方法。
1.1.1 实验结果
1.1.1.1 实验1
环境:10G 发包板
Packet ID:* packet数:12921 字节数:16973900
Top speed模式,实际速率 2000 M/s 左右
设置1024 M/s, 实发速率 760 M/s 左右
设置500 M/s, 实发速率 450 M/s 左右
设置100 M/s, 实发速率 99 M/s 左右
设置50 M/s, 实发速率 49 M/s 左右
设置10 M/s, 实发速率 9.6 M/s 左右
设置5 M/s, 实发速率 4.9 M/s 左右
1.1.1.2 实验2
环境:10G 发包板
ID: * 包数:4 字节: 1930
设置1024 M/s, 实发速率 14 M/s 左右
设置500 M/s, 实发速率 14 M/s 左右
设置100 M/s, 实发速率 16.9 M/s 左右
设置50 M/s, 实发速率 13 M/s 左右
设置10 M/s, 实发速率7.5 M/s 左右
设置5 M/s, 实发速率 1.8 M/s 左右
设置1 M/s, 实发速率 1.34 M/s 左右
1.1.1.3 实验3
环境:10G 发包板
ID: * 包数:1 字节: 34
设置1024 M/s, 实发速率 0.3 M/s 左右
设置100 M/s, 实发速率 0.27 M/s 左右
设置1 M/s, 实发速率 0.32 M/s 左右
1.1.1.4 实验结果分析
从实验结果可以看出,速率控制是否准确与pcap包本身的大小有密切关系,当pcap的大小过小时,速率控制算法失效,反之,pcap包很大时,速率控制算法非常准确。
造成以上现象的原因,与时间控制变量的精度有关。由于精度过大,当pcap非常小(实验2和3相对于实验1而言)的时候,换算成的时间结果的一些关键点会被忽略,导致结果非常不一致。改进的方法可以尝试将所有时间变量改成 timespec 的情况,但这样一来又有问题,大多数睡眠的实现都只支持timeval,对于timespec精度级别的无法支持。
1.1.2 tcpreplay速率控制改进历史
1.1.2.1 1.4.* 版本的改进
1.4.beta5 版本的时候,用了nanosleep函数替代了原先的sleep函数,提高精度
1.4.2 版本的时候,用了 timerdiv 函数,在 Multi 这种模式下,算UST的时候更精确了。
上面这两次修改着力点是一样的,就是原先处理一个timeval,是 sec 和 usec 两个精度分别处理,现在先将sec换成usec的精度来算,就是变量整个精度提高了1百万。这样的结果是,包的大小比较小时,速度控制会更精确。
1.1.2.2 3.0.* 版本的改进
3.0.beta10作者在这一版本想出了一个睡眠的实现方法,据说比nanosleep更精确,叫做sleep_loop
Sleep_loop原理是,先gettimeofday获取系统时间t1,将你要睡眠的时间t2与t1相加得t,然后在一个循环里,每次循环取gettimeofday与t比较,小于t就接着循环,直到不小于t。其代价是CPU使用率更高(事实上,在睡眠的时候,CPU会达到100)
1.1.2.3 3.3.* 版本的改进
3.3.0 比较大的改动:一是使得时间变量的精度升高,从usec提高1000倍到了nsec,
二是睡眠函数的实现更丰富,针对不同的操作系统使用不同的睡眠函数。
时间变量精度提高使得在处理小包的时候当然更精确,附带问题是给睡眠造成难题,因为不是所有睡眠实现都支持这么高精度的时间。为了解决这个问题,作者设计了几个调整函数adjust,作用大体说来是将nsec调整为usec,比如,nsec>500,就直接在usec+1,小于500,则usec-1;另外,对于Pkts的情况,提出了另外一种类似的调整。
此外,针对不同OS提供不同的睡眠实现,可以让更多用户获得好的体验,无论用户使用什么系统。
1.1.3 发现该算法的一个问题
在实现速率控制Mbps时,tcpreplay 实际上是用第 N+1 个包来调整第 N 个包。假设前面N-1个包已经发送完,在发送第N个包前,根据算法,会根据 len(N) 算出一个时间t1,这个t1会加在N-2个包实际使用时间T上,假设第N-1个包的实际发送时间为t2,这样,发第N个包前,就有两个时间,一个是 T+t1,一个是T+t2,后者是前面N-1个包发送的时间,前者是根据Mbps设置前面N-1个包应该发送的时间,如果 t1>t2,就要通过睡眠 (t1-t2)来使得前面 N-1 个包的发送时间等于T+t1,也就是达到用户设定时间,然后,发送第N个包,在发送第N+1个包前又要调整前面N个包的发送时间,这时候运行调整时间的包是第N+1个包,也就是说,tcpreplay总是用下一个包的长度来调整前一个包的时间(仅限于Mbps模式)。
这有什么影响呢?目前的分析结果是:tcpreplay的速率控制算法,也即时间调整算法,其实是一个逐包调整的过程,相当于每一个包都会在整体调整中贡献力量(除了第一个包)。所以,上述问题的关键是第一个包与其余包的对比情况,如果第一个包远远大于其他的包,很可能导致实际速率大于设定的速率。反之,影响不大