天天看點

容器跨主機網絡通信學習筆記(以Flannel為例)

我們知道在Docker的預設配置下,不同主控端上的容器通過 IP 位址進行互相通路是根本做不到的。 而正是為了解決這個容器“跨主通信”的問題,社群裡才出現了很多的容器網絡方案。

要了解容器“跨主通信”的原理,就一定要先從 Flannel 這個項目說起。 Flannel 項目是 CoreOS 公司主推的容器網絡方案。事實上,Flannel 項目本身隻是一個架構,真正為我們提供容器網絡功能的,是 Flannel 的後端實作。目前,Flannel 支援三種後端實作,分别是: 1. VXLAN; 2. host-gw; 3. UDP。 這三種不同的後端實作,代表了三種容器跨主網絡的主流實作方法。

UDP

Flannel 項目最早支援的一種方式,卻也是性能最差的一種方式。是以,這個模式目前已經被棄用。不過,Flannel 之是以最先選擇 UDP 模式,就是因為這種模式是最直接、也是最容易了解的容器跨主網絡實作。

首先我們需要在Flannel的配置檔案中指定

Backend type

UPD

$ kubectl edit configmap kube-flannel-cfg -n kube-system
.....
data:
    cni-conf.json: |
      {
        "name": "cbr0",
        "cniVersion": "0.3.1",
        "plugins": [
          {
            "type": "flannel",
            "delegate": {
              "hairpinMode": true,
              "isDefaultGateway": true
            }
          },
          {
            "type": "portmap",
            "capabilities": {
              "portMappings": true
            }
          }
        ]
      }
    net-conf.json: |
      {
        "Network": "10.244.0.0/16",
        "Backend": {
          "Type": "udp"   # 修改後端類型為udp
        }
      }
  kind: ConfigMap
......
           

采用 UDP 模式時後端預設為端口為 8285,即 Flanneld 的監聽端口。

當采用 UDP 模式時,Flanneld 程序在啟動時會通過打開

/dev/net/tun

的方式生成一個

TUN

裝置,

TUN

裝置可以簡單了解為 Linux 當中提供的一種核心網絡與使用者空間通信的一種機制,即應用可以通過直接讀寫 TUN 裝置的方式收發 RAW IP 包。是以我們還需要将主控端的

/dev/net/tun

檔案挂載到容器中去:

$ kubectl edit ds kube-flannel-ds-amd64 -n kube-system
......
  volumeMounts:
    - mountPath: /run/flannel
    name: run
    - mountPath: /etc/kube-flannel/
    name: flannel-cfg
    - mountPath: /dev/net  # 指定主控端的挂載路徑
    name: tun
......
volumes:
- hostPath:
    path: /run/flannel
    type: ""
  name: run
- hostPath:
    path: /etc/cni/net.d
    type: ""
  name: cni
- hostPath:
    path: /dev/net  # 挂載主控端的 /dev/net/tun 檔案
    type: ""
  name: tun
......
           

這時候 Flanneld 的 Pod 會自動重建,重建完成後,可以随便檢視一個 Pod 的日志:

