4.3 Kubernetes网络组件之 Flannel
Flannel是CoreOS维护的一个网络组件,Flannel为每个Pod提供全局唯一的IP,Flannel使用ETCD来存储Pod子网与Node IP之间的关系。flanneld守护进程在每台主机上运行,并负责维护ETCD信息和路由数据包。
其实k8s网络组件flannel和calico主要解决的问题是k8s节点之间容器网络的通信,flannel要保证每个pod的IP是唯一的,怎么保证是唯一的,大部分组件的做法是在每个Node上分配一个唯一的子网,node1是一个单独的子网,node2是一个单独的子网,可以理解是不同网段,不同vlan,所以每个节点都是一个子网,所以flannel会预先设置一个大的子网,然后在这个每个node上分配子网,这些信息都会由flannel存储到etcd中,并且每个子网绑定到node上都有关系记录的,然后方便下次进行二次的数据包传输,并且flannel在node上会启动一个守护进程并运行,守护进程主要维护的是本地的路由规则,和维护etcd中的信息。

1、Flannel 部署

 https://github.com/coreos/flannel 
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

部署好之后会以daemonset的形式在每个node上启动一个pod,来启动一个flannel的守护进程,主要负责本机路由表的设定和etcd中的数据,本地的子网上报到etcd中,所以守护进程是非常重要的
可以在flannel的配置文件去设定大的子网,还有属性模式

 net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "vxlan"
      }
    }

---

这个配置完之后会放到cni这个目录下,由于flannel是使用网桥的模式,实现的同节点数据包到达宿主机这个的通信,所以子网信息并没写到这个配置文件里,而是放到了这个 cat /var/run/flannel/subnet.env 下,这个通过ip a也能看到设备分配的ip,每个节点都会分配一个子网,网络接口设备为cni0,也就是一个node上可以分配255个小的子网

[root@k8s-node2 ~]# cat /var/run/flannel/subnet.env 
FLANNEL_NETWORK=10.244.0.0/16
FLANNEL_SUBNET=10.244.0.1/24
FLANNEL_MTU=1450
FLANNEL_IPMASQ=true

还有一个cni的二进制文件, /opt/cni/bin,这个就是kubelet调用这个二进制接口为创建的每个pod创建网络信息,并且是从我们的配置的子网中去拿IP

配置的话修改的是就是预先设定它的子网,以及工作模式,另外就是这个网络不能与k8s本身的内网冲突,否则导致网络不通的状况

2、 Flannel工作模式及原理
Flannel支持多种数据转发方式:
UDP:最早支持的一种方式,由于性能最差,目前已经弃用。
VXLAN:Overlay Network方案,源数据包封装在另一种网络包里面进行路由转发和通信
这也是网络的虚拟化技术,也就是原来是有一个包数据包,有源IP和目的IP,但由于某些情况这个数据包到达不了目的地址上,这可能就会借助物理上的以太网网络进行封装一个数据包带上,然后通过这种物理网络传输到目的地址上,这是一种叠加式的网络,里面是有两种数据包的,这种也叫做隧道方案
Host-GW:Flannel通过在各个节点上的Agent进程,将容器网络的路由信息刷到主机的路由表上,这样一来所有的主机都有整个容器网络的路由数据了,这样它就知道这个数据包到达这个节点转发到这个机器上,也就是路由表之间转发的,这种也叫路由方案
VXLAN

使用kubeadm部署的话默认是支持的

kubeadm部署指定Pod网段
kubeadm init --pod-network-cidr=10.244.0.0/16

但是使用二进制部署就得去启动cni的支持,默认我ansible部署的k8s集群都是启动的
二进制部署指定

cat /opt/kubernetes/cfg/kube-controller-manager.conf
--allocate-node-cidrs=true \     允许node自动分配cidr这个网络
--cluster-cidr=10.244.0.0/16 \   指定pod网络的网段,这个网段要和flannel的网段对应上

另外也都要在每个node节点的kubelet的配置文件上进行对cni的支持

[root@k8s-node1 ~]# cat /opt/kubernetes/cfg/kubelet.conf 
--network-plugin=cni \

这样的话就能以cni的标准来为k8s配置网络

kube-flannel.yml
net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "vxlan"
      }
    }

在节点1上有个容器,与节点2上的容器进行通信,这两个是进行跨主机进行的通信,如果本机通信之间使用网桥使用二层的传输了,像原生的docker网就能解决了,最重要的是这两个节点的数据包传输

flannel保证每个node都是唯一的ip,它是在每个node上都分配一个子网
可以看到flannel是基于宿主机创建的,它会为每个node创建独立的子网,并为当前pod分配ip

