在CNI插件外还有一种纯三层网络,典型例子就是 Flannel 的 host-gw 模式和 Calico 项目了。
Flannel 的 host-gw 模式。
Node 1 的 Infra-container-1,要访问 Node 2 的 Infra-container-2。
设置 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 设备发出;下一跳地址是 10.168.0.3。
配置了下一跳地址,IP 包封装成帧时,eth0 使用下一跳地址对应的 MAC 地址作为数据帧的目的 MAC 地址。
数据帧从 Node 1 通过宿主机的二层网络到达 Node 2 。Node 2 拿到 IP 包后,看到目的 IP 是 10.244.1.3,即 Infra-container-2。根据路由表匹配到第二条路由规则,进入 cni0 网桥,进入到 Infra-container-2 当中。
host-gw 模式的工作原理就是将每个 Flannel 子网(比如:10.244.1.0/24)的“下一跳”,设置成该子网对应的宿主机的 IP 地址。
这台“主机”充当容器通信路径里的“网关”。这也正是“host-gw”的含义。
Flannel 子网和主机的信息保存在 Etcd。flanneld 只需要 WACTH 数据的变化实时更新路由表即可。(非必选项,也可以用别的方法更新路由表)
在这种模式下,容器通信过程免除了额外的封解包带来的性能损耗。
host-gw 模式能正常工作的核心在于 IP 包在封装成帧发出时,使用路由表里的“下一跳”来设置目的 MAC 地址。这样它就会经过二层网络到达目的宿主机。
Flannel host-gw 模式必须要求集群宿主机之间是二层连通的。 因为目的IP是容器的,在网络中可无法通过arp找到容器的mac地址,只能通过目的mac地址在二层直接通信
Calico
Calico 与 Flannel 的 host-gw 模式几乎一样。也会在宿主机上,添加一个格式如下所示的路由规则:
< 目的容器 IP 地址段 > via < 网关的 IP 地址 > dev eth0
网关的 IP 地址是目的容器所在宿主机的 IP 地址。
不同于 Flannel 通过 Etcd 和宿主机上的 flanneld 维护路由信息的做法,Calico 使用“BGP”来自动地在整个集群中分发路由信息。
BGP 即:边界网关协议。专门用在大规模数据中心里维护不同的“自治系统”之间路由信息的、无中心的路由协议。
图中有两个自治系统:AS 1 和 AS 2。自治系统指一个组织管辖下的所有 IP 网络和路由器的全体。可以想象成一个小公司里的所有主机和路由器。正常情况下自治系统不会有“来往”。
但如果两个 AS 里的主机要通过 IP 地址进行通信,就必须使用路由器把这两个自治系统连接。
AS 1 的主机 10.10.0.2 要访问 AS 2 的主机 172.17.0.3。IP 包就会先到达路由器 Router 1。
Router 1 的路由表里有这样一条规则,即:目的地址是 172.17.0.2 包,应该经过 C 接口,发往网关 Router 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 包,应该经过 C 接口发往网关 Router 1。
像这样负责把 AS 连接在一起的路由器称为:边界网关。跟普通路由器的不同之处在于它的路由表里拥有其他 AS 里的主机路由信息。
在复杂的网络结构中如果还依靠人工来对边界网关的路由表进行配置和维护,那是绝对不现实的。
这时 BGP 大显身手的时刻就到了。 BGP 在每个边界网关上运行一个程序,将各自的路由表信息,通过 TCP 传输给其他的边界网关。其他边界网关上的这个程序对收到的数据进行分析,将需要的信息添加到自己的路由表。
图中 Router 2 的路由表里,就会自动出现 10.10.0.2 和 10.10.0.3 对应的路由规则了。
所谓 BGP,就是在大规模网络中实现节点路由信息共享的一种协议。
而 BGP 正好可以取代 Flannel 维护主机上路由表的功能。BGP 这种原生就是为大规模网络环境而实现的协议,其可靠性和可扩展性,远非 Flannel 自己的方案可比。
回到 Calico。由三个部分组成:
- Calico 的 CNI 插件。这是 Calico 与 K8s 对接的部分。
- Felix。一个 DaemonSet,负责在宿主机上插入路由规则(即:写入 Linux 内核的 FIB 转发信息库),以及维护 Calico 所需的网络设备等工作。
- BIRD。BGP 的客户端,专门负责在集群里分发路由规则信息。
除了对路由信息的维护方式之外,Calico 与 Flannel 的 host-gw 模式的另一个不同之处就是它不会在宿主机上创建任何网桥设备。Calico 的工作方式如下
Calico 的 CNI 插件会为每个容器设置一个 Veth Pair 设备,把其中的一端放置在宿主机上
由于 Calico 没有使用 CNI 的网桥模式,Calico 的 CNI 插件还需要在宿主机上为每个容器的 Veth Pair 设备配置一条路由规则,用于接收传入的 IP 包。比如,宿主机 Node 2 上的 Container 4 对应的路由规则,如下所示:
10.233.2.3 dev cali5863f3 scope link
即:发往 10.233.2.3 的 IP 包,应该进入 cali5863f3 设备。
基于上述原因,Calico 在宿主机上设置的路由规则,要比 Flannel 多得多。不过 host-gw 模式使用 CNI 网桥的主要原因是为了跟 VXLAN 模式保持一致。否则需要维护两套 CNI 插件。
有了 Veth Pair 后,容器发出的 IP 包经过 Veth Pair 出现在宿主机上。宿主机根据路由规则的下一跳 IP 地址,把它们转发给正确的网关。接下来的流程就跟 Flannel host-gw 模式完全一致了。
最核心的“下一跳”路由规则,就是由 Calico 的 Felix 进程负责维护的。这些路由规则信息,则是通过 BGP Client 也就是 BIRD 组件,使用 BGP 协议传输而来的。
而通过 BGP 协议传输的消息可以简单地理解为如下格式:
[BGP 消息]
我是宿主机 192.168.1.3
10.233.2.0/24 网段的容器都在我这里
这些容器的下一跳地址是我
Calico 实际上将集群里的所有节点都当作是边界路由器来处理,一起组成了一个全连通的网络,互相之间通过 BGP 协议交换路由规则。这些节点称为 BGP Peer。
Calico 维护的网络在默认配置下,是一个被称为“Node-to-Node Mesh”的模式。每台宿主机上的 BGP Client 都需要跟其他所有节点的 BGP Client 通信以交换路由信息。随着节点 N 的增加,连接数量就会以 N²的规模快速增长,给集群网络带来巨大的压力。
在更大规模的集群中,用到的是 Route Reflector 的模式。
在这种模式下,Calico 会指定一个或者几个专门的节点负责跟所有节点建立 BGP 连接从而学习到全局的路由规则。而其他节点只需跟这几个节点交换路由信息,就可以获得整个集群的路由规则信息了。
Route Reflector 节点扮演了“中间代理”的角色,从而把 BGP 连接的规模控制在 N 的数量级上。
Flannel host-gw 模式最主要的限制就是要求集群宿主机之间是二层连通的。而这个限制对于 Calico 也同样存在。
举个例子,有两台处于不同子网的宿主机 Node 1 和 Node 2,对应的 IP 地址分别是 192.168.1.2 和 192.168.2.2。这两台机器通过路由器实现了三层转发,所以这两个 IP 地址之间是可以相互通信的。
现在 Container 1 要访问 Container 4。Calico 会尝试在 Node 1 上添加如下所示的一条路由规则:
10.233.2.0/16 via 192.168.2.2 eth0
上面这条规则里的下一跳地址是 192.168.2.2,可是它对应的 Node 2 跟 Node 1 却根本不在一个子网里,没办法通过二层网络把 IP 包发送到下一跳地址。
这种情况下就需要为 Calico 打开 IPIP 模式。
在 IPIP 模式下,Felix 进程在 Node 1 上添加的路由规则,会稍微不同,如下所示:
10.233.2.0/24 via 192.168.2.2 tunl0
下一跳仍是 Node 2 的 IP 地址,但将 IP 包发出去的设备变成了 tunl0。不是 Flannel UDP 模式使用的 tun0,这两种设备的功能完全不一样。
tunl0 是一个 IP 隧道设备。
IP 包进入 IP 隧道设备后被 Linux 内核的 IPIP 驱动接管。IPIP 驱动会将这个 IP 包直接封装在宿主机网络的 IP 包中,如下所示:
经过封装后的新的 IP 包的目的地址(Outer IP Header),正是原 IP 包的下一跳地址,即 Node 2 的 IP 地址:192.168.2.2。
由于宿主机之间已经使用路由器配置了三层转发,也就是设置了宿主机之间的“下一跳”。所以这个 IP 包在离开 Node 1 之后,就可以经过路由器,最终“跳”到 Node 2 上。
Node 2 的网络内核栈使用 IPIP 驱动解包拿到原始 IP 包。经过路由规则和 Veth Pair 设备到达目的容器内部。
使用 IPIP 模式时,集群的网络性能会因为额外的封解包而下降。实测 IPIP 模式与 Flannel VXLAN 模式的性能大致相当。所以在实际使用时,如非硬性需求,建议将所有宿主机节点放在一个子网里,避免使用 IPIP。
如果 Calico 能够让宿主机之间的路由设备也通过 BGP 协议“学习”到 Calico 网络里的路由规则,那么从容器发出的 IP 包,不就可以通过这些设备路由到目的宿主机了么?
比如,只要在上面“IPIP 示意图”中的 Node 1 上,添加如下所示的一条路由规则:
10.233.2.0/24 via 192.168.1.1 eth0
然后在 Router 1 上(192.168.1.1),添加如下所示的一条路由规则:
10.233.2.0/24 via 192.168.2.1 eth0
Container 1 发出的 IP 包就可以通过两跳到达 Router 2(192.168.2.1)了。
遗憾的是,在 K8s 被广泛使用的公有云场景里完全不可行。公有云环境下,宿主机之间的网关不允许用户进行干预和设置。
在大多数公有云环境下,宿主机本身往往就是二层连通的,所以这个需求也不强烈。
在私有部署的环境下,宿主机属于不同子网反而是更加常见的状态。这时候想办法将宿主机网关也加入到 BGP Mesh 里从而避免使用 IPIP,就成了一个非常迫切的需求。
而 Calico 提供了两种将宿主机网关设置成 BGP Peer 的解决方案。
第一种方案,就是所有宿主机都跟宿主机网关建立 BGP Peer 关系。
这种方案下,Node 1 和 Node 2 就需要主动跟宿主机网关 Router 1 和 Router 2 建立 BGP 连接。从而将类似于 10.233.2.0/24 这样的路由信息同步到网关上去。
这种方式下要求宿主机网关必须支持一种叫作 Dynamic Neighbors 的 BGP 配置方式。常规的路由器 BGP 配置里,运维人员必须明确给出所有 BGP Peer 的 IP 地址。考虑到 K8s 集群可能会有成百上千个宿主机,而且还会动态地添加和删除节点,这时候再手动管理路由器的 BGP 配置就非常麻烦了。而 Dynamic Neighbors 则允许你给路由器配置一个网段,然后路由器就会自动跟该网段里的主机建立起 BGP Peer 关系。
第二种方案,使用一个或多个独立组件负责搜集整个集群里的所有路由信息,然后通过 BGP 协议同步给网关。在大规模集群中,Calico 本身就推荐使用 Route Reflector 节点的方式进行组网。这里负责跟宿主机网关进行沟通的独立组件,直接由 Route Reflector 兼任即可。
更重要的是,这种情况下网关的 BGP Peer 个数是有限并且固定的。所以我们就可以直接把这些独立组件配置成路由器的 BGP Peer,而无需 Dynamic Neighbors 的支持。
这些独立组件的工作原理也很简单:它们只需要 WATCH Etcd 里的宿主机和对应网段的变化信息,然后把这些信息通过 BGP 协议分发给网关即可。
在大规模集群里,三层网络方案在宿主机上的路由规则可能会非常多,这会导致错误排查变得困难。在系统故障时,路由规则出现重叠冲突的概率也会变大。
基于上述原因,如果是在公有云上,由于宿主机网络本身比较“直白”,推荐更加简单的 Flannel host-gw 模式。
但不难看到,在私有部署环境里,Calico 项目才能够覆盖更多的场景,并为你提供更加可靠的组网方案和架构思路。