本来上一篇文章应该是介绍flannel的udp模式的了,但因为其中的内容会涉及到linux虚拟网络设备tun的原理,所以先介绍一下tun设备,然后在这一篇才转入正题。
flannel一共有UDP、VXLAN、HOST-GW三种工作模式,如果开启了Directrouting的话,会使用VXLAN和HOST-GW组合,不跨网段就使用HOST-GW,跨网段就使用VXLAN。对于host-gw和vxlan我们在前面的文章中有过简单地介绍,在这篇文章我们来分析一下flannel的UDP模式,虽然这种模式现在基本上已经不在生产上用了,但通过对UDP模式的了解,可以让我们更好地理解linux中的虚拟网络设备tun,以及为我们提供一种在用户态操作内核协议栈数据的思路。
当flannel以udp模式运行的时候,每个节点会有两个守护进程以及一个binary文件:
- 以k8s的daemonset运行的kube-flannel,负责与ETCD交互,获取最新的节点与子网信息,通过unix domain socket 的方式把信息同步给相同节点的flanneld进程
- 一个名叫flanneld守护进程,负责监听UDP 8285端口(默认端口,可以修改)并打开/dev/net/tun设备,不管哪一端来的数据,都往另一端转;同时会打开一个unix domain socket,接收来自kube-flannel的指令,更新路由。
- binary文件,存放目录为:/opt/cni/bin/flannel,在kubelet创建pod时会调用这个binary文件,负责创建同主机容器的相互通信,但它不具体做创建bridge或veth的工作,它只是生成了一个配置文件,然后调用本机的其它cni插件(例如:bridge和host-local,通常cni插件都在/opt/cni/bin目录下)来完成配置同主机容器的通信。
另外,在每个节点还会:
- 创建一个名为flannel.1的tun设备
- 创建了一条到POD-CIDR的直连路由,流量全部引到flannel.1的tun设备,注意是pod-cidr,不是某个node-cidr,这就意味着这台机出的流量只要是去容器的(当然除了本机的容器),就会走这张网卡。
下面一步步地解释上面的部件是怎么联动起来并完成跨主机的POD之间的通信的,在下面我们创建的tun设备名为tun0。
环境准备
首先介绍一下准备的环境,如下图:
(host1)
(host2)
两台主机,host1和host2:
---------------------------------------------------------------------------------
其中host1:
物理网卡eth0 IP:10.57.4.20;
linux veth设备veth1连着POD1,没有设置IP地址;
linux tun设备tun0,设置了IP为10.244.1.1(POD与POD通信时这个通常用不上,主机到POD时会用上)
主机路由三条,一条是默认网关;另外两条直连路由,分别到pod1和去往10.244.0.0/16
POD1的地址为10.244.1.3
---------------------------------------------------------------------------------
host2:
物理网卡eth0 IP:10.57.4.21;
linux veth设备veth1连着POD2,没有设置IP地址;
linux tun设备tun0,设置了IP为10.244.2.1(POD与POD通信时这个通常用不上,主机到POD时会用上)
主机路由三条,一条是默认网关;另外两条直连路由,分别到pod2和去往10.244.0.0/16
POD2的地址为10.244.2.3
---------------------------------------------------------------------------------
另外在两主机上都:
- 开启了路由转发(net.ipv4.ip_forward=1)。
- 分别已经运行了kube-flannel和flanneld进程,kube-flannel已经和etcd或api-server正常连接,并订阅到了全部节点和节点的子网信息,也已经传递给了主机的flanneld上的路由表。
- 主机的pod都用veth的方式与主机相互正常连接,pod上都像前面的文章提到的配置了默认网关169.254.2.2,并且对端veth1都开启了ARP代答,所以pod1此时已经能把出容器的流量送到主机端的veth1了。
下面我们将通过一个例子来详细说明数据包的发送和接收的全过程,这里binary文件flannel的工作就不再说明了,这并非本文的重点。
发送流程
我们假设pod2有一个web服务在运行,当我们从pod1发送一个http请求给pod2时,会经历以下步骤:
- 数据包从pod1的用户进程出来,进入pod1的协议栈,协议栈发现要去往目的地并非在相同网段,于是设置了下一跳为默认网关169.254.2.2,通过ARP查到默认网关的IP对应的MAC为主机veth1的MAC地址,于是把veth1的MAC地址填进目标MAC地址,完成MAC头的封装,把包发送到veth1,进入主机host1的网络协议栈。
- 数据包在host1的网络协议栈经过ROUTE判断,发现目标地址10.244.2.3并非本机地址,因为本机开启了路由转发,所以走FORWARD链。
- 主机协议栈在主机路由中为数据包寻找合适的路由,匹配到去往10.244.0.0/16网段的包应该走tun0,于是从tun0转发出去。
- tun0是一个linux tun设备,从协议栈收的包,会被另一端的用户进程收到,在这里tun0的另一端由flanneld进程打开(就是打开了/dev/net/tun这个文件),于是flanneld收到了数据包。
- flanneld查看数据包的IP头,发现目的地是10.244.2.3,于是从自己的路由中找去这个目的地下一跳,从kube-flannel传来的信息指示10.244.2.0/24这个子网在主机10.57.4.21上(这一步已经在kube-flannel和flanneld进程启动后就立刻完成了),于是flanneld进程经过一些必要的处理后,把从tun0来的数据包(包括TCP头、IP头)当成数据,通过打开的udp端口发往了10.57.4.21的UDP8285端口。
这时候由POD1的协议栈发出来的包,已经变成了另一个包的数据区的数据,从host1的eth0出来的数据包的结构如下:
接收流程
主机网段按正常的流程把数据包传输到了host2的eth0网卡上,来看看接收的全过程:
从10.244.2.3回包的流程也类似。
可以看到,flannel的udp模式在一次跨主机的pod与pod通信的过程中,需要两次切换用户态与内核态,所以性能其实远低于vxlan模式,虽然vxlan模式用的也是udp协议,但因为是在内核态完成数据包的处理,所以性能要远高于udp模式。
在flannel的源码中,flanneld是由c语言直接实现的,关键代码在/backend/udp/proxy_adm64.c,而kube-flannel则是由go语言实现的,关键部分在/backend/udp/cproxy_adm64.go。
proxy_adm64.c中最关键的就是tun_to_udp和udp_to_tun,光是看名字就已经明白了用途,而且代码还贼精简:
不知道为什么还要无端实现一个udp模式,据说是因为作者实现UDP模式时,linux的内核还不支持vxlan,但我去网上查了一下,linux内核从3.7开始支持vxlan,到3.12开始对vxlan已经完备
https://kernelnewbies.org/Linux_3.7#Virtual_extensible_LAN_tunneling_protocol
https://kernelnewbies.org/Linux_3.12#Networking
而3.7是2012年底发布的,3.12是2013年2月发布的,flannel代码的初次提交是2014年,应该不是这个原因,而且从实现上来说,明显是vxlan更容易实现,真是让人费解。