OpenVPN效能-OpenVPN的第一個瓶頸在tun驅動
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
OpenVPN的第一個瓶頸在於tun字元裝置是按照一個一個鏈路層幀的讀取和寫入的,使用者態的OpenVPN程序之所以要兩端的link-mtu一致,是因為每次OpenVPN從/dev/net/tun字元裝置讀取的是一個完整的以太幀,不多也不少,而庫介面: ssize_t read(int fd, void *buf, size_t count);中的count則是啟動OpenVPN時設定的mtu值,如果OpenVPN收發兩端的mtu值設定的不同,比如分別為2000和5000,且tun裝置的ifconfig值也不同,分別為3000和5000,由於傳送端每次從tun網絡卡傳送5000位元組,且傳送端的OpenVPN從字元裝置中也讀5000位元組,這是一個鏈路層幀的長度,然而接收端只接收2000,餘下的3000位元組將截斷,會出錯。這種情況在OpenVPN做forward的情況下很難出現,因為傳送端tun網絡卡的資料是從真實網絡卡forward過來的,真實網絡卡一般的mtu都是1500。
我們知道了,對於OpenVPN而言,兩端的mtu最好設定成一致。在我們不太可能更改OpenVPN前後的真實網路的mtu的情況下,基本上tun都是按照1500左右的資料收發的,雖然完全模擬了網路的行為,然而卻和真實的網路環境有著根本的不同,在真實的環境下,資料是直接發往網線的,這基本上是線速度,然而在OpenVPN的環境下,資料通過tun字元裝置每次一幀被髮往了使用者態的OpenVPN程序,然後OpenVPN將資料加密後又通過socket每次一幀通過物理網絡卡發往對端,這個使用者態和核心態的切換就是瓶頸,且還是每次一幀的處理,速率肯定很慢,如果我們能在tun字元裝置將資料做緩衝,也就是每次將N個幀一起發往OpenVPN,然後每次N個幀發往對端,使用者-核心態的切換額外開銷被緩衝抵消,在OpenVPN的socket批量往對端送資料的時候,傳送端只是在以大致相同的速率往tun字元裝置的佇列中填充資料,OpenVPN每次N幀的收發資料。
舉個例子:火車運貨並且需要在一個站點中轉,一般的中轉只是將貨物從一個火車搬到另一個火車,然後運走,這是很快的,然而OpenVPN的方式是將貨物從一列火車搬出火車站,經過有關部門的檢測和封裝後再進入火車站,搬入另一個火車,如果按照貨物一件一件的這麼處理(比如站臺上沒有積壓貨物的空間),肯定很慢,一般的解決方式是將貨物在火車站先積壓到一個總的數量,然後統一運出火車站,處理後統一運入火車站運走,在前一批接受處理的時候,第二批正在被積壓,沒有什麼察覺,等到第一批運達了對端,第二批被取走,接受處理,依次類推...關鍵是貨物運出車站-接受處理-運進車站這三個環節消耗太大,消耗在於路上的時間和處理的時間,如果每次處理很多貨物,路上的時間和處理時間和總貨物的比值就會減小,效率和吞吐量增加,正如我們不可能用萬噸貨輪每次只運送一個皮箱的原因一樣。
我們先實現一個簡單的例子,脫離OpenVPN,目的是證明這種“積壓”的方式有利於吞吐量和速率的提升。實際上沒有必要在tun字元裝置的read/write介面上也按照一個鏈路幀的大小就行io,因為只要skb出了start_xmit函式之後就進入“物理層”了,對於tun而言,物理層就是字元裝置和使用者態read/write這個字元裝置的程式,物理鏈路如何傳輸就很自由了,因此tun字元裝置這個物理層完全可以批量傳輸,只要保證到對端寫入tun字元裝置後,鏈路層幀是一個接一個寫入tun虛擬網絡卡就可以了。read/write作用於tun字元裝置和socket,都是系統呼叫,系統呼叫的開銷比較大且每次系統呼叫的開銷一定,和操作引數無關,因此需要一種類似mmap的方式將系統呼叫的次數減少,這樣可以增加吞吐量。因此需要針對tun驅動進行相關的修改,不再每次read一個鏈路層幀,而是儘可能的積累。實際上pci總線上的網絡卡的tso(tcp segment offload)也是應用了類似的機制減少對pci匯流排的訪問次數的,因為訪問匯流排開銷很大,如果tcp被分成了N個小段,那麼每傳送一個段都要訪問pci匯流排,需要訪問N次,於是將一個很大的tcp段直接發往網絡卡,由網絡卡來分段,這樣訪問一次pci匯流排就夠了。理論上分層模型很好,但是那是針對理解問題的,實際應有的時候,不得不破壞這一模型,讓網絡卡處理傳輸層資料從而提高效率,這又是實用主義獲勝的一個例子,正如不存在單純的cisc或者risc處理器而都是其混合體一樣,也如雙絞線勝過同軸線一樣。
以下對tun網絡卡的修改僅僅使用tun方式,而不適用於tap方式,如果想支援tap模式,也很容易,修改tun_chr_aio_write中解析包的方式即可。主要修改了兩個函式,一個是aio_read,一個是aio_write,核心版本為2.6.32.27:
static ssize_t tun_chr_aio_read(struct kiocb *iocb, struct iovec *iv,
unsigned long count, loff_t pos)
{
struct file *file = iocb->ki_filp;
struct tun_file *tfile = file->private_data;
struct tun_struct *tun = __tun_get(tfile);
DECLARE_WAITQUEUE(wait, current);
struct sk_buff *skb;
ssize_t len, ret = 0;
char __user *buf = iv->iov_base; //取出使用者態buf
int len1 = iv->iov_len; //取出使用者態buf的長度
int to2 = 0;
int getone = 0;
int result = 0;
if (!tun)
return -EBADFD;
len = iov_length(iv, count);
if (len < 0) {
ret = -EINVAL;
goto out;
}
add_wait_queue(&tun->socket.wait, &wait);
while (len1 > 0) {
current->state = TASK_INTERRUPTIBLE;
if (len1 - dev->mtu < 0) break; //如果剩餘的空間不足以容納一個skb,則返回
if (!(skb=skb_dequeue(&tun->socket.sk->sk_receive_queue))&& !getone) { //起碼要返回一個包
if (file->f_flags & O_NONBLOCK) {
ret = -EAGAIN;
break;
}
if (signal_pending(current)) {
ret = -ERESTARTSYS;
break;
}
if (tun->dev->reg_state != NETREG_REGISTERED) {
ret = -EIO;
break;
}
/* Nothing to read, let's sleep */
schedule();
continue;
} else if (skb == NULL && getone){ //只要複製了一個skb,skb為NULL說明取空了佇列
break;
}
netif_wake_queue(tun->dev);
iv->iov_base = buf; //將當前的buf指標和長度賦值給iov
iv->iov_len = dev->mtu;
ret = tun_put_user(tun, skb, iv, dev->mtu); //取一個skb
kfree_skb(skb);
result += ret; //推進總長度
buf += ret; //推進使用者態緩衝區
len1 -= ret; //減少剩餘長度
getone += 1; //增加統計資料
//最後不要break;
}
current->state = TASK_RUNNING;
remove_wait_queue(&tun->socket.wait, &wait);
out:
tun_put(tun);
return result;
}
static ssize_t tun_chr_aio_write(struct kiocb *iocb, const struct iovec *iv,
unsigned long count, loff_t pos)
{
struct file *file = iocb->ki_filp;
struct tun_struct *tun = tun_get(file);
ssize_t result = 0;
char __user *buf = iv->iov_base;
int len = iv->iov_len;
int i = 0;
struct iovec iv2;
int ret = 0;
if (!tun)
return -EBADFD;
while(len>0) {
uint8_t hi = (uint8_t)*(buf+2); //由於只用於tun裝置,因此使用者態寫入字元裝置的資料是一個完整的ip資料包或者多個順序連線在一起的ip資料包,這裡通過ip協議頭的格式將多個可能的ip資料包解析出來。ip協議頭的第三個和第四個位元組表示總長度欄位,這裡先取出第三個位元組
uint8_t lo = (uint8_t)*(buf+3); //再取出第四個位元組
uint16_t tl = (hi<<8) + lo; //拼裝成16位的總長度
iv2.iov_base = buf;
iv2.iov_len = tl; //總長賦值給iov長度,後面按照這個長度來分配skb
ret = tun_get_user(tun, &iv2, iov_length(&iv2, 1),
file->f_flags & O_NONBLOCK);
result += ret; //推進寫入總長
buf += ret; //推進寫入緩衝區
len -= ret; //減少剩餘緩衝區
}
tun_put(tun);
return result;
}
使用者態的程式可以選擇透明直傳,也可以選擇解析修改,後者更適合管理的需要,就像OpenVPN那樣,在使用者態解析出原始IP資料包的資訊。此處的測試使用的是一個simpletun程式,IO框架如下:
while(1) {
int ret;
fd_set rd_set;
FD_ZERO(&rd_set);
FD_SET(tap_fd, &rd_set);
FD_SET(net_fd, &rd_set);
ret = select(100 + 1, &rd_set, NULL, NULL, NULL);
if (ret < 0 && errno == EINTR){
continue;
}
if (ret < 0) {
perror("select()");
exit(1);
}
if(FD_ISSET(tap_fd, &rd_set)) {
nread = cread(tap_fd, buffer, BUFSIZE);
plength = htons(nread);
nwrite = cwrite(net_fd, (char *)&plength, sizeof(plength));
nwrite = cwrite(net_fd, buffer, nread);
}
if(FD_ISSET(net_fd, &rd_set)) {
nread = read_n(net_fd, (char *)&plength, sizeof(plength));
if(nread == 0) {
break;
}
nread = read_n(net_fd, buffer, ntohs(plength));
nwrite = cwrite(tap_fd, buffer, nread);
}
}
客戶端和伺服器選擇BUFSIZE為:
#define BUFSIZE 1500*4
測試命令:ab -k -c 8 -n 500 http://10.0.188.139/5m.html
機器部署:
S0:
eth0:192.168.188.194 mtu 1500 e1000e 1000baseT-FD flow-control
tun0:172.16.0.2 mtu 1500
route:10.0.188.139 dev tun0
S1:
eth0:192.168.188.193 mtu 1500 e1000e 1000baseT-FD flow-control
eth1:10.0.188.193 mtu 1500 e1000e 1000baseT-FD flow-control
tun0:172.16.0.1 mtu 1500
S2:
eth1:10.0.188.139 mtu 1500 e1000e 1000baseT-FD flow-control
route:172.16.0.0 gw 10.0.188.193
測試資料:
使用修改後的tun驅動:
Transfer rate: 111139.88 [Kbytes/sec] received
如果使用原生的tun驅動測試,資料:
Transfer rate: 102512.37 [Kbytes/sec] received
如果不走tun虛擬網絡卡,物理網絡卡通過ip_forward裸跑速率:
Transfer rate: 114089.42 [Kbytes/sec] received
影響:
1.如果tun網絡卡載荷是tcp:
增加了總的吞吐量的同時,也增加了單個tcp包的延遲,因此會影響tcp視窗的滑動,給收發兩端造成“路途很遠”的假象,從而調整rtt。
2.如果tun網絡卡載荷是udp:
udp本來就更好支援實時,對丟包倒是不很在意,修改後的tun網絡卡增加了單包延遲,實時性沒有以往好。
3.引數關聯性:
有幾個引數比較重要,第一是使用者態的緩衝區大小,第二是tun網絡卡的傳送佇列長度,第三是物理網絡卡的mtu,這三個引數如何配合以獲取馬鞍面上的最佳位置(吞吐量和延遲的最佳權衡點),仍需要測試。
更猛的效果:
以上測試並沒有體現出修改了tun驅動所帶來的革命性速率提升,因此修改OpenVPN程式碼的開銷開來更大且不值得,那是因為這個simpletun是直接轉發的,也就是從tun字元裝置來了資料就直接發往socket,從socket收到資料直接發往tun字元裝置,這樣的話積累效應是體現不出來的,如果資料在simpletun應用程式中經歷了一些比較耗時的操作,更猛的效果就有了,這是下篇《OpenVPN效能-OpenVPN的第二個瓶頸在ssl加解密》的總結。