理解k8s的Flannel網絡

此文始發於我的簡書博客: k8s的Flannel網絡,特此聲明

引: 之前寫過一篇文章介紹如何管理linux設備上的bridge(網橋)和docker bridge, 今天我們來看看k8s的網絡模型。

我們先來看圖示例,下面則個是k8s的網絡模型圖。

k8s的網絡模型

我們知道,在k8s裏面最小的管理單元是pod,一個主機可以跑多個pod,一個pod裏面可以跑多個容器。

如上面所示,一個pod裏面所有的容器共享一個網絡命名空間(network namespace),所以,pod裏面的容器之間通信,可以直接通過localhost來完成,pod裏面的容器之間通過localhost+端口的方式來通信(這和應用程序在宿主機的通信方式是一樣的)。

那麼pod和pod之間的通信呢?通常來說,我們給應用程序定死端口會給應用程序水平擴展帶來很多不便,所以k8s不會使用定死端口這樣的方法,而是採用其他方法來解決pod之間尋址的問題

每個pod都會有一個自己的ip,可以將Pod像VM或物理主機一樣對待。這樣pod和pod之間的通信就不需要像容器一樣,通過內外端口映射來通信了,這樣就避免了端口衝突的問題。

特殊的情況下(比如運維做網絡檢測或者程序調試),可以在pod所在的宿主機想向pod的ip+端口發起請求,這些請求會轉發到pod的端口,但是pod本身它自己是不知道端口的存在的。

因此,k8s的網絡遵循以下原則:

  • 一個節點的pod和其他節點的pod通信不需要通過做網絡地址轉換(NAT)
  • 一個節點上所有的agent控制程序(如deamon和kubelet)可以和這個節點上的pod通信
  • 節點主機網絡中的Pod可以與其他所有節點上的所有Pod通信,而無需NAT

把上面這個pod替換成容器也是成立的,因爲pod裏面的容器和pod共享網絡。

基本上的原則就是,k8s的裏面的pod可以自由的和集羣裏面的任何其他pod通信(即使他們是部署在不同的宿主機),而且pod直接的通信是直接使用pod自己的ip來通信,他們不知道宿主機的ip,所以,對於pod之間來說,宿主機的網絡信息是透明的,好像不存在一樣。

然後,定了這幾個原則之後,具體的實現k8s的這個網絡模型有好多種實現,我們這裏介紹的是Flannel,是其中最簡單的一種實現。

Flannel實現pod之間的通信,是通過一種覆蓋網絡(overlay network),把數據包封裝在另外一個網絡來做轉發,這個覆蓋網絡可以給每一個pod分配一個獨立的ip地址,使他們看起來都是一臺具有獨立ip的物理主機一樣。

下面這個就是k8s用覆蓋網絡來實現的一個例子:

flannel覆蓋網絡

可以看到有3個node,在多個node上建立一個覆蓋網絡,子網網段是100.95.0.0/16,然後,最終到容器級別,每個容器在這個網段裏面獲取到一個獨立的ip。而宿主機所在的局域網絡的網段是172.20.32.0/19

看這兩個網段,就知道,fannel給這個集羣創建了一個更大的網絡給pod使用,可以容納的主機數量達到65535(2^16)個。

對於每個宿主機,fannel給每個了一個小一點的網絡100.96.x.0/24,提供給每個這個宿主機的每一個pod使用,也就是說,每一個宿主機可以有256(2^8)個pod。docker默認的網橋docker0用的就是這個網絡,也就是所有的docker通過docker0來使用這個網絡。即使說,對於容器來說,都是通過docker0這個橋來通信,和我們平常單機的容器是一樣的(如果你不給創建的容器指定網絡的話,默認用的是docker0,參考我以前寫的關於docker bridge的文章)