# kubectl logs -f kube-flannel-ds-amd64-thfbg -n kube-system
I0104 05:15:21.709236       1 main.go:518] Determining IP address of default interface
I0104 05:15:21.709940       1 main.go:531] Using interface with name ens32 and address 192.168.47.135
I0104 05:15:21.709976       1 main.go:548] Defaulting external address to interface address (192.168.47.135)
W0104 05:15:21.709998       1 client_config.go:517] Neither --kubeconfig nor --master was specified.  Using the inClusterConfig.  This might not work.
I0104 05:15:21.814339       1 kube.go:119] Waiting 10m0s for node controller to sync
I0104 05:15:21.814420       1 kube.go:306] Starting kube subnet manager
I0104 05:15:22.814988       1 kube.go:126] Node controller sync successful
I0104 05:15:22.815021       1 main.go:246] Created subnet manager: Kubernetes Subnet Manager - node2
I0104 05:15:22.815026       1 main.go:249] Installing signal handlers
I0104 05:15:22.815146       1 main.go:390] Found network config - Backend type: udp
I0104 05:15:23.021984       1 main.go:305] Setting up masking rules
I0104 05:15:23.023228       1 main.go:313] Changing default FORWARD chain policy to ACCEPT
I0104 05:15:23.023310       1 main.go:321] Wrote subnet file to /run/flannel/subnet.env
I0104 05:15:23.023316       1 main.go:325] Running backend.
I0104 05:15:23.023323       1 main.go:343] Waiting for all goroutines to exit
I0104 05:15:23.023339       1 udp_network_amd64.go:100] Watching for new subnet leases
I0104 05:15:23.023358       1 udp_network_amd64.go:195] Subnet added: 10.244.0.0/24
I0104 05:15:23.023372       1 udp_network_amd64.go:195] Subnet added: 10.244.2.0/24
           

可以看到

Found network config -Backend type: udp

這個資訊證明現在網絡模式已經變成了UDP了。

Flanneld程序啟動後通過

ip a

指令可以發現目前節點中已經多了一個叫

flannel0

的網絡裝置。

由于是 UDP 的服務,是以我們需要通過

netstat -ulnp

指令檢視程序:

$ netstat -ulnp | grep flanneld
udp        0      0 192.168.47.133:8285     0.0.0.0:*    32592/flanneld  
           

現在我有node1和node2兩個主控端:

node1 (192.168.47.134)上運作pod-a,它的IP位址是:10.244.2.4,對應的cni0網橋位址是:10.244.2.1/24;

node2(192.168.47.135)上運作pod-b,它的IP位址是:10.244.1.3,對應的cni0網橋位址是:10.244.1.1/24

$ kubectl get pod -o wide
NAME    READY   STATUS    RESTARTS   AGE     IP           NODE    NOMINATED NODE   READINESS GATES
pod-a   1/1     Running   0          2m1s    10.244.2.4   node1   <none>     <none>
pod-b   1/1     Running   0          2m31s   10.244.1.3   node2   <none>     <none>
           

現在的任務就是讓pod-a(10.244.2.4)通路 pod-b(10.244.1.3)。

pod-a容器裡的程序發起IP包,其源位址就是10.244.2.4,目标位址就是10.244.1.3。由于目标位址10.244.1.3并不在node1的cni0的網橋網段裡,是以這個IP包會被交給預設路由規則,通過容器的網關進入cni0網橋,進而出現在主控端上。

這個 IP 包的下一個目的地,就取決于主控端上的路由規則了。此時,Flannel 已經在主控端上建立出了一系列的路由規則,以 node 1 為例,如下所示:

$ ip route
default via 192.168.47.2 dev ens32 proto static metric 100 
10.244.0.0/16 dev flannel0 
10.244.2.0/24 dev cni0 proto kernel scope link src 10.244.2.1 
169.254.0.0/16 dev ens32 scope link metric 1002 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 
192.168.47.0/24 dev ens32 proto kernel scope link src 192.168.47.134 metric 100 
           

可以看到,由于我們的 IP 包的目标位址是 10.244.1.3,它比對不到本機 cni0 網橋對應的 10.244.2.0/24 網段,隻能比對到第一條、也就是 100.244.0.0/16 對應的這條路由規則,進而進入到一個叫作 flannel0 的裝置中。

而這個 flannel0 裝置的類型就比較有意思了:它是一個 TUN 裝置(Tunnel 裝置)。

在 Linux 中,TUN 裝置是一種工作在三層(Network Layer)的虛拟網絡裝置。TUN 裝置的功能非常簡單,即:在作業系統核心和使用者應用程式之間傳遞 IP 包。

以 flannel0 裝置為例:

像上面提到的情況,當作業系統将一個 IP 包發送給 flannel0 裝置之後,flannel0 就會把這個 IP 包,交給建立這個裝置的應用程式,也就是 Flannel 程序。這是一個從核心态(Linux 作業系統) 向使用者态(Flannel 程序)的流動方向。

