36. K8s 网络原理——CNI 网络插件
本章讲解知识点
- Flannel 原理概述
- 直接路由的原理和部署示例
- Calico 插件原理概述
<br>
1. Flannel 原理概述
Flannel 是一个用于容器网络的开源解决方案,它使用了虚拟网络接口技术(如 VXLAN)和 etcd 存储来提供网络服务。它的原理概述如下:
- Flannel 协助 Kubernetes,给每一个 Node 上的 Dockers 容器都分配互不冲突的 IP 地址。
- 它能在这些 IP 之间建立一个覆盖网络(Overlay Network),通过这个覆盖网络,将数据包原封不动地传递到目标容器内。
那么一条网络报文是怎么从一个容器发送到另外一个容器的呢?
- 容器直接使用目标容器的 ip 访问,默认通过容器内部的 eth0 发送出去。报文通过 veth pair 被发送到 vethXXX。
- vethXXX 是直接连接到虚拟交换机 docker0 的,报文通过虚拟 bridge docker0 发送出去。
- 查找路由表,外部容器 ip 的报文都会转发到 flannel0 虚拟网卡,这是一个 P2P 的虚拟网卡,然后报文就被转发到监听在另一端的 flanneld。
- flanneld 通过 etcd 维护了各个节点之间的路由表,把原来的报文 UDP 封装一层,通过配置的 iface 发送出去。
- 报文通过主机之间的网络找到目标主机。
- 报文继续往上,到传输层,交给监听在 8285 端口的 flanneld 程序处理。
- 数据被解包,然后发送给 flannel0 虚拟网卡。
- 查找路由表,发现对应容器的报文要交给 docker0。
- docker0 找到连到自己的容器,把报文发送过去。
Flannel 首先创建了一个名为 flannel0 的网桥,而且这个网桥的一端连接 docker0 网桥,另一端连接一个叫做 flanneld 的服务进程。
flanneld 进程不简单,它上连 etcd,利用 etcd 来管理可分配的 IP 地址段资源,同时监控 etcd 中每个 Pod 的实际地址,并在内存中建立了一个 Pod 节点路由表;它下连 docker0 和物理网络,使用内存中的 Pod 节点路由表,将 docker0 发给它的数据包包装起来,利用物理网络的连接将数据包投递到目标 flanneld 上,从而完成 Pod 到 Pod 之间的直接地址通信。
Flannel 之间底层通信协议的可选技术包括 UDP、VxLan、AWS VPC等多种方式。通过源 flanneld 封包、目标 flanneld 解包,docker0 最终收到的就是原始数据,对容器应用来说是透明的,感觉不到中间 flannel 的存在。
我们看一下 Flannel 是如何做到为不同 Node 上的 Pod 分配的 IP 不产生冲突的。其实想到 Flannel 使用了集中的 etcd 存储就很容易理解了。它每次分配的地址段都在同一个公共区域获取,这样大家自然能够相互协调,不产生冲突了。而且在 Flannel 分配好地址段后,后面的事情是由 Docker 完成的,Flannel 通过修改 Docker 的启动参数将分配给它的地址段传递进去:
--bip=172.17.16.1/24
通过这些操作,Flannel 就控制了每个 Node 上的 docker0 地址段的地址,就保障了所有 Pod 的 IP 地址都在同一个水平网络中且不产生冲突了。
Flannel 完美地实现了对 Kubernetes 网络的支持,但是它引入了多个网络组件,在网络通信时需要转到 flannel0 网络接口,再转到用户态的 flanneld 程序,到对端后还需要走这个过程的反过程,所以也会引入一些网络的时延损耗。
另外,Flannel 模型默认采用了 UDP 作为底层传输协议,UDP 本身是非可靠协议,虽然两端的 TCP 实现了可靠传输,但在大流量、高并发的应用场景下还需要反复测试,确保没有问题。
<br>
2. 直接路由的原理和部署示例
我们知道,docker0 网桥上的 IP 地址在 Node 网络上是看不到的。从一个 Node 到一个 Node 内的 docker0 是不通的,因为它不知道某个 IP 地址在哪里。如果能够让这些机器知道对端 docker0 地址在哪里,就可以让这些 docker0 相互通信了。这样,在所有 Node 上运行的 Pod 就可以相互通信了。
我们先来看一下 Flannel 的 host-gw 模式。它的工作原理非常简单。为了方便叙述,接下来我会称这张图为“host-gw 示意图”。
假设现在,Node 1 上的 Infra-container-1,要访问 Node 2 上的 Infra-container-2。
当你设置 Flannel 使用 host-gw 模式之后,flanneld 会在宿主机上创建这样一条规则,以 Node 1 为例:
$ ip route ... 10.244.1.0/24 via 10.168.0.3 dev eth0
这条路由规则的含义是:目的 IP 地址属于 10.244.1.0/24 网段的 IP 包,应该经过本机的 eth0 设备发出去(即:dev eth0);并且,它下一跳地址(next-hop)是 10.168.0.3(即:via 10.168.0.3)。
所谓下一跳地址就是:如果 IP 包从主机 A 发到主机 B,需要经过路由设备 X 的中转。那么 X 的 IP 地址就应该配置为主机 A 的下一跳地址。
而从 host-gw 示意图中我们可以看到,这个下一跳地址对应的,正是我们的目的宿主机 Node 2。
一旦配置了下一跳地址,那么接下来,当 IP 包从网络层进入链路层封装成帧的时候,eth0 设备就会使用下一跳地址对应的 MAC 地址,作为该数据帧的目的 MAC 地址。显然,这个 MAC 地址,正是 Node 2 的 MAC 地址。
这样,这个数据帧就会从 Node 1 通过宿主机的二层网络顺利到达 Node 2 上。
而 Node 2 的内核网络栈从二层数据帧里拿到 IP 包后,会“看到”这个 IP 包的目的 IP 地址是 10.244.1.3,即 Infra-container-2 的 IP 地址。这时候,根据 Node 2 上的路由表,该目的地址会匹配到第二条路由规则(也就是 10.244.1.0 对应的路由规则),从而进入 cni0 网桥,进而进入到 Infra-container-2 当中。
可以看到,host-gw 模式的工作原理,其实就是将每个 Flannel 子网(Flannel Subnet,比如:10.244.1.0/24)的“下一跳”,设置成了该子网对应的宿主机的 IP 地址。
也就是说,这台“主机”(Host)会充当这条容器通信路径里的“网关”(Gateway)。这也正是“host-gw”的含义。
当然,Flannel 子网和主机的信息,都是保存在 Etcd 当中的。flanneld 只需要 WACTH 这些数据的变化,然后实时更新路由表即可。
而在这种模式下,容器通信的过程就免除了额外的封包和解包带来的性能损耗。根据实际的测试,host-gw 的性能损失大约在 10% 左右,而其他所有基于 VXLAN“隧道”机制的网络方案,性能损失都在 20%~30% 左右。
当然,通过上面的叙述,你也应该看到,host-gw 模式能够正常工作的核心,就在于 IP 包在封装成帧发送出去的时候,会使用路由表里的“下一跳”来设置目的 MAC 地址。这样,它就会经过二层网络到达目的宿主机。
所以说,Flannel host-gw 模式必须要求集群宿主机之间是二层连通的。
需要注意的是,宿主机之间二层不连通的情况也是广泛存在的。比如,宿主机分布在了不同的子网(VLAN)里。但是,在一个 Kubernetes 集群里,宿主机之间必须可以通过 IP 地址进行通信,也就是说至少是三层可达的。否则的话,你的集群将不满足上一篇文章中提到的宿主机之间 IP 互通的假设(Kubernetes 网络模型)。当然,“三层可达”也可以通过为几个子网设置三层转发来实现。
<br>
3. Calico 插件原理概述
而在容器生态中,要说到像 Flannel host-gw 这样的三层网络方案,我们就不得不提到 Calico 项目了。
实际上,Calico 项目提供的网络解决方案,与 Flannel 的 host-gw 模式,几乎是完全一样的。也就是说,Calico 也会在每台宿主机上,添加一个格式如下所示的路由规则:
<目的容器IP地址段> via <网关的IP地址> dev eth0
其中,网关的 IP 地址,正是目的容器所在宿主机的 IP 地址。
而正如前所述,这个三层网络方案得以正常工作的核心,是为每个容器的 IP 地址,找到它所对应的、“下一跳”的网关。
不过,不同于 Flannel 通过 Etcd 和宿主机上的 flanneld 来维护路由信息的做法,Calico 项目使用了一个“重型武器”来自动地在整个集群中分发路由信息。
这个“重型武器”,就是 BGP。
BGP 的全称是 Border Gateway Protocol,即:边界网关协议。它是一个 Linux 内核原生就支持的、专门用在大规模数据中心里维护不同的“自治系统”之间路由信息的、无中心的路由协议。
我可以用一个非常简单的例子来为你讲清楚。
在这个图中,我们有两个自治系统(Autonomous System,简称为 AS):AS 1 和 AS 2。而所谓的一个自治系统,指的是一个组织管辖下的所有 IP 网络和路由器的全体。你可以把它想象成一个小公司里的所有主机和路由器。在正常情况下,自治系统之间不会有任何“来往”。
但是,如果这样两个自治系统里的主机,要通过 IP 地址直接进行通信,我们就必须使用路由器把这两个自治系统连接起来。
比如,AS 1 里面的主机 10.10.0.2,要访问 AS 2 里面的主机 172.17.0.3 的话。它发出的 IP 包,就会先到达自治系统 AS 1 上的路由器 Router 1。
而在此时,Router 1 的路由表里,有这样一条规则,即:目的地址是 172.17.0.2 包,应该经过 Router 1 的 C 接口,发往网关 Router 2(即:自治系统 AS 2 上的路由器)。
所以 IP 包就会到达 Router 2 上,然后经过 Router 2 的路由表,从 B 接口出来到达目的主机 172.17.0.3。
但是反过来,如果主机 172.17.0.3 要访问 10.10.0.2,那么这个 IP 包,在到达 Router 2 之后,就不知道该去哪儿了。因为在 Router 2 的路由表里,并没有关于 AS 1 自治系统的任何路由规则。
所以这时候,网络管理员就应该给 Router 2 也添加一条路由规则,比如:目标地址是 10.10.0.2 的 IP 包,应该经过 Router 2 的 C 接口,发往网关 Router 1。
像上面这样负责把自治系统连接在一起的路由器,我们就把它形象地称为:边界网关。它跟普通路由器的不同之处在于,它的路由表里拥有其他自治系统里的主机路由信息。
上面的这部分原理,相信你理解起来应该很容易。毕竟,路由器这个设备本身的主要作用,就是连通不同的网络。
但是,你可以想象一下,假设我们现在的网络拓扑结构非常复杂,每个自治系统都有成千上万个主机、无数个路由器,甚至是由多个公司、多个网络提供商、多个自治系统组成的复合自治系统呢?
这时候,如果还要依靠人工来对边界网关的路由表进行配置和维护,那是绝对不现实的。而这种情况下,BGP 大显身手的时刻就到了。
在使用了 BGP 之后,你可以认为,在每个边界网关上都会运行着一个小程序,它们会将各自的路由表信息,通过 TCP 传输给其他的边界网关。而其他边界网关上的这个小程序,则会对收到的这些数据进行分析,然后将需要的信息添加到自己的路由表里。
这样,图 2 中 Router 2 的路由表里,就会自动出现 10.10.0.2 和 10.10.0.3 对应的路由规则了。所以说,所谓 BGP,就是在大规模网络中实现节点路由信息共享的一种协议。
而 BGP 的这个能力,正好可以取代 Flannel 维护主机上路由表的功能。而且,BGP 这种原生就是为大规模网络环境而实现的协议,其可靠性和可扩展性,远非 Flannel 自己的方案可比。
需要注意的是,BGP 协议实际上是最复杂的一种路由协议。我在这里的讲述和所举的例子,仅是为了能够帮助你建立对 BGP 的感性认识,并不代表 BGP 真正的实现方式。
接下来,我们还是回到 Calico 项目上来。在了解了 BGP 之后,Calico 项目的架构就非常容易理解了。它由三个部分组成:
- Calico 的 CNI 插件。这是 Calico 与 Kubernetes 对接的部分。
- Felix。它是一个 DaemonSet,负责在宿主机上插入路由规则(即:写入 Linux 内核的 FIB 转发信息库)
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专刊适合于立志转行云计算的小白,有一定的编程、操作系统、计算机网络、数据结构、算法基础。 本专刊同时也适合于面向云计算(Docker + Kubernetes)求职的从业者。 本专刊囊括了云计算、VMWare、Docker、Kubernetes、Containerd等一系列知识点的讲解,并且最后总