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

     

     

  • 相关阅读:
    函数的声明
    数组遍历的方法
    运算符
    变量命名规则
    js条件判断
    vuex使用
    高盛伦敦分部面试
    野村证券伦敦分部面试
    Linux Performance Monitoring Commands
    Linux server上too many open files问题
  • 原文地址:https://www.cnblogs.com/jiayy/p/3447047.html
Copyright © 2011-2022 走看看