反之,如果 Flannel 程序向 flannel0 裝置發送了一個 IP 包,那麼這個 IP 包就會出現在主控端網絡棧中,然後根據主控端的路由表進行下一步處理。這是一個從使用者态向核心态的流動方向。

是以,當 IP 包從容器經過 cni0 出現在主控端,然後又根據路由表進入 flannel0 裝置後,主控端上的 flanneld 程序(Flannel 項目在每個主控端上的主程序),就會收到這個 IP 包。

flanneld 看到了這個 IP 包的目的位址是 10.244.1.3,就把它發送給了 node 2 主控端。

等一下,flanneld 又是如何知道這個 IP 位址對應的容器,是運作在 Node 2 上的呢?

這裡,就用到了 Flannel 項目裡一個非常重要的概念:子網(Subnet)。

事實上,在由 Flannel 管理的容器網絡裡,一台主控端上的所有容器,都屬于該主控端被配置設定的一 個“子網”。在我們的例子中,node 1 的子網是 10.244.2.0/24,pod-a 的 IP 位址是 10.244.2.4。node 2 的子網是 10.244.1.0/24,container-2 的 IP 位址是 10.244.1.3。而這些子網與主控端的對應關系,正是儲存在 Etcd 當中。

是以當flanneld程序處理有flannel0傳入的IP包時,就可以根據目的IP位址(比如10.244.1.3),比對到對應的子網(比如10.244.1.0/24),這時候查詢etcd,找到這個子網對應的主控端IP正是192.168.47.135,也就是node2的IP位址。

而對于 flanneld 來說,隻要 node 1 和 node 2 是互通的,那麼 flanneld 作為 node 1 上的一個 普通程序,就一定可以通過上述 IP 位址(192.168.47.135)通路到 node 2,這沒有任何問題。

是以說,flanneld 在收到 pod-a發給 pod-b 的 IP 包之後,就會把這個 IP 包直接封裝 在一個 UDP 包裡,然後發送給 node 2。

不難了解,這個 UDP 包的源位址,就是 flanneld 所在 的 node 1 的位址,而目的位址,則是 pod-b 所在的主控端 node 2 的位址。 當然,這個請求得以完成的原因是,每台主控端上的 flanneld,都監聽着一個 8285 端口,是以 flanneld 隻要把 UDP 包發往 node 2 的 8285 端口即可。

通過這樣一個普通的主控端之間的 UDP 通信,一個 UDP 包就從 node 1 到達了 node 2。

而 node 2 上監聽 8285 端口的程序也是 flanneld,是以這時候,flanneld 就可以從這個 UDP 包裡解析出封裝在裡面的pod-a 發來的原 IP 包。

接下來 flanneld 的工作就非常簡單了:flanneld 會直接把這個 IP 包發送給它所管理的 TUN 設 備,即 flannel0 裝置。 這正是一個從使用者态向核心态的流動方向(Flannel 程序向 TUN 裝置發送資料包),是以 Linux 核心網絡棧就會負責處理這個 IP 包,具體的處理方法,就是 通過本機的路由表來尋找這個 IP 包的下一步流向。 而 node 2 上的路由表,跟 node 1 非常類似,如下所示:

$ ip route
default via 192.168.47.2 dev ens32 proto static metric 100 
10.244.0.0/16 dev flannel0 
10.244.1.0/24 dev cni0 proto kernel scope link src 10.244.1.1 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 
192.168.47.0/24 dev ens32 proto kernel scope link src 192.168.47.135 metric 100 
           

由于這個 IP 包的目的位址是 10.244.1.3,它跟第三條、也就是 10.244.1.0/24 網段對應的路由規則比對更加精确。是以,Linux 核心就會按照這條路由規則,把這個 IP 包轉發給cni0 網橋。

接下來cni0 網橋會扮演 二層交換機的角色,将資料包發送給正确的端口,進而通過 Veth Pair 裝置進入到 pod-b的 Network Namespace 裡。

