Kubernetes 叢集網路從懵圈到熟悉,看這一篇就夠了

語言: CN / TW / HK

在 Kubernetes 中要保證容器之間網路互通,網路至關重要。而Kubernetes本身並沒有自己實現容器網路,而是通過外掛化的方式自由接入進來。在容器網路接入進來需要滿足如下基本原則:

  • Pod 無論執行在任何節點都可以互相直接通訊,而不需要藉助NAT地址轉換實現。

  • Node 與 Pod 可以互相通訊,在不限制的前提下,Pod 可以訪問任意網路。

  • Pod 擁有獨立的網路棧,Pod 看到自己的地址和外部看見的地址應該是一樣的,並且同個 Pod 內所有的容器共享同個網路棧。

容器網路基礎

一個 Linux 容器的網路棧是被隔離在它自己的 Network Namespace 中,Network Namespace 包括了:網絡卡(Network Interface),迴環裝置(Lookback Device),路由表(Routing Table)和 iptables 規則,對於服務程序來講這些就構建了它發起請求和相應的基本環境。而要實現一個容器網路,離不開以下Linux網路功能:

  • 網路名稱空間: 將獨立的網路協議棧隔離到不同的命令空間中,彼此間無法通訊

  • Veth Pair: Veth裝置對的引入是為了實現在不同網路名稱空間的通訊,總是以兩張虛擬網絡卡(veth peer)的形式成對出現的。並且,從其中一端發出的資料,總是能在另外一端收到

  • Iptables/Netfilter: Netfilter負責在核心中執行各種掛接的規則(過濾、修改、丟棄等),執行在核心中;Iptables模式是在使用者模式下執行的程序,負責協助維護核心中Netfilter的各種規則表;通過二者的配合來實現整個Linux網路協議棧中靈活的資料包處理機制

  • 網橋: 網橋是一個二層網路虛擬裝置,類似交換機,主要功能是通過學習而來的Mac地址將資料幀轉發到網橋的不同埠上

  • 路由: Linux系統包含一個完整的路由功能,當IP層在處理資料傳送或轉發的時候,會使用路由表來決定發往哪裡

基於以上的基礎,同宿主機的容器時間如何通訊呢?

我們可以簡單把他們理解成兩臺主機,主機之間通過網線連線起來,如果要多臺主機通訊,我們通過交換機就可以實現彼此互通,在linux中,我們可以通過網橋來轉發資料。

在容器中,以上的實現是通過docker0網橋,凡是連線到docker0的容器,就可以通過它來進行通訊。要想容器能夠連線到docker0網橋,我們也需要類似網線的虛擬裝置Veth Pair來把容器連線到網橋上。

我們啟動一個容器:

docker run -d --name c1 hub.pri.ibanyu.com/devops/alpine:v3.8 /bin/sh

然後檢視網絡卡裝置:

 docker exec -it c1  /bin/sh
/ # ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:AC:11:00:02
inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:14 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:1172 (1.1 KiB) TX bytes:0 (0.0 B)


lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)


/ # route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0

可以看到其中有一張eth0的網絡卡,它就是veth peer其中的一端的虛擬網絡卡。然後通過route -n 檢視容器中的路由表,eth0也正是預設路由出口。所有對172.17.0.0/16網段的請求都會從eth0出去。

我們再來看Veth peer的另一端,我們檢視宿主機的網路裝置:

ifconfig
docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
inet6 fe80::42:6aff:fe46:93d2 prefixlen 64 scopeid 0x20<link>
ether 02:42:6a:46:93:d2 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 8 bytes 656 (656.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0


eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 10.100.0.2 netmask 255.255.255.0 broadcast 10.100.0.255
inet6 fe80::5400:2ff:fea3:4b44 prefixlen 64 scopeid 0x20<link>
ether 56:00:02:a3:4b:44 txqueuelen 1000 (Ethernet)
RX packets 7788093 bytes 9899954680 (9.2 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 5512037 bytes 9512685850 (8.8 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0


lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 (Local Loopback)
RX packets 32 bytes 2592 (2.5 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 32 bytes 2592 (2.5 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0


veth20b3dac: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::30e2:9cff:fe45:329 prefixlen 64 scopeid 0x20<link>
ether 32:e2:9c:45:03:29 txqueuelen 0 (Ethernet)
RX packets 0 bytes 0 (0.0 B)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 8 bytes 656 (656.0 B)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

我們可以看到,容器對應的 Veth peer 另一端是宿主機上的一塊虛擬網絡卡叫veth20b3dac,並且可以通過brctl 檢視網橋資訊看到這張網絡卡是在 docker0 上。

# brctl show
docker0 8000.02426a4693d2 no veth20b3dac

然後我們再啟動一個容器,從第一個容器是否能 ping 通第二個容器。

$ docker run -d --name c2 -it hub.pri.ibanyu.com/devops/alpine:v3.8 /bin/sh
$ docker exec -it c1 /bin/sh
/ # ping 172.17.0.3
PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.291 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.129 ms
64 bytes from 172.17.0.3: seq=2 ttl=64 time=0.142 ms
64 bytes from 172.17.0.3: seq=3 ttl=64 time=0.169 ms
64 bytes from 172.17.0.3: seq=4 ttl=64 time=0.194 ms
^C
--- 172.17.0.3 ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 0.129/0.185/0.291 ms

可以看到,能夠ping通,其原理就是我們ping 目標IP172.17.0.3時,會匹配到我們的路由表第二條規則,閘道器為0.0.0.0,這就意味著是一條直連路由,通過二層轉發到目的地。

要通過二層網路到達172.17.0.3,我們需要知道它的Mac地址,此時就需要第一個容器傳送一個ARP廣播,來通過IP地址查詢Mac。此時Veth peer另外一段是docker0網橋,它會廣播到所有連線它的veth peer 虛擬網絡卡去,然後正確的虛擬網絡卡收到後會響應這個ARP報文,然後網橋再回給第一個容器。

以上就是同宿主機不同容器通過 docker0 通訊,如下圖所示:

預設情況下,通過 network namespace 限制的容器程序,本質上是通過Veth peer裝置和宿主機網橋的方式,實現了不同network namespace 的資料交換。

與之類似地,當你在一臺宿主機上,訪問該宿主機上的容器的 IP 地址時,這個請求的資料包,也是先根據路由規則到達 docker0 網橋,然後被轉發到對應的 Veth Pair 裝置,最後出現在容器裡。

跨主機網路通訊

在 Docker 的預設配置下,不同宿主機上的容器通過 IP 地址進行互相訪問是根本做不到的。為了解決這個問題,社群中出現了很多網路方案。同時k8s為了更好的控制網路的接入,推出了CNI即容器網路的API介面。它是k8s中標準的一個呼叫網路實現的介面,kubelet通過這個API來呼叫不同的網路外掛以實現不同的網路配置,實現了這個介面的就是CNI外掛,它實現了一系列的CNI API介面。目前已經有的包括flannel、calico、weave、contiv等等。

實際上CNI的容器網路通訊流程跟前面的基礎網路一樣,只是CNI維護了一個單獨的網橋來代替 docker0。這個網橋的名字就叫作:CNI 網橋,它在宿主機上的裝置名稱預設是:cni0。cni的設計思想,就是:Kubernetes 在啟動 Infra 容器之後,就可以直接呼叫 CNI 網路外掛,為這個 Infra 容器的 Network Namespace,配置符合預期的網路棧。

CNI外掛三種網路實現模式:

  • overlay 模式是基於隧道技術實現的,整個容器網路和主機網路獨立,容器之間跨主機通訊時將整個容器網路封裝到底層網路中,然後到達目標機器後再解封裝傳遞到目標容器。不依賴與底層網路的實現。實現的外掛有flannel(UDP、vxlan)、calico(IPIP)等等

  • 三層路由模式中容器和主機也屬於不通的網段,他們容器互通主要是基於路由表打通,無需在主機之間建立隧道封包。但是限制條件必須依賴大二層同個區域網內。實現的外掛有flannel(host-gw)、calico(BGP)等等

  • underlay網路是底層網路,負責互聯互通。容器網路和主機網路依然分屬不同的網段,但是彼此處於同一層網路,處於相同的地位。整個網路三層互通,沒有大二層的限制,但是需要強依賴底層網路的實現支援.實現的外掛有calico(BGP)等等

我們看下路由模式的一種實現flannel Host-gw:

如圖可以看到當node1上container-1要發資料給node2上的container2時,會匹配到如下的路由表規則:

10.244.1.0/24 via 10.168.0.3 dev eth0

表示前往目標網段10.244.1.0/24的IP包,需要經過本機eth0出去發往的下一跳ip地址為10.168.0.3(node2).然後到達10.168.0.3以後再通過路由錶轉發cni網橋,進而進入到container2。

以上可以看到host-gw工作原理,其實就是在每個node節點配置到每個pod網段的下一跳為pod網段所在的node節點IP,pod網段和node節點ip的對映關係,flannel儲存在etcd或者k8s中。flannel只需要watch 這些資料的變化來動態更新路由表即可.

這種網路模式最大的好處就是避免了額外的封包和解包帶來的網路效能損耗。缺點我們也能看見主要就是容器ip包通過下一跳出去時,必須要二層通訊封裝成資料幀傳送到下一跳。如果不在同個二層區域網,那麼就要交給三層閘道器,而此時閘道器是不知道目標容器網路的(也可以靜態在每個閘道器配置pod網段路由)。所以flannel host-gw必須要求叢集宿主機是二層互通的。

而為了解決二層互通的限制性,calico提供的網路方案就可以更好的實現,calico 大三層網路模式與flannel 提供的類似,也會在每臺宿主機新增如下格式的路由規則:

<目標容器IP網段> via <閘道器的IP地址> dev eth0

其中閘道器的IP地址不通場景有不同的意思,如果宿主機是二層可達那麼就是目的容器所在的宿主機的IP地址,如果是三層不同區域網那麼就是本機宿主機的閘道器IP(交換機或者路由器地址)。

不同於flannel通過k8s或者etcd儲存的資料來維護本機路由資訊的做法,calico是通過BGP動態路由協議來分發整個叢集路由資訊。

BGP全稱是 Border Gateway Protocol邊界閘道器協議,linxu原生支援的、專門用於在大規模資料中心為不同的自治系統之間傳遞路由資訊。只要記住BGP簡單理解其實就是實現大規模網路中節點路由資訊同步共享的一種協議。而BGP這種協議就能代替flannel 維護主機路由表功能。

calico 主要由三個部分組成:

  • calico cni外掛: 主要負責與kubernetes對接,供kubelet呼叫使用。

  • felix: 負責維護宿主機上的路由規則、FIB轉發資訊庫等。

  • BIRD: 負責分發路由規則,類似路由器。

  • confd: 配置管理元件。

除此之外,calico還和flannel host-gw不同之處在於,它不會建立網橋裝置,而是通過路由表來維護每個pod的通訊,如下圖所示:

可以看到calico 的cni外掛會為每個容器設定一個veth pair裝置,然後把另一端接入到宿主機網路空間,由於沒有網橋,cni外掛還需要在宿主機上為每個容器的veth pair裝置配置一條路由規則,用於接收傳入的IP包,路由規則如下:

10.92.77.163 dev cali93a8a799fe1 scope link

以上表示傳送10.92.77.163的IP包應該發給cali93a8a799fe1裝置,然後到達另外一段容器中。

有了這樣的veth pair裝置以後,容器發出的IP包就會通過veth pair裝置到達宿主機,然後宿主機根據路有規則的下一條地址,傳送給正確的閘道器(10.100.1.3),然後到達目標宿主機,在到達目標容器.

10.92.160.0/23 via 10.106.65.2 dev bond0 proto bird

這些路由規則都是felix維護配置的,而路由資訊則是calico bird元件基於BGP分發而來。calico實際上是將叢集裡所有的節點都當做邊界路由器來處理,他們一起組成了一個全互聯的網路,彼此之間通過BGP交換路由,這些節點我們叫做BGP Peer。

需要注意的是calico 維護網路的預設模式是 node-to-node mesh ,這種模式下,每臺宿主機的BGP client都會跟叢集所有的節點BGP client進行通訊交換路由。這樣一來,隨著節點規模數量N的增加,連線會以N的2次方增長,會叢集網路本身帶來巨大壓力。

所以一般這種模式推薦的叢集規模在50節點左右,超過50節點推薦使用另外一種RR(Router Reflector)模式,這種模式下,calico 可以指定幾個節點作為RR,他們負責跟所有節點BGP client建立通訊來學習叢集所有的路由,其他節點只需要跟RR節點交換路由即可。這樣大大降低了連線數量,同時為了叢集網路穩定性,建議RR>=2.

以上的工作原理依然是在二層通訊,當我們有兩臺宿主機,一臺是10.100.0.2/24,節點上容器網路是10.92.204.0/24;另外一臺是10.100.1.2/24,節點上容器網路是10.92.203.0/24,此時兩臺機器因為不在同個二層所以需要三層路由通訊,這時calico就會在節點上生成如下路由表:

10.92.203.0/23 via 10.100.1.2 dev eth0 proto bird

這時候問題就來了,因為 10.100.1.2 跟我們 10.100.0.2 不在同個子網,是不能二層通訊的。這之後就需要使用 Calico IPIP 模式,當宿主機不在同個二層網路時就是用overlay網路封裝以後再發出去。如下圖所示:

IPIP 模式下在非二層通訊時,calico 會在node節點新增如下路由規則:

10.92.203.0/24 via 10.100.1.2 dev tunnel0

可以看到儘管下一條任然是 node 的 IP 地址,但是出口裝置卻是 tunnel0,其是一個IP隧道裝置,主要有 Linux 核心的 IPIP 驅動實現。會將容器的 ip 包直接封裝宿主機網路的IP包中,這樣到達 node2 以後再經過 IPIP 驅動拆包拿到原始容器IP包,然後通過路由規則傳送給veth pair裝置到達目標容器。

以上儘管可以解決非二層網路通訊,但是仍然會因為封包和解包導致效能下降。如果calico 能夠讓宿主機之間的 router 裝置也學習到容器路由規則,這樣就可以直接三層通訊了。比如在路由器新增如下的路由表:

10.92.203.0/24 via 10.100.1.2 dev interface1

而node1新增如下的路由表:

10.92.203.0/24 via 10.100.1.1 dev tunnel0

那麼 node1 上的容器發出的 IP 包,基於本地路由表傳送給 10.100.1.1 閘道器路由器,然後路由器收到 IP 包檢視目的IP,通過本地路由表找到下一跳地址傳送到 node2,最終到達目的容器。這種方案,我們是可以基於underlay 網路來實現,只要底層支援 BGP 網路,可以和我們 RR 節點建立 EBGP 關係來交換叢集內的路由資訊。

以上就是 kubernetes 常用的幾種網路方案了,在公有云場景下一般用雲廠商提供的或者使用 flannel host-gw 這種更簡單,而私有物理機房環境中,Calico 專案更加適合。根據自己的實際場景,再選擇合適的網路方案。

參考

  • https://github.com/coreos/flannel/blob/master/Documentation/backends.md

  • https://coreos.com/flannel/

  • https://docs.projectcalico.org/getting-started/kubernetes/

  • https://www.kancloud.cn/willseecloud/kubernetes-handbook/1321338

來源:https://tech.ipalfish.com/blog/2020/03/06/kubernetes_container_network/

XOPS 風向標!GOPS 全球運維大會 2021 · 深圳站,5月21-22日,近 50 位專家已集結完畢,文末掃碼立即購票~

長按二維碼,進入大會官網 :arrow_down:

近期好文:

深度好文:雲網絡丟包故障定位,看這一篇就夠了~

“高效運維”公眾號誠邀廣大技術人員投稿,

投稿郵箱:[email protected],或新增聯絡人微信:greatops1118.

點個“在看”,一年不宕機