IPVS從入門到精通kube-proxy實現(xiàn)原理(轉(zhuǎn))

本文有點長,但是筆者還是忍不住想轉(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ò)模型,均需要保證滿足如下三個條件:

  1. 容器之間要求不需要任何NAT能直接通信;

  2. 容器與Node之間要求不需要任何NAT能直接通信;

  3. 容器看到自身的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模式, -ggatewaying,即直連路由模式, -iipip,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é)點抓包如下:

image

我們發(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]"

此時查看日志如下:

image

我們發(fā)現(xiàn)在mangle表中可以看到DNAT后的包。

只是mangle表的POSTROUTING并不支持NAT功能:

image

對比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抓包:

image

如期望,修改了源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上抓包:

image

正如前面所說,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é)點上抓包如下:

image

我們發(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通:

image

當然由于這個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可以驗證:

image

我們查看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,則把包標記 0x40000x4000。

我們查看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é)點做如下三件事:

  1. 如果沒有dummy類型虛擬網(wǎng)卡,則創(chuàng)建一個,默認名稱為 kube-ipvs0;

  2. 把Kubernetes ClusterIP地址添加到 kube-ipvs0,同時添加到ipset中。

  3. 創(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)后端。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容