而 pod-b 傳回給 pob-a 的資料包,則會經過與上述過程完全相反的路徑回到 pod-a 中。

需要注意的是,上述流程要正确工作還有一個重要的前提,那就是 cni0 網橋的位址範圍必須是 Flannel 為主控端配置設定的子網。

容器跨主機網絡通信學習筆記(以Flannel為例)

Flannel UDP 模式提供的其實是一個三層的 Overlay 網絡,即:它首先對發出端的 IP 包進行 UDP 封裝,然後在接收端進行解封裝拿到原始的 IP 包,進而把這個 IP 包轉發給目标容器。這就好比,Flannel 在不同主控端上的兩個容器之間打通了一條“隧道”,使得這兩個容器可以直接使用 IP 位址進行通信,而無需關心容器和主控端的分布情況。

實際上,相比于兩台主控端之間的直接通信,基于 Flannel UDP 模式的容器通信多了一個額外的步驟,即 flanneld 的處理過程。而這個過程,由于使用到了 flannel0 這個 TUN 裝置,僅在發出 IP 包的過程中,就需要經過三次使用者态與核心态之間的資料拷貝:

第一次:使用者态的容器程序發出的 IP 包經過 cni0 網橋進入核心态;

第二次:IP 包根據路由表進入 TUN(flannel0)裝置,進而回到使用者态的 flanneld 程序;

第三次:flanneld 進行 UDP 封包之後重新進入核心态,将 UDP 包通過主控端的 eth0 發出去。

此外,Flannel 進行 UDP 封裝(Encapsulation)和解封裝(Decapsulation) 的過程,也都是在使用者态完成的。在 Linux 作業系統中,上述這些上下文切換和使用者态操作的代價其實是比較高的,這也正是造成 Flannel UDP 模式性能不好的主要原因。

VXLAN 方式

VXLAN

,即 Virtual Extensible LAN(虛拟可擴充區域網路),是 Linux 核心本身就支援的一種網絡虛似化技術。是以說,VXLAN 可以完全在核心态實作上述封裝和解封裝的工作,進而通過與前面相似的“隧道”機制,建構出覆寫網絡(Overlay Network)。

同樣的當我們使用

VXLAN

模式的時候需要将 Flanneld 的 Backend 類型修改為

vxlan

$ kubectl edit cm kube-flannel-cfg -n kube-system
apiVersion: v1
data:
  cni-conf.json: |
    {
      "cniVersion": "0.2.0",
      "name": "cbr0",
      "plugins": [
        {
          "type": "flannel",
          "delegate": {
            "hairpinMode": true,
            "isDefaultGateway": true
          }
        },
        {
          "type": "portmap",
          "capabilities": {
            "portMappings": true
          }
        }
      ]
    }
  net-conf.json: |
    {
        {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "vxlan"  # 修改後端類型為 vxlan
      }
    }
kind: ConfigMap
......
           

将類型修改為

vxlan

過後,需要重建下 Flanneld 的所有 Pod 才能生效:

$ kubectl delete pod -n kube-system -l app=flannel
           

重建完成後同樣可以随便檢視一個 Pod 的日志,出現如下

Found network config - Backend type: vxlan

的日志資訊就證明已經配置成功了:

$ kubectl logs -f kube-flannel-ds-amd64-xjfvk -n kube-system
I0104 06:55:07.677610       1 main.go:518] Determining IP address of default interface
I0104 06:55:07.677915       1 main.go:531] Using interface with name ens32 and address 192.168.47.133
I0104 06:55:07.677940       1 main.go:548] Defaulting external address to interface address (192.168.47.133)
W0104 06:55:07.677948       1 client_config.go:517] Neither --kubeconfig nor --master was specified.  Using the inClusterConfig.  This might not work.
I0104 06:55:07.683938       1 kube.go:119] Waiting 10m0s for node controller to sync
I0104 06:55:07.684002       1 kube.go:306] Starting kube subnet manager
I0104 06:55:08.684743       1 kube.go:126] Node controller sync successful
I0104 06:55:08.684790       1 main.go:246] Created subnet manager: Kubernetes Subnet Manager - master
I0104 06:55:08.684798       1 main.go:249] Installing signal handlers
I0104 06:55:08.685020       1 main.go:390] Found network config - Backend type: vxlan
I0104 06:55:08.685105       1 vxlan.go:121] VXLAN config: VNI=1 Port=0 GBP=false Learning=false DirectRouting=false
I0104 06:55:08.696326       1 main.go:305] Setting up masking rules
I0104 06:55:08.697498       1 main.go:313] Changing default FORWARD chain policy to ACCEPT
I0104 06:55:08.697608       1 main.go:321] Wrote subnet file to /run/flannel/subnet.env
I0104 06:55:08.697631       1 main.go:325] Running backend.
I0104 06:55:08.697640       1 main.go:343] Waiting for all goroutines to exit
I0104 06:55:08.697658       1 vxlan_network.go:60] watching for new subnet leases
           

VXLAN 的覆寫網絡的設計思想是:在現有的三層網絡之上,“覆寫”一層虛拟的、由核心 VXLAN 子產品負責維護的二層網絡,使得連接配接在這個 VXLAN 二層網絡上的“主機”(虛拟機或者容器都可 以)之間,可以像在同一個區域網路(LAN)裡那樣自由通信。

實際上,這些“主機”可能分布在不同的主控端上,甚至是分布在不同的實體機房裡。 而為了能夠在二層網絡上打通“隧道”,VXLAN 會在主控端上設定一個特殊的網絡裝置作為“隧 道”的兩端。這個裝置就叫作 VTEP,即:VXLAN Tunnel End Point(虛拟隧道端點)。

而 VTEP 裝置的作用,其實跟前面的 flanneld 程序非常相似。隻不過,它進行封裝和解封裝的對象,是二層資料幀(Ethernet frame);而且這個工作的執行流程,全部是在核心裡完成的 VXLAN 本身就是 Linux 核心中的一個子產品)。

容器跨主機網絡通信學習筆記(以Flannel為例)

可以看到,圖中每台主控端上名叫 flannel.1 的裝置,就是 VXLAN 所需的 VTEP 裝置,它既有 IP 位址,也有 MAC 位址。

現在,我們的pod-a的 IP 位址是 10.244.2.4,要通路的 pod-b的 IP 位址是 10.244.1.3。

那麼,與前面 UDP 模式的流程類似,當 pod-a送出請求之後,這個目的位址是 10.244.1.3的 IP 包,會先出現在 cni0 網橋,然後被路由到本機 flannel.1 裝置進行處理。也就是說,來到了“隧道”的入口。為了友善叙述,接下來會把這個 IP 包稱為“原始 IP 包”。 為了能夠将“原始 IP 包”封裝并且發送到正确的主控端,VXLAN 就需要找到這條“隧道”的出口,即:目的主控端的 VTEP 裝置。 而這個裝置的資訊,正是每台主控端上的 flanneld 程序負責維護的。

比如,當 node 2 啟動并加入 Flannel 網絡之後,在 node 1(以及所有其他節點)上,flanneld 就會添加一條如下所示的路由規則:

$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
......
10.244.1.0      10.244.1.0      255.255.255.0   UG    0      0        0 flannel.1
           

這條規則的意思是:凡是發往 10.244.1.0/24 網段的 IP 包,都需要經過 flannel.1 裝置發出,并 且,它最後被發往的網關位址是:10.244.1.0。

從上圖的Flannel VXLAN模式的流程圖可以看出,10.244.1.0 正是 node 2 上的 VTEP 裝置(也就是 flannel.1 裝置)的 IP 位址。

為了友善叙述,接下來我會把 node 1 和 node 2 上的 flannel.1 裝置分别稱為“源 VTEP 設 備”和“目的 VTEP 裝置”。

而這些 VTEP 裝置之間,就需要想辦法組成一個虛拟的二層網絡,即:通過二層資料幀進行通信。

