曾记得有一位哲人说过:“在云计算当中,计算最基础,存储最重要,网络最复杂”,而 PaaS 云平台 Kubernetes 的出现也使得网络的应用场景变得更加复杂多变。本文试图从 Kubernetes 当中容器跨节点网络通信方案 Flannel 的实际应用场景出发,带领读者梳理 Kubernetes 当中容器跨节点网络通信的实现过程以及背后的实现原理。
Kubernetes 网络模型
通过之前的文章《Kubernetes网络全解:机制、方法、实操的超强指南》我们知道了 Docker 通过 VEth 虚拟网络设备以及 Linux bridge 实现了同一台主机上的容器网络通信,再通过 iptables 进行 NAT 实现了容器和外部网络的通信。
这对于简单部署的应用已经能够满足网络通信的需求了,但是 Kubernetes 作为一个容器编排平台,要处理的是生产环境当中高并发大规模的弹性扩展问题,首先要解决的就是整个集群当中所有节点上的容器网络通信问题。
为了解决 Kubernetes 当中网络通信的问题,Kubernetes 作为一个容器编排平台提出了 Kubernetes 网络模型,但是并没有自己去实现,具体网络通信方案通过网络插件来实现。
其实 Kubernetes 网络模型当中总共只作了三点要求:
运行在一个节点当中的 Pod 能在不经过 NAT 的情况下跟集群中所有的 Pod 进行通信
节点当中的客户端(system daemon、kubelet)能跟该节点当中的所有 Pod 进行通信
以 host network 模式运行在一个节点上的 Pod 能跟集群中所有的 Pod 进行通信
从 Kubernetes 的网络模型我们可以看出来,在 Kubernetes 当中希望做到的是每一个 Pod 都有一个在集群当中独一无二的 IP,并且可以通过这个 IP 直接跟集群当中的其他 Pod 以及节点自身的网络进行通信,一句话概括就是 Kubernetes 当中希望网络是扁平化的。
针对 Kubernetes 网络模型也涌现出了许多的实现方案,例如 Calico、Flannel、Weave 等等,虽然实现原理各有千秋,但都围绕着同一个问题即如何实现 Kubernetes 当中的扁平网络进行展开。Kubernetes 只需要负责编排调度相关的事情,修桥铺路的事情交给相应的网络插件即可。
Flannel 简介
Flannel 项目为 CoreOS 团队对 Kubernetes 网络设计实现的一种三层网络通信方案,安装部署方式可以参考官方示例文档:
https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml,故关于 Flannel 的安装部署部分这里暂时不进行赘述,有兴趣的同学可以参考官方文档进行部署测试。
为了便于理解和说明,以下内容将用一个 1(master)+2(work node)的 Kubernetes 集群进行举例说明。
在安装部署完成之后应该能看到在各个节点上通过 DaemonSet 的方式运行了一个 Flannel 的 Pod。
每一个 Flannel 的 Pod 当中都运行了一个 flanneld 进程,且 flanneld 的配置文件以 ConfigMap 的形式挂载到容器内的/etc/kube-flannel/目录供 flanneld 使用。
Flannel Backend
Flannel 通过在每一个节点上启动一个叫 flanneld 的进程,负责每一个节点上的子网划分,并将相关的配置信息如各个节点的子网网段、外部 IP 等保存到 etcd 当中,而具体的网络包转发交给具体的 Backend 来实现。
flanneld 可以在启动的时候通过配置文件来指定不同的 Backend 来进行网络通信,目前比较成熟的 Backend 有 VXLAN、host-gw 以及 UDP 三种方式,也已经有诸如 AWS,GCE and AliVPC 这些还在实验阶段的 Backend。VXLAN 是目前官方最推崇的一种 Backend 实现方式,host-gw 一般用于对网络性能要求比较高的场景,但需要基础架构本身的支持,UDP 则一般用于 Debug 和一些比较老的不支持 VXLAN 的 Linux 内核。
这里只展开讲讲最成熟也是最通用的三种 Backend 网络通信实现流程:
UDP
VXLAN
host-gw
UDP
由于 UDP 模式相对容易理解,故这里先采用 UDP 这种 Backend 模式进行举例说明然后再对其他 Backend 模式进行展开讲解。
采用 UDP 模式时需要在 flanneld 的配置文件当中指定 Backend type 为 UDP,可以通过直接修改 flanneld 的 ConfigMap 的方式实现,配置修改完成之后如下:
关键字段为 Backend 当中的 Type 字段,采用 UDP 模式时 Backend Port 默认为 8285,即 flanneld 的监听端口。
flanneld 的 ConfigMap 更新完成之后 delete flannel pod 进行配置更新:
当采用 UDP 模式时,flanneld 进程在启动时会通过打开/dev/net/tun 的方式生成一个 TUN 设备,TUN 设备可以简单理解为 Linux 当中提供的一种内核网络与用户空间(应用程序)通信的一种机制,即应用可以通过直接读写 tun 设备的方式收发 RAW IP 包。
flanneld 进程启动后通过 ip a 命令可以发现节点当中已经多了一个叫 flannel0 的网络接口:
细心的同学就会发现此时 flannel0 这个网络接口上的 MTU 为 1472,相比 Kubernetes 集群网络接口 eth1 小了 28 个字节,为什么呢?
通过可以 ip -d link show flannel0 可以看到这是一个 tun 设备:
通过 netstat -ulnp 命令可以看到此时 flanneld 进程监听在 8285 端口:
容器跨节点通信实现流程:
假设在节点 A 上有容器 A(10.244.1.96),在节点 B 上有容器 B(10.244.2.194),此时容器 A 向容器发送一个 ICMP 请求报文(ping),我们来逐步分析一下 ICMP 报文是如何从容器 A 到达容器 B 的。
1、容器 A 当中发出 ICMP 请求报文,通过 IP 封装后形式为:10.244.1.96 -> 10.244.2.194,此时通过容器 A 内的路由表匹配到应该将 IP 包发送到网关 10.244.1.1(cni0 网桥)。
完整的帧格式为:
2、此时到达 cni0 的 IP 包目的地 IP 10.244.2.194 匹配到节点 A 上第一条路由规则(10.244.0.0),内核将 RAW IP 包发送给 flannel0 接口。
3、flannel0 为 tun 设备,发送给 flannel0 接口的 RAW IP 包(无 MAC 信息)将被 flanneld 进程接收到,flanneld 进程接收到 RAW IP 包后在原有的基础上进行 UDP 封包,UDP 封包的形式为:172.16.130.140:src port -> 172.16.130.164:8285。
这里有一个问题就是 flanneld 怎么知道 10.244.2.194 这个容器到底是在哪个节点上呢?
flanneld 在启动时会将该节点的网络信息通过 api-server 保存到 etcd 当中,故在发送报文时可以通过查询 etcd 得到 10.244.2.194 这个容器的 IP 属于 host B,且 host B 的 IP 为 172.16.130.164。
RAW IP 包示例:
4、flanneld 将封装好的 UDP 报文经 eth1 发出,从这里可以看出网络包在通过 eth1 发出前先是加上了 UDP 头(8 个字节),再然后加上了 IP 头(20 个字节)进行封装,这也是为什么 flannel0 的 MTU 要比 eth1 的 MTU 小 28 个字节的原因(防止封装后的以太网帧超过 eth1 的 MTU 而在经过 eth1 时被丢弃)。
此时完整的以太网帧格式为:
5、网络包经节点 A 和节点 B 之间的网络连接到达 host B。
6、host B 收到 UDP 报文后经 Linux 内核通过 UDP 端口号 8285 将包交给正在监听的应用 flanneld。
7、运行在 host B 当中的 flanneld 将 UDP 包解包后得到 RAW IP 包:10.244.1.96 -> 10.244.2.194。
8、解封后的 RAW IP 包匹配到 host B 上的路由规则(10.244.2.0),内核将 RAW IP 包发送到 cni0。
此时的完整的以太网帧格式为:
9、cni0 将 IP 包转发给连接在 cni0 网桥上的 container B,而 flanneld 在整个过程中主要主要负责两个工作:
UDP 封包解包
节点上的路由表的动态更新
从上面虚线部分就可以看到 container A 和 container B 虽然在物理网络上并没有直接相连,但在逻辑上就好像是处于同一个三层网络当中,这种基于底下的物理网络设备通过 Flannel 等软件定义网络技术实现的网络我们称之为 Overlay 网络。
那么上面通过 UDP 这种 Backend 实现的网络传输过程有没有问题呢?最明显的问题就是,网络数据包先是通过 tun 设备从内核当中复制到用户态的应用,然后再由用户态的应用复制到内核,仅一次网络传输就进行了两次用户态和内核态的切换,显然这种效率是不会很高的。那么有没有高效一点的办法呢?当然,最简单的方式就是把封包解包这些事情都交给内核去干好了,事实上 Linux 内核本身也提供了比较成熟的网络封包解包(隧道传输)实现方案 VXLAN,下面我们就来看看通过内核的 VXLAN 跟 flanneld 自己通过 UDP 封装网络包在实现上有什么差别。
VXLAN
VXLAN 简介
VXLAN 全称 Virtual Extensible LAN,是一种虚拟化隧道通信技术,主要是为了突破 VLAN 的最多 4096 个子网的数量限制,以满足大规模云计算数据中心的需求。VLAN 技术的缺陷是 VLAN Header 预留的长度只有 12 bit,故最多只能支持 2 的 12 次方即 4096 个子网的划分,无法满足云计算场景下主机数量日益增长的需求。当前 VXLAN 的报文 Header 内有 24 bit,可以支持 2 的 24 次方个子网,并通过 VNI(Virtual Network Identifier)来区分不同的子网,相当于 VLAN 当中的 VLAN ID。
不同于其他隧道协议,VXLAN 是一个一对多的网络,并不仅仅是一对一的隧道协议。一个 VXLAN 设备能通过像网桥一样的学习方式学习到其他对端的 IP 地址,也可以直接配置静态转发表。
VXLAN 包格式:
从 VXLAN 的包格式就可以看到原本的二层以太网帧被放在 VXLAN 包头里进行封装,VXLAN 实际实现的是一个二层网络的隧道,通过 VXLAN 让处于同一个 VXLAN 网络(VNI 相同则为同一个 VXLAN 网络)当中的机器看似处在同一个二层网络当中(逻辑上处于同一个二层网络),而网络包转发的方式也类似二层网络当中的交换机(这样虽然不是很准确,但更便于理解)。
当采用 VXLAN 模式时,flanneld 在启动时会通过 Netlink 机制与 Linux 内核通信,建立一个 VTEP(Virtual Tunnel Access End Point)设备 flannel.1 (命名规则为 flannel.[VNI],VNI 默认为 1),类似于交换机当中的一个网口。
可以通过 ip -d link 查看 VTEP 设备 flannel.1 的配置信息,从以下输出可以看到,VTEP 的 local IP 为 172.16.130.244,destination port 为 8472。
在 UDP 模式下由 flanneld 进程进行网络包的封包和解包的工作,而在 VXLAN 模式下解封包的事情交由内核处理,那么此时 FlannnelD 的作用是什么呢?带着这个疑问我们先来简单看一下 VXLAN Backend 是如何工作的。
VXLAN Backend 工作原理
Flannel 当中对 VXLAN Backend 的实现经过了几个版本的改进之后目前最新版本的 flanneld 当中的处理流程为:
当 flanneld 启动时将创建 VTEP 设备(默认为 flannel.1,若已经创建则跳过),并将 VTEP 设备的相关信息上报到 etcd 当中,而当在 Flannel 网络中有新的节点发现时,各个节点上的 flanneld 将依次执行以下流程:
在节点当中创建一条该节点所属网段的路由表,主要是能让 Pod 当中的流量路由到 flannel.1 接口。
通过 route -n 可以查看到节点当中已经有两条 flannel.1 接口的路由:
在节点当中添加一条该节点的 IP 以及 VTEP 设备的静态 ARP 缓存。
可通过 arp -n 命令查看到 master 节点当中已经缓存了另外两个节点以及 VTEP 的 ARP 信息(已删除无关 ARP 缓存信息)。
在节点当中添加一条该节点的转发表。
通过 bridge 命令查看节点上的 VXLAN 转发表(FDB entry),MAC 为对端 VTEP 设备即 flannel.1 的 MAC,IP 为 VTEP 对应的对外 IP(可通过 flanneld 的启动参数–iface=eth1 指定,若不指定则按默认网关查找网络接口对应的 IP),可以看到已经有两条转发表。
VXLAN Backend 配置
跟 UDP Backend 一样,将 Flannel Backend 修改为 VXLAN 只需要将 Flannel ConfigMap 当中的 Backend type 字段修改为 VXLAN 即可。由于 VXLAN 类型相对 UDP 复杂并且有较好的灵活性,这里简单说一下 VXLAN 当中的几个配置选项:
VNI(Number):VXLAN Identifier,默认为 1
Port(Number):用于发送 VXLAN UDP 报文的端口,默认为 8472
DirectRouting(Boolean):当两台主机处于同一个网段当中时,启用后将采用直接路由的方式进行跨节点网络通信(此时工作模式跟后面要讲的 host-gw Backend 一样),只有当两台主机处于不同的网段当中时才会采用 VXLAN 进行封包,默认为关闭状态。
修改完成后的 ConfigMap 如下:
同样在更新配置后 delete pod 使配置生效,并可以通过 Flannel 的日志查看到 Backend 已经更新为 VXLAN 模式:
同样可以通过 netstat -ulnp 命令查看 VXLAN 监听的端口:
但跟 UDP 模式下查看 flanneld 监听的端口的区别为,最后一栏显示的不是进程的 ID 和名称,而是一个破折号“-”,这说明 UDP 的 8472 端口不是由用户态的进程在监听的,也证实了 VXLAN 模块工作在内核态模式下。
时容器跨节点网络通信实现流程为:
同 UDP Backend 模式,容器 A 当中的 IP 包通过容器 A 内的路由表被发送到 cni0
到达 cni0 当中的 IP 包通过匹配 host A 当中的路由表发现通往 10.244.2.194 的 IP 包应该交给 flannel.1 接口
flannel.1 作为一个 VTEP 设备,收到报文后将按照 VTEP 的配置进行封包,首先通过 etcd 得知 10.244.2.194 属于节点 B,并得到节点 B 的 IP,通过节点 A 当中的转发表得到节点 B 对应的 VTEP 的 MAC,根据 flannel.1 设备创建时的设置的参数(VNI、local IP、Port)进行 VXLAN 封包
通过 host A 跟 host B 之间的网络连接,VXLAN 包到达 host B 的 eth1 接口
通过端口 8472,VXLAN 包被转发给 VTEP 设备 flannel.1 进行解包
解封装后的 IP 包匹配 host B 当中的路由表(10.244.2.0),内核将 IP 包转发给 cni0
cni0 将 IP 包转发给连接在 cni0 上的容器 B
这么一看是不是觉得相比 UDP 模式单单从步骤上就少了很多步?VXLAN 模式相比 UDP 模式高效也就不足为奇了。
host-gw
host-gw 即 Host Gateway,从名字中就可以想到这种方式是通过把主机当作网关来实现跨节点网络通信的。那么具体如何实现跨节点通信呢?
同理 UDP 模式和 VXLAN 模式,首先将 Backend 中的 type 改为 host-gw,这里就不再赘述,只讲一下网络通信的实现流程。
采用 host-gw 模式后 flanneld 的唯一作用就是负责主机上路由表的动态更新, 想一下这样会不会有什么问题?
使用 host-gw Backend 的 Flannel 网络的网络包传输过程如下图所示:
同 UDP、VXLAN 模式一致,通过容器 A 的路由表 IP 包到达 cni0
到达 cni0 的 IP 包匹配到 host A 当中的路由规则(10.244.2.0),并且网关为 172.16.130.164,即 host B,所以内核将 IP 包发送给 host B(172.16.130.164)
IP 包通过物理网络到达 host B 的 eth1
到达 host B eth1 的 IP 包匹配到 host B 当中的路由表(10.244.2.0),IP 包被转发给 cni0
cni0 将 IP 包转发给连接在 cni0 上的容器 B
host-gw 模式其中一个局限性就是,由于是通过节点上的路由表来实现各个节点之间的跨节点网络通信,那么就得保证两个节点是可以直接路由过去的。按照内核当中的路由规则,网关必须在跟主机当中至少一个 IP 处于同一网段,故造成的结果就是采用 host-gw 这种 Backend 方式时则集群中所有的节点必须处于同一个网络当中,这对于集群规模比较大时需要对节点进行网段划分的话会存在一定的局限性。另外一个则是随着集群当中节点规模的增大,flanneld 需要维护主机上成千上万条路由表的动态更新也是一个不小的压力。
后记
本文主要讲述了 Flannel 当中 UDP、VXLAN、host-gw 三种 Backend 的跨节点网络通信实现流程,并作了简单的分析对比,跨节点网络通信的流程从 UDP 到 VXLAN 再到 host-gw 依次变得简单,效率也逐步提升。这一切还是离不开 Linux 的设计哲学:Less Is More。
在 Kubernetes 当中往往很多问题都是出在网络层面,如何在出现问题时能快速地定位到问题原因需要我们对 Kubernetes 的网络模型有一个熟悉的认知。
如今,Kubernetes 的部署实现了网络虚拟化,让容器可以在同一集群中的多个节点运行并相互通信。然而,越来越多的企业将 Kubernetes 用作为跨所有公有云和私有云基础设施的基础计算平台,可在不同的 Kubernetes 集群中运行的容器想要实现互相通信,实现的方法依然是传统的通过 ingress controller 或者节点端口来完成。
想要更简单快速地实现跨集群的网络连接,Rancher Labs 贡献的开源项目Submariner值得一试。Submariner 支持多个 Kubernetes 集群之间的跨集群网络连接,它创建了必要的隧道和路径,能为部署在需要相互通信的多个 Kubernetes 集群中的微服务提供网络连接。
这一全新的解决方案解决了 Kubernetes 集群之间的连接障碍,为多集群部署提供了更多实现方式,例如在跨地区的 Kubernetes 内复制数据库,以及跨集群部署服务网格。感兴趣的朋友可以在 GitHub 上了解及下载噢:
https://github.com/rancher/submariner
评论