1. 程式人生 > >OpenVPN效能-OpenVPN的第一個瓶頸在tun驅動

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加解密》的總結。

           

給我老師的人工智慧教程打call!
http://blog.csdn.net/jiangjunshow

這裡寫圖片描述