是以在我們的例子中,“源 VTEP 裝置”收到“原始 IP 包”後,就要想辦法把“原始 IP 包”加上 一個目的 MAC 位址,封裝成一個二層資料幀,然後發送給“目的 VTEP 裝置”(這麼做還是因為這個 IP 包的目的位址不是本機)。

這裡需要解決的問題就是:“目的 VTEP 裝置”的 MAC 位址是什麼? 此時,根據前面的路由記錄,我們已經知道了“目的 VTEP 裝置”的 IP 位址。而要根據三層 IP 地 址查詢對應的二層 MAC 位址,這正是 ARP(Address Resolution Protocol )表的功能。

這裡要用到的 ARP 記錄,也是 flanneld 程序在 node 2 節點啟動時,自動添加在 node1 上 的。我們可以通過 ip 指令看到它,如下所示:

# 在node1上執行
$ ip neigh show dev flannel.1
10.244.1.0 lladdr 9a:f4:d0:1e:29:1c PERMANENT
           

這條記錄的意思非常明确,即:IP 位址 10.244.1.0,對應的 MAC 位址是9a:f4:d0:1e:29:1c 。

有了這個“目的 VTEP 裝置”的 MAC 位址,Linux 核心就可以開始二層封包工作了。這個二層幀 的格式,如下所示:

容器跨主機網絡通信學習筆記(以Flannel為例)

可以看到,Linux 核心會把“目的 VTEP 裝置”的 MAC 位址,填寫在圖中的 Inner Ethernet Header 字段,得到一個二層資料幀。 需要注意的是,上述封包過程隻是加一個二層頭,不會改變“原始 IP 包”的内容。

是以圖中的 Inner IP Header 字段,依然是 pod-b 的 IP 位址,即 10.244.1.3。

但是,上面提到的這些 VTEP 裝置的 MAC 位址,對于主控端網絡來說并沒有什麼實際意義。是以 上面封裝出來的這個資料幀,并不能在我們的主控端二層網絡裡傳輸。

為了友善叙述,我們把它稱 為“内部資料幀”(Inner Ethernet Frame)。

是以接下來,Linux 核心還需要再把“内部資料幀”進一步封裝成為主控端網絡裡的一個普通的資料幀,好讓它“載着”“内部資料幀”,通過主控端的 eth0 網卡進行傳輸。 我們把這次要封裝出來的是主控端對應的資料幀稱為“外部資料幀”(Outer Ethernet Frame)。

為了實作這個“搭便車”的機制,Linux 核心會在“内部資料幀”前面,加上一個特殊的 VXLAN 頭,用來表示這個“乘客”實際上是一個 VXLAN 要使用的資料幀。 而這個 VXLAN 頭裡有一個重要的标志叫作VNI,它是 VTEP 裝置識别某個資料幀是不是應該歸自己處理的重要辨別。

而在 Flannel 中,VNI 的預設值是 1,這也是為何,主控端上的 VTEP 裝置都叫作 flannel.1 的原因,這裡的“1”,其實就是 VNI 的值。

然後,Linux 核心會把這個資料幀封裝進一個 UDP 包裡發出去。 是以,跟 UDP 模式類似,在主控端看來,它會以為自己的 flannel.1 裝置隻是在向另外一台主控端 的 flannel.1 裝置,發起了一次普通的 UDP 連結。它哪裡會知道,這個 UDP 包裡面,其實是一個 完整的二層資料幀。

不過,不要忘了,一個 flannel.1 裝置隻知道另一端的 flannel.1 裝置的 MAC 位址,卻不知道對應 的主控端位址是什麼。 也就是說,這個 UDP 包該發給哪台主控端呢?

在這種場景下,flannel.1 裝置實際上要扮演一個“網橋”的角色,在二層網絡進行 UDP 包的轉 發。而在 Linux 核心裡面,“網橋”裝置進行轉發的依據,來自于一個叫作 FDB(Forwarding Database)的轉發資料庫。 不難想到,這個 flannel.1“網橋”對應的 FDB 資訊,也是 flanneld 程序負責維護的。它的内容可以通過 bridge fdb 指令檢視到,如下所示:

# 在node1上,使用"目的VTEP裝置"的mac位址進行查詢
$ bridge fdb show dev flannel.1 | grep 9a:f4:d0:1e:29:1c
9a:f4:d0:1e:29:1c dst 192.168.47.135 self permanent
           

可以看到,在上面這條 FDB 記錄裡,指定了這樣一條規則,即: 發往我們前面提到的“目的 VTEP 裝置”(MAC 位址是 9a:f4:d0:1e:29:1c)的二層資料幀,應該通過 flannel.1 裝置,發往 IP 位址為 192.168.47.135 的主機。

顯然,這台主機正是 node 2,UDP 包要發往的目的地就找到了。 是以接下來的流程,就是一個正常的、主控端網絡上的封包工作。

UDP 包是一個四層資料包,是以 Linux 核心會在它前面加上一個 IP 頭,即原理圖中的 Outer IP Header,組成一個 IP 包。并且,在這個 IP 頭裡,會填上前面通過 FDB 查詢出來的目的 主機的 IP 位址,即 node 2 的 IP 位址 192.168.47.135。

然後,Linux 核心再在這個 IP 包前面加上二層資料幀頭,即原理圖中的 Outer Ethernet Header, 并把 node 2 的 MAC 位址填進去。這個 MAC 位址本身,是 node 1 的 ARP 表要學習的内容, 無需 Flannel 維護。這時候,我們封裝出來的“外部資料幀”的格式,如下所示:

容器跨主機網絡通信學習筆記(以Flannel為例)

這樣,封包工作就宣告完成了。

接下來,node 1 上的 flannel.1 裝置就可以把這個資料幀從 node 1 的 eth0 網卡發出去。

顯然, 這個幀會經過主控端網絡來到 node 2 的 eth0 網卡。 這時候,node 2 的核心網絡棧會發現這個資料幀裡有 VXLAN Header,并且 VNI=1。是以 Linux 核心會對它進行拆包,拿到裡面的内部資料幀,然後根據 VNI 的值,把它交給 node 2 上的 flannel.1 裝置。 而 flannel.1 裝置則會進一步拆包,取出“原始 IP 包”。最終,IP 包就進入到了 pod-b 容器的 Network Namespace 裡。

host-gw

host-gw

即 Host Gateway,從名字中就可以想到這種方式是通過把主機當作網關來實作跨節點網絡通信的。那麼具體如何實作跨節點通信呢?

同 UDP 模式和 VXLAN 模式一樣,首先将 Backend 中的 type 改為

host-gw

,這裡就不再贅述,更新完成後,随便檢視一個 flannel 的 Pod 日志,如果出現如下所示的

Found network config - Backend type: host-gw

日志就證明已經是

host-gw

模式了:

