36. K8s 网络原理——CNI 网络插件

本章讲解知识点

  • Flannel 原理概述
  • 直接路由的原理和部署示例
  • Calico 插件原理概述

<br>

1. Flannel 原理概述

Flannel 是一个用于容器网络的开源解决方案,它使用了虚拟网络接口技术(如 VXLAN)和 etcd 存储来提供网络服务。它的原理概述如下:

  1. Flannel 协助 Kubernetes,给每一个 Node 上的 Dockers 容器都分配互不冲突的 IP 地址。
  2. 它能在这些 IP 之间建立一个覆盖网络(Overlay Network),通过这个覆盖网络,将数据包原封不动地传递到目标容器内。

那么一条网络报文是怎么从一个容器发送到另外一个容器的呢?

  1. 容器直接使用目标容器的 ip 访问,默认通过容器内部的 eth0 发送出去。报文通过 veth pair 被发送到 vethXXX。
  2. vethXXX 是直接连接到虚拟交换机 docker0 的,报文通过虚拟 bridge docker0 发送出去。
  3. 查找路由表,外部容器 ip 的报文都会转发到 flannel0 虚拟网卡,这是一个 P2P 的虚拟网卡,然后报文就被转发到监听在另一端的 flanneld。
  4. flanneld 通过 etcd 维护了各个节点之间的路由表,把原来的报文 UDP 封装一层,通过配置的 iface 发送出去。
  5. 报文通过主机之间的网络找到目标主机。
  6. 报文继续往上,到传输层,交给监听在 8285 端口的 flanneld 程序处理。
  7. 数据被解包,然后发送给 flannel0 虚拟网卡。
  8. 查找路由表,发现对应容器的报文要交给 docker0。
  9. 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 项目的架构就非常容易理解了。它由三个部分组成:

  1. Calico 的 CNI 插件。这是 Calico 与 Kubernetes 对接的部分。
  2. Felix。它是一个 DaemonSet,负责在宿主机上插入路由规则(即:写入 Linux 内核的 FIB 转发信息库)

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

云计算面试题全解析 文章被收录于专栏

本专刊适合于立志转行云计算的小白,有一定的编程、操作系统、计算机网络、数据结构、算法基础。 本专刊同时也适合于面向云计算(Docker + Kubernetes)求职的从业者。 本专刊囊括了云计算、VMWare、Docker、Kubernetes、Containerd等一系列知识点的讲解,并且最后总

全部评论

相关推荐

不愿透露姓名的神秘牛友
12-17 17:43
Java抽象带篮子:绝绝子暴风吸入啊
点赞 评论 收藏
分享
勤奋努力的椰子这就开摆:美团骑手在美团工作没毛病
投递美团等公司10个岗位
点赞 评论 收藏
分享
11-04 14:10
东南大学 Java
_可乐多加冰_:去市公司包卖卡的
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务