Flannel作为容器和Kubernetes网络虚拟化插件,为多机集群的容器间通信提供了支持,目前有3种后端类型。这里我先简单分析Docker和Kubernetes的网络通信原理和问题,然后再分析Flannel的解决方案。
Docker网络
Docker会为每一个容器创建若干种命名空间,拥有独立的协议栈、路由表、进程、套接字和网络设备。容器内与主机之间的通信通过虚拟设备进行,把一个虚拟设备设为容器的命名空间,另一个虚拟设备在默认命名空间,两个虚拟设备相连,容器内的网络数据会通过虚拟设备流出,主机端虚拟设备接收。
为了能更好的转发数据包是,Docker会创建一个网桥docker0,所有主机端的虚拟设备都会与docker0相连,docker0被分配一个虚拟IP,所有容器的虚拟设备地址都属于docker0网段,各容器的虚拟设备地址会被网桥docker0记录。当发现数据包是从一个容器发至另一个容器,docker0通过解析其目的地址就能正确转发至对应容器;若目的地址不属于docker0网段,则会被发至协议栈。
K8s网络
K8s以Pod为单位管理,一个Pod内的所有容器共享网络命名空间,同样通过虚拟设备与主机通信。在单机情况下,K8s网络与Docker网络无异,但虚拟地址仅能在主机内使用,为了能使得集群内的每一个Pod能通过虚拟IP相互通信,需要做一些额外的工作。
这实际涉及到一个网络虚拟化方案的问题,其实能做到这一点的方案有很多,各家公司或团队偏好和需求可能各不相同,为了满足这一点,CoreOS公司提出了CNI模型,旨在统一网络虚拟化的接口,使得各种虚拟化方案能够变为插件,在集群内可插拔的使用。
目前K8s机器支持的网络插件有Calico, Flannel, Wave Net等等。下面我以Flannel为例,讲讲它如何实现网络虚拟化。
Flannel后端类型
Fannel支持几种网络虚拟化方案,根据官方文档可知Fannel有三种后端类型。定义想要使用的后端类型只要在插入etcd的配置中声明即可,配置格式详见这里。下面分别介绍三种后端类型。
VXLAN
VXLAN(Virtual Extensible Local Area Network,虚拟扩展局域网)是一种隧道技术,在集群中建立一条基于IP网络的逻辑隧道,这里的隧道可以理解为由很多个网络设备(VTEP)组成的网络,这里的网络设备相当于物理网桥或虚拟网桥,可以把报文相互转发。
VTEP将数据帧加一个VXLAN头部并进行封装在UDP报文里面,然后放入隧道进行传输,这里的封装是报文在隧道中传输的凭据,就像网络层报头是在网络层传输的凭据一般,隧道在把报文转发给目标主机前由一个VTEP设备把之前的封装拆掉。所以对于主机来说,它们发出和接收的数据帧依旧是原始数据帧,隧道中发生的一切对于主机来说都是无感。
关于VXLAN更详细的讲解可以参考这篇文章。
host-gw
host-gw(host gateway,主机网关)。我们像划分物理网段那样,对所有主机划分虚拟网络段。原本一台主机的虚拟网络与另一台主机的虚拟网络没有关系,冲突了也无碍,但现在需要把集群中所有主机的虚拟网络统一起来,并按照规则划分,使其不会发生冲突。当确保虚拟网段不会冲突时,我们就可以预先的在每一台主机上对路由表进行修改,把所有虚拟网段直接映射到其对应的物理地址上,这样当一台主机访问一个虚拟地址时,就可以通过路由表直接投递到对应的物理地址并被目的主机接收。
举个例子,我们往路由表里加入一条规则,把虚拟网段映射到某个物理地址上, 1
10.1.88.0/24 via 192.168.223.13 dev ens33
10.1.88.0/24
范围内的包,都通过设备ens33
发送到,发送到物理网关192.168.223.13
上。
一般来说,via
后面都是定义下一跳物理网关(或路由器)的,这样就能使数据包通过网关传输。但这里的192.168.223.13
实际上并非网关地址,而是主机地址,相当于我们把一台主机的物理地址作为另一条主机的虚拟网段的网关。
这一后端类型全程都在内核中完成,是三种类型中性能最高的。
UDP
使用UDP模式的前提同样是对集群的虚拟网段进行划分,我们会把虚拟网段和物理地址的映射记录到数据库中,比如etcd。当用户尝试向某个虚拟地址发包时会被路由到TUN设备flannel0上,并传输给常驻进程flanneld,flanneld把用户报文通过UDP封装一层,并查询数据库,把UDP报头目的地址设为目标物理地址,然后再发出去。目标主机收到包后传到flanneld进程里,flanneld对报文拆包,并把原始报文传给flannel0,然后就进入协议栈的流程。
UDP类型导致数据包传输过程中多4次用户态与内核态之间的切换,源主机上从flannel0到flanneld,再从flanneld到协议栈,目的主机上从协议栈到flanneld,再从flanneld到flannel0。性能最差,官方的说法是不建议在生产环境使用。
Flannel UDP的网络传输
当开启UDP类型后,会新增一个flannel0的TUN设备,具有一个网段,并且网桥docker0网段会属于该网段。下面以某个k8s节点的网络设备情况举例,可以看到docker0属于flannel0的子网, 1
2
3
4
5
6# ip addr
flannel0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1472 qdisc pfifo_fast state UNKNOWN group default qlen 500
inet 10.1.72.0/32 brd 10.1.72.0 scope global flannel0
docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1472 qdisc noqueue state UP group default
inet 10.1.72.1/24 brd 10.1.72.255 scope global docker0
我们先来看看在UDP类型下K8s和Flannel是如何处理网络传输的。其实在K8s里,我们发送网络包的情况无非分为以下8种:
- 在容器内通过Cluster-IP发送到另一台主机上
- 在主机通过Cluster-IP发送到另一台主机上
- 在容器内通过Pod-IP发送到另一台主机上
- 在主机通过Pod-IP发送到另一台主机上
- 在容器内通过Cluster-IP发送到自己主机上
- 在主机通过Cluster-IP发送到自己主机上
- 在容器内通过Pod-IP发送到自己主机上
- 在主机通过Pod-IP发送到自己主机上
这里可以先下结论,在UDP类型下,只有情况2“在主机通过Cluster-IP发送到另一台主机上”是不可行的,其他情况都能做到。下面我会逐一分析以上几种情况的网络传输流程,具体验证方法可以通过Linux的iptables, tcpdump, ip route等工具完成。
情况1:在容器内通过Cluster-IP发送到另一台主机上
我们先看第一种情况,整个传输流程可以看下图,
传输流程:
- 当Pod A请求Cluster-IP时,网络包经过协议栈并会从容器中的eth0发送到宿主机veth;
- 网络包通过veth进入docker0,通过docker0时,包的源地址被改写为docker0的地址;
- 网络包从docker0进入协议栈,netfilter的PREROUTING阶段的会发生NAT,把目的地址从Cluster-IP改写为Pod B-IP。NAT规则是由主机上的kube-proxy动态修改的;
- 网络包通过NAT转发至TUN设备flannel0,NAT后会马上进行一次路由选择,并且根据路由配置,会把源地址改为docker0地址;
- 从flannel0进入用户态进程flanneld,flanneld会包的源地址改为flannel0地址,并把包封装到UDP报文中,UDP报头的源地址设为主机地址,目的地址设为Pod B所在的主机地址及其flanneld端口;
- UDP报文通过协议栈进入主机网卡eth0,并发送到物理网络上;
- 另一台主机Node B的eth0收到网络包,网络包通过协议栈进入到flanneld进程;
- flanneld拆开UDP报文,并把原报文发给flannel0
- 网络包从flannel0进入协议栈,在通过PREROUTING后被路由表路由到docker0;
- 网络包从docker0到veth,再到容器内的eth0,通过协议栈到达用户进程。
情况2:在主机通过Cluster-IP发送到另一台主机上
前面下了结论,情况2无法做到,这里分析一下流程:
- 在宿主机请求Cluster-IP;
- 网络包会通过协议栈,在netfilter的OUT阶段发送NAT,并转发至flannel0,flannel0把包发至flanneld;
- 按常理,flanneld应该会如同情况1一般处理该网络包,但这里它并没有这么做。原因在于flanneld发现该网络包的源地址是主机地址,而非flannel0网段地址,所以会把这个网络包丢掉。
情况3:在容器内通过Pod-IP发送到另一台主机上
该情况与情况1大体相同,区别在于网络包从docker0进入协议栈后,并不会在PREROUTING阶段发送NAT,而是在之后被路由表路由到flannel0上,之后的步骤就和情况1完全相同了。
情况4:在主机通过Pod-IP发送到另一台主机上
该情况的流程为:
- 在宿主机请求Cluster-IP;
- 网络包被路由表路由至flannel0;
- 理论上来说,这一步会遇到和情况2相同的问题,那就是源地址是主机地址,会被flanneld扔掉,但路由表具有改写源地址的功能,在路由到flannel0之前,就会把包的源地址改写成flannel0地址,所以可以被flanneld处理,之后的步骤就与情况1完全相同了。
情况5:在容器内通过Cluster-IP发送到自己主机上
该情况与情况1的区别是,网络包从docker0进入协议栈,在netfilter的PREROUTING阶段会被NAT到docker0而非flannel0,因为docker0是flannel0的子网,Pod B的地址同时符合两个网段,而路由时会路由到更具体的网段上。网桥docker0会把网络包发至对应的虚拟设备上。
情况6:在主机通过Cluster-IP发送到自己主机上
这种情况下,网络包会通过OUT阶段的NAT转发到docker0,网桥docker0会把网络包发至对应的虚拟设备上。
情况7:在容器内通过Pod-IP发送到自己主机上
该情况下,网络包通过docker0后进入协议栈,被路由表路由至docker0。值得注意的是,因为源地址属于与docker0同一个网段,所以路由的时候会保留源地址。
情况8:在主机通过Pod-IP发送到自己主机上
这种情况下,从宿主机发出的网络包会被路由表直接路由至docker0,网桥docker0会把网络包发至对应的虚拟设备上,全程没有经过宿主机协议栈。值得注意的是,因为源地址属于与docker0不再同一个网段,所以路由的时候把源地址改为docker0地址。
Flannel host-gw的网络传输
在host-gw后端下,不会创建flannel0,flanneld进程不再负责封包与拆包。当flanneld启动后,会先从etcd当前集群的虚拟网段信息,然后自己创建一个新的虚拟网段并记录在etcd,这个网段是给本机docker0使用的,以便于集群中所有docker0的网段都不会发生冲突。同时,flanneld会把集群中所有网段信息,即虚拟网段与物理节点地址的映射,写入本机路由表。flanneld会监听etcd上关于集群网段的信息变化,以便于及时更新本机的路由表。
下面以集群中某个节点的路由表为例, 1
2
3
4# ip route
10.1.29.0/24 via 192.168.223.13 dev ens33
10.1.97.0/24 dev docker0 proto kernel scope link src 10.1.97.1
...10.1.97.0/24
是本机的docker0网段,而10.1.29.0/24
则是另一个节点的docker0网段,通过via 192.168.223.13
直接指定其下一跳路由地址,即直接通过路由表将数据包传送至目标主机上。同时这样的配置不会修改数据包的目的地址,离开网卡时目的地址仍然是一个虚拟地址,只是通过路由表直接投递到目标主机上,当数据包到达目标主机时,可以通过目标主机的路由表能转发至docker0上。
使用host-gw模式,因为不存在flanneld把数据包丢掉的情况,所以上面提到的8种情况下,所有数据包都是可达的。同时因为数据包全程都在内核转发,不经过内核态和用户态的拷贝,性能是最高的。
下图是在host-gw后端下,在容器内通过Cluster-IP访问另一台主机的网络传输过程,
Comments