[root@k8s-master1 ~]# kubectl get pod -n kube-system -o wide
kube-flannel-ds-amd64-4jjmm           1/1     Running   0          14d   10.4.7.11     k8s-master1   <none>           <none>
kube-flannel-ds-amd64-9f9vq           1/1     Running   0          14d   10.4.7.21     k8s-node2     <none>           <none>
kube-flannel-ds-amd64-gcf9s           1/1     Running   0          14d   10.4.7.12     k8s-node1     <none>           <none>

为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。下图flannel.1的设备就是VXLAN所需的VTEP设备。示意图如下:
Kubernetes网络组件之Flannel策略实践(vxlan、host-gw)

vxlan是怎么工作的?
vxlan是Linux上支持的一个隧道的技术,隧道也就是点到点,端到端的两个设备的通信,其实vxlan实现是有一个vtep的设备做数据包的封装与解封装,而现在已经封装到flannel.1这个进程里面了,也就是这个虚拟网卡包含了veth来去使用对这个vxlan进行封装和解封装。

现在是Node1节点上的pod 1是1.10,现在要与Node2节点上的pod 2的2.10进行通信,他们是不在一个网络的,当这个数据包发出去的时候,pod1 的容器的网卡eth0,先出这个网卡,然后会连接这个veth这个好比就是一个网线,etch0是一头,veth是一头,也就是veth是这个设备的另一头,
这个veth是在宿主机上,那么这个宿主机就拿到了这个容器的数据包,然后这个数据包到达这个网桥上面,这个网桥也好比一个二层的交换机,所有的容器都会加入到这个网桥里面,可以通过yum -y install bridge-utils看到veth的另一端是不是加入到cni的网桥中,这个网桥就是flannel创建的,并且这个网桥有独立的mac地址和IP都可以看到

[root@k8s-node2 ~]# brctl show cni0
bridge name bridge id       STP enabled interfaces
cni0        8000.4a025e87aa87   no      veth08925d5a
                            veth2591a36f
                            veth676a1e86
                            veth718beeac
                            veth81dadcbd
                            veth8a96f11c
                            veth8c90fdb6
                            veth8f350182
                            veth90818f0b
                            vetha471152b

这个就是当我们创建好pod的时候由flannel去分配并加入这个网桥中的,这个后面有个interfaces有这个接口,这个相当于交换机的接口,这正是宿主机上的虚拟网卡,如果本地的话,直接走这个网桥就能直接找到了,然后就可以发送一个ARP广播包进行封包传输了,cni0就相当于一个二层交换机,帮你扩散,找目的的mac进行响应,所以说同节点就可以直接走网桥这个,那么这个目的地址不在这个网桥里面,就像2.10,当前的node是不知道2.10上的pod在哪,那么它只能走路由表了,也就是它它不一定目的地址的时候就会走默认网关,所以flannel会在宿主机上生成很多路由表通过ip router可以看到

[root@k8s-node2 ~]# ip route 
default via 10.4.7.1 dev eth0 proto static metric 100 
10.4.7.0/24 dev eth0 proto kernel scope link src 10.4.7.21 metric /
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1  /
10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink /
10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink /
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 

部署docker生成的路由表,这里的docker0网桥是没有用到,当部署flannel的时候是默认使用的是自己的网桥,这个的原理和flannel的是一样的,只不过flannel用的是自己做的,也是为了方便自己处理数据包

这里的路由表都记录下来了,它会找哪个是目的地址2.10pod2的IP地址,所以它会根据这个路由表,然后发送到flannel的这个设备上,这个flannel是采用vxlan的模式,vxlan需要veth的数据封装与解封装,所以flannel就把这个数据包交给vxlan,而vxlan是一个内核级的驱动程序,有它去封装这个包,因为vxlan本身是工作在二层的,它还需要目的的mac地址

那么就可以通过ip neigh show dev flannel.1去查看mac地址

 [root@k8s-node2 ~]# ip neigh show dev flannel.1
10.244.2.0 lladdr ea:ca:d6:62:be:21 PERMANENT
10.244.1.0 lladdr 4e:e3:fa:5f:d2:34 PERMANENT