那麼,對於同一個host裏面的容器通信,我們上面說了是通過這個臺宿主機的裏面的docker0這個網橋來通信。那對於跨宿主機,也即是兩個宿主機之間的容器是怎麼通信的呢?fannel使用了宿主機操作系統的kernel route和UDP(這是其中一種實現)包封裝來完成。下圖演示了這個通信過程:

fannel網絡中跨宿主機的容器通信

如圖所示,100.96.1.2(container-1) 要和100.96.2.3(container-2)通信,兩個容器分別處於不同的宿主機。
假設有一個包是從100.96.1.2發出去給100.96.2.3,它會先經過docker0,因爲docker0這個橋是所有容器的網關。 然後這個包會經過route table處理,轉發出去到局域網172.20.32.0/19. 而這個route table的對應處理這類包的規則又是從哪裏來的呢?它們是由fannel的一個守護程序flanneld創建的。

每一臺宿主機都會跑一個flannel的deamon的進程,這個進程的程序會往宿主機的route table裏面寫入特定的路由規則,這個規則大概是這樣的。

Node1的route table

admin@ip-172-20-33-102:~$ ip route
default via 172.20.32.1 dev eth0
100.96.0.0/16 dev flannel0  proto kernel  scope link  src 100.96.1.0
100.96.1.0/24 dev docker0  proto kernel  scope link  src 100.96.1.1
172.20.32.0/19 dev eth0  proto kernel  scope link  src 172.20.33.102

圖例的數據包發出去的目標地址是100.96.2.3,它屬於網段100.96.0.0/16,這個目標地址命中第二條規則,也就是這個包會發到flannel0這個設備(dev),這flannel0是一個TUN設備。是在內核裏面的一個虛擬網絡設備(虛擬網卡)

在內核(kernel)裏面,有兩種虛擬網卡設備,分別是TUN和TAP,其中TAP處理的是第二層(數據鏈路層)的幀,而TUN處理的是第三層(網絡層)的ip包。

應用程序可以綁定到TUN和TAP設備,內核會把數據通過TUN或者TAP設備發送給這些程序,反過來,應用程序也可以通過TUN和TAP向內核寫入數據,進而由內核的路由處理這些發出去的數據包。

那麼上面這個flannel0就是一個這樣的TUN設備。這個設備連到的是一個flannel的守護進程程序flanneld

而這個flanneld是幹嘛的呢?它可以接受所有發往flannel0這個設備的數據包,然後做數據封裝處理,它的封裝的邏輯也很簡單,就是根據目標地址,找到這個這地址對應的在整個flannel網絡裏面對應物理ip和端口(這裏是Node2對應的物理ip),然後增加一個包頭,增加的包頭裏面目標地址爲這個實際的物理ip和端口(當然源地址也改成了局域網絡的ip),將原來的數據包嵌入在新的數據包中,然後再把這個封裝後的包扔回去給內核,內核根據目標地址去路由規則匹配規則,發現目標地址ip是172.20.54.98,端口是8285. 根據ip匹配不到任何特定的規則,就用第一條default(默認)的規則,通過eth0這個物理網卡,把數據包發給局域網(這裏是UDP廣播出去)

當Node2的收到這個包後,然後根據端口8285發現他的目標地址原來是發給flanneld的,然後就直接交給flanneld這程序,flanneld收到包後,把包頭去掉,發現原來目標地址是100.96.2.3,然後就交換flannel0,flannel0把這個解開後的原包交給內核,內核發現它的目標地址是100.96.2.3,應該交給docker0來處理。(圖例裏面畫的是直接由flannel0交給docker0,沒有圖示出內核,實際上flannel0是一個TUN設備,是跑在內核的,數據經過它後可以交給內核,由內核根據路由決定進一步怎麼forward)

以上就是這個通信的過程,那麼這裏有一個問題: flanneld是怎麼知道100.96.2.3對應的目標地址是172.20.54.98:8285的呢?

