滴普技术荟 - 云原生基座 OpenKube 开放容器实践( 七):flannel-udp 模式原理分析
本来上一篇文章应该是介绍 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 文件:
1. 以 k8s 的 daemonset 运行的 kube-flannel,负责与 ETCD 交互,获取最新的节点与子网信息,通过 unix domain socket 的方式把信息同步给相同节点的 flanneld 进程
2. 一个名叫 flanneld 守护进程,负责监听 UDP 8285 端口(默认端口,可以修改)并打开/dev/net/tun 设备,不管哪一端来的数据,都往另一端转;同时会打开一个 unix domain socket,接收来自 kube-flannel 的指令,更新路由。
3. binary 文件,存放目录为:/opt/cni/bin/flannel,在 kubelet 创建 pod 时会调用这个 binary 文件,负责创建同主机容器的相互通信,但它不具体做创建 bridge 或 veth 的工作,它只是生成了一个配置文件,然后调用本机的其它 cni 插件(例如:bridge 和 host-local,通常 cni 插件都在/opt/cni/bin 目录下)来完成配置同主机容器的通信。
另外,在每个节点还会:
1. 创建一个名为 flannel.1 的 tun 设备
2. 创建了一条到 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
---------------------------------------------------------------------------------
另外在两主机上都:
1. 开启了路由转发(net.ipv4.ip_forward=1)。
2. 分别已经运行了 kube-flannel 和 flanneld 进程,kube-flannel 已经和 etcd 或 api-server 正常连接,并订阅到了全部节点和节点的子网信息,也已经传递给了主机的 flanneld 上的路由表。
3. 主机的 pod 都用 veth 的方式与主机相互正常连接,pod 上都像前面的文章提到的配置了默认网关 169.254.2.2,并且对端 veth1 都开启了 ARP 代答,所以 pod1 此时已经能把出容器的流量送到主机端的 veth1 了。
下面我们将通过一个例子来详细说明数据包的发送和接收的全过程,这里 binary 文件 flannel 的工作就不再说明了,这并非本文的重点。
发送流程
我们假设 pod2 有一个 web 服务在运行,当我们从 pod1 发送一个 http 请求给 pod2 时,会经历以下步骤:
1. 数据包从 pod1 的用户进程出来,进入 pod1 的协议栈,协议栈发现要去往目的地并非在相同网段,于是设置了下一跳为默认网关 169.254.2.2,通过 ARP 查到默认网关的 IP 对应的 MAC 为主机 veth1 的 MAC 地址,于是把 veth1 的 MAC 地址填进目标 MAC 地址,完成 MAC 头的封装,把包发送到 veth1,进入主机 host1 的网络协议栈。
2. 数据包在 host1 的网络协议栈经过 ROUTE 判断,发现目标地址 10.244.2.3 并非本机地址,因为本机开启了路由转发,所以走 FORWARD 链。
3. 主机协议栈在主机路由中为数据包寻找合适的路由,匹配到去往 10.244.0.0/16 网段的包应该走 tun0,于是从 tun0 转发出去。
4. tun0 是一个 linux tun 设备,从协议栈收的包,会被另一端的用户进程收到,在这里 tun0 的另一端由 flanneld 进程打开(就是打开了/dev/net/tun 这个文件),于是 flanneld 收到了数据包。
5. 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 更容易实现,真是让人费解。
了解更多信息请登录:https://www.deepexi.com/bbs/developer-bbs
版权声明: 本文为 InfoQ 作者【滴普科技2048实验室】的原创文章。
原文链接:【http://xie.infoq.cn/article/e1807b766c2ec7f2b79545669】。文章转载请联系作者。
评论