Linux提供了強大的數據包處理和管理能力,開發人員依賴這些系統級別的能力創建防火墻,記錄流量,路由數據包以及實現負載均衡功能。Kubernetes在POD之間的連接性,POD和NODE之間的連通性,以及Kubernetes服務功能上重度依賴于這些數據包處理能力,因此咱們(下)這篇文章的核心是詳細的剖析Kubernetes平臺使用最多的三個操作系統網絡工具,包括iptables,IPVS和eBPF。
iptables可以說是Linux操作系統管理最常使用的工具都不為過,已經在Linux操作系統上存在很多年了,系統管理員可以使用iptables來創建防火墻規則,收集數據包處理日志,修改和重新路由數據包等,iptales的背后實現使用了Netfiler,允許我們通過配置iptables rule來處理數據包。iptables rules雖然說功能強大,但是非常不適合手動編輯,幸運的是有很多工具可以用來管理iptables rules,比如我們可以用ufw或指責firewalld來管理防火墻rules。
在Kubernetes環境中,工作節點上的iptables rules主要由kubelet和kube-proxy兩個組件創建和管理,因此理解iptables是理解POD的訪問流量如何被接入的基礎。具體來說,iptables有三個核心的概念:tables,chains和rules。這三個概念相互關聯,形成一種層級結構,tables包含chains,chains包含rules。
由于iptables包含了范圍眾多的功能,因此設計人員按照tables將這些不同的功能進行分類,咱們在使用和運維Kubernetes集群最常見的有三個tables類型:Filter(包含了和防火墻相關的rules),NAT(包含了和網絡地址轉化相關的rules),Mangle(包含非NAT,數據包修改的rules),并且iptables會按特定的順序來執行tables,咱們稍后會詳細介紹。
Chains中包含一組rules,當數據包packet在一個chain中被處理的時候,這些rules會被按順序逐個處理。Chains隸屬于table,并且chains中的rule按Netfiler定義的hooks規則進行組織。具體來說,iptables中有5個內建的頂層chains,這個五個chains和Netfiler的hooks一一對應,因此當我們要給chain中insert一個新的rule,具體要插入的chain取決于我們希望這個rule在什么情況下以及什么時候被執行。
最后一個概念是rule,規則rule由condition條件和action動作組合而成,比如如果packet的目標端口號是22(條件condition),那么就丟棄這個數據包(action動作)。因此我們通常說iptables用來處理機器接收到的數據包,本質上是包含在chains和tables中的rules來具體對流經的數據包進行評估(是否符合特定的條件)和處理(action)。
坦白講iptables設計的這種table-chain-rule(target)的結構和執行規則非常復雜,咱們上邊描述的內容只是大體的把這幾個核心組件是什么,以及它們之間是如何進行協作進行了說明。筆者反復強調過理解iptables是理解容器網絡,特別是Kubernetes網絡機制的核心,因此咱們接下來就對這三個概念進行更深入的介紹。
iptables中的table本質上是用來將不容的功能進行分類管理,因此每個table只負責特定類型的action,用大白話說就是每個table都有特定的功能范圍,并且多個table之間不應該有相同功能的重復。iptables默認包含了五種類型的tables,詳細介紹如下邊的清單:
- Filter表,用來處理數據包的acceptance或者rejection
- NAT表,用來對數據包的源地址或者目的地址進行修改
- Mangle表,用來對數據包進行通用的操作,比如修改packet的headers等
- Raw表,用來對數據包在進行connection tracking或者其他表的rule處理之前進行修改操作
- Security表,專門用來支持SELinux
iptables按照Raw,Mangle,NAT,Filter的順序來執行tables中定義的rules,但是由于chain的存在,因此嚴格意義上來講,這是一個二維的執行順序,在table維度上按Raw,Mangle,NAT,Filter這樣的順序,而在另外一個維度上按chain的順序執行。雖然在Linux系統管理領域,大家都會說tables contains chains,實際上這句話稍微有點歧義,因為執行的順序是先chains,然后才是tablestate)。Conntrack會將受到的每個數據包和特定的連接關聯起來,如果沒有Conntrack提供的連接追蹤能力,內核收到的數據流會顯得非常混亂,Conntrack是操作系統的防火墻和NAT功能模塊的基石。
舉個例子,某個特定的packet會按照 Raw PREROUTONG,Mangle PREROUTONG,NAT PREROUTONG先執行完PREROUTING chain,然后是INPUT或者FORWARD chain,依次類推。
iptables中chains的概念大家可以簡單理解為包含一組rules的鏈表,當數據包被處理的時候,chain的rule會按先后順序對數據包進行處理,如果數據包符合某個rule定義的“終結”動作(比如drop),那么數據包的處理就結束了,直至到chain的最后一個rule。
iptables中定義的頂層(top-level)chain有五個,他們和Netfiler中定義的hooks鉤子函數想呼應:PREROUTING,INPUT,NAT,OUTPUT和POSTROUTING。Netfiler hooks和chains的映射對應關系如下所示:
- PREROUTING對應的Netfiler hooks是NF_IP_PRE_ROUTING
- INPUT對應的Netfiler hooks是NF_IP_LOCAL_IN
- NAT對應的Netfiler hooks是NF_IP_FORWARD
- OUTPUT對應的Netfiler hooks是NF_IP_LOCAL_OUT
- POSTROUTING對應的Netfiler hooks是NF_IP_POST_ROUTING
上邊文字描述的五個chain和Netfiler鉤子函數的映射通過下圖會理解的更加清楚:
對于每個進入機器的數據包(packet)來說,只有特定的chain執行路徑,咱們通過一個三臺機器組成的場景來說明,假設我們有三臺機器,IP地址分別是10.0.0.1,10.0.0.2和10.0.0.3。從機器10.0.0.1的角度,會有以下幾種數據包在iptables chain中路由的路徑:
- 場景1:數據包從外部發送到機器10.0.0.1,發送數據的機器為10.0.0.2,執行的tables為PREROUTING,INPUT
- 場景2:數據包從外部發送到機器10.0.0.1,但是目的地址不是機器10.0.0.1,發送數據的機器為10.0.0.2,目的機器為10.0.0.3,執行的tables為PREROUTING,NAT,POSTROUTING
- 場景3:數據包從本地機器的進程發送給本機上的進程,源地址和目的地址都是127.0.0.1,執行的tables為OUTPUT,POSTROUTING,PREROUTING,INPUT
- 場景4:數據包從本地放給外部機器,發送數據的機器為10.0.0.1,目的地址為10.0.0.2,執行的tables為OUTPUT,POSTROUTING
咱們前邊介紹iptables的tables執行順序為RAW,MANGLE,NAT,Filter,對于大部分chains來說,一般并不包含所有五個tables,但是table的執行順訊不變。舉個例子,Raw tables主要用來對進入iptables的數據包進行修改操作,因此Raw tables中只包含PREROUTING和OUTPUT這兩個chains,這和Netfiler的packet處理流程一致。具體tables中包含chains的詳細清單如下圖所示:
在Linux操作系統上,我們可以通過命令iptables -L -t來查看iptables包含的內容,以下是筆者的minikube集群上的虛擬機機返回的結果:
$ sudo iptables -L -t filter
Chain INPUT (policy ACCEPT)
target? ? prot opt source? ? ? ? ? ? ? destination
KUBE-SERVICES? all? --? anywhere? ? ? ? ? ? anywhere? ? ? ? ? ? ctstate NEW /* kubernetes service portals */
KUBE-EXTERNAL-SERVICES? all? --? anywhere? ? ? ? ? ? anywhere? ? ? ? ? ? ctstate NEW /* kubernetes externally-visible service portals */
KUBE-FIREWALL? all? --? anywhere? ? ? ? ? ? anywhere
Chain FORWARD (policy ACCEPT)
target? ? prot opt source? ? ? ? ? ? ? destination
KUBE-FORWARD? all? --? anywhere? ? ? ? ? ? anywhere? ? ? ? ? ? /* kubernetes forwarding rules */
KUBE-SERVICES? all? --? anywhere? ? ? ? ? ? anywhere? ? ? ? ? ? ctstate NEW /* kubernetes service portals */
DOCKER-USER? all? --? anywhere? ? ? ? ? ? anywhere
DOCKER-ISOLATION-STAGE-1? all? --? anywhere? ? ? ? ? ? anywhere
ACCEPT? ? all? --? anywhere? ? ? ? ? ? anywhere? ? ? ? ? ? ctstate RELATED,ESTABLISHED
DOCKER? ? all? --? anywhere? ? ? ? ? ? anywhere
ACCEPT? ? all? --? anywhere? ? ? ? ? ? anywhere
ACCEPT? ? all? --? anywhere? ? ? ? ? ? anywhere
(省略若干)
注:對NAT來說,分為DNAT和SNAT,其中DNAT作用于chain PREROUTING或者OUTPUT;而SNAT作用于chain INPUT或者POSTROUTING。
我們繼續通過例子來說明讓讀者加深理解,假設有inbound的數據包目的地址是本機,那么table和chain的執行順序看起來如下:
1.PREROUTING
? a.Raw
? b.Mangle
? c.NAT
2.INPUT
? a.Mangle
? b.NAT
? c.Filter
最后,如果我們把前邊所有和Netfiler hooks,tables和chains介紹的信息放在一張表里,結果就是packet在iptables的處理流程,如下圖所示:
上圖中的圓圈代表的是iptables rule,并且rule隸屬于table和chain。對于特定的chain,iptables會按固定的順序來觸發table/chain中的rule,咱們以本地發往外部機器的數據包為例,執行的順序為:
1. Raw/OUTPUT
2. Mangle/OUTPUT
3. NAT/OUTPUT
4. Filter/OUTPUT
5. Mangle/POSTROUTING
6. NAT/POSTROUTING
最后詳細說明一下規則Rule,咱們前邊說過由兩部分組成,condition和action。condition部分其實描述是packet的特征,當有match這個condition時,action會被執行。如果packet不matchcondition,iptables會繼續評估后續的table/chain中的rule,直到最后一個rule。基于condition對packet是否匹配的檢查包括但不限于packt的目的地址是否和條件中ip地址一致等,下圖展示了一些通用的條件類型:
action部分有兩種類型,terminating類型和nonterminating類型,其中terminating類型會“終止”iptables繼續對數據包的處理,因為terminating類型的action已經完成了數據包的最終處理。而nonterminating類型的action在處理完后,后續iptables的chain定義的rule會被繼續處理。具體來說,ACCEPT,DROP,REJECT和RETURN都是terninating類型action。咱們來看看幾個具體的iptables命令,來把前邊的內容串在一起看看,如下圖所示:
最后咱們再重復一次iptables幾個關鍵概念的關系,rule隸屬于table和chain,并且rule在table和chain中的類型決定了rule的執行時機。
注:iptables中的數據在機器重啟后會丟失,不過iptables提供了iptables-save以及iptables-restore工具來應對這種場景。運維人員可以在機器重啟之前通過命令iptables-save來手動的備份機器上的iptables配置,在機器重啟后,通過工具iptables-restore來回復數據。這也是很多底層的自動化資源擴展機制背后的工作原理。
對于Kubernetes來說,使用iptables提供的masquerade連接的功能,讓POD使用Node的IP地址,能夠在集群外部的網絡上進行傳輸,即便是我們都知道POD有自己的私有IP地址。MASQUERADE類型的action和SNAT稍微不同,我們使用SNAT的時候需要通過參數--source-address來指定具體的IP地址,而MASQUERADE不需要,它會使用指定網絡接口的IP地址來修改source地址。不過正是由于MASQUERADE提供的這種動態性,它的性能會比SNAT差很多。下邊是一條使用MASQUERADE類型的命令:
$iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
上邊這條命令的含義是,在數據被離開機器的時候(POSTROUTING),用eth0以太網接口的ip地址替換數據包的源地址。另外iptables還可以在連接級進行負載均衡,大白話說就是連接上數據的扇出,咱們在介紹Kukbernetes的Service具體實現原理的文章中,關于Service如何將收到的服務請求負載到多個后端POD就是原理,并且iptables還提供了隨機性,避免所有的服務請求都被DNAT到第一個POD上(也叫DNAT target)。
$ iptables -t nat -A OUTPUT -p tcp --dport 80 -d $FRONT_IP -m statistic \
--mode random --probability 0.5 -j DNAT --to-destination $BACKEND1_IP:80
$ iptables -t nat -A OUTPUT -p tcp --dport 80 -d $FRONT_IP \ -j DNAT --to-destination $BACKEND2_IP:80
如上是筆者本地minikube集群上的一個有兩個replica的服務,從配置信息中可以看到,請求有50%的幾率被DNAT到第一個POD($BACKEND1_IP)上,如果第一個目標target沒有被選中,那么請求會默認被第二個POD($BACKEND2_IP)處理。這種n分之一的目標比率配置顯得有點呆板,因為如果有3個pod,那么每個pod就有三分之一的比率被選中來處理服務請求。眼尖的讀者應該能看出使用DNAT這種扇出模式的負載均衡存在一些問題。首先因為沒有反饋機制(iptables根本不知道每臺后臺pod的處理情況和狀態),因此在長連接的情況下,即便是我們緊急給服務增加了處理POD,很多已經和服務建立連接并且連接沒有失效的情況下,負載不會被均勻的分配到新增的處理POD上。
因此在很多長連接的場景下,筆者建議大家采用客戶端負載均衡會更加實用來規避這個問題,因為客戶端會定期去API Server上獲取服務可用的IP地址清單,然后定期強制客戶端重新連接來解決長連接的問題。雖說iptables已經被廣泛適用了很多年,但是由于iptables本身的工作原理無法支撐大規模集群的網絡處理場景,因此特別是在負載流量負載均衡的場景中,替代方案被開發出來,這就是我們接下來要講IPVS機制。
IPVS也叫IP Virtual Server,IP虛擬服務器機制,是Linux系統上的L4層連接負載均衡機制,下圖可以幫助大家理解IPVS在整個Linux操作系統處理數據包的位置:
如咱們在前邊介紹DNAT機制所述,iptables對L4層負載均衡的支持考DNAT rules和weights(百分比率),而IPVS原生就支持多種模式的負載均衡機制,因此就負載均衡這個場景來說,IPVS的性能要優于iptabes,當然也取決于IPVS的配置和流量模型。IPVS支持的負載均衡模式如下圖所示:
另外IPVS支持網絡連上的接數據包以下幾種forwarding模式:
- NAT重新源和目的IP地址
- DR將IP數據包封裝進IP數據包來在主機網絡中傳遞
- 使用IP通道技術(IP tunneling)來直接把L2數據包的目標MAC地址更新為選中機器的MAC地址
iptables的DNAT機制提供的負載均衡功能最被大家詬病的原因有三個:
- 隨著集群節點的增多,比如5000個節點的集群,kube-proxy和itables就會變成整個集群的瓶頸。比如說NodePort類型的服務,如果我們在集群上有2000個NodePort類型的服務,每個服務有10個POD實例,那么每個節點上的iptables中大概有20000個記錄,這會占用大量的內核資源
- 如果我們在這樣的集群上增加一個rule,測試結果顯示,如果有5000個服務,需要11分鐘,如果有20000個服務,就需要大概5個小時
- 客戶訪問服務的延遲會增加,因為每個數據包都需要遍歷整個iptables直到terminating rule
而IPVS提供的多種模式有效的解決了iptables帶來的約束。我們可以通過ipvsadm工具來管理節點上的IPVS配置,比如我們可以通過如下幾個命令來配置ipvs,
# ipvsadm -A -t 1.1.1.1:80 -s lc
# ipvsadm -a -t 1.1.1.1:80 -r 2.2.2.2 -m -w 100
# ipvsadm -a -t 1.1.1.1:80 -r 3.3.3.3 -m -w 100
然后我們可以通過IPVS -L來羅列機器上的虛擬服務器,如下是筆者機器上的輸出結果,
# ipvsadm -L
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 1.1.1.1.80:http lc
-> 2.2.2.2:http Masq 100 0 0
-> 3.3.3.3:http Masq 100 0 0
最后我們來看看eBPF機制,eBPF也叫擴展伯克利數據包過濾器,允許我們在內核運行特殊的應用程序而不是在內核和用戶態來回傳遞數據包,咱們前邊介紹的Netfiler和iptables就是后者,在內核和用戶態之間來回傳遞數據包。
讀者看到eBPF這個名字,肯定能猜到有BPF機制,沒錯BPF是eBPF之前內核處理數據包的機制,主要被用來對網絡數據包進行分析。比如大家熟知的tcpdump工具,當我們使用tcpdump來抓取特定的數據包來進行分析,背后使用的就是BPF提供的內核功能。特別是當我們在tcpdump命令后邊指定了過濾條件,這個條件會被傳遞到BPF,BPF接著會使用過濾器把符合條件的數據包抓取并返回給運行在用戶空間的應用程序tcpdump,并顯示在控制臺。
?? data sudo tcpdump -i en0 arp -vvv
tcpdump: listening on en0, link-type EN10MB (Ethernet), capture size 262144 bytes
上邊是筆者在本地通過tcpdump來抓arp請求,在有多臺機器的局域網內,這個命令會返回所有的arp請求信息,因此返回的數據會非常非常多。
eBPF是對BPF功能的擴充,并且eBPF代碼可以直接調用操作系統的接口以及監控系統調用,而不需要通過運行在用戶空間的應用程序使用操作系統提供的鉤子函數來實現。這種直接在內核運行特定條件數據包的方式性能有很大的提升,因此很適合作為網絡工具的技術實現手段。eBPF除了提供數據包過濾之外,還提供了如下羅列的其他功能:
- Kprobes模塊提供了動態追蹤內核模塊的能力
- Uprobes提供了用戶態追蹤的能力
- Tracepoints提供了靜態的內核模塊追蹤能力
- perf_events提供時序內核運行監控數據
- XDP提供了在驅動層面的數據包抓取能力,大家要注意的是XDP可以深入到內核之下,在驅動層工作
如果我們以tcpdump為例,看看eBPF具體是如何工作的,如下圖所示:
假設我們運行tcpdump -i any命令,這個命令會被pcap_compile模塊編譯成BPF程序,接著內核會使用這個編譯好的BPF程序來過濾所有網絡接口的所有的數據包,這個過濾條件就是-i any。tcpdump不是直接從內核讀取滿足條件的數據,而是通過eBPF map結構,map是一個key-value類型的數據結構,來協助內核和BPF程序之間進行數據交換,在我們的例子中就是tcpdump。
文章的最后我們來總結一下使用eBPF帶來的好處(特別是在Kubernetes場景下):
- 性能的提升。使用iptables在有20000個NodePort類型服務的集群中,更新一條rule最壞需要5個小時,這肯定是不能接受的,因此使用IPVS的哈希表會更加有效
- 指標追蹤,通過使用BPF,我們可以收集pod和容器級別的網絡運行信息。特別是eBPF提供的cgroups級別的資源使用監控能力,能夠讓我們做耕細粒度的控制。
- 監控kubectl exec,我們可以通過編寫特定的代碼來記錄集群匯總kubeclt exec執行命令,并持久化來支持后續的審計和分析工作
- 安全提升,我們可以通過eBPF來限制容器實例可執行的系統調用,以及向Falco這樣的運行時安全工具來提升集群的整體安全性。
另外Kuberntes集群中使用eBPF最好的例子就是Cilum,一種遵守CNI規范的網絡插件,并提供取代默認kube-prox提供的服務能力。具體來說,Cilium會直接解析請求并把數據包直接路由到內核,繞過iptables,效率得到的極大的提升,特別是application級別的數據包路由場景。
好了,咱們這邊文章的內容就這么多了,在詳細介紹容器網絡之前,我們下篇文章先介紹幾個常用的網絡工具,敬請期待!