而flannel.1的vxlan实现是有一个vtep的设备做数据包的封装与解封装,因为它在2层进行封包,就要知道目的的mac地址,那么这个目的mac就由flannel去提供给vetp,flannel去存储对应下一跳的网关,那么这个网关肯定不是在本地,当我们拿到目的的mac地址之后你们就封装成一个完整的帧,那么封装好之后,对于宿主机没有太多的实际意义,因为这个数据包帧发不出去,要是按二层的走肯定到不了另外一个节点,因为在不同的子网里面,如果没有路由的介入肯定是通信不了的,接下来就需要linux内核的数据帧封装一个宿主机普通的数据帧,也就是udp封装一个普通的数据帧,也就是在这之上再加一层udp的包,这样做的目的能让数据包直接传输到目的容器的主机上。
Kubernetes网络组件之Flannel策略实践(vxlan、host-gw)
vxlan是使用的udp协议,它会将原始的报文放在内部,而外部由udp封装的源IP与目的地址

[root@k8s-master1 ~]# bridge fdb show  dev flannel.1
a6:a4:e5:5d:19:9b dst 10.4.7.21 self permanent
ea:ca:d6:62:be:21 dst 10.4.7.12 self permanent

可以看到,上面用的对方flannel.1的MAC地址对应宿主机IP,也就是UDP要发往的目的地。使用这个目的IP进行封装。
也就是这些flannel都是知道的,为什么说flannel维护这etcd的数据,守护本地的路由规则,其实etcd的数据要和flannel,把它当前的数据写到etcd中,由各个节点都存储一份,所以根据这个地址拿到了mac地址,然后这又是一个完整的包,由vxlan封装的udp的包,这个udp包里面就有两个IP包的存在,udp就直接能发送到node2的节点上,数据包已经传输过去了,因为宿主机之间是同网段的,到达31.63上之后,接收到udp的包之后,会进行拆分,解包会将原始的包拿出来,所以这里就有一个vxlan的标记,本身flannel是由vtep处理的,所以在封包的时候对着干包打了个标记,也就是vxlan header的标记,首先打上vxlan的头部,那么这就意味着这就是一个vxlan的数据包,并且加了一个VNI的编号,VNI是为了区分vxlan的点对点隧道,多个数据包也是分外多个编号,也是为了区分,而这个编号被flannel引用到了,所以这是内部的一个编号,确认这个数据包无误,然后交给flannel.1这个设备,它处理这个数据包,拿到了源IP和目的IP,而去判断,会发现这个是cni网桥的,所以它根据路由表放到了cni网桥,根据这个路由表拆分这个目的地址,正好这个目的地址匹配到了,所以它会将这个转发到cni网桥里,到cni就跟之前一样了,就相当于一个二层交换机,拿到这个数据包,它会进行一个ARP的广播,发现正在这个网桥里面,然后就进行数据包的转发了。

从此看来;vxlan使用重叠网络,进行封包解封包,性能就下降了很多

小结:

  1. 容器路由:容器根据路由表从eth0发出
    / # ip route
    default via 10.244.0.1 dev eth0 
    10.244.0.0/24 dev eth0 scope link  src 10.244.0.45 
    10.244.0.0/16 via 10.244.0.1 dev eth0 
  2. 主机路由:数据包进入到宿主机虚拟网卡cni0,根据路由表转发到flannel.1虚拟网卡,也就是,来到了隧道的入口。
    ip route
    default via 192.168.31.1 dev ens33 proto static metric 100 
    10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1 
    10.244.1.0/24 via 10.244.1.0 dev flannel.1 onlink 
    10.244.2.0/24 via 10.244.2.0 dev flannel.1 onlink 
  3. VXLAN封装:而这些VTEP设备(二层)之间组成二层网络必须要知道目的MAC地址。这个MAC地址从哪获取到呢?其实在flanneld进程启动后,就会自动添加其他节点ARP记录,可以通过ip命令查看,如下所示:
ip neigh show dev flannel.1
10.244.1.0 lladdr ca:2a:a4:59:b6:55 PERMANENT
10.244.2.0 lladdr d2:d0:1b:a7:a9:cd PERMANENT
  1. 二次封包:知道了目的MAC地址,封装二层数据帧(容器源IP和目的IP)后,对于宿主机网络来说这个帧并没有什么实际意义。接下来,Linux内核还要把这个数据帧进一步封装成为宿主机网络的一个普通数据帧,好让它载着内部数据帧,通过宿主机的eth0网卡进行传输。
  2. 封装到UDP包发出去:现在能直接发UDP包嘛?到目前为止,我们只知道另一端的flannel.1设备的MAC地址,却不知道对应的宿主机地址是什么。
    flanneld进程也维护着一个叫做FDB的转发数据库,可以通过bridge fdb命令查看:

    bridge fdb show  dev flannel.1
    
    d2:d0:1b:a7:a9:cd dst 192.168.31.61 self permanent
    ca:2a:a4:59:b6:55 dst 192.168.31.63 self permanent

    可以看到,上面用的对方flannel.1的MAC地址对应宿主机IP,也就是UDP要发往的目的地。使用这个目的IP进行封装。

  3. 数据包到达目的宿主机:Node1的eth0网卡发出去,发现是VXLAN数据包,把它交给flannel.1设备。flannel.1设备则会进一步拆包,取出原始二层数据帧包,发送ARP请求,经由cni0网桥转发给container。

Host-GW
host-gw模式相比vxlan简单了许多, 直接添加路由,将目的主机当做网关,直接路由原始封包。
切换成host-gw的模式,上面的转发还是一样,pod1容器的网卡先连接veth到宿主机上,然后到达cni0的网桥上,这个网桥就相当于一个二层的交换机,这个数据包到cni的网桥之后,也就是到达宿主机上,那么宿主机的网络协议栈会根据路由表决定转发到哪个网关上,因为它的目的IP地址不是同网段的,肯定走路由表,它会根据路由表判断目的地址是2.10,也就是来自这个数据的数据包转发到了它的下一跳,也就是网关,通过接口之间转发到31.63上了,也就是直接安照宿主机的网络,因为这个数据包是宿主机处理的,所以宿主机要想访问31.63,会进行重新封包,目的地址就是31.63,它判断了31.63下一跳的网关是同一子网,而且是二层的传输,二层的传输又需要获取到目的的mac地址,如果它本地不知道31.63的mac地址的话,它会发送一个ARP广播包,知道了对方的mac就进行封包,所以经过二层的传输到达31.63,31.63收到之后数据包之后,它又会去判断路由表了,然后进入cni的网桥,二层又转发到了容器里面。

最重要两条,host-gw是把每个节点都当成一个网关,它会加入其他节点并设成网关,当数据包到达这个节点的时候,就根据路由表之间发送到下一跳了,也就是节点IP,这个都是同网段的IP,直接通过2层之间把这个数据,转发到另一个节点上,另一个节点再根据另一条规则,根据目的的地址转发到cni网桥,cni网桥根据2层又转发到容器里面,一个是数据的流入,就是当数据包到达这个节点之后,然后发给谁,这是流入数据包,一个是数据包的流出,当从节点出来的数据包,应该转发到哪个node上,这些都是由flannel去维护的
这个的局限是每个node在2层都能通,否则下一跳转发不过去,但是它的性能要比vxlan的性能高很多,不需要封包解封包,这种接近原生,性能也是最好的

下面是示意图:
Kubernetes网络组件之Flannel策略实践(vxlan、host-gw)

kube-flannel.yml

net-conf.json: |
    {
      "Network": "10.244.0.0/16",
      "Backend": {
        "Type": "host-gw"
      }
    }

看名字就能看出hots-gw它把目的的主机当作网关,直接路由原始的封包
将vxlan切换成host-gw的模式,重建之后可以看到路由表发生变化,切换的时候也会对网络进行影响,一般是在夜深人静的时候去做

之前的路由表都是通过flannel.1去转发到设备上,也就是使用host-gw,flannel.1这个设备就不用了,所以就不会用vxlan进行去封包了
当你设置flannel使用host-gw模式,flanneld会在宿主机上创建节点的路由表:

ip route

default via 192.168.31.1 dev ens33 proto static metric 100 
10.244.0.0/24 dev cni0 proto kernel scope link src 10.244.0.1 
10.244.1.0/24 via 192.168.31.63 dev ens33 
10.244.2.0/24 via 192.168.31.61 dev ens33 
192.168.31.0/24 dev ens33 proto kernel scope link src 192.168.31.62 metric 100

目的 IP 地址属于 10.244.1.0/24 网段的 IP 包,应该经过本机的 eth0 设备发出去(即:dev eth0);并且,它下一跳地址是 192.168.31.63(即:via 192.168.31.63)。
一旦配置了下一跳地址,那么接下来,当 IP 包从网络层进入链路层封装成帧的时候,eth0 设备就会使用下一跳地址对应的 MAC 地址,作为该数据帧的目的 MAC 地址。
而 Node 2 的内核网络栈从二层数据帧里拿到 IP 包后,会“看到”这个 IP 包的目的 IP 地址是 10.244.1.20,即 container-2 的 IP 地址。这时候,根据 Node 2 上的路由表,该目的地址会匹配到第二条路由规则(也就是 10.244.1.0 对应的路由规则),从而进入 cni0 网桥,进而进入到 container-2 当中。

小结:
如果想追求性能的话,二层可以通信,那么就可以选择host-gw,那么如果两个节点之间是不能通过二层通信,那么可能需要路由的转发,那么可能在不同的vlan中,那么使用vxlan是最好的,因为可以满足这样的一个需求。