這是因爲flanneld維護了一個映射關係,它沒創造一個虛擬的容器ip(分配給容器新ip的時候),它就知道這個容器的ip實際上是在哪臺宿主機上,然後把這個映射關係存儲起來,在k8s裏面flanneld存儲的這個映射關係放在etd上,這就是爲什麼flanneld爲什麼知道這個怎麼去封裝這些包了,下面就是etcd裏面的數據的:

admin@ip-172-20-33-102:~$ etcdctl ls /coreos.com/network/subnets
/coreos.com/network/subnets/100.96.1.0-24
/coreos.com/network/subnets/100.96.2.0-24
/coreos.com/network/subnets/100.96.3.0-24
admin@ip-172-20-33-102:~$ etcdctl get /coreos.com/network/subnets/100.96.2.0-24
{"PublicIP":"172.20.54.98"}

看上面這個數據,etcd裏面存儲的100.96.2.0-24這個網段的容器是放在172.20.54.98這臺宿主機上的。

那麼還有一個問題,端口8285又是怎麼知道的?

這個很簡單,flanneld的默認監聽的端口就是這個8285端口,flanneld啓動的時候,就監聽了UDP端口8285. 所以發給Node2:8285的所有UDP數據包會,flanneld這個進程會直接處理,如何去掉包頭就還原出來原來的包了,還原後交給TUN設備flannel0,由flannel0交給內核,內核根據Node2的路由規則交給docker0(Node2的路由規則和node1是基本上一樣的,除了第三位的網段標識不一樣,一個是100.96.1一個是100.92.2):

admin@ip-172-20-54-98:~$ ip route
default via 172.20.32.1 dev eth0
100.96.0.0/16 dev flannel0  proto kernel  scope link  src 100.96.2.0
100.96.2.0/24 dev docker0  proto kernel  scope link  src 100.96.2.1
172.20.32.0/19 dev eth0  proto kernel  scope link  src 172.20.54.98

看Node2的這個規則,flannld去掉包頭解出來的原包的目標ip是100.96.2.3,由flannel0交回去給kennel,kennel發現命中第三條規則,所以會把這個包叫給docker0,繼而就進入了docker0這個橋的子網了,接下去就是docker的事情了,參考以前寫的文章

最後一個問題,怎麼配置docker去使用100.96.x.0/24這個子網呢,如果是手工創建容器的話,這個也是非常簡單的,參考以前寫的關於docker bridge的這篇文章,但是在k8s裏面,是通過配置來實現的:

flanneld會把子網信息寫到一個配置文件/run/flannel/subnet.env

admin@ip-172-20-33-102:~$ cat /run/flannel/subnet.env
FLANNEL_NETWORK=100.96.0.0/16
FLANNEL_SUBNET=100.96.1.1/24
FLANNEL_MTU=8973
FLANNEL_IPMASQ=true

docker會使用這個配置的環境變了來作爲它的bridge的配置

dockerd --bip=$FLANNEL_SUBNET --mtu=$FLANNEL_MTU

以上,就是k8s如何使用flannel網絡來跨機器通信的原理,總體來講,由於flanneld這個守護神幹了所有的髒活累活(其實已經是k8s的網絡實現裏面最簡單的一種了),使得pod和容器能夠連接另外一個pod或者容器變得非常簡單,就像連一個大局域網裏面任意以太主機一樣,他們只需要知道對方的虛擬ip就可以直接通信了,不需要做
NAT等複雜的規則處理。

那麼性能怎麼樣?

新版本的flannel不推薦在生產環境使用UDP的包封裝這種實現。只用它來做測試和調試用,因爲它的性能表現和其他的實現比差一些。

flannel0 利用的TUN設備做包封裝原理

看上面這個圖解,一個upd包需要來回在用戶空間(user space)和內核空間(kennel space)複製3次,這會大大增加網絡開銷。

官方的文檔裏面可以看到其他的包轉發實現方式,可以進一步閱讀,其中host-gw的性能比較好,它是在第二層去做數據包處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章