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的速率控制算法,也即時間調整算法,其實是一個逐包調整的過程,相當於每一個包都會在整體調整中貢獻力量(除了第一個包)。所以,上述問題的關鍵是第一個包與其余包的對比情況,如果第一個包遠遠大於其他的包,很可能導致實際速率大於設定的速率。反之,影響不大

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM