1. 程式人生 > >動手寫一個OpenVPN的wrapper來優化OpenVPN效能

動手寫一個OpenVPN的wrapper來優化OpenVPN效能

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow

也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!

                OpenVPN,一個讓人想說愛你不容易的VPN,曾經耗費了我大量精力的VPN,其效能,...最終還是不咋地!以下是一個大致的統計資料:
純千兆環境,4核心至強3.0GHZ處理器,OpenVPN使用BF-CBC加密,SHA1摘要,OpenVPN不繫結特定CPU,頻寬可達20-30MB/s;

純千兆環境,4核心至強3.0GHZ處理器,OpenVPN不加密,不摘要,OpenVPN不繫結特定CPU,頻寬可達40-45MB/s;
純千兆環境,4核心至強3.0GHZ處理器,OpenVPN不加密,不摘要,OpenVPN繫結特定CPU,頻寬可達45-55MB/s;
純千兆環境,4核心至強3.0GHZ處理器,OpenVPN使用BF-CBC加密,不摘要,OpenVPN繫結特定CPU,頻寬可達35-40MB/s;
純千兆環境,繫結OpenVPN到特定CPU,該CPU會跑滿,頻寬無法提升受制於CPU;
純千兆環境,不繫結OpenVPN到特定CPU,沒有一個CPU跑滿,頻寬無法提升受制於OpenVPN的單程序模型以及作業系統多處理器排程開銷;

百兆環境,無論如何,OpenVPN的加密隧道頻寬基本接近物理網絡卡的百兆頻寬。

既然是OpenVPN的軟體模型遠遠跟不上硬體的提升,那麼就要想辦法以量取勝,辦法是執行多個OpenVPN程序,每一個CPU上繫結一個,通過Linux的核心介面可以很容易做到這一點:
1.首先mount一個cpuset,建立3個set
mount -t cpuset none /mntmkdir /mnt/{vpn1,vpn2,vpn3}
2.簡單配置一個cpuset
echo 0 > /mnt/vpn1/cpusecho 0 > /mnt/vpn1/memsecho 1 > /mnt/vpn2/cpusecho 0 > /mnt/vpn2/memsecho 2 > /mnt/vpn3/cpusecho 0 > /mnt/vpn3/mems
3.以不同埠號和虛擬網段啟動3個不同的OpenVPN程序
openvpn --config cfg1 --port 1234 --server 121.121.0.0 255.255.0.0openvpn --config cfg2 --port 2234 --server 122.122.0.0 255.255.0.0openvpn --config cfg3 --port 3234 --server 123.123.0.0 255.255.0.0
4.繫結上述3個openvpn程序到不同的cpuset
i=1;for pid in $(ps -e|grep openvpn|awk -F ' ' '{print $1}');do echo $pid > /mnt/vpn$i/tasks; ((i=i+1)); done
5.將多個客戶端連線在這不同的三個OpenVPN程序上,拉取大檔案,測試OpenVPN伺服器的tap0-tap2的總流量
經過上述配置後,新的統計資料如下:
純千兆環境,4核心至強3.0GHZ處理器,OpenVPN使用BF-CBC加密,SHA1摘要,頻寬可達35-40MB/s;
純千兆環境,4核心至強3.0GHZ處理器,OpenVPN不加密,不摘要,頻寬可達75-80MB/s;
純千兆環境,4核心至強3.0GHZ處理器,OpenVPN使用BF-CBC加密,不摘要,頻寬可達50-55MB/s;
純千兆環境,3個CPU會跑滿,頻寬無法再提升受制於CPU;
純千兆環境,將物理網絡卡中斷(同時也是協議棧接收處理的軟中斷)釘在第四個CPU上,上述值再有5左右的提升。

可見,多個處理程序會帶來頻寬的大幅提升。實際上上述的方案完全可以通過虛擬網絡卡bondding或者多VPN程序共享虛擬網絡卡的方式做到,然而這些配置都不簡單,有時還要修改虛擬網絡卡的驅動程式,可擴充套件性很不好,因此簡單的以多OpenVPN例項來說明問題是再好不過的了,如需bonding方案或者shared-tap方案,請參考《 關於OpenVPN文章的目錄》。
        雖然上述方案解決了部分效能問題,然而配置還是比較複雜,能不能通過一個wrapper將上述的操作包裝起來呢?既能做到多例項並行-繫結特定CPU,又能做到OpenVPN客戶端動態自動獲取OpenVPN伺服器埠號和虛擬網段。這需要增加一個層次,就是在OpenVPN連線之前增加一個“埠/虛擬網段”協商的層。該包裝器的使用如下:
伺服器端:openvpn-wrapper -t server -p 1111 -v /usr/sbin/openvpn -c /home/zy/cfg ....
客戶端:openvpn-wrapper -t client -p 1111 -a 192.168.1.23 -v /usr/sbin/openvpn -c /home/zy/cfg-client
引數意義如下:
-t:型別,分為server和client,和openvpn一樣
-p:wrapper伺服器監聽的TCP埠號
-v:openvpn的路徑
-c:openvpn配置檔案的路徑,該檔案中沒有那些自動協商出來的配置,比如openvpn的埠,虛擬網段等等。

openvpn-wrapper的程式碼結構如下:
struct vpn_opt {    char vpn_path[255]; //openvpn的路徑    char cfg_path[255]; //openvpn配置檔案路徑,配置檔案不含server指令和port指令    int port_base;      //起始埠號    int num_process;    //openvpn程序數量    int num_cpu;        //CPU數量    int sintr_cpu;      //單獨分配的處理物理網絡卡軟中斷的CPU};struct vpn_opt vopt;int vpn_servers(){    int pid;    int ps = vopt.num_process;    //建立ps個openvpn伺服器程序    while (ps--) {        pid = fork();        if (pid == 0) {            //每一個openvpn伺服器程序繫結在一個CPU上            sched_setaffinity(getpid(), ..., ps%vopt.num_cpu);            execve(vopt.vpn_path --config vopt.cfg_path --port vopt.port_base+ps --server $不同虛擬網段...);        } else if (pid > 0) {        } else {        }    }} int accept_clients(){    //建立TCP伺服器,接收client請求,回送埠資訊    //埠排程演算法:    //1.輪轉排程:N個客戶端按照先後順序在vopt.num_process個程序之間輪轉    //2.空閒優先:紀錄M個VPN程序之間的負載情況或client連線數,取最小值    //3.其它的演算法...    //對應客戶端的wrapper先連線這個TCP server,得到VPN埠資訊後呼叫exec啟動    //openvpn客戶端程序。int main(int argc, char **argv){    char c;    while ((c = getopt (argc, argv, "")) != -1)        switch (c)        {        case 'v':            strcpy(vopt.vpn_path, optarg);            break;        case 'c':            strcpy(vopt.cfg_path, optarg);            break;        //可供配置的其它引數,比如CPU數量,中斷處理均衡等        default:            abort ();    }    //下述引數必須由命令列提供:    //vopt.port_base = 61195;    //vopt.num_process = 2;    //vopt.num_cpu = 2;    vpn_servers();    accept_clients();}
有了這個wrapper,就可以將之直接替代openvpn了,但是對於大網對大網的拓撲,如此混亂的虛擬網段讓客戶端怎麼管理呢?這可以通過將所有的配置全部集中在OpenVPN伺服器端來解決,客戶端需要怎麼配置全由伺服器端來推送,涉及到自定義資訊的推送,請參考push setenv-safe這個指令。如果涉及到全網互通的路由配置,你就不能單靠OpenVPN的client-to-client了,還要在不同的虛擬子網之間配置路由,怎麼辦呢?還記得ICMP Redirect嗎?統一配置預設閘道器(注意單加物理網段互通的路由)該虛擬網段的OpenVPN伺服器的虛擬IP,如果目標屬於同一個OpenVPN例項管轄,那麼tap模式下OpenVPN伺服器會直接傳送ICMP Redirect,如果不是由同一個OpenVPN例項管轄,那麼確保OpenVPN伺服器上擁有管轄目標網段的OpenVPN例項即可,一切都可以靠單點路由配置搞定。
        總之一,將配置集中於一個點,最終由同一個人來配置,這樣最不容易引起混亂,剩下的全部由機器來做。能推送下去的儘量推送下去,做到單點配置。OpenVPN提供了豐富的可推送的配置,實在滿足不了的可以使用setenv-safe這個萬能鑰匙。
       總之二,優化無極限,如果你單看多個OpenVPN例項執行帶來了效能提升就沾沾自喜了,那麼你就會錯過繫結單個例項到一個CPU上帶來的進一步效能提升,如果你有幸看到了這一點,不要停步,看看top輸出,你會發現軟中斷可能在和OpenVPN搶奪CPU,因此你會進入/proc/interrupts看個究竟,於是你可以通過設定/proc/irq/smp_affinity檔案來分離中斷,突然,你看到了以下資訊:
  95:   18250769     193538      28997      45831          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-1
  96:       4115          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-2
  97:   52535493          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-3
  98:   75459635          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-4
  99:       4115          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-5
 100:   44074216          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-6
 101:   20545603          0          0          0          0          0          0          0  IR-PCI-MSI-edge      eth3-TxRx-7
於是你不得不拿起千兆乙太網卡的手冊看看多佇列相關的內容。這樣完了嗎?虛擬網絡卡頻寬之和已經接近物理網絡卡的千兆帶寬了--98MB/s,不要止步,記得Linux有一個renice命令,提升一下OpenVPN的優先順序試一下...105MB/s。我想還可以更好,只是時間來不及了...出事了!
        如果你只是看到了這,那麼你可能錯過了一件大事。重新看上面的eth3-TxRx-1資訊,然後思考結果。當我將每一個OpenVPN綁定於特定的不同CPU之後,所有的網絡卡TxRx佇列卻還只是由一個CPU處理,這就形成了一個多對一的關係,考慮如下的拓撲:

伺服器端每一個VPN程序由一個單獨的CPU來處理,它們很顯然都會將包發往網絡卡ETH3,而該網絡卡ETH3的中斷卻只由一個CPU來處理,不管是傳送中斷還是接收中斷。能不能均分一下任務呢?在均分之前,先要想一個方案。我有8個CPU核心,分別處於兩個封裝上,每個封裝4個核心,於是我啟動6個VPN例項,分別繫結在編號(從0開始)為1,2,3,4,5,6的CPU上,然後讓CPU0和CPU1各自處理一半的軟中斷(大多數是接收中斷的軟中斷),這是因為一個封裝內部的Cache親和性以及CPU核心親和性要比屬於不同封裝的核心好很多,因此兩個封裝中各自都有一個核心處理軟中斷,可以高效地將資料踢給綁定於同一封裝核心的VPN例項。測試下來雖然效能提升不顯著,但是畢竟有了3MB/s的提升。
        說了這麼多,所謂的OpenVPN的優化都是在OpenVPN外部進行的,要知道其本身也有很多的引數可以調節效能,比如sndbuf和rcvbuf以及txqueuelen這三個引數,如果rcvbuf和sndbuf不一致相差太多的話,會造成UDP以及ICMP的大量丟包,雖然TCP能調節自身的速率,但是當rcvbuf小到比TCP管道最細的部位還要小的時候,丟包就顯著了,比如rcvbuf是M,而TCP的慢啟動閥值為N,且N遠大於M,這就會導致TCP頻繁大量丟包,然後慢啟動,然後再丟包,再慢啟動...此時要麼調小慢啟動閥值,要麼調小TCP傳送緩衝區,對於Linux則是tcp_wmem,不過最好的辦法就是調整sndbuf和rcvbuf,將其調整為一樣大,並且適當比TCP管道更寬一些,讓TCP可以在其中自由發揮流控以及擁塞控制,而不是將OpenVPN模擬成一段及其惡劣的線路,如果你將sndbuf設定成了30000,將rcvbuf設成了1234567,那麼雖然TCP也能進入VPN隧道,但是這條隧道太惡劣了,有效速率將會很低很低。
        可是還有個小問題,為何不把中斷均分到VPN例項所在的CPU呢?這是可行的,然而必須用測試結果說話,有一點可以確定的是,如果那樣的話,雖然可能會得益於千兆網絡卡的DCA,然而VPN例項和軟中斷將會搶奪CPU,搶奪的激烈程度不僅僅受制於CPU的效能,還要受制於時間的序列性的本質,畢竟同一時刻一個CPU只能做一件事,因此有時候,你用top發現每一個CPU都沒有泡滿,然而效能卻反而因為中斷均分而下降了,值得注意的是,Cache親和性雖然很重要,但是相比訪問同一個核心的Cache的開銷,訪問同一顆封裝的Cache的開銷也不會差太多,畢竟現代超猛的多核心處理器的每個封裝,甚至封裝之間都有共享Cache的。於是這又扯到了一級Cache,二級Cache,...記憶體,磁碟,網路等儲存裝置的大小和層次問題了...
        通過測試終端的TCP重傳次數計算,上述的結果非常不錯,優化無極限,如果考慮到一路統一MTU,那將必須又是一個優化切入點,另外還有e1000e千兆網絡卡驅動的很多引數還沒有調整,另外,如果你的CPU核心在綁了一個OpenVPN例項且滿資料跑時top顯示其idle百分比仍然很高,那麼就在其上繫結兩個或者多個OpenVPN例項,總之,CPU利用率達到90%以上並不是壞事,而是好事,這個和桌面系統是完全不同的...
        如果CPU跑不滿,先別急著綁多個VPN例項,還有一招,那就是壓縮,在OpenVPN配置中增加comp-lzo即可,非常簡單,理論和測試結果均證明,啟用壓縮可以大大減少丟包率,且使得吞吐量得到大幅提高。最基本的一點,對於TCP而言,我覺得多個OpenVPN載荷包打包壓縮要比單個載荷包單獨壓縮效果更好些,OpenVPN隧道一端對多個TCP包進行壓縮,另一端簡單進行解壓縮,隧道途中,壓縮包中載荷資料包順序決不會亂掉,因此也就使得序列的協議比如TCP的順序性得到了加強,多包加密隧道不但封裝了資料加密了資料,還帶著序列的TCP資料走過了最坎坷的一段路使它們不會亂序。下圖說明了這一點:

但是,即使是單包壓縮,也會使隧道傳輸速率提高,因為隧道內的資料包尺寸減小了,更有利於傳輸(轎車要比卡車快...?),同時通過調節中途裝置的網絡卡驅動引數還能將隧道傳輸速率進一步提高,舉個例子,Intel千兆網絡卡就有可以調節的引數使它更有利於小包的收發,或者更有利於大包的收發,或者折中。MTU導致的IP分段也是使用壓縮的重要理由。壓縮可以將包壓小,如果傳輸檔案,傳送資料塊而不是小包的可能性較大(相反的則是ssh之類的延遲敏感的程式,它們一般使用小包通訊),那麼起始端可能根據網絡卡的MTU來擷取資料包,然而OpenVPN在使用者態使用socket為其進行封裝,肯定會超過MTU值,因此OpenVPN的封裝對於端到端的MTU發現是不可見的,所以IP分段並沒有發生在端到端的載荷包上,而是發生在OpenVPN的資料傳輸本身。通過抓包,發現大量:
10:07:26.243655 IP (tos 0x0, ttl 64, id 50897, offset 0, flags [+], proto UDP (17), length 1500)
    172.16.2.1.61195 > 172.16.2.2.43050: UDP, length 1529
10:07:26.243659 IP (tos 0x0, ttl 64, id 50897, offset 1480, flags [none], proto UDP (17), length 77)
    172.16.2.1 > 172.16.2.2: udp

因此很大一部分開銷花在了IP分段/重組上,故採用壓縮是明智的。一個等價的解決方案是不使用壓縮(畢竟它消耗了CPU),取而代之的是將OpenVPN兩個端點之間的所有鏈路的MTU調大那麼一點點(多處一個OpenVPN協議頭以及OpenVPN使用的TCP/UDP頭等協議頭)。
        做到這一步,現實意義上已經夠了,剩下的就全屬業餘愛好了...
        最後要說的是,雖然以上的方式有效的提升了OpenVPN構建的VPN隧道效能,然而卻不能將所有多條隧道的頻寬全部供給一個OpenVPN客戶端,只是說它們的和是一個不小的值,如果想實現單條隧道的頻寬提升,那就需要多例項bonding或者多例項路由負載均衡了。本文所寫的其實不是什麼創新,很多Linux發行版自帶的OpenVPN本身就提供了多例項OpenVPN並行的配置,只需要service包裝命令啟動一下即可。
           

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

這裡寫圖片描述