1. 程式人生 > >OpenVPN效能-多OpenVPN共享一個虛擬網絡卡

OpenVPN效能-多OpenVPN共享一個虛擬網絡卡

                OpenVPN是一個很難評價的東西,我可以說出超過10個它的好處,併發出一些感慨。然而針對每一個優點,它卻又有些美中不足,然而這又有什麼大不了的呢,夠用就可以了吧!不過,如果你遇到了一些場合,它真的就不夠用了怎麼辦?答案只有一個,改掉它!
     OpenVPN架設方便,配置簡單,安全策略豐富,這些都很好,然而卻不適合大型的網到網的拓撲,那種應用大家都用高階的專用裝置了比如Cisco或者華為的VPN箱子。對於一個技術偏執狂來講,在linux或者BSD上能完成這樣的小盒子也算是對自己的一種鼓舞。本文介紹一個自己對tun網絡卡的小修改,使得多個OpenVPN程序可以負載均衡,分擔大流量。討論基於Linux,核心版本2.6.8

0.問題出在哪

OpenVPN的效能問題出在哪呢?前面的《OpenVPN效能》系列文章深入的探討了這個問題,本文著重分析這一篇(《OpenVPN效能-OpenVPN的第一個瓶頸在tun驅動》)談到的tun驅動。
     Linux下的tun驅動很簡單,多個使用者態程序無法共享一個虛擬網絡卡,猛擡頭,這是很不合理的,你見過物理網絡卡是程序獨佔的麼?肯定沒有!然而低頭沉思後,發現這個對比不合理,物理網絡卡在程序的下端,而tun虛擬網絡卡則在使用者程序上端,對於虛擬網絡卡而言,使用者態程序就是一根線纜,姑且叫做程序線纜吧。

1.TUN網絡卡怎麼啦

要理解一個技術以及評價它,不要認為只瞭解它的原理並且會使用就夠了,更重要的要從歷史的角度去考慮,這樣你就會明白設計者當初為何這麼搞,其實最終很多技術純粹是瞎搞。舉個例子,科班出身的,沒有誰不知道Intel處理器的分段,背得滾瓜爛熟的大有人在,可是這個技術在純技術方面就是一種瞎搞,只是為了產品的相容!
     那麼TUN網絡卡怎麼啦?通過閱讀Linux核心的文件Documentation/networking/tuntap.txt,我們看到:
Universal TUN/TAP device driver.
Copyright (C) 1999-2000 Maxim Krasnyansky <[email protected]
>
    
Linux, Solaris drivers
Copyright (C) 1999-2000 Maxim Krasnyansky <[email protected]>
    
FreeBSD TAP driver
Copyright (c) 1999-2000 Maksim Yevmenkin <[email protected]>
通用的tun驅動設計和其Linux平臺以及Solaris平臺的設計是同一人,當初在這些開放平臺上設計tun驅動的初衷是將資料包匯入匯出到使用者態,這個理念一定要徹底理解才行,不要將tun/tap網絡卡和那些其它的虛擬網絡卡相混淆,這些其它的虛擬網絡卡包括FreeSWAN使用的隧道口,以及Cisco的VTI,注意,TUN僅僅為了將資料包匯入匯出到使用者態。
     TUN驅動完全模擬一個點對點的或者乙太網的網絡卡,一個例項僅僅模擬一個這樣的網絡卡,這是很合理的,因為對於點對點網絡卡而言,比如HDLC介面卡,和對端僅僅通過一根線纜連線,如果一個例項可以模擬任意多個點對點網絡卡,就需要對端也模擬同樣多個,而我們不希望有這種額外的耦合;對於乙太網卡的模擬,雖然一個例項一個,仍然可以通過配置將多個例項的tap網絡卡bonding在一起,bonding驅動在linux中實現的很好,專門針對乙太網,而且配置簡單,除了這個,我們還可以通過配置將多個tap網絡卡bridge在一起,bridge驅動在linux中實現的很好,而且配置簡單。
     因此,目前為止,TUN驅動沒有錯。

2.OpenVPN又怎麼啦

從第0小節可知,此時OpenVPN只是一根線纜,而不是傳統的寫入物理網絡卡資料的程序,它單程序,大迴圈,結構超原始。一個網絡卡口只能插一根線啊,那麼也就是隻能有一個OpenVPN程序,如果這樣,難道沒有辦法了麼?在寫前面那篇文章(http://blog.csdn.net/dog250/article/details/6300347)時,我很失望,於是我改了tun驅動,增加了吞吐量。現在想想,突然又有了新的想法,雖然我們不能在一個虛擬網絡卡上插兩根程序線纜,但是我們可以加粗它啊,不怪路太少,只怪線太細。
     於是,新的辦法就來了,多個OpenVPN共享一個虛擬網絡卡。可是問題又來了,這需要修改tun驅動,因為原生的tun是獨佔性的,不過這個倒不難。於是問題就僅僅成為將OpenVPN改成多程序的了,為何不改成多執行緒的呢,因為怕遇到全域性變數和併發問題...改成多程序的方式很簡單,bash指令碼即可做到,啟動多個OpenVPN程序,使用同一個dev引數即可。
     因此,目前,OpenVPN本身的架構出了點問題。

3.它們結合呢

本小節暫且不談共享虛擬網絡卡的問題,僅僅說一下單個的OpenVPN和單路的虛擬網絡卡結合會有什麼效能瓶頸。
     這個效能瓶頸體現在幾個排隊上:首先,進入OpenVPN的socket資料會排在一個單一的佇列上上,由於OpenVPN單程序大迴圈,造成即使你有X個cpu,Intel82576千兆卡,在大資料量下也會出現只有一個cpu忙碌,網絡卡利用率超低的現象;其次,由於tun驅動只從單一的OpenVPN程序收取和傳送封裝後的IP資料報或者以太幀,進入tun/tap網絡卡的流量限於OpenVPN的流進tun字元裝置的速率,十分有限,對於從tun/tap發出的IP資料報或者以太幀,由於單個的OpenVPN的socket傳送能力有限,和tun/tap網絡卡forward對接的是一個高效能千兆卡,也毫無用武之地!如下圖:

     單一的CPU超級忙碌,其它CPU無法分擔,超棒的千兆卡閒在那裡...這簡直是憤怒爆發的千兆!其實Intel千兆卡的收發晶片處理能力要比CPU高的,因為每塊卡要應對所有的CPU的併發讀寫,有時甚至需要自己去記憶體收攬資料...如此的高僧,閒著可惜!唯一快累死的就是那唯一不幸的cpu,OpenVPN此時在它之上執行!

4.改掉TUN驅動

我們看看改了TUN驅動會發生什麼。改是肯定能改的,問題是怎麼改,前天受老大又一番教誨:“時刻寫出可重用的程式碼”,我決定不再往程式碼中隨意加入printk("dsdsffgerg####################:------------%s\n",ptr);之類的啦,我決定一開始就將它做成可配置的獨佔策略,也就是說完全相容現有的獨佔策略,具體實現看第7小節吧。現在還不是實現的時候,現在只是說一下解決了哪些效能問題。我先給出圖示,然後再分析:

多個OpenVPN共享一個虛擬網絡卡之後,多個OpenVPN可以執行在多個CPU上,並且都可以從單一的虛擬網絡卡收發資料,虛擬網絡卡字元裝置的排隊延遲現象減輕了,吞吐量更高,由於出現了多個OpenVPN程序,充分利用的多個CPU可以向Intel82576千兆卡同時傳送大量的資料,網絡卡的利用率增加了,相信Intel的乙太網晶片吧,它同時還會減輕單個OpenVPN程序的socket排隊現象。
     此時的整臺機器執行的比較和諧,沒有閒著的,既然都很忙,計算機體系結構的可用性以及能量守恆定律會保證資料包的轉發率以及吞吐量的增加。以上的結論是實際測試的結果,採用這種方式後,檢視伺服器端的日誌,會發現負載分擔給了多個OpenVPN程序,這正是想要的結果。如果能配合CPU繫結,配置中斷繫結,那麼多CPU的伺服器就能作為多臺機器使用了,實驗得真知!
     對於一個需要forward到虛擬網絡卡的資料包,最終到達了tun的字元裝置,在linux驅動中tun_net_xmit函式中(每一個網絡卡都會有一個hard_xmit回撥函式)注意以下這一句:
if (tun->flags & TUN_FASYNC)    kill_fasync(&tun->fasync, SIGIO, POLL_IN);wake_up_interruptible(&tun->read_wait);return 0;

如果我們使用了OpenVPN共享一個虛擬網絡卡,那麼在tun->read_wait上睡眠的就可能不止一個程序,可能是很多程序,對於效能比較好的CPU和網絡卡而言,會出現有多少CPU喚醒多少OpenVPN程序的壯觀現象。

5.OpenVPN和TUN的多對多結合

在列出真的實現之前,先考慮另一種替代方案,這種方案不需要修改任何東西,那就是執行多個OpenVPN例項,然後將它們的tap網絡卡bonding在一起,注意這種方案使用了bonding驅動,而bonding驅動只適用於乙太網場合,這就意味著你不能使用tun網絡卡了,而tun網絡卡不需要乙太網封裝,沒有arp等鏈路層地址解析,效率更高些,這就產生了一個矛盾,然而也能帶來一些效能提升,具體操作可參見《OpenVPN效能-當tap遇到bonding》。
     最終,這種方案的應用場合有限,並且bonding驅動本身就能帶來一些開銷。我倒是覺得,如果需要bridge VPN,將bonding和bridge結合是一種很好的方式。

6.OpenVPN和TUN的多對一結合

這個就不多說了,就是本文最終推薦的方式,你需要做的僅僅是修改下tun驅動。但是如果你使用Windows客戶端且僅有一個客戶端的話,修改Window的TAP-win32也是必要的,否則只能帶來OpenVPN伺服器的最大連線數的增加。然則本人的DDK環境壞掉了,也不想面對頻繁的藍屏,因此也就不給出windows的驅動程式碼了,說實話,這個程式碼我還沒有完成!

7.第6小節的實現

實現是評估後的最後的步驟,其實也是相對簡單的步驟,對於linux平臺的tun驅動的修改,再也沒有比這個更簡單的了,本文基於kernel 2.6.8,因為這個版本簡單,可以最快速度做實驗,對於高版本修改思想類似。首先修改linux/if_tun.h標頭檔案:
struct tun_struct {    struct list_head        list;    ...    int    users;        //增加引用計數#define    IFF_MULTI    0x08     //增加一個flags,指示是否獨佔方式,1為共享,2為獨佔};

然後修改driver/net/tun.c吧
第一步:修改ioctl函式tun_chr_ioctl。增加一個命令,設定或者清除IFF_MULTI位。
第二步:修改tun_set_iff函式:
tun = tun_get_by_name(ifr->ifr_name);if (tun) {    if (tun->attached)        return -EBUSY;    /* Check permissions */    if (tun->owner != -1 &&        current->euid != tun->owner && !capable(CAP_NET_ADMIN))        return -EPERM;}

將之修改為:
tun = tun_get_by_name(ifr->ifr_name);if (tun) {    if (tun->flags & IFF_MULTI) {  //即使找到了現已經存在裝置,如果設定了共享,那麼直接使用它        file->private_data = tun;        tun->attached = 1;        tun->users++;     //增加引用計數。原則上應該使用atomic_inc        strcpy(ifr->ifr_name, tun->dev->name);        return 0;    }    if (tun->attached)        return -EBUSY;        /* Check permissions */    if (tun->owner != -1 &&        current->euid != tun->owner && !capable(CAP_NET_ADMIN))        return -EPERM;} 

在第一次建立裝置時需要遞增users計數:
    file->private_data = tun;        tun->attached = 1;    strcpy(ifr->ifr_name, tun->dev->name);    return 0;err_free_dev:    free_netdev(dev);

修改為:
    file->private_data = tun;    tun->attached = 1;    tun->users++;    strcpy(ifr->ifr_name, tun->dev->name);    return 0;err_free_dev:    free_netdev(dev);

第三步:修改tun_chr_close函式:
/* Detach from net device */file->private_data = NULL;tun->attached = 0;/* Drop read queue */skb_queue_purge(&tun->readq);if (!(tun->flags & TUN_PERSIST)) {    list_del(&tun->list);    unregister_netdevice(tun->dev);}

修改為:
tun->users--;  //遞減引用計數,原則上該使用atomic_decif(!tun->users) {    /* Detach from net device */    file->private_data = NULL;    tun->attached = 0;    /* Drop read queue */    skb_queue_purge(&tun->readq);    if (!(tun->flags & TUN_PERSIST)) {        list_del(&tun->list);        unregister_netdevice(tun->dev);    }}

修改完畢,重新載入tun.ko即可看到效果。
     實驗很簡單,在伺服器端和客戶端分別準備N個配置檔案,伺服器端的N個配置檔案除了埠需要不同外其它配置一律一樣,客戶端的N個配置檔案除了remote引數中的埠分別對應於伺服器的服務埠外其它完全一樣,注意都需要指定一個特定的虛擬網絡卡,比如dev tap0,然後啟動它們,馬上見效果。
8.總結於展望
OpenVPN很不錯,但是我還是覺得它最大的好處在於可以隨心定製。最近在OpenVPN社群也沒有發現其有開發多執行緒版本的跡象,不知道是不是受tun驅動限制,但是不管怎麼說,在其現有的原始碼中,存在一個函式:
voidtunnel_server_udp (struct context *top){  tunnel_server_udp_single_threaded (top);}

從它的名子來看,thread也是可以期待的。我們沒有必要自己實現thread,而只是等待即可,但是如果你有更好的主意,千萬別獨吞。現如今,通過外部指令碼去自動化生成不同的配置檔案以及啟動不同的OpenVPN例項是最簡單的方式。