本文有點長,但是筆者還是忍不住想轉(zhuǎn)載下,深入淺出介紹了kube-proxy的實現(xiàn)和原理,非常值得一讀,本文是轉(zhuǎn)載于微信公眾號int32bit,原文鏈接:https://mp.weixin.qq.com/s?__biz=MzI1MDI3NDE1Mg==&mid=2247483931&idx=1&sn=bbfbf5221f86457ec089b02276afb6d9&chksm=e9858a1cdef2030af694b640b7c6569f937cfe0d1ce98c597816a229eae86aff3f546fba3371&scene=21#wechat_redirect
1 kube-proxy介紹
1.1 為什么需要kube-proxy
我們知道容器的特點是快速創(chuàng)建、快速銷毀,Kubernetes Pod和容器一樣只具有臨時的生命周期,一個Pod隨時有可能被終止或者漂移,隨著集群的狀態(tài)變化而變化,一旦Pod變化,則該Pod提供的服務(wù)也就無法訪問,如果直接訪問Pod則無法實現(xiàn)服務(wù)的連續(xù)性和高可用性,因此顯然不能使用Pod地址作為服務(wù)暴露端口。
解決這個問題的辦法和傳統(tǒng)數(shù)據(jù)中心解決無狀態(tài)服務(wù)高可用的思路完全一樣,通過負載均衡和VIP實現(xiàn)后端真實服務(wù)的自動轉(zhuǎn)發(fā)、故障轉(zhuǎn)移。
這個負載均衡在Kubernetes中稱為Service,VIP即Service ClusterIP,因此可以認為Kubernetes的Service就是一個四層負載均衡,Kubernetes對應(yīng)的還有七層負載均衡Ingress,本文僅介紹Kubernetes Service。
這個Service就是由kube-proxy實現(xiàn)的,ClusterIP不會因為Podz狀態(tài)改變而變,需要注意的是VIP即ClusterIP是個假的IP,這個IP在整個集群中根本不存在,當然也就無法通過IP協(xié)議棧無法路由,底層underlay設(shè)備更無法感知這個IP的存在,因此ClusterIP只能是單主機(Host Only)作用域可見,這個IP在其他節(jié)點以及集群外均無法訪問。
Kubernetes為了實現(xiàn)在集群所有的節(jié)點都能夠訪問Service,kube-proxy默認會在所有的Node節(jié)點都創(chuàng)建這個VIP并且實現(xiàn)負載,所以在部署Kubernetes后發(fā)現(xiàn)kube-proxy是一個DaemonSet。
而Service負載之所以能夠在Node節(jié)點上實現(xiàn)是因為無論Kubernetes使用哪個網(wǎng)絡(luò)模型,均需要保證滿足如下三個條件:
容器之間要求不需要任何NAT能直接通信;
容器與Node之間要求不需要任何NAT能直接通信;
容器看到自身的IP和外面看到它的IP必須是一樣的,即不存在IP轉(zhuǎn)化的問題。
至少第2點是必須滿足的,有了如上幾個假設(shè),Kubernetes Service才能在Node上實現(xiàn),否則Node不通Pod IP也就實現(xiàn)不了了。
有人說既然kube-proxy是四層負載均衡,那kube-proxy應(yīng)該可以使用haproxy、nginx等作為負載后端???
事實上確實沒有問題,不過唯一需要考慮的就是性能問題,如上這些負載均衡功能都強大,但畢竟還是基于用戶態(tài)轉(zhuǎn)發(fā)或者反向代理實現(xiàn)的,性能必然不如在內(nèi)核態(tài)直接轉(zhuǎn)發(fā)處理好。
因此kube-proxy默認會優(yōu)先選擇基于內(nèi)核態(tài)的負載作為后端實現(xiàn)機制,目前kube-proxy默認是通過iptables實現(xiàn)負載的,在此之前還有一種稱為userspace模式,其實也是基于iptables實現(xiàn),可以認為當前的iptables模式是對之前userspace模式的優(yōu)化。
本節(jié)接下來將詳細介紹kube-proxy iptables模式的實現(xiàn)原理。
1.2 kube-proxy iptables模式實現(xiàn)原理
1.2.1 ClusterIP
首先創(chuàng)建了一個ClusterIP類型的Service:
# kubectl get svc -l owner=int32bit`
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes-bootcamp-v1 ClusterIP 10.106.224.41 <none> 8080/TCP 163m
其中ClusterIP為10.106.224.41,我們可以驗證這個IP在本地是不存在的:
root@ip-192-168-193-172:~# ping -c 2 -w 2 10.106.224.41
PING 10.106.224.41 (10.106.224.41) 56(84) bytes of data.
--- 10.106.224.41 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1025ms
root@ip-192-168-193-172:~# ip a | grep 10.106.224.41
root@ip-192-168-193-172:~#
所以不要嘗試去ping ClusterIP,它不可能通的。
此時在Node節(jié)點192.168.193.172上訪問該Service服務(wù),首先流量到達的是OUTPUT鏈,這里我們只關(guān)心nat表的OUTPUT鏈:
iptables-save -t nat | grep -- '-A OUTPUT'`
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
該鏈跳轉(zhuǎn)到 KUBE-SERVICES子鏈中:
# iptables-save -t nat | grep -- '-A KUBE-SERVICES'
...
-A KUBE-SERVICES ! -s 10.244.0.0/16 -d 10.106.224.41/32 -p tcp -m comment --comment "default/kubernetes-bootcamp-v1: cluster IP" -m tcp --dport 8080 -j KUBE-MARK-MASQ
-A KUBE-SERVICES -d 10.106.224.41/32 -p tcp -m comment --comment "default/kubernetes-bootcamp-v1: cluster IP" -m tcp --dport 8080 -j KUBE-SVC-RPP7DHNHMGOIIFDC
我們發(fā)現(xiàn)與之相關(guān)的有兩條規(guī)則:
第一條負責打標記MARK
0x4000/0x4000,后面會用到這個標記。第二條規(guī)則跳到
KUBE-SVC-RPP7DHNHMGOIIFDC子鏈。
其中 KUBE-SVC-RPP7DHNHMGOIIFDC子鏈規(guī)則如下:
# iptables-save -t nat | grep -- '-A KUBE-SVC-RPP7DHNHMGOIIFDC
-A KUBE-SVC-RPP7DHNHMGOIIFDC -m statistic --mode random --probability 0.33332999982 -j KUBE-SEP-FTIQ6MSD3LWO5HZX
-A KUBE-SVC-RPP7DHNHMGOIIFDC -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-SQBK6CVV7ZCKBTVI
-A KUBE-SVC-RPP7DHNHMGOIIFDC -j KUBE-SEP-IAZPHGLZVO2SWOVD
這幾條規(guī)則看起來復(fù)雜,其實實現(xiàn)的功能很簡單:
1/3的概率跳到子鏈
KUBE-SEP-FTIQ6MSD3LWO5HZX,剩下概率的1/2,(1 - 1/3) * 1/2 == 1/3,即1/3的概率跳到子鏈
KUBE-SEP-SQBK6CVV7ZCKBTVI,剩下1/3的概率跳到
KUBE-SEP-IAZPHGLZVO2SWOVD。
我們查看其中一個子鏈 KUBE-SEP-FTIQ6MSD3LWO5HZX規(guī)則:
# iptables-save -t nat | grep -- '-A KUBE-SEP-FTIQ6MSD3LWO5HZX'
...
-A KUBE-SEP-FTIQ6MSD3LWO5HZX -p tcp -m tcp -j DNAT --to-destination 10.244.1.2:8080
可見這條規(guī)則的目的是做了一次DNAT,DNAT目標為其中一個Endpoint,即Pod服務(wù)。
由此可見子鏈 KUBE-SVC-RPP7DHNHMGOIIFDC的功能就是按照概率均等的原則DNAT到其中一個Endpoint IP,即Pod IP,假設(shè)為10.244.1.2,
此時相當于:
192.168.193.172:xxxx -> 10.106.224.41:8080`
|
|
|
| DNAT
|
V
192.168.193.172:xxxX -> 10.244.1.2:8080
接著來到POSTROUTING鏈:
# iptables-save -t nat | grep -- '-A POSTROUTING'
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
# iptables-save -t nat | grep -- '-A KUBE-POSTROUTING'
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE
這兩條規(guī)則只做一件事就是只要標記了 0x4000/0x4000的包就一律做MASQUERADE(SNAT),由于10.244.1.2默認是從flannel.1轉(zhuǎn)發(fā)出去的,因此會把源IP改為flannel.1的IP 10.244.0.0。
192.168.193.172:xxxx -> 10.106.224.41:8080
|
| DNAT
V
192.168.193.172:xxxx -> 10.244.1.2:8080
|
| SNAT
V
10.244.0.0:xxxx -> 10.244.1.2:8080
剩下的就是常規(guī)的走Vxlan隧道轉(zhuǎn)發(fā)流程了,這里不再贅述,感興趣的可以參考我之前的文章淺聊幾種主流Docker網(wǎng)絡(luò)的實現(xiàn)原理。
1.2.2 NodePort
接下來研究下NodePort過程,首先創(chuàng)建如下Service:
# kubectl get svc -l owner=int32bit`
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE`
kubernetes-bootcamp-v1 NodePort 10.106.224.41 <none> 8080:30419/TCP 3h30m
其中Service的NodePort端口為30419。
假設(shè)有一個外部IP 192.168.193.197,通過 192.168.193.172:30419訪問服務(wù)。
首先到達PREROUTING鏈:
# iptables-save -t nat | grep -- '-A PREROUTING
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
# iptables-save -t nat | grep -- '-A KUBE-SERVICES'
...
-A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
PREROUTING的規(guī)則非常簡單,凡是發(fā)給自己的包,則交給子鏈 KUBE-NODEPORTS處理。注意前面省略了判斷ClusterIP的部分規(guī)則。
KUBE-NODEPORTS規(guī)則如下:
# iptables-save -t nat | grep -- '-A KUBE-NODEPORTS'
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/kubernetes-bootcamp-v1:" -m tcp --dport 30419 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "default/kubernetes-bootcamp-v1:" -m tcp --dport 30419 -j KUBE-SVC-RPP7DHNHMGOIIFDC
這個規(guī)則首先給包打上標記 0x4000/0x4000,然后交給子鏈 KUBE-SVC-RPP7DHNHMGOIIFDC處理, KUBE-SVC-RPP7DHNHMGOIIFDC剛剛已經(jīng)見面過了,其功能就是按照概率均等的原則DNAT到其中一個Endpoint IP,即Pod IP,假設(shè)為10.244.1.2。
192.168.193.197:xxxx -> 192.168.193.172:30419
|
| DNAT
V
192.168.193.197:xxxx -> 10.244.1.2:8080
此時發(fā)現(xiàn)10.244.1.2不是自己的IP,于是經(jīng)過路由判斷目標為10.244.1.2需要從flannel.1發(fā)出去。
接著到了 FORWARD鏈,
# iptables-save -t filter | grep -- '-A FORWARD'
-A FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD
# iptables-save -t filter | grep -- '-A KUBE-FORWARD'
-A KUBE-FORWARD -m conntrack --ctstate INVALID -j DROP
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
FORWARD表在這里只是判斷下,只允許打了標記 0x4000/0x4000的包才允許轉(zhuǎn)發(fā)。
最后來到 POSTROUTING鏈,這里和ClusterIP就完全一樣了,在 KUBE-POSTROUTING中做一次 MASQUERADE(SNAT),最后結(jié)果:
192.168.193.197:xxxx -> 192.168.193.172:30419
|
| DNAT
V
192.168.193.197:xxxx -> 10.244.1.2:8080
|
| SNAT
V
10.244.0.0:xxxx -> 10.244.1.2:8080
1.3 kube-proxy使用iptables存在的問題
我們發(fā)現(xiàn)基于iptables模式的kube-proxy ClusterIP和NodePort都是基于iptables規(guī)則實現(xiàn)的,我們至少發(fā)現(xiàn)存在如下幾個問題:
iptables規(guī)則復(fù)雜零亂,真要出現(xiàn)什么問題,排查iptables規(guī)則必然得掉層皮。
LOG+TRACE大法也不好使。iptables規(guī)則多了之后性能下降,這是因為iptables規(guī)則是基于鏈表實現(xiàn),查找復(fù)雜度為O(n),當規(guī)模非常大時,查找和處理的開銷就特別大。據(jù)官方說法,當節(jié)點到達5000個時,假設(shè)有2000個NodePort Service,每個Service有10個Pod,那么在每個Node節(jié)點中至少有20000條規(guī)則,內(nèi)核根本支撐不住,iptables將成為最主要的性能瓶頸。
iptables主要是專門用來做主機防火墻的,而不是專長做負載均衡的。雖然通過iptables的
statistic模塊以及DNAT能夠?qū)崿F(xiàn)最簡單的只支持概率輪詢的負載均衡,但是往往我們還需要更多更靈活的算法,比如基于最少連接算法、源地址HASH算法等。而同樣基于netfilter的ipvs卻是專門做負載均衡的,配置簡單,基于散列查找O(1)復(fù)雜度性能好,支持數(shù)十種調(diào)度算法。因此顯然ipvs比iptables更適合做kube-proxy的后端,畢竟專業(yè)的人做專業(yè)的事,物盡其美。
本文接下來將介紹kube-proxy的ipvs實現(xiàn),由于本人之前也是對ipvs很陌生,沒有用過,專門學(xué)習(xí)了下ipvs,因此在第二章簡易介紹了下ipvs,如果已經(jīng)很熟悉ipvs了,可以直接跳過,這一章和Kubernetes幾乎沒有任何關(guān)系。
另外由于本人對ipvs也是初學(xué),水平有限,難免出錯,歡迎指正!
2 IPVS 簡易入門
2.1 IPVS簡介
我們接觸比較多的是應(yīng)用層負載均衡,比如haproxy、nginx、F5等,這些負載均衡工作在用戶態(tài),因此會有對應(yīng)的進程和監(jiān)聽socket,一般能同時支持4層負載和7層負載,使用起來也比較方便。
LVS是國內(nèi)章文嵩博士開發(fā)并貢獻給社區(qū)的(章文嵩博士和他背后的負載均衡帝國 ),主要由ipvs和ipvsadm組成,ipvs是工作在內(nèi)核態(tài)的4層負載均衡,和iptables一樣都是基于內(nèi)核底層netfilter實現(xiàn),netfilter主要通過各個鏈的鉤子實現(xiàn)包處理和轉(zhuǎn)發(fā)。ipvsadm和ipvs的關(guān)系,就好比netfilter和iptables的關(guān)系,它運行在用戶態(tài),提供簡單的CLI接口進行ipvs配置。
由于ipvs工作在內(nèi)核態(tài),直接基于內(nèi)核處理包轉(zhuǎn)發(fā),所以最大的特點就是性能非常好。又由于它工作在4層,因此不會處理應(yīng)用層數(shù)據(jù),經(jīng)常有人問ipvs能不能做SSL證書卸載、或者修改HTTP頭部數(shù)據(jù),顯然這些都不可能做的。
我們知道應(yīng)用層負載均衡大多數(shù)都是基于反向代理實現(xiàn)負載的,工作在應(yīng)用層,當用戶的包到達負載均衡監(jiān)聽器listening后,基于一定的算法從后端服務(wù)列表中選擇其中一個后端服務(wù)進行轉(zhuǎn)發(fā)。當然中間可能還會有一些額外操作,最常見的如SSL證書卸載。
而ipvs工作在內(nèi)核態(tài),只處理四層協(xié)議,因此只能基于路由或者NAT進行數(shù)據(jù)轉(zhuǎn)發(fā),可以把ipvs當作一個特殊的路由器網(wǎng)關(guān),這個網(wǎng)關(guān)可以根據(jù)一定的算法自動選擇下一跳,或者把ipvs當作一個多重DNAT,按照一定的算法把ip包的目標地址DNAT到其中真實服務(wù)的目標IP。針對如上兩種情況分別對應(yīng)ipvs的兩種模式--網(wǎng)關(guān)模式和NAT模式,另外ipip模式則是對網(wǎng)關(guān)模式的擴展,本文下面會針對這幾種模式的實現(xiàn)原理進行詳細介紹。
2.2 IPVS用法
ipvsadm命令行用法和iptables命令行用法非常相似,畢竟是兄弟,比如 -L列舉, -A添加, -D刪除。
ipvsadm -A -t 192.168.193.172:32016 -s rr
但是其實ipvsadm相對iptables命令簡直太簡單了,因為沒有像iptables那樣存在各種table,table嵌套各種鏈,鏈里串著一堆規(guī)則,ipvsadm就只有兩個核心實體,分別為service和server,service就是一個負載均衡實例,而server就是后端member,ipvs術(shù)語中叫做real server,簡稱RS。
如下命令創(chuàng)建一個service實例 172.17.0.1:32016, -t指定監(jiān)聽的為 TCP端口, -s指定算法為輪詢算法rr(Round Robin),ipvs支持簡單輪詢(rr)、加權(quán)輪詢(wrr)、最少連接(lc)、源地址或者目標地址散列(sh、dh)等10種調(diào)度算法。
ipvsadm -A -t 172.17.0.1:32016 -s rr
然后把10.244.1.2:8080、10.244.1.3:8080、10.244.3.2:8080添加到service后端member中。
ipvsadm -a -t 172.17.0.1:32016 -r 10.244.1.2:8080 -m -w 1
ipvsadm -a -t 172.17.0.1:32016 -r 10.244.1.3:8080 -m -w 1
ipvsadm -a -t 172.17.0.1:32016 -r 10.244.3.2:8080 -m -w 1
其中 -t指定service實例, -r指定server地址, -w指定權(quán)值, -m即前面說的轉(zhuǎn)發(fā)模式,其中 -m表示為 masquerading,即NAT模式, -g為 gatewaying,即直連路由模式, -i為 ipip,ji即IPIP隧道模式。
與iptables-save、iptables-restore對應(yīng)的工具ipvs也有ipvsadm-save、ipvsadm-restore。
2.3 NAT(network access translation)模式
NAT模式由字面意思理解就是通過NAT實現(xiàn)的,但究竟是如何NAT轉(zhuǎn)發(fā)的,我們通過實驗環(huán)境驗證下。
現(xiàn)環(huán)境中LB節(jié)點IP為192.168.193.197,三個RS節(jié)點如下:
192.168.193.172:30620
192.168.193.194:30620
192.168.193.226:30620
為了模擬LB節(jié)點IP和RS不在同一個網(wǎng)絡(luò)的情況,在LB節(jié)點中添加一個虛擬IP地址:
ip addr add 10.222.0.1/24 dev ens5
創(chuàng)建負載均衡Service并把RS添加到Service中:
ipvsadm -A -t 10.222.0.1:8080 -s rr
ipvsadm -a -t 10.222.0.1:8080 -r 192.168.193.194:30620 -m
ipvsadm -a -t 10.222.0.1:8080 -r 192.168.193.226:30620 -m
ipvsadm -a -t 10.222.0.1:8080 -r 192.168.193.172:30620 -m
這里需要注意的是,和應(yīng)用層負載均衡如haproxy、nginx不一樣的是,haproxy、nginx進程是運行在用戶態(tài),因此會創(chuàng)建socket,本地會監(jiān)聽端口,而ipvs的負載是直接運行在內(nèi)核態(tài)的,因此不會出現(xiàn)監(jiān)聽端口:
root@ip-192-168-193-197:/var/log# netstat -lnpt
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 674/systemd-resolve
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 950/sshd
tcp6 0 0 :::22 :::* LISTEN 950/sshd
可見并沒有監(jiān)聽10.222.0.1:8080 Socket。
Client節(jié)點IP為192.168.193.226,為了和LB節(jié)點的虛擬IP 10.222.0.1通,我們手動添加靜態(tài)路由如下:
ip r add 10.222.0.1 via 192.168.193.197 dev ens5
此時Client節(jié)點能夠ping通LB節(jié)點VIP:
root@ip-192-168-193-226:~# ping -c 2 -w 2 10.222.0.1
PING 10.222.0.1 (10.222.0.1) 56(84) bytes of data.
64 bytes from 10.222.0.1: icmp_seq=1 ttl=64 time=0.345 ms
64 bytes from 10.222.0.1: icmp_seq=2 ttl=64 time=0.249 ms
--- 10.222.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1022ms
rtt min/avg/max/mdev = 0.249/0.297/0.345/0.048 ms
可見Client節(jié)點到VIP的鏈路沒有問題,那是否能夠訪問我們的Service呢?
我們驗證下:
root@ip-192-168-193-226:~# curl -m 2 --retry 1 -sSL 10.222.0.1:8080
curl: (28) Connection timed out after 2001 milliseconds
非常意外的結(jié)果是并不通。
在RS節(jié)點抓包如下:
我們發(fā)現(xiàn)數(shù)據(jù)包的源IP為Client IP,目標IP為RS IP,換句話說,LB節(jié)點IPVS只做了DNAT,把目標IP改成RS IP了,而沒有修改源IP。此時雖然RS和Client在同一個子網(wǎng),鏈路連通性沒有問題,但是由于Client節(jié)點發(fā)出去的包的目標IP和收到的包源IP不一致,因此會被直接丟棄,相當于給張三發(fā)信,李四回的信,顯然不受信任。
既然IPVS沒有給我們做SNAT,那自然想到的是我們手動做SNAT,在LB節(jié)點添加如下iptables規(guī)則:
iptables -t nat -A POSTROUTING -m ipvs --vaddr 10.222.0.1 --vport 8080 -j LOG --log-prefix '[int32bit ipvs]'
iptables -t nat -A POSTROUTING -m ipvs --vaddr 10.222.0.1 --vport 8080 -j MASQUERADE
再次檢查Service是否可以訪問:
root@ip-192-168-193-226:~# curl -m 2 --retry 1 -sSL 10.222.0.1:8080
curl: (28) Connection timed out after 2001 milliseconds
服務(wù)依然不通。并且在LB節(jié)點的iptables日志為空:
root@ip-192-168-193-197:~# cat /var/log/syslog | grep 'int32bit ipvs'
root@ip-192-168-193-197:~#
也就是說,ipvs的包根本不會經(jīng)過iptables nat表POSTROUTING鏈?
那mangle表呢?我們打開LOG查看下:
iptables -t mangle -A POSTROUTING -m ipvs --vaddr 10.222.0.1 --vport 8080 -j LOG --log-prefix "[int32bit ipvs]"
此時查看日志如下:
我們發(fā)現(xiàn)在mangle表中可以看到DNAT后的包。
只是mangle表的POSTROUTING并不支持NAT功能:
對比Kubernetes配置發(fā)現(xiàn)需要設(shè)置如下系統(tǒng)參數(shù):
sysctl net.ipv4.vs.conntrack=1
再次驗證:
root@ip-192-168-193-226:~# curl -i 10.222.0.1:8080
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Wed, 27 Nov 2019 15:28:06 GMT
Connection: keep-alive
Transfer-Encoding: chunked
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-v1-c5ccf9784-g9bkx | v=1
終于通了,查看RS抓包:
如期望,修改了源IP為LB IP。
原來需要配置 net.ipv4.vs.conntrack=1參數(shù),這個問題折騰了一個晚上,不得不說目前ipvs的文檔都太老了。
前面是通過手動iptables實現(xiàn)SNAT的,性能可能會有損耗,于是如下開源項目通過修改lvs直接做SNAT:
小米運維部在LVS的FULLNAT基礎(chǔ)上,增加了SNAT網(wǎng)關(guān)功能,參考xiaomi-sa/dsnat
lvs-snat
除了SNAT的辦法,是否還有其他辦法呢?想想我們最初的問題,Client節(jié)點發(fā)出去的包的目標IP和收到的包源IP不一致導(dǎo)致包被丟棄,那解決問題的辦法就是把包重新引到LB節(jié)點上,只需要在所有的RS節(jié)點增加如下路由即可:
ip r add 192.168.193.226 via 192.168.193.197 dev ens5
此時我們再次檢查我們的Service是否可連接:
root@ip-192-168-193-226:~# curl -i -m 2 --retry 1 -sSL 10.222.0.1:8080
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Wed, 27 Nov 2019 03:21:47 GMT
Connection: keep-alive
Transfer-Encoding: chunked
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-v1-c5ccf9784-4v9z4 | v=1
結(jié)果沒有問題。
不過我們是通過手動添加Client IP到所有RS的明細路由實現(xiàn)的,如果Client不固定,這種方案仍然不太可行,所以通常做法是干脆把所有RS默認路由指向LB節(jié)點,即把LB節(jié)點當作所有RS的默認網(wǎng)關(guān)。
由此可知,用戶通過LB地址訪問服務(wù),LB節(jié)點IPVS會把用戶的目標IP由LB IP改為RS IP,源IP不變,包不經(jīng)過iptables的OUTPUT直接到達POSTROUTING轉(zhuǎn)發(fā)出去,包回來的時候也必須先到LB節(jié)點,LB節(jié)點把目標IP再改成用戶的源IP,最后轉(zhuǎn)發(fā)給用戶。
顯然這種模式來回都需要經(jīng)過LB節(jié)點,因此又稱為雙臂模式。
2.4 網(wǎng)關(guān)(Gatewaying)模式
網(wǎng)關(guān)模式(Gatewaying)又稱為直連路由模式(Direct Routing)、透傳模式,所謂透傳即LB節(jié)點不會修改數(shù)據(jù)包的源IP、端口以及目標IP、端口,LB節(jié)點做的僅僅是路由轉(zhuǎn)發(fā)出去,可以把LB節(jié)點看作一個特殊的路由器網(wǎng)關(guān),而RS節(jié)點則是網(wǎng)關(guān)的下一跳,這就相當于對于同一個目標地址,會有多個下一跳,這個路由器網(wǎng)關(guān)的特殊之處在于能夠根據(jù)一定的算法選擇其中一個RS作為下一跳,達到負載均衡和冗余的效果。
既然是通過直連路由的方式轉(zhuǎn)發(fā),那顯然LB節(jié)點必須與所有的RS節(jié)點在同一個子網(wǎng),不能跨子網(wǎng),否則路由不可達。換句話說,這種模式只支持內(nèi)部負載均衡(Internal LoadBalancer)。
另外如前面所述,LB節(jié)點不會修改源端口和目標端口,因此這種模式也無法支持端口映射,換句話說LB節(jié)點監(jiān)聽的端口和所有RS節(jié)點監(jiān)聽的端口必須一致。
現(xiàn)在假設(shè)有LB節(jié)點IP為 192.168.193.197,有三個RS節(jié)點如下:
192.168.193.172:30620
192.168.193.194:30620
192.168.193.226:30620
創(chuàng)建負載均衡Service并把RS添加到Service中:
ipvsadm -A -t 192.168.193.197:30620 -s rr
ipvsadm -a -t 192.168.193.197:30620 -r 192.168.193.194:30620 -g
ipvsadm -a -t 192.168.193.197:30620 -r 192.168.193.226:30620 -g
ipvsadm -a -t 192.168.193.197:30620 -r 192.168.193.172:30620 -g
注意到我們的Service監(jiān)聽的端口30620和RS的端口是一樣的,并且通過 -g參數(shù)指定為直連路由模式(網(wǎng)關(guān)模式)。
Client節(jié)點IP為192.168.193.226,我們驗證Service是否可連接:
root@ip-192-168-193-226:~# curl -m 5 -sSL 192.168.193.197:30620`
curl: (28) Connection timed out after 5001 milliseconds`
我們發(fā)現(xiàn)并不通,在其中一個RS節(jié)點192.168.193.172上抓包:
正如前面所說,LB是通過路由轉(zhuǎn)發(fā)的,根據(jù)路由的原理,源MAC地址修改為LB的MAC地址,而目標MAC地址修改為RS MAC地址,相當于RS是LB的下一跳。
并且源IP和目標IP都不會修改。問題就來了,我們Client期望訪問的是RS,但RS收到的目標IP卻是LB的IP,發(fā)現(xiàn)這個目標IP并不是自己的IP,因此不會通過INPUT鏈轉(zhuǎn)發(fā)到用戶空間,這時要不直接丟棄這個包,要不根據(jù)路由再次轉(zhuǎn)發(fā)到其他地方,總之兩種情況都不是我們期望的結(jié)果。
那怎么辦呢?為了讓RS接收這個包,必須得讓RS有這個目標IP才行。于是不妨在lo上添加個虛擬IP,IP地址偽裝成LB IP 192.168.193.197:
ifconfig lo:0 192.168.193.197/32
問題又來了,這就相當于有兩個相同的IP,IP重復(fù)了怎么辦?辦法是隱藏這個虛擬網(wǎng)卡,不讓它回復(fù)ARP,其他主機的neigh也就不可能知道有這么個網(wǎng)卡的存在了,參考Using arp announce/arp ignore to disable ARP。
sysctl net.ipv4.conf.lo.arp_ignore=1
sysctl net.ipv4.conf.lo.arp_announce=2
此時再次從客戶端curl:
root@ip-192-168-193-226:~# curl -m 2 --retry 1 -sSL 192.168.193.197:30620
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-v1-c5ccf9784-4v9z4 | v=1
終于通了。
我們從前面的抓包中知道,源IP為Client IP 192.168.193.226,因此直接回包給Client即可,不可能也不需要再回到LB節(jié)點了,即A->B,B->C,C->A,流量方向是三角形狀的,因此這種模式又稱為三角模式。
我們從原理中不難得出如下結(jié)論:
Client、LB以及所有的RS必須在同一個子網(wǎng)。
LB節(jié)點直接通過路由轉(zhuǎn)發(fā),因此性能非常高。
不能做端口映射。
2.5 ipip隧道模式
前面介紹了網(wǎng)關(guān)直連路由模式,要求所有的節(jié)點在同一個子網(wǎng),而ipip隧道模式則主要解決這種限制,LB節(jié)點IP和RS可以不在同一個子網(wǎng),此時需要通過ipip隧道進行傳輸。
現(xiàn)在假設(shè)有LB節(jié)點IP為 192.168.193.77/25,在該節(jié)點上增加一個VIP地址:
ip addr add 192.168.193.48/25 dev eth0
有三個RS節(jié)點如下:
192.168.193.172:30620
192.168.193.194:30620
192.168.193.226:30620
如上三個RS節(jié)點子網(wǎng)掩碼均為255.255.255.128,即25位子網(wǎng),顯然和VIP 192.168.193.48/25不在同一個子網(wǎng)。
創(chuàng)建負載均衡Service并把RS添加到Service中:
ipvsadm -A -t 192.168.193.48:30620 -s rr
ipvsadm -a -t 192.168.193.48:30620 -r 192.168.193.194:30620 -i
ipvsadm -a -t 192.168.193.48:30620 -r 192.168.193.226:30620 -i
ipvsadm -a -t 192.168.193.48:30620 -r 192.168.193.172:30620 -i
注意到我們的Service監(jiān)聽的端口30620和RS的端口是一樣的,并且通過 -i參數(shù)指定為ipip隧道模式。
在所有的RS節(jié)點上加載ipip模塊以及添加VIP(和直連路由類型):
modprobe ipip
ifconfig tunl0 192.168.193.48/32
sysctl net.ipv4.conf.tunl0.arp_ignore=1
sysctl net.ipv4.conf.tunl0.arp_announce=2
Client節(jié)點IP為192.168.193.226/25,我們驗證Service是否可連接:
root@ip-192-168-193-226:~# curl -i -sSL 192.168.193.48:30620
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Wed, 27 Nov 2019 07:05:40 GMT
Connection: keep-alive
Transfer-Encoding: chunked
Hello Kubernetes bootcamp! | Running on: kubernetes-bootcamp-v1-c5ccf9784-dgn74 | v=1
root@ip-192-168-193-226:~#`
Service可訪問,我們在RS節(jié)點上抓包如下:
我們發(fā)現(xiàn)和直連路由一樣,源IP和目標IP沒有修改。
所以IPIP模式和網(wǎng)關(guān)(Gatewaying)模式原理基本一樣,唯一不同的是網(wǎng)關(guān)(Gatewaying)模式要求所有的RS節(jié)點和LB節(jié)點在同一個子網(wǎng),而IPIP模式則可以支持跨子網(wǎng)的情況,為了解決跨子網(wǎng)通信問題,使用了ipip隧道進行數(shù)據(jù)傳輸。
2.4 總結(jié)
ipvs是一個內(nèi)核態(tài)的四層負載均衡,支持NAT、Gateway以及IPIP隧道模式,Gateway模式性能最好,但LB和RS不能跨子網(wǎng),IPIP性能次之,通過ipip隧道解決跨網(wǎng)段傳輸問題,因此能夠支持跨子網(wǎng)。而NAT模式?jīng)]有限制,這也是唯一一種支持端口映射的模式。
我們不難猜想,由于Kubernetes Service需要使用端口映射功能,因此kube-proxy必然只能使用ipvs的NAT模式。
3 kube-proxy使用ipvs模式
3.1 配置kube-proxy使用ipvs模式
使用kubeadm安裝Kubernetes可參考文檔Cluster Created by Kubeadm,不過這個文檔的安裝配置有問題kubeadm #1182,如下官方配置不生效:
---
kubeProxy:
config:
featureGates:
SupportIPVSProxyMode: true
mode: ipvs
...
需要修改為如下配置:
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs
可以通過如下命令確認kube-proxy是否修改為ipvs:
# kubectl get configmaps kube-proxy -n kube-system -o yaml | awk '/mode/{print $2}'
ipvs
3.2 Service ClusterIP原理
創(chuàng)建一個ClusterIP類似的Service如下:
# kubectl get svc | grep kubernetes-bootcamp-v1
kubernetes-bootcamp-v1 ClusterIP 10.96.54.11 <none> 8080/TCP 2m11s
ClusterIP 10.96.54.11為我們查看ipvs配置如下:
# ipvsadm -S -n | grep 10.96.54.11
-A -t 10.96.54.11:8080 -s rr
-a -t 10.96.54.11:8080 -r 10.244.1.2:8080 -m -w 1
-a -t 10.96.54.11:8080 -r 10.244.1.3:8080 -m -w 1
-a -t 10.96.54.11:8080 -r 10.244.2.2:8080 -m -w 1
可見ipvs的LB IP為ClusterIP,算法為rr,RS為Pod的IP。
另外我們發(fā)現(xiàn)使用的模式為NAT模式,這是顯然的,因為除了NAT模式支持端口映射,其他兩種均不支持端口映射,所以必須選擇NAT模式。
由前面的理論知識,ipvs的VIP必須在本地存在,我們可以驗證:
# ip addr show kube-ipvs0
kube-ipvs0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default
link/ether 46:6b:9e:af:b0:60 brd ff:ff:ff:ff:ff:ff
inet 10.96.0.1/32 brd 10.96.0.1 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.96.0.10/32 brd 10.96.0.10 scope global kube-ipvs0
valid_lft forever preferred_lft forever
inet 10.96.54.11/32 brd 10.96.54.11 scope global kube-ipvs0
valid_lft forever preferred_lft forever
# ethtool -i kube-ipvs0 | grep driver
1driver: dummy
可見kube-proxy首先會創(chuàng)建一個dummy虛擬網(wǎng)卡kube-ipvs0,然后把所有的Service IP添加到kube-ipvs0中。
我們知道基于iptables的Service,ClusterIP是一個虛擬的IP,因此這個IP是ping不通的,但ipvs中這個IP是在每個節(jié)點上真實存在的,因此可以ping通:
當然由于這個IP就是配置在本地虛擬網(wǎng)卡上,所以對診斷問題沒有一點用處的。
我們接下來研究下ClusterIP如何傳遞的。
當我們通過如下命令連接服務(wù)時:
curl 10.96.54.11:8080
此時由于10.96.54.11就在本地,所以會以這個IP作為出口地址,即源IP和目標IP都是10.96.54.11,此時相當于:
10.96.54.11:xxxx -> 10.96.54.11:8080
其中xxxx為隨機端口。
然后經(jīng)過ipvs,ipvs會從RS ip列中選擇其中一個Pod ip作為目標IP,假設(shè)為10.244.2.2:
10.96.54.11:xxxx -> 10.96.54.11:8080
|
| IPVS
v
10.96.54.11:xxxx -> 10.244.2.2:8080
我們從iptables LOG可以驗證:
我們查看OUTPUT安全組規(guī)則如下:
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
其中ipset集合 KUBE-CLUSTER-IP保存著所有的ClusterIP以及監(jiān)聽端口。
如上規(guī)則的意思就是除了Pod以外訪問ClusterIP的包都打上 0x4000/0x4000。
到了POSTROUTING鏈:
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE
如上規(guī)則的意思就是只要匹配mark 0x4000/0x4000的包都做SNAT,由于10.244.2.2是從flannel.1出去的,因此源ip會改成flannel.1的ip 10.244.0.0:
10.96.54.11:xxxx -> 10.96.54.11:8080
|
| IPVS
v
10.96.54.11:xxxx -> 10.244.2.2:8080
|
| MASQUERADE
v
10.244.0.0:xxxx -> 10.244.2.2:8080`
最后通過Vxlan 隧道發(fā)到Pod的Node上,轉(zhuǎn)發(fā)給Pod的veth,回包通過路由到達源Node節(jié)點,源Node節(jié)點通過之前的MASQUERADE再把目標IP還原為10.96.54.11。
3.3 NodeIP實現(xiàn)原理
查看Service如下:
root@ip-192-168-193-172:~# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 30h
kubernetes-bootcamp-v1 NodePort 10.96.54.11 <none> 8080:32016/TCP 8h
Service kubernetes-bootcamp-v1的NodePort為32016。
現(xiàn)在假設(shè)集群外的一個IP 192.168.193.197訪問192.168.193.172:32016:
192.168.193.197:xxxx -> 192.168.193.172:32016
最先到達PREROUTING鏈:
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A KUBE-SERVICES -m addrtype --dst-type LOCAL -j KUBE-NODE-PORT
-A KUBE-NODE-PORT -p tcp -m comment --comment "Kubernetes nodeport TCP port for masquerade purpose" -m set --match-set KUBE-NODE-PORT-TCP dst -j KUBE-MARK-MASQ
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
如上4條規(guī)則看起來復(fù)雜,其實就做一件事,如果目標地址為NodeIP,則把包標記 0x4000, 0x4000。
我們查看ipvs:
# ipvsadm -S -n | grep 32016
-A -t 192.168.193.172:32016 -s rr
-a -t 192.168.193.172:32016 -r 10.244.1.2:8080 -m -w 1
-a -t 192.168.193.172:32016 -r 10.244.1.3:8080 -m -w 1
-a -t 192.168.193.172:32016 -r 10.244.3.2:8080 -m -w 1
我們發(fā)現(xiàn)和ClusterIP實現(xiàn)原理非常相似,ipvs Service的VIP為Node IP,端口為NodePort。ipvs會選擇其中一個Pod IP作為DNAT目標,這里假設(shè)為10.244.3.2:
192.168.193.197:xxxx -> 192.168.193.172:32016
|
| DNAT
v
192.168.193.197:xxx --> 10.244.3.2:8080
剩下的到了POSTROUTING鏈就和Service ClusterIP完全一樣了,只要匹配 0x4000/0x4000的包就會做SNAT。
3.4 總結(jié)
Kubernetes的ClusterIP和NodePort都是通過ipvs service實現(xiàn)的,Pod當作ipvs service的server,通過NAT MQSQ實現(xiàn)轉(zhuǎn)發(fā)。
簡單來說kube-proxy主要在所有的Node節(jié)點做如下三件事:
如果沒有dummy類型虛擬網(wǎng)卡,則創(chuàng)建一個,默認名稱為
kube-ipvs0;把Kubernetes ClusterIP地址添加到
kube-ipvs0,同時添加到ipset中。創(chuàng)建ipvs service,ipvs service地址為ClusterIP以及Cluster Port,ipvs server為所有的Endpoint地址,即Pod IP及端口。
使用ipvs作為kube-proxy后端,不僅提高了轉(zhuǎn)發(fā)性能,結(jié)合ipset還使iptables規(guī)則變得更“干凈”清楚,從此再也不怕iptables。
更多關(guān)于kube-proxy ipvs參考IPVS-Based In-Cluster Load Balancing Deep Dive.
4 總結(jié)
本文首先介紹了kube-proxy的功能以及kube-proxy基于iptables的實現(xiàn)原理,然后簡單介紹了ipvs,了解了ipvs支持的三種轉(zhuǎn)發(fā)模式,最后介紹了kube-proxy基于ipvs的實現(xiàn)原理。
ipvs是專門設(shè)計用來做內(nèi)核態(tài)四層負載均衡的,由于使用了hash表的數(shù)據(jù)結(jié)構(gòu),因此相比iptables來說性能會更好?;趇pvs實現(xiàn)Service轉(zhuǎn)發(fā),Kubernetes幾乎能夠具備無限的水平擴展能力。隨著Kubernetes的部署規(guī)模越來越大,應(yīng)用越來越廣泛,ipvs必然會取代iptables成為Kubernetes Service的默認實現(xiàn)后端。