容器跨主机网络通信学习笔记(以Flannel为例)

摘要:
正是为了解决这个容器的“跨主通信”问题,社区中出现了许多容器网络解决方案。要理解集装箱“跨主通信”的原理,我们必须从法兰内项目开始。事实上,Flannel项目本身只是一个框架。真正为我们提供容器网络功能的是Flannel的后端实现。目前,Flannel支持三种后端实现:1.VXLAN;2.主机-gw3.预算UDPFlannel项目是最早支持的方法之一,但它的性能也最差。启动Flanneld进程后,您可以通过ipa命令发现当前节点有一个名为fla兰绒0的附加网络设备。

我们知道在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 为宿主机分配的子网。

image

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 内核中的一个模块)。

image

可以看到,图中每台宿主机上名叫 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 内核就可以开始二层封包工作了。这个二层帧 的格式,如下所示:

image

可以看到,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 维护。这时候,我们封装出来的“外部数据帧”的格式,如下所示:

image

这样,封包工作就宣告完成了。

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

image

从 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 模式必须要求集群宿主机之间是二层连通的。

免责声明:文章转载自《容器跨主机网络通信学习笔记(以Flannel为例)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇CoreAnimation1-图层树、寄宿图以及图层几何学Jmeter做接口测试下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

linux常用命令(不断更新)

睡眠命令(第一步可省去): 1.查看你的系统支持什么模式:cat /sys/power/state(我的系统为:freeze mem disk) 2.切换到管理员模式下,执行命令:echo "mem">/sys/power/state 下次直接按开机键就回到睡眠之前的状态了 服务器时间本地化(Centos和Ubuntu都试过,可行): rm -...

Linux使用unzip打开zip文件失败提示无法打开压缩包:Not zip archive

0x01问题概述: 常规解压使用UNzip,但有时会报错。目前UNzip最大可解压2G的文件 0x02解决方法 环境检测 首先确保下列程序已安装 如果不是因为程序缺失那执行第二步 检查gcc-c++ yum list | grep gcc 检查gcc-c++ 安装unzip yum install unzip 一条命令开始解压:sudo apt de...

linux下redis4.0.2集群部署(利用Ruby脚本命令)

一、原生命令方式和Ruby脚本方式区别 利用Ruby脚本部署和用原生命令部署,节点准备的步骤都是一样的,节点启动后的握手,以及主从、槽分配,利用Ruby脚本一步就能完成,利用原生命令需要一步一步地执行命令完成。 二、部署架构:3台服务器,互为主从,3主3从 三、准备6个节点配置文件 在172.28.18.75上操作 cd /etc/redis vim r...

【Linux 驱动】第九章 与硬件通信

在学习有关I/O总线的内容时,最好先看看相关的知识:从PC总线到ARM的内部总线 一,I/O 端口和 I/O 内存 每种外设都是通过读写寄存器来进行控制。 大部分外设都有几个寄存器,不管是在内存地址空间还是在I/O地址空间,这些寄存器的访问地址都是连续的。在硬件层,内存区和 I/O 区域没有概念上的区别: 它们都是通过向在地址总线和控制总线发出电平信号来进...

Mac OS X操作系统常见快捷键集锦

Mac OS X操作系统常见快捷键集锦 启动时的快捷键 启动时按住 X 键 : 强制从 Mac OS X 启动(适用于那些在同一宗卷上安装了 Mac OS X 和 Mac OS 9 双系统的 Mac ,不要把 Boot Camp 混淆进来) 启动时同时按住 Option-Command-Shift-Delete 键 : 跳过默认的启动宗卷,搜索其他可启动...

在macOS苹果电脑上安装Azure DevOps Server(TFS)代理

1. 概述 MacOS是一套运行于苹果Macintosh系列电脑上的操作系统,是首个在商用领域成功的图形用户界面操作系统。Iphone应用软件的开发人员,都使用运行macOS的电脑或mini盒子进行软件开发和调试,其中XCode则是开发人员主要使用集成开发工具(IDE)。作为一款软件协作开发管理平台产品,Azure DevOps Server完全支持基于m...