OpenVPN多處理之-netns容器與iptables CLUSTER
阿新 • • 發佈:2019-01-10
如果還是沉湎於之前的戰果以及強加的感嘆,不要冥想,將其昇華。
我想重構它們,同時將其改造成“能經得起繼續複雜化”的系統,因此我不得不想辦法將這些關係理順。是的,bash太複雜了,那麼改用什麼好呢?用Python?或者PHP?再或者Java?或者直接用C?總之,不再用bash了。
指令碼的好處在於,你可以隨時實驗新想法,所見即所得,不用攜帶任何裝備,只要有個終端就能做事。缺點在於,正是因為上述那些隨時編寫隨時執行的隨意性,bash寫出的東西很容易發散開來以後便無法收斂,Python聲稱自己支援OO,支援複雜資料結構組織與容易,看上去會比bash好些,但是那依賴程式設計師擁有良好的設計與編碼能力,讓一個菜鳥比如我寫Python,你能想象他能寫得多麼噁心嗎?能用C寫出好軟體的不算高手,用指令碼寫出好軟體的才叫猛士。新手寫bash指令碼,一般都會搞得關係錯綜複雜,畢竟bash指令碼中並沒有什麼好用的容器可以容納資料結構,所以,bash天生就是發散了。
C的缺點正是指令碼的優點,那就是你不得不隨身攜帶一些重量級裝備,比如gcc,gnu make,gdb,strace...而這些一般在運維環境中是沒有的,所以你要擁有一臺時刻可以派上用場的開發用虛擬機器,並且隨身攜帶,如果沒有,那就很悲慘了。上週的某天,我就中午從公司回家去除錯C程式,只因為我的筆記本沒有帶到公司。在雲上架一臺開發機是好主意,但是那需要你有財力支援以及你時刻都要可以接入網際網路。C的優點在於它是內斂的,對於初學者而言,一般都會將所有邏輯寫到一個檔案中,也不善於呼叫外部的庫函式或者指令碼,如果說bash引誘你利用其它小命令組織大程式的話,那麼C就是阻止這一切的發生,只有高手才傾向於寫小的C程式,然後通過動態庫或者指令碼將其組織起來,對於新手而言,一般都是傾向於寫完備的大程式,即所有的邏輯集中在一起。
我寫了蜘蛛網般的bash程式碼,說明我是一個新手,為了將邏輯稍微集中一下,作為新手,寫出來的C應該是超級內斂的,很可能所有的使用者態邏輯都在一個so中,所有的核心邏輯都在一個ko中...然而,這是大忌諱,怎麼辦?簡單,那就是最大限度利用系統本身提供的功能而不是自己用bash組織邏輯。
試想,在一臺機器上啟用一個OpenVPN服務端是一件多麼簡單的事!
但是,為何這樣不行,為何我非要費勁地折騰什麼多例項多程序,因為OpenVPN本身不支援這些。上一篇系列文章中,我已經在OpenVPN內部使其支援了多執行緒,但是如果我沒有修改OpenVPN程式碼的能力呢?如果換另外一個人來做這件事呢。我決定重新給出一個方案。
既然一臺機器啟動一個OpenVPN服務端程序超級簡單,那麼如果有N臺機器的話,每臺機器上啟1個OpenVPN服務端也就是個體力活。在實際上沒有N臺機器的前提下,換個思路,如何將一臺機器當N臺機器使用。
Linux的netns完美解決了這個問題:
ip netns add vpn1
ip netns add vpn2
這樣就添加了兩個名稱空間。接下來就是要為這兩個名稱空間新增網絡卡,如果我的機器上只有一塊網絡卡,給了vpn1,它就被vpn1獨佔了,外面以及vpn2就都看不到了,顯然不行,我又不可能在機器上插物理網絡卡,此時veth虛擬網絡卡幫了忙。
ip link add veth0_vpn1 type veth peer name veth_vpn1
ip link add veth0_vpn2 type veth peer name veth_vpn2
隨後將veth0_vpn1給了vpn1,將veth0_vpn2給了vpn2
ip link set veth0_vpn1 netns vpn1
ip link set veth0_vpn2 netns vpn2
然後將veth_vpn1,veth_vpn2,eth0橋接在一起:
brctl addbr br0
brctl addif br0 eth0 veth_vpn1 veth_vpn2
好了,接下來就是在這兩個名稱空間執行OpenVPN了:
ip netns exec vpn1 ifconfig veth0_vpn1 192.168.1.1/24
ip netns exec vpn2 ifconfig veth0_vpn2 192.168.1.1/24
ip netns exec vpn1 openvpn --config /home/zy/vpn/server.conf
ip netns exec vpn2 openvpn --config /home/zy/vpn/server.conf
兩個openvpn程序讀取相同的配置檔案,但是此時它們的網路已經是隔離的了。可以看出,兩個名稱空間的veth網絡卡地址完全一樣,事實上,對於網路配置而言,vpn1和vpn2是完全相同的。由於此時兩個名稱空間的veth的peer已經和eth0橋接在一起了,接下來的問題是如何將資料包分發到兩個名稱空間,此時iptables的CLUSTER target來幫忙了。給出結論之前,目前的系統原理圖如下:
現在的問題就是如何實現將資料包廣播到所有的這些名稱空間中,對於上圖為例,任何廣播到veth_vpn1和veth_vpn2這兩個橋接埠中。iptables的CLUSTER target支援將veth0_vpn1和veth0_vpn2這兩個網絡卡的MAC設定成同一個“組播MAC地址”,而我的工作就是在資料包從eth0進入後,將其目標MAC地址轉換為那個組播地址,接下來在網橋forward資料的時候,看到目標是組播,便從veth_vpn1和veth_vpn2兩個口都發出去了。這個難道不能通過ebtables的dnat來做嗎?
要問如何來設定iptables的CLUSTER,也很簡單,兩個名稱空間除了local-node不一樣之外,其餘的都一樣(這倆名稱空間實際上相當於兩臺機器):
iptables -A INPUT -p udp --dport 1194 -j CLUSTERIP --new --hashmode sourceip --clustermac 01:00:5e:00:00:20 --total-nodes 2 --local-node 1
iptables -A INPUT -p udp --dport 1194 -j CLUSTERIP --new --hashmode sourceip --clustermac 01:00:5e:00:00:20 --total-nodes 2 --local-node 2
CLUSTER target是怎麼將hash值對映到node-num的不重要,重要的是它確實可以將來自一個流的hash值對映到1~total-nodes中的一個,而且僅對映到那一個,這種固定的對映方式保證了一個數據流始終被同一個名稱空間處理。現在的圖示如下:
知識的廣度可以是來自別處的,比如教科書,網際網路論壇,部落格等,但是知識的深度更多的是自己挖掘出來或者悟出來的,對於後者而言,那就是能力了。一個簡單的例子,那就是TCP伺服器端大量的TIME_WAIT狀態套接字對系統的影響,人們提出了很多的解決方式,比如設定recycle,reuse等,而且都是千篇一律的,是的,這樣是能解決問題,但是有誰去挖掘過TIME_WAIT到底帶來了什麼問題嗎?並且是大量的TW套接字,其數量超過ESTABLISH套接字幾個數量級的情況。人們普遍的回答是佔用系統資源,耗盡socket資源,可是在如今伺服器拼硬體的時代,動不動就幾百個G的記憶體的情況下,這都不是什麼問題。有人把思路從空間開銷轉向時間開銷嗎?有是有,但很少。為什麼呢?可能是因為他們總覺得升級記憶體比升級CPU划算吧,可事實上,幾乎每個人都知道資料結構的組織不僅僅影響記憶體佔用,還影響操縱效率。只需要稍微想一下TCP socket的實現就會明白以下的事實:一個數據包對應到一個socket,需要一個查表的過程,對於TCP而言,首先要檢查該資料包是否已經對應到了一個socket,如果沒有查到再去查是否有listen socket與之對應(否則怎麼辦呢?)。也就是說,listen socket的查詢是最後才做的。對於一個新建的連線而言,首先它不可能在ESTABLISH狀態的socket連結串列中找到,如果ESTABLISH socket不多的話,這個開銷可以忽略,即便很多,也是必須要例行公事,因此這個查詢是必然的開銷,但是接下來還要看它是否和一個TIME_WAIT狀態的socket對應,此時如果存在大量的TW套接字,那麼這種開銷就是額外的開銷,是可以避免的,但是有一個前提,那就是必須避開TW帶來的問題(在取消一個機制之前,必須明白該機制的所有方方面面)。因此,大量的TW套接字除了消耗空間外,還會降低新建連線的效率,大量的時間會消耗在查表上,對於已經建立的連線的資料傳輸效率則影響不大,因為在查詢TW狀態套接字之前,它已經查到了一個ESTABLISH套接字了。如果你本身就懂Linux的TCP層的實現,那麼以上的問題很容易分析,但是如果你從沒看過原始碼的實現,就需要自己思考了。誠然,熟悉介面而不關注細節可以提高編碼的效率,但是這並不是箴言,因為熟悉實現細節更能提高出了問題後的排錯效率。所以,知識的深度和廣度都是不可缺少的,關鍵是你處在哪個階段。
如果過度在意學到的東西,那麼就會比較僵化,如果過度在意挖掘或者感悟出來的東西,就會容易鑽入牛角尖且變得自負。如何權衡知識的利用方式,十分重要。
1.C還是指令碼
曾經,我用bash組織了複雜的iptables,ip rule等邏輯來配合OpenVPN,將其應用於幾乎所有可以想象得到的複雜網路場景中,實現網間VPN隧道。後來我發現玩大了,要不是當時留下一份文件,我自己幾乎已經無法通過這些關係錯綜複雜的bash指令碼還原當時的思路,一切太複雜了。我想重構它們,同時將其改造成“能經得起繼續複雜化”的系統,因此我不得不想辦法將這些關係理順。是的,bash太複雜了,那麼改用什麼好呢?用Python?或者PHP?再或者Java?或者直接用C?總之,不再用bash了。
指令碼的好處在於,你可以隨時實驗新想法,所見即所得,不用攜帶任何裝備,只要有個終端就能做事。缺點在於,正是因為上述那些隨時編寫隨時執行的隨意性,bash寫出的東西很容易發散開來以後便無法收斂,Python聲稱自己支援OO,支援複雜資料結構組織與容易,看上去會比bash好些,但是那依賴程式設計師擁有良好的設計與編碼能力,讓一個菜鳥比如我寫Python,你能想象他能寫得多麼噁心嗎?能用C寫出好軟體的不算高手,用指令碼寫出好軟體的才叫猛士。新手寫bash指令碼,一般都會搞得關係錯綜複雜,畢竟bash指令碼中並沒有什麼好用的容器可以容納資料結構,所以,bash天生就是發散了。
C的缺點正是指令碼的優點,那就是你不得不隨身攜帶一些重量級裝備,比如gcc,gnu make,gdb,strace...而這些一般在運維環境中是沒有的,所以你要擁有一臺時刻可以派上用場的開發用虛擬機器,並且隨身攜帶,如果沒有,那就很悲慘了。上週的某天,我就中午從公司回家去除錯C程式,只因為我的筆記本沒有帶到公司。在雲上架一臺開發機是好主意,但是那需要你有財力支援以及你時刻都要可以接入網際網路。C的優點在於它是內斂的,對於初學者而言,一般都會將所有邏輯寫到一個檔案中,也不善於呼叫外部的庫函式或者指令碼,如果說bash引誘你利用其它小命令組織大程式的話,那麼C就是阻止這一切的發生,只有高手才傾向於寫小的C程式,然後通過動態庫或者指令碼將其組織起來,對於新手而言,一般都是傾向於寫完備的大程式,即所有的邏輯集中在一起。
我寫了蜘蛛網般的bash程式碼,說明我是一個新手,為了將邏輯稍微集中一下,作為新手,寫出來的C應該是超級內斂的,很可能所有的使用者態邏輯都在一個so中,所有的核心邏輯都在一個ko中...然而,這是大忌諱,怎麼辦?簡單,那就是最大限度利用系統本身提供的功能而不是自己用bash組織邏輯。
2.一臺機器當多臺用
但是,為何這樣不行,為何我非要費勁地折騰什麼多例項多程序,因為OpenVPN本身不支援這些。上一篇系列文章中,我已經在OpenVPN內部使其支援了多執行緒,但是如果我沒有修改OpenVPN程式碼的能力呢?如果換另外一個人來做這件事呢。我決定重新給出一個方案。
既然一臺機器啟動一個OpenVPN服務端程序超級簡單,那麼如果有N臺機器的話,每臺機器上啟1個OpenVPN服務端也就是個體力活。在實際上沒有N臺機器的前提下,換個思路,如何將一臺機器當N臺機器使用。
Linux的netns完美解決了這個問題:
ip netns add vpn1
ip netns add vpn2
這樣就添加了兩個名稱空間。接下來就是要為這兩個名稱空間新增網絡卡,如果我的機器上只有一塊網絡卡,給了vpn1,它就被vpn1獨佔了,外面以及vpn2就都看不到了,顯然不行,我又不可能在機器上插物理網絡卡,此時veth虛擬網絡卡幫了忙。
ip link add veth0_vpn1 type veth peer name veth_vpn1
ip link add veth0_vpn2 type veth peer name veth_vpn2
隨後將veth0_vpn1給了vpn1,將veth0_vpn2給了vpn2
ip link set veth0_vpn1 netns vpn1
ip link set veth0_vpn2 netns vpn2
brctl addbr br0
brctl addif br0 eth0 veth_vpn1 veth_vpn2
好了,接下來就是在這兩個名稱空間執行OpenVPN了:
ip netns exec vpn1 ifconfig veth0_vpn1 192.168.1.1/24
ip netns exec vpn2 ifconfig veth0_vpn2 192.168.1.1/24
ip netns exec vpn1 openvpn --config /home/zy/vpn/server.conf
ip netns exec vpn2 openvpn --config /home/zy/vpn/server.conf
兩個openvpn程序讀取相同的配置檔案,但是此時它們的網路已經是隔離的了。可以看出,兩個名稱空間的veth網絡卡地址完全一樣,事實上,對於網路配置而言,vpn1和vpn2是完全相同的。由於此時兩個名稱空間的veth的peer已經和eth0橋接在一起了,接下來的問題是如何將資料包分發到兩個名稱空間,此時iptables的CLUSTER target來幫忙了。給出結論之前,目前的系統原理圖如下:
3.構建分散式Cluster
現在的問題就是如何把資料包分發到這兩個(實際環境是多個,視CPU數量而定)名稱空間。難道要在Bridge這個層次再搞一個類似LVS之類的東西嗎?思路是對的,但是我不會那麼做,因為那樣做還不如不搞名稱空間直接在LVS上跑多個服務呢。事實上,之所以搞名稱空間,就是因為iptables提供了一種分散式的叢集負載均衡演算法模型,將集中式的決定“由哪個節點處理資料包”這個問題轉化為分散式的“這個資料包是否由我來處理”。也就是說,計算分佈化了,不再處於一個點上,具體的思想可以參見我的另一篇文章以及早期全廣播乙太網的定址思想。現在的問題就是如何實現將資料包廣播到所有的這些名稱空間中,對於上圖為例,任何廣播到veth_vpn1和veth_vpn2這兩個橋接埠中。iptables的CLUSTER target支援將veth0_vpn1和veth0_vpn2這兩個網絡卡的MAC設定成同一個“組播MAC地址”,而我的工作就是在資料包從eth0進入後,將其目標MAC地址轉換為那個組播地址,接下來在網橋forward資料的時候,看到目標是組播,便從veth_vpn1和veth_vpn2兩個口都發出去了。這個難道不能通過ebtables的dnat來做嗎?
要問如何來設定iptables的CLUSTER,也很簡單,兩個名稱空間除了local-node不一樣之外,其餘的都一樣(這倆名稱空間實際上相當於兩臺機器):
iptables -A INPUT -p udp --dport 1194 -j CLUSTERIP --new --hashmode sourceip --clustermac 01:00:5e:00:00:20 --total-nodes 2 --local-node 1
iptables -A INPUT -p udp --dport 1194 -j CLUSTERIP --new --hashmode sourceip --clustermac 01:00:5e:00:00:20 --total-nodes 2 --local-node 2
CLUSTER target是怎麼將hash值對映到node-num的不重要,重要的是它確實可以將來自一個流的hash值對映到1~total-nodes中的一個,而且僅對映到那一個,這種固定的對映方式保證了一個數據流始終被同一個名稱空間處理。現在的圖示如下:
4.知識的廣度與深度
懂多少知識不重要,重要的是這些知識能用來幹什麼。事實上,我認為兩類人是不同的,擁有構建能力的人不需要擁有多少知識的細節,屬於比較有廣度的人,而專攻一點的人往往對細節理解很深入,屬於有深度的人,對於系統工程的構建階段,我個人認為廣度比深度要來得重要,但同時絕不能忽略深度,相反,需要一種昇華,即你需要擁有極強的洞察力,不需要深入細節的前提下迅速捕捉到關鍵點,做到這一點,沒有對知識的深度理解與積累是很難做到的。但是對於系統的除錯,排錯和優化階段,知識深度的重要性就要大於知識的廣度的重要性了。知識的廣度可以是來自別處的,比如教科書,網際網路論壇,部落格等,但是知識的深度更多的是自己挖掘出來或者悟出來的,對於後者而言,那就是能力了。一個簡單的例子,那就是TCP伺服器端大量的TIME_WAIT狀態套接字對系統的影響,人們提出了很多的解決方式,比如設定recycle,reuse等,而且都是千篇一律的,是的,這樣是能解決問題,但是有誰去挖掘過TIME_WAIT到底帶來了什麼問題嗎?並且是大量的TW套接字,其數量超過ESTABLISH套接字幾個數量級的情況。人們普遍的回答是佔用系統資源,耗盡socket資源,可是在如今伺服器拼硬體的時代,動不動就幾百個G的記憶體的情況下,這都不是什麼問題。有人把思路從空間開銷轉向時間開銷嗎?有是有,但很少。為什麼呢?可能是因為他們總覺得升級記憶體比升級CPU划算吧,可事實上,幾乎每個人都知道資料結構的組織不僅僅影響記憶體佔用,還影響操縱效率。只需要稍微想一下TCP socket的實現就會明白以下的事實:一個數據包對應到一個socket,需要一個查表的過程,對於TCP而言,首先要檢查該資料包是否已經對應到了一個socket,如果沒有查到再去查是否有listen socket與之對應(否則怎麼辦呢?)。也就是說,listen socket的查詢是最後才做的。對於一個新建的連線而言,首先它不可能在ESTABLISH狀態的socket連結串列中找到,如果ESTABLISH socket不多的話,這個開銷可以忽略,即便很多,也是必須要例行公事,因此這個查詢是必然的開銷,但是接下來還要看它是否和一個TIME_WAIT狀態的socket對應,此時如果存在大量的TW套接字,那麼這種開銷就是額外的開銷,是可以避免的,但是有一個前提,那就是必須避開TW帶來的問題(在取消一個機制之前,必須明白該機制的所有方方面面)。因此,大量的TW套接字除了消耗空間外,還會降低新建連線的效率,大量的時間會消耗在查表上,對於已經建立的連線的資料傳輸效率則影響不大,因為在查詢TW狀態套接字之前,它已經查到了一個ESTABLISH套接字了。如果你本身就懂Linux的TCP層的實現,那麼以上的問題很容易分析,但是如果你從沒看過原始碼的實現,就需要自己思考了。誠然,熟悉介面而不關注細節可以提高編碼的效率,但是這並不是箴言,因為熟悉實現細節更能提高出了問題後的排錯效率。所以,知識的深度和廣度都是不可缺少的,關鍵是你處在哪個階段。
如果過度在意學到的東西,那麼就會比較僵化,如果過度在意挖掘或者感悟出來的東西,就會容易鑽入牛角尖且變得自負。如何權衡知識的利用方式,十分重要。