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的速率控制算法,也即时间调整算法,其实是一个逐包调整的过程,相当于每一个包都会在整体调整中贡献力量(除了第一个包)。所以,上述问题的关键是第一个包与其余包的对比情况,如果第一个包远远大于其他的包,很可能导致实际速率大于设定的速率。反之,影响不大

 

 

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注