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