用戶態使能eth0節點
Ⅰ IPVS從入門到精通kube-proxy實現原理
我們知道容器的特點是快速創建、快速銷毀,Kubernetes Pod和容器一樣只具有臨時的生命周期,一個Pod隨時有可能被終止或者漂移,隨著集群的狀態變化而變化,一旦Pod變化,則該Pod提供的服務也就無法訪問,如果直接訪問Pod則無法實現服戚滲務的連續性和高可用性,因此顯然不能使用Pod地址作為服務暴露埠。
解決這個問題的辦法和傳統數據中心解決無狀態服務高可用的思路完全一樣,通過負載均衡和VIP實現後端真實服務的自動轉發、故障轉移。
這個負載均衡在Kubernetes中稱為Service,VIP即Service ClusterIP,因此可以認為Kubernetes的Service就是一個四層負載均衡,Kubernetes對應的還有七層負載均衡Ingress,本文僅介紹Kubernetes Service。
這個Service就是由kube-proxy實現的,ClusterIP不會因為Podz狀態改變而變,需要注意的是VIP即ClusterIP是個假的IP,這個IP在整個集群中根本不存在,當然也就無法通過IP協議棧無法路由,底層underlay設備更無法感知這個IP的存在,因此ClusterIP只能是單主機(Host Only)作用域可見,這個IP在其他節點以及集群外均無法訪問。
Kubernetes為了實現在集群所有的節點都能夠訪問Service,kube-proxy默認會在所有的Node節點都創建這個VIP並且實現負載,所以在部署Kubernetes後發現kube-proxy是一個DaemonSet。
而Service負載之所以能夠在Node節點上實現是因為無論Kubernetes使用哪個網兄差絡模型,均需要保證滿足如下三個條件:
至少第2點是必須滿足的,有了如上幾個假設,Kubernetes Service才能在Node上實現,否則Node不通Pod IP也就實現不了了。
有人說既然kube-proxy是四層負載均衡,那kube-proxy應該可以使用haproxy、nginx等作為負載後端啊?
事實上確實沒有問題,不過唯一需要考慮的就是性能問題,如上這些負載均衡功能都強大,但畢竟還是基於用戶態轉發或者反向代理實現的,性能必然不如在內核態直接轉發處理好。
因此kube-proxy默認會優先選擇基於內核態的負載作為後端實現機制,目前kube-proxy默認是通過iptables實現負載的,在此之前還有一種稱為userspace模式,其實也是基於iptables實現,可以認為當前的iptables模式是對之前userspace模式的優化。
本節接下來將詳細介紹kube-proxy iptables模式的實現原理。
首先創建了一個ClusterIP類型的Service:
其中ClusterIP為10.106.224.41,我們可以驗證這個IP在羨仔皮本地是不存在的:
所以 不要嘗試去ping ClusterIP,它不可能通的 。
此時在Node節點192.168.193.172上訪問該Service服務,首先流量到達的是OUTPUT鏈,這里我們只關心nat表的OUTPUT鏈:
該鏈跳轉到 KUBE-SERVICES 子鏈中:
我們發現與之相關的有兩條規則:
其中 KUBE-SVC-RPP7DHNHMGOIIFDC 子鏈規則如下:
這幾條規則看起來復雜,其實實現的功能很簡單:
我們查看其中一個子鏈 KUBE-SEP-FTIQ6MSD3LWO5HZX 規則:
可見這條規則的目的是做了一次DNAT,DNAT目標為其中一個Endpoint,即Pod服務。
由此可見子鏈 KUBE-SVC-RPP7DHNHMGOIIFDC 的功能就是按照概率均等的原則DNAT到其中一個Endpoint IP,即Pod IP,假設為10.244.1.2,
此時相當於:
接著來到POSTROUTING鏈:
這兩條規則只做一件事就是只要標記了 0x4000/0x4000 的包就一律做MASQUERADE(SNAT),由於10.244.1.2默認是從flannel.1轉發出去的,因此會把源IP改為flannel.1的IP 10.244.0.0 。
剩下的就是常規的走Vxlan隧道轉發流程了,這里不再贅述,感興趣的可以參考我之前的文章 淺聊幾種主流Docker網路的實現原理 。
接下來研究下NodePort過程,首先創建如下Service:
其中Service的NodePort埠為30419。
假設有一個外部IP 192.168.193.197,通過 192.168.193.172:30419 訪問服務。
首先到達PREROUTING鏈:
PREROUTING的規則非常簡單,凡是發給自己的包,則交給子鏈 KUBE-NODEPORTS 處理。注意前面省略了判斷ClusterIP的部分規則。
KUBE-NODEPORTS 規則如下:
這個規則首先給包打上標記 0x4000/0x4000 ,然後交給子鏈 KUBE-SVC-RPP7DHNHMGOIIFDC 處理, KUBE-SVC-RPP7DHNHMGOIIFDC 剛剛已經見面過了,其功能就是按照概率均等的原則DNAT到其中一個Endpoint IP,即Pod IP,假設為10.244.1.2。
此時發現10.244.1.2不是自己的IP,於是經過路由判斷目標為10.244.1.2需要從flannel.1發出去。
接著到了 FORWARD 鏈,
FORWARD表在這里只是判斷下,只允許打了標記 0x4000/0x4000 的包才允許轉發。
最後來到 POSTROUTING 鏈,這里和ClusterIP就完全一樣了,在 KUBE-POSTROUTING 中做一次 MASQUERADE (SNAT),最後結果:
我們發現基於iptables模式的kube-proxy ClusterIP和NodePort都是基於iptables規則實現的,我們至少發現存在如下幾個問題:
本文接下來將介紹kube-proxy的ipvs實現,由於本人之前也是對ipvs很陌生,沒有用過,專門學習了下ipvs,因此在第二章簡易介紹了下ipvs,如果已經很熟悉ipvs了,可以直接跳過,這一章和Kubernetes幾乎沒有任何關系。
另外由於本人對ipvs也是初學,水平有限,難免出錯,歡迎指正!
我們接觸比較多的是應用層負載均衡,比如haproxy、nginx、F5等,這些負載均衡工作在用戶態,因此會有對應的進程和監聽socket,一般能同時支持4層負載和7層負載,使用起來也比較方便。
LVS是國內章文嵩博士開發並貢獻給社區的( 章文嵩博士和他背後的負載均衡帝國 ),主要由ipvs和ipvsadm組成,ipvs是工作在內核態的4層負載均衡,和iptables一樣都是基於內核底層netfilter實現,netfilter主要通過各個鏈的鉤子實現包處理和轉發。ipvsadm和ipvs的關系,就好比netfilter和iptables的關系,它運行在用戶態,提供簡單的CLI介面進行ipvs配置。
由於ipvs工作在內核態,直接基於內核處理包轉發,所以最大的特點就是性能非常好。又由於它工作在4層,因此不會處理應用層數據,經常有人問ipvs能不能做SSL證書卸載、或者修改HTTP頭部數據,顯然這些都不可能做的。
我們知道應用層負載均衡大多數都是基於反向代理實現負載的,工作在應用層,當用戶的包到達負載均衡監聽器listening後,基於一定的演算法從後端服務列表中選擇其中一個後端服務進行轉發。當然中間可能還會有一些額外操作,最常見的如SSL證書卸載。
而ipvs工作在內核態,只處理四層協議,因此只能基於路由或者NAT進行數據轉發,可以把ipvs當作一個特殊的路由器網關,這個網關可以根據一定的演算法自動選擇下一跳,或者把ipvs當作一個多重DNAT,按照一定的演算法把ip包的目標地址DNAT到其中真實服務的目標IP。針對如上兩種情況分別對應ipvs的兩種模式–網關模式和NAT模式,另外ipip模式則是對網關模式的擴展,本文下面會針對這幾種模式的實現原理進行詳細介紹。
ipvsadm命令行用法和iptables命令行用法非常相似,畢竟是兄弟,比如 -L 列舉, -A 添加, -D 刪除。
但是其實ipvsadm相對iptables命令簡直太簡單了,因為沒有像iptables那樣存在各種table,table嵌套各種鏈,鏈里串著一堆規則,ipvsadm就只有兩個核心實體,分別為service和server,service就是一個負載均衡實例,而server就是後端member,ipvs術語中叫做real server,簡稱RS。
如下命令創建一個service實例 172.17.0.1:32016 , -t 指定監聽的為 TCP 埠, -s 指定演算法為輪詢演算法rr(Round Robin),ipvs支持簡單輪詢(rr)、加權輪詢(wrr)、最少連接(lc)、源地址或者目標地址散列(sh、dh)等10種調度演算法。
然後把10.244.1.2:8080、10.244.1.3:8080、10.244.3.2:8080添加到service後端member中。
其中 -t 指定service實例, -r 指定server地址, -w 指定權值, -m 即前面說的轉發模式,其中 -m 表示為 masquerading ,即NAT模式, -g 為 gatewaying ,即直連路由模式, -i 為 ipip ,ji即IPIP隧道模式。
與iptables-save、iptables-restore對應的工具ipvs也有ipvsadm-save、ipvsadm-restore。
NAT模式由字面意思理解就是通過NAT實現的,但究竟是如何NAT轉發的,我們通過實驗環境驗證下。
現環境中LB節點IP為192.168.193.197,三個RS節點如下:
為了模擬LB節點IP和RS不在同一個網路的情況,在LB節點中添加一個虛擬IP地址:
創建負載均衡Service並把RS添加到Service中:
這里需要注意的是,和應用層負載均衡如haproxy、nginx不一樣的是,haproxy、nginx進程是運行在用戶態,因此會創建socket,本地會監聽埠,而 ipvs的負載是直接運行在內核態的,因此不會出現監聽埠 :
可見並沒有監聽10.222.0.1:8080 Socket 。
Client節點IP為192.168.193.226,為了和LB節點的虛擬IP 10.222.0.1通,我們手動添加靜態路由如下:
此時Client節點能夠ping通LB節點VIP:
可見Client節點到VIP的鏈路沒有問題,那是否能夠訪問我們的Service呢?
我們驗證下:
非常意外的結果是並不通。
在RS節點抓包如下:
我們發現數據包的源IP為Client IP,目標IP為RS IP,換句話說,LB節點IPVS只做了DNAT,把目標IP改成RS IP了,而沒有修改源IP。此時雖然RS和Client在同一個子網,鏈路連通性沒有問題,但是由於Client節點發出去的包的目標IP和收到的包源IP不一致,因此會被直接丟棄,相當於給張三發信,李四回的信,顯然不受信任。
既然IPVS沒有給我們做SNAT,那自然想到的是我們手動做SNAT,在LB節點添加如下iptables規則:
再次檢查Service是否可以訪問:
服務依然不通。並且在LB節點的iptables日誌為空:
也就是說,ipvs的包根本不會經過iptables nat表POSTROUTING鏈?
那mangle表呢?我們打開LOG查看下:
此時查看日誌如下:
我們發現在mangle表中可以看到DNAT後的包。
只是可惜mangle表的POSTROUTING並不支持NAT功能:
對比Kubernetes配置發現需要設置如下系統參數:
再次驗證:
終於通了,查看RS抓包:
如期望,修改了源IP為LB IP。
原來需要配置 net.ipv4.vs.conntrack=1 參數,這個問題折騰了一個晚上,不得不說目前ipvs的文檔都太老了。
前面是通過手動iptables實現SNAT的,性能可能會有損耗,於是如下開源項目通過修改lvs直接做SNAT:
除了SNAT的辦法,是否還有其他辦法呢?想想我們最初的問題,Client節點發出去的包的目標IP和收到的包源IP不一致導致包被丟棄,那解決問題的辦法就是把包重新引到LB節點上,只需要在所有的RS節點增加如下路由即可:
此時我們再次檢查我們的Service是否可連接:
結果沒有問題。
不過我們是通過手動添加Client IP到所有RS的明細路由實現的,如果Client不固定,這種方案仍然不太可行,所以通常做法是乾脆把所有RS默認路由指向LB節點,即把LB節點當作所有RS的默認網關。
由此可知,用戶通過LB地址訪問服務,LB節點IPVS會把用戶的目標IP由LB IP改為RS IP,源IP不變,包不經過iptables的OUTPUT直接到達POSTROUTING轉發出去,包回來的時候也必須先到LB節點,LB節點把目標IP再改成用戶的源IP,最後轉發給用戶。
顯然這種模式來回都需要經過LB節點,因此又稱為雙臂模式。
網關模式(Gatewaying)又稱為直連路由模式(Direct Routing)、透傳模式, 所謂透傳即LB節點不會修改數據包的源IP、埠以及目標IP、埠 ,LB節點做的僅僅是路由轉發出去,可以把LB節點看作一個特殊的路由器網關,而RS節點則是網關的下一跳,這就相當於對於同一個目標地址,會有多個下一跳,這個路由器網關的特殊之處在於能夠根據一定的演算法選擇其中一個RS作為下一跳,達到負載均衡和冗餘的效果。
既然是通過直連路由的方式轉發,那顯然LB節點必須與所有的RS節點在同一個子網,不能跨子網,否則路由不可達。換句話說, 這種模式只支持內部負載均衡(Internal LoadBalancer) 。
另外如前面所述,LB節點不會修改源埠和目標埠,因此這種模式也無法支持埠映射,換句話說 LB節點監聽的埠和所有RS節點監聽的埠必須一致 。
現在假設有LB節點IP為 192.168.193.197 ,有三個RS節點如下:
創建負載均衡Service並把RS添加到Service中:
注意到我們的Service監聽的埠30620和RS的埠是一樣的,並且通過 -g 參數指定為直連路由模式(網關模式)。
Client節點IP為192.168.193.226,我們驗證Service是否可連接:
我們發現並不通,在其中一個RS節點192.168.193.172上抓包:
正如前面所說,LB是通過路由轉發的,根據路由的原理,源MAC地址修改為LB的MAC地址,而目標MAC地址修改為RS MAC地址,相當於RS是LB的下一跳。
並且源IP和目標IP都不會修改。問題就來了,我們Client期望訪問的是RS,但RS收到的目標IP卻是LB的IP,發現這個目標IP並不是自己的IP,因此不會通過INPUT鏈轉發到用戶空間,這時要不直接丟棄這個包,要不根據路由再次轉發到其他地方,總之兩種情況都不是我們期望的結果。
那怎麼辦呢?為了讓RS接收這個包,必須得讓RS有這個目標IP才行。於是不妨在lo上添加個虛擬IP,IP地址偽裝成LB IP 192.168.193.197:
問題又來了,這就相當於有兩個相同的IP,IP重復了怎麼辦?辦法是隱藏這個虛擬網卡,不讓它回復ARP,其他主機的neigh也就不可能知道有這么個網卡的存在了,參考 Using arp announce/arp ignore to disable ARP 。
此時再次從客戶端curl:
終於通了。
我們從前面的抓包中知道,源IP為Client IP 192.168.193.226,因此直接回包給Client即可,不可能也不需要再回到LB節點了,即A->B,B->C,C->A,流量方向是三角形狀的,因此這種模式又稱為三角模式。
我們從原理中不難得出如下結論:
前面介紹了網關直連路由模式,要求所有的節點在同一個子網,而ipip隧道模式則主要解決這種限制,LB節點IP和RS可以不在同一個子網,此時需要通過ipip隧道進行傳輸。
現在假設有LB節點IP為 192.168.193.77/25 ,在該節點上增加一個VIP地址:
ip addr add 192.168.193.48/25 dev eth0
有三個RS節點如下:
如上三個RS節點子網掩碼均為255.255.255.128,即25位子網,顯然和VIP 192.168.193.48/25不在同一個子網。
創建負載均衡Service並把RS添加到Service中:
注意到我們的Service監聽的埠30620和RS的埠是一樣的,並且通過 -i 參數指定為ipip隧道模式。
在所有的RS節點上載入ipip模塊以及添加VIP(和直連路由類型):
Client節點IP為192.168.193.226/25,我們驗證Service是否可連接:
Service可訪問,我們在RS節點上抓包如下:
我們發現和直連路由一樣,源IP和目標IP沒有修改。
所以IPIP模式和網關(Gatewaying)模式原理基本一樣,唯一不同的是網關(Gatewaying)模式要求所有的RS節點和LB節點在同一個子網,而IPIP模式則可以支持跨子網的情況,為了解決跨子網通信問題,使用了ipip隧道進行數據傳輸。
ipvs是一個內核態的四層負載均衡,支持NAT、Gateway以及IPIP隧道模式,Gateway模式性能最好,但LB和RS不能跨子網,IPIP性能次之,通過ipip隧道解決跨網段傳輸問題,因此能夠支持跨子網。而NAT模式沒有限制,這也是唯一一種支持埠映射的模式。
我們不難猜想,由於Kubernetes Service需要使用埠映射功能,因此kube-proxy必然只能使用ipvs的NAT模式。
使用kubeadm安裝Kubernetes可參考文檔 Cluster Created by Kubeadm ,不過這個文檔的安裝配置有問題 kubeadm #1182 ,如下官方配置不生效:
需要修改為如下配置:
可以通過如下命令確認kube-proxy是否修改為ipvs:
創建一個ClusterIP類似的Service如下:
ClusterIP 10.96.54.11為我們查看ipvs配置如下:
可見ipvs的LB IP為ClusterIP,演算法為rr,RS為Pod的IP。
另外我們發現使用的模式為NAT模式,這是顯然的,因為除了NAT模式支持埠映射,其他兩種均不支持埠映射,所以必須選擇NAT模式。
由前面的理論知識,ipvs的VIP必須在本地存在,我們可以驗證:
可見kube-proxy首先會創建一個mmy虛擬網卡kube-ipvs0,然後把所有的Service IP添加到kube-ipvs0中。
我們知道基於iptables的Service,ClusterIP是一個虛擬的IP,因此這個IP是ping不通的,但ipvs中這個IP是在每個節點上真實存在的,因此可以ping通:
當然由於這個IP就是配置在本地虛擬網卡上,所以對診斷問題沒有一點用處的。
我們接下來研究下ClusterIP如何傳遞的。
當我們通過如下命令連接服務時:
此時由於10.96.54.11就在本地,所以會以這個IP作為出口地址,即源IP和目標IP都是10.96.54.11,此時相當於:
其中xxxx為隨機埠。
然後經過ipvs,ipvs會從RS ip列中選擇其中一個Pod ip作為目標IP,假設為10.244.2.2:
我們從iptables LOG可以驗證:
我們查看OUTPUT安全組規則如下:
其中ipsetj集合 KUBE-CLUSTER-IP 保存著所有的ClusterIP以及監聽埠。
如上規則的意思就是除了Pod以外訪問ClusterIP的包都打上 0x4000/0x4000 。
到了POSTROUTING鏈:
如上規則的意思就是只要匹配mark 0x4000/0x4000 的包都做SNAT,由於10.244.2.2是從flannel.1出去的,因此源ip會改成flannel.1的ip 10.244.0.0 :
最後通過Vxlan 隧道發到Pod的Node上,轉發給Pod的veth,回包通過路由到達源Node節點,源Node節點通過之前的MASQUERADE再把目標IP還原為10.96.54.11。
查看Service如下:
Service kubernetes-bootcamp-v1的NodePort為32016。
現在假設集群外的一個IP 192.168.193.197訪問192.168.193.172:32016:
最先到達PREROUTING鏈:
如上4條規則看起來復雜,其實就做一件事,如果目標地址為NodeIP,則把包標記 0x4000 , 0x4000 。
我們查看ipvs:
我們發現和ClusterIP實現原理非常相似,ipvs Service的VIP為Node IP,埠為NodePort。ipvs會選擇其中一個Pod IP作為DNAT目標,這里假設為10.244.3.2:
剩下的到了POSTROUTING鏈就和Service ClusterIP完全一樣了,只要匹配 0x4000/0x4000 的包就會做SNAT。
Kubernetes的ClusterIP和NodePort都是通過ipvs service實現的,Pod當作ipvs service的server,通過NAT MQSQ實現轉發。
簡單來說kube-proxy主要在所有的Node節點做如下三件事:
使用ipvs作為kube-proxy後端,不僅提高了轉發性能,結合ipset還使iptables規則變得更「干凈」清楚,從此再也不怕iptables。
更多關於kube-proxy ipvs參考 IPVS-Based In-Cluster Load Balancing Deep Dive .
本文首先介紹了kube-proxy的功能以及kube-proxy基於iptables的實現原理,然後簡單介紹了ipvs,了解了ipvs支持的三種轉發模式,最後介紹了kube-proxy基於ipvs的實現原理。
ipvs是專門設計用來做內核態四層負載均衡的,由於使用了hash表的數據結構,因此相比iptables來說性能會更好。基於ipvs實現Service轉發,Kubernetes幾乎能夠具備無限的水平擴展能力。隨著Kubernetes的部署規模越來越大,應用越來越廣泛,ipvs必然會取代iptables成為Kubernetes Service的默認實現後端。
轉自 https://zhuanlan.hu.com/p/94418251