1. 程式人生 > 其它 >容器跨主機網路通訊學習筆記(以Flannel為例)

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

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

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

UDP

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

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

$ 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 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.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 核心就可以開始二層封包工作了。這個二層幀 的格式,如下所示:

可以看到,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 維護。這時候,我們封裝出來的“外部資料幀”的格式,如下所示:

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

接下來,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 的下一跳地址。

從 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 模式必須要求叢集宿主機之間是二層連通的。