$ kubectl logs -f kube-flannel-ds-amd64-r84tj -n kube-system
I0104 08:10:57.379474       1 main.go:518] Determining IP address of default interface
I0104 08:10:57.379779       1 main.go:531] Using interface with name ens32 and address 192.168.47.133
I0104 08:10:57.379804       1 main.go:548] Defaulting external address to interface address (192.168.47.133)
W0104 08:10:57.379817       1 client_config.go:517] Neither --kubeconfig nor --master was specified.  Using the inClusterConfig.  This might not work.
I0104 08:10:57.478488       1 kube.go:119] Waiting 10m0s for node controller to sync
I0104 08:10:57.478602       1 kube.go:306] Starting kube subnet manager
I0104 08:10:58.479224       1 kube.go:126] Node controller sync successful
I0104 08:10:58.479358       1 main.go:246] Created subnet manager: Kubernetes Subnet Manager - master
I0104 08:10:58.479365       1 main.go:249] Installing signal handlers
I0104 08:10:58.480235       1 main.go:390] Found network config - Backend type: host-gw
I0104 08:10:58.498148       1 main.go:305] Setting up masking rules
I0104 08:10:58.676918       1 main.go:313] Changing default FORWARD chain policy to ACCEPT
I0104 08:10:58.677031       1 main.go:321] Wrote subnet file to /run/flannel/subnet.env
I0104 08:10:58.677036       1 main.go:325] Running backend.
I0104 08:10:58.677044       1 main.go:343] Waiting for all goroutines to exit
I0104 08:10:58.677055       1 route_network.go:53] Watching for new subnet leases
I0104 08:10:58.677427       1 route_network.go:85] Subnet added: 10.244.2.0/24 via 192.168.47.134
I0104 08:10:58.677605       1 route_network.go:85] Subnet added: 10.244.1.0/24 via 192.168.47.135
W0104 08:10:58.677628       1 route_network.go:88] Ignoring non-host-gw subnet: type=vxlan
I0104 08:11:03.026155       1 route_network.go:85] Subnet added: 10.244.1.0/24 via 192.168.47.135
           

假設現在,node 1 上的 pod-a,要通路 node 2 上的 pod-b。 當設定 Flannel 使用 host-gw 模式之後,flanneld 會在主控端上建立這樣一條規則,以node1為例:

$ ip route
......
10.244.1.0/24 via 192.168.47.135 dev ens32 
           

這條路由規則的含義是: 目的 IP 位址屬于 10.244.1.0/24 網段的 IP 包,應該經過本機的 eth0 裝置發出去(即:dev eth32);并且,它下一跳位址(next-hop)是 192.168.47.135(即:via 192.168.47.135)。

所謂下一跳位址就是:如果 IP 包從主機 A 發到主機 B,需要經過路由裝置 X 的中轉。那麼 X 的 IP 位址就應該配置為主機 A 的下一跳位址。

容器跨主機網絡通信學習筆記(以Flannel為例)

從 host-gw 示意圖中我們可以看到,這個下一跳位址對應的,正是我們的目的主控端 node 2。 一旦配置了下一跳位址,那麼接下來,當 IP 包從網絡層進傳入連結路層封裝成幀的時候,eth0 裝置 就會使用下一跳位址對應的 MAC 位址,作為該資料幀的目的 MAC 位址。顯然,這個 MAC 地 址,正是 node 2 的 MAC 位址。 這樣,這個資料幀就會從 node 1 通過主控端的二層網絡順利到達 node 2 上。

而 node 2 的核心網絡棧從二層資料幀裡拿到 IP 包後,會“看到”這個 IP 包的目的 IP 位址是 10.244.1.3,即pod-b 的 IP 位址。

這時候,根據 node 2 上的路由表,該目的位址會比對到第三條路由規則(也就是 10.244.1.0 對應的路由規則),進而進入 cni0 網橋,進而 進入到 pod-b 當中。

可以看到,host-gw 模式的工作原理,其實就是将每個 Flannel 子網(Flannel Subnet,比 如:10.244.1.0/24)的“下一跳”,設定成了該子網對應的主控端的 IP 位址。

也就是說,這台“主機”(Host)會充當這條容器通信路徑裡的“網關”(Gateway)。這也 正是“host-gw”的含義。

Flannel 子網和主機的資訊,都是儲存在 Etcd 當中的。flanneld 隻需要 WACTH 這些資料的變化,然後實時更新路由表即可。

在這種模式下,容器通信的過程就免除了額外的封包和解包帶來的性能損耗。根據實際的測 試,host-gw 的性能損失大約在 10% 左右,而其他所有基于 VXLAN“隧道”機制的網絡方 案,性能損失都在 20%~30% 左右。

host-gw 模式能夠正常工作的核心,就在于 IP 包在封裝成幀發送出去的時候,會使用路由表裡的“下一跳”來設定目的 MAC 位址。這樣,它就會經 過二層網絡到達目的主控端。 是以說,Flannel host-gw 模式必須要求叢集主控端之間是二層連通的。