一文说透容器跨主机网络

时间: 2023-08-18 admin IT培训

一文说透容器跨主机网络

一文说透容器跨主机网络

文章目录

  • 一、Flannel
    • 1、UDP
    • 2、VXLAN
        • (1)VXLAN核心流程总结
        • (2)VTEP隧道通信流程详解
            • 【1】封装 inner Ethernet header(依据VTEP IP查MAC)
            • 【2】设置VNI(标识数据包应该交给那个处理设备)
            • 【3】封装 outer header(依赖三层宿主机网络"搭便车")
    • 3、Host-gw(节点在一个二层网络)
  • 二、Calico
    • 1、BGP简介
    • 2、Calico的实现(节点在一个二层网络)
    • 3、Calico交换路由信息模式
        • (1)Node-to-Node Mesh
        • (2)Route Reflector(Calico推荐)
    • 4、K8s节点不在一个二层网络怎么办?
        • (1)IPIP 隧道模式
        • (2)网关加入BGP Peer
  • 三、Cilium

单机环境下,容器之间的通信可以通过docker0网桥和veth pair来实现。但是在 Docker 的默认配置下,不同宿主机上的容器通过 IP 地址进行互相访问是根本做不到的。 因此就出现了各种容器网络方案,目的就是解决容器跨主通信。接下来我们就来分析一下各种网络方案的原理。

一、Flannel

Flannel 项目是 CoreOS 公司主推的容器网络方案。事实上,Flannel 项目本身只是一个框架,真正为我们提供容器网络功能的,是 Flannel 的后端实现。目前,Flannel 支持三种后端实现,分别是:VXLAN;host-gw;UDP。

1、UDP

下面是两个容器的网络图,我们现在的任务,就是让 container-1 访问 container-2。

这种情况下,container-1 容器里的进程发起的 IP 包,其源地址就是 192.168.1.1,目的地址就是 192.168.2.1。由于目的地址 192.168.2.1 并不在 Node 1 的 docker0 网桥的网段里,所以这个 IP 包会被交给默认路由规则,通过容器的网关进入 docker0 网桥(如果是同一台宿主机上的容器间通信,走的是直连规则),从而出现在宿主机上

这时候,这个 IP 包的下一个目的地,就取决于宿主机上的路由规则了。此时,Flannel 已经在宿主机上创建出了一系列的路由规则,以 Node 1 为例,如下所示:

# 在Node 1上
$ ip route
192.168.0.0/16 dev flannel0 
192.168.1.0/24 dev docker0

可以看到最后匹配到 192.168.0.0/16 对应的这条路由规则,从而进入到一个叫作 flannel0 的设备中。而这个 flannel0 设备的类型就比较有意思了:它是一个 TUN 设备(Tunnel 设备)

在 Linux 中,TUN 设备是一种工作在三层(Network Layer)的虚拟网络设备。TUN 设备的功能非常简单,即:在操作系统内核和用户应用程序之间传递 IP 包

接下来的数据包流转图我先放出来,有助于大家后续理解:

根据上图我们可以发现,其实就是1、让创建了flannel0设备的flanneld程序处理了下原始数据包,发出去就能实现容器跨主通信了,接下来我们就是要分析它到底处理了什么?而且通过这张图还能发现,2、数据包传出去,是发生了三次用户态和内核态的切换的以及数据包拷贝的开销,开销还是有点大,因此UDP这种模式,性能肯定很差

  • flanneld做了什么?

宿主机上的 flanneld 进程(Flannel 项目在每个宿主机上的主进程),就会收到这个 IP 包。然后,flanneld 看到了这个 IP 包的目的地址,属于 192.168.2.0/24 网段,就把它发送给了 Node 2 宿主机。

  • flanneld怎么知道192.168.2.0/24网段在Node 2 宿主机?
// 在划分pod网段的时候,会将每个主机所划分的对应的pod cidr存入etcd中
$ etcdctl ls /coreos.com/network/subnets
/coreos.com/network/subnets/192.168.1.0-24 // 这些网段的value就是node ip
/coreos.com/network/subnets/192.168.2.0-24
/coreos.com/network/subnets/192.168.3.0-24

所以说,flanneld 在收到 container-1 发给 container-2 的 IP 包之后,就会把这个 IP 包直接封装在一个 UDP 包里,然后发送给 Node 2。且这个 UDP 包的源地址,就是 Node 1 的地址,而目的地址,则是 Node 2 的地址,源端口和目的端口都是8285。其实这就是类似翻嫱的双层封包,这也是tunnel隧道的特征

然后node 2上的flanneld的8285端口就收到了这个包,然后flanneld 就可以从这个 UDP 包里解析出封装在里面的 container-1 发来的原 IP 包。然后Flanneld 进程向 flannel0 设备发送数据包,触发中断两阶段,交给Linux 内核网络协议栈处理这个 IP 包,解析到网络层时,会进行routing decision,就是通过本机的路由表来寻找这个 IP 包的下一步流向。那么它就会走192.168.2.0/24 dev docker0这个路由,然后送到docker0网桥,然后通过veth pair送到目的容器。

以上,就是基于 Flannel UDP 模式的跨主通信的基本原理了。我把它总结成了一幅原理图,如下所示。

文章前面就已经说了,这种方案最大的弊端就是,仅在发出 IP 包的过程中,就需要经过三次用户态与内核态之间的数据拷贝以及切换

因此接下来的VXLAN就是为了解决这个性能问题的手段。

2、VXLAN

  • VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网)简介:

是 Linux 内核本身就支持的一种网络虚似化技术。所以说,VXLAN 可以完全在内核态实现上述封装和解封装的工作,从而通过与前面相似的“隧道”机制,构建出覆盖网络(Overlay Network)。

  • VXLAN 的覆盖网络的设计思想是:

在现有的三层网络之上,“覆盖”一层虚拟的、由内核 VXLAN 模块负责维护的二层网络,使得连接在这个 VXLAN 二层网络上的“主机”(虚拟机或者容器都可以)之间,可以像在同一个局域网(LAN)里那样自由通信。当然,实际上,这些“主机”可能分布在不同的宿主机上,甚至是分布在不同的物理机房里。

  • 什么是VTEP?

而为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。

  • VTEP 设备的作用?

其实跟前面的 flanneld 进程作用相似。只不过,它进行封装和解封装的对象,是二层数据帧(Ethernet frame);而且这个工作的执行流程,全部是在内核里完成的(因为 VXLAN 本身就是 Linux 内核中的一个模块)。

(1)VXLAN核心流程总结

其实在我看来,VXLAN的核心在于:

  • 找到目的宿主机(outer Ethernet header + outer ip header), 宿主机ip,可以依据 "目的VTEP设备”的MAC地址"FDB(Forwarding Database)的转发数据库 查询目的VTEP设备所在宿主机的ip。因为一个集群的节点都在一个网段里,因此目的mac,是 Node 1 的 ARP 表要学习的内容,无需 Flannel 维护
  • 找到目的VTEP 设备(VNI + inner Ethernet header),Flannel的VTEP设备的VNI都是1,因此网卡叫flannel.1。目的VTEP设备的mac,可以通过route -n先获得目的VTEP设备的ip,再通过ip neigh 获得其mac。

注意:inner ip header不是目的VTEP设备的ip,而是原始ip包,即目的容器的ip地址。

原始ip包(container到container),到了flannel.1,封装inner、vni、outer header,然后根据outer header找到相应目的宿主机,然后根据vni的值,将数据包交给flanneld.1处理flanneld.1发现目的mac是自己,就拆开了,发现inner ip header 目的容器地址,发现要去本机的某个container,因此发给docker0,最后发给目的container。

画龙点睛:不看outer header,VTEP之间的请求,只是二层的mac地址的请求,因此其实就是可以看出,vxlan是构建了一个基于现有宿主机三层网络上的VXLAN维护的虚拟二层网络。

(2)VTEP隧道通信流程详解

【1】封装 inner Ethernet header(依据VTEP IP查MAC)

可以看到,图中每台宿主机上名叫 flannel.1 的设备,就是 VXLAN 所需的 VTEP 设备,它既有 IP 地址,也有 MAC 地址。

现在,我们的 container-1 的 IP 地址是 10.1.15.2,要访问的 container-2 的 IP 地址是 10.1.16.3。

与前面 UDP 模式的流程类似,当 container-1 发出请求之后,这个目的地址是 10.1.16.3 的 IP 包,会先出现在 docker0 网桥,然后被路由到本机 flannel.1 设备进行处理。来到了“隧道”的入口。为了方便叙述,我接下来会把这个 IP 包称为“原始 IP 包”。

因为要先封装inner header嘛,因此我们先细说 inner header。之前就说明了inner header是为了找到目的VTEP 设备。怎么找到?

VTEP设备的信息,正是每台宿主机上的 flanneld 进程负责维护的。当 Node 2 启动并加入 Flannel 网络之后,在 Node 1(以及所有其他节点)上,flanneld 就会添加一条如下所示的路由规则

$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
...
10.1.16.0       10.1.16.0       255.255.255.0   UG    0      0        0 flannel.1

这条规则的意思是:凡是发往 10.1.16.0/24 网段的 IP 包,都需要经过 flannel.1 设备发出,并且,它最后被发往的网关地址是:10.1.16.0。而10.1.16.0 正是 Node 2 上的 VTEP 设备(也就是 flannel.1 设备)的 IP 地址。

接下来我会把 Node 1 和 Node 2 上的 flannel.1 设备分别称为“源 VTEP 设备”和“目的 VTEP 设备”,这些 VTEP 设备之间,就需要想办法组成一个虚拟的二层网络,即:通过二层数据帧进行通信

所以在我们的例子中,“源 VTEP 设备”收到“原始 IP 包”后,就要想办法把“原始 IP 包”加上一个目的 MAC 地址,封装成一个二层数据帧,然后发送给“目的 VTEP 设备”,这里需要解决的问题就是:“目的 VTEP 设备”的 MAC 地址是什么?

根据前面的路由记录,我们已经知道了“目的 VTEP 设备”的 IP 地址。而要根据三层 IP 地址查询对应的二层 MAC 地址,这正是 ARP(Address Resolution Protocol )表的功能。

这里要用到的 ARP 记录,是flanneld 进程在 Node 2 节点启动时,自动添加在 Node 1 上的。我们可以通过 ip 命令看到它,如下所示:

# 在Node 1上
$ ip neigh show dev flannel.1
10.1.16.0 lladdr 5e:f8:4f:00:e3:37 PERMANENT

可以看到,最新版本的 Flannel 并不依赖 L3 MISS 事件和 ARP 学习,而会在每台节点启动时把它的 VTEP 设备对应的 ARP 记录,直接下放到其他每台宿主机上。

有了这个“目的 VTEP 设备”的 MAC 地址,Linux 内核就可以开始二层封包工作了。这个二层帧的格式,如下所示:


可以看到,Linux 内核会把“目的 VTEP 设备”的 MAC 地址,填写在图中的 Inner Ethernet Header 字段,得到一个二层数据帧。

需要注意的是,上述封包过程只是加一个二层头,不会改变“原始 IP 包”的内容。所以图中的 Inner IP Header 字段,依然是 container-2 的 IP 地址,即 10.1.16.3。

【2】设置VNI(标识数据包应该交给那个处理设备)

但是,上面提到的这些 VTEP 设备的 MAC 地址,对于宿主机网络来说并没有什么实际意义。所以上面封装出来的这个数据帧,并不能在我们的宿主机二层网络里传输。为了方便叙述,我们把它称为“内部数据帧”(Inner Ethernet Frame)。所以接下来,Linux 内核还需要再把“内部数据帧”进一步封装成为宿主机网络里的一个普通的数据帧,好让它“载着”“内部数据帧”,通过宿主机的 eth0 网卡进行传输。

我们把这次要封装出来的、宿主机对应的数据帧称为“外部数据帧”(Outer Ethernet Frame)。

为了实现这个“搭便车”的机制,Linux 内核会在“内部数据帧”前面,加上一个特殊的 VXLAN 头,用来表示这个“乘客”实际上是一个 VXLAN 要使用的数据帧。

而这个 VXLAN 头里有一个重要的标志叫作 VNI,它是 VTEP 设备识别某个数据帧是不是应该归自己处理的重要标识。而在 Flannel 中,VNI 的默认值是 1,这也是为何,宿主机上的 VTEP 设备都叫作 flannel.1 的原因,这里的“1”,其实就是 VNI 的值。

【3】封装 outer header(依赖三层宿主机网络"搭便车")

上面在为原始ip包打了目的VTEP mac header之后,又打了VXLAN header(VNI标识)。然后,Linux 内核会把这个数据帧封装进一个 UDP 包里发出去。,那么这个 UDP 包该发给哪台宿主机呢?

在这种场景下,flannel.1 设备实际上要扮演一个“网桥”的角色,在二层网络进行 UDP 包的转发。而在 Linux 内核里面,“网桥”设备进行转发的依据,来自于一个叫作 FDB(Forwarding Database)的转发数据库

不难想到,这个 flannel.1“网桥”对应的 FDB 信息,也是 flanneld 进程负责维护的。它的内容可以通过 bridge fdb 命令查看到,如下所示:

# 在Node 1上,使用“目的VTEP设备”的MAC地址进行查询
$ bridge fdb show flannel.1 | grep 5e:f8:4f:00:e3:37
5e:f8:4f:00:e3:37 dev flannel.1 dst 10.168.0.3 self permanent

可以看到,在上面这条 FDB 记录里,指定了这样一条规则,即:发往我们前面提到的“目的 VTEP 设备”(MAC 地址是 5e:f8:4f:00:e3:37)的二层数据帧,应该通过 flannel.1 设备,发往 IP 地址为 10.168.0.3 的主机。显然,这台主机正是 Node 2,UDP 包要发往的目的地就找到了

所以接下来的流程,就是一个正常的、宿主机网络上的封包工作

我们知道,UDP 包是一个四层数据包,所以 Linux 内核会在它前面加上一个 IP 头,即原理图中的 Outer IP Header,组成一个 IP 包。并且,在这个 IP 头里,会填上前面通过 FDB 查询出来的目的主机的 IP 地址,即 Node 2 的 IP 地址 10.168.0.3。

然后,Linux 内核再在这个 IP 包前面加上二层数据帧头,即原理图中的 Outer Ethernet Header,并把 Node 2 的 MAC 地址填进去。这个 MAC 地址本身,是 Node 1 的 ARP 表要学习的内容,无需 Flannel 维护。这时候,我们封装出来的“外部数据帧”的格式,如下所示:

这样,封包工作就宣告完成了。

接下来,Node 1 上的 flannel.1 设备就可以把这个数据帧从 Node 1 的 eth0 网卡发出去。显然,这个帧会经过宿主机网络来到 Node 2 的 eth0 网卡。

这时候,Node 2 的内核网络栈会发现这个数据帧里有 VXLAN Header,并且 VNI=1。所以 Linux 内核会对它进行拆包,拿到里面的内部数据帧,然后根据 VNI 的值,把它交给 Node 2 上的 flannel.1 设备。

而 flannel.1 设备发现目的mac地址是自己的,则会进一步拆包,取出“原始 IP 包”。接下来就回到了我在上一篇文章中分享的单机容器网络的处理流程。最终,IP 包就进入到了 container-2 容器的 Network Namespace 里。

以上,就是 Flannel VXLAN 模式的具体工作原理了。

3、Host-gw(节点在一个二层网络)

上面,我以网桥类型的 Flannel 插件为例,为你讲解了 Kubernetes 里容器网络和 CNI 插件的主要工作原理。不过,除了这种模式之外,还有一种纯三层(Pure Layer 3)网络方案非常值得你注意。其中的典型例子,莫过于 Flannel 的 host-gw 模式Calico 项目了。

Flannel 的 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)。而从 host-gw 示意图中我们可以看到,这个下一跳地址对应的,正是我们的目的宿主机 Node 2

注意: 下一跳地址,必须是同一个局域网内的地址或者说同一个子网内的地址,即二层可达!

一旦配置了下一跳地址,那么接下来,当 IP 包从网络层进入链路层封装成帧的时候,eth0 设备就会使用下一跳地址对应的 MAC 地址,作为该数据帧的目的 MAC 地址。显然,这个 MAC 地址,正是 Node 2 的 MAC 地址。

这样,这个数据帧就会从 Node 1 通过宿主机的二层网络顺利到达 Node 2 上。

可以看到,host-gw 模式的工作原理,其实就是将每个 Flannel 子网(Flannel Subnet,比如:10.244.1.0/24)的“下一跳”,设置成了该子网对应的宿主机的 IP 地址。 也就是说,这台“主机”(Host)会充当这条容器通信路径里的“网关”(Gateway)。这也正是“host-gw”的含义。

当然,Flannel 子网和主机的信息,都是保存在 Etcd 当中的。flanneld 只需要 WACTH 这些数据的变化,然后实时更新路由表即可

注意:在 Kubernetes v1.7 之后,类似 Flannel、Calico 的 CNI 网络插件都是可以直接连接 Kubernetes 的 APIServer 来访问 Etcd 的,无需额外部署 Etcd 给它们使用。

而在这种模式下,容器通信的过程就免除了额外的封包和解包带来的性能损耗。根据实际的测试,host-gw 的性能损失大约在 10% 左右,而其他所有基于 VXLAN“隧道”机制的网络方案,性能损失都在 20%~30% 左右

当然,通过上面的叙述,你也应该看到,host-gw 模式能够正常工作的核心,就在于 IP 包在封装成帧发送出去的时候,会使用路由表里的“下一跳”来设置目的 MAC 地址。这样,它就会经过二层网络到达目的宿主机

所以说,Flannel host-gw 模式必须要求集群宿主机之间是二层连通的。

二、Calico

而在容器生态中,要说到像 Flannel host-gw 这样的三层网络方案,我们就不得不提到这个领域里的“龙头老大”Calico 项目了。

实际上,Calico 项目提供的网络解决方案,与 Flannel 的 host-gw 模式,几乎是完全一样的。也就是说,Calico 也会在每台宿主机上,添加一个格式如下所示的路由规则:

// 其中,网关的 IP 地址,正是目的容器所在宿主机的 IP 地址。
<目的容器IP地址段> via <网关的IP地址> dev eth0

三层网络方案得以正常工作的核心,是为每个容器的 IP 地址,找到它所对应的、“下一跳”的网关

1、BGP简介

不同于 Flannel 通过 Etcd 和宿主机上的 flanneld 来维护路由信息的做法,Calico 项目使用了一个“重型武器”(BGP)来自动地在整个集群中分发路由信息

BGP 的全称是 Border Gateway Protocol,即:边界网关协议。它是一个 Linux 内核原生就支持的、专门用在大规模数据中心里维护不同的“自治系统”之间路由信息的、无中心的路由协议。


在这个图中,我们有两个自治系统(Autonomous System,简称为 AS):AS 1 和 AS 2。而所谓的一个自治系统,指的是一个组织管辖下的所有 IP 网络和路由器的全体。你可以把它想象成一个小公司里的所有主机和路由器。在正常情况下,自治系统之间不会有任何“来往”。

但是,如果这样两个自治系统里的主机,要通过 IP 地址直接进行通信,我们就必须使用路由器把这两个自治系统连接起来

BGP边界网关路由器的作用:

  • 比如,AS 1 里面的主机 10.10.0.2,要访问 AS 2 里面的主机 172.17.0.3 的话。它发出的 IP 包,就会先到达自治系统 AS 1 上的路由器 Router 1。
  • 而在此时,Router 1 的路由表里,有这样一条规则,即:目的地址是 172.17.0.3 包,应该经过 Router 1 的 C 接口,发往网关 Router 2(即:自治系统 AS 2 上的路由器)。
  • 所以 IP 包就会到达 Router 2 上,然后经过 Router 2 的路由表,从 B 接口出来到达目的主机 172.17.0.3。

像上面这样负责把自治系统连接在一起的路由器,我们就把它形象地称为:边界网关。它跟普通路由器的不同之处在于,它的路由表里拥有其他自治系统里的主机路由信息

你可以想象一下,假设我们现在的网络拓扑结构非常复杂,每个自治系统都有成千上万个主机、无数个路由器,甚至是由多个公司、多个网络提供商、多个自治系统组成的复合自治系统呢?

答:这时候,如果还要依靠人工来对边界网关的路由表进行配置和维护,那是绝对不现实的。因此就衍生出了BGP去做这个事情。

在使用了 BGP 之后,你可以认为,在每个边界网关上都会运行着一个小程序,它们会将各自的路由表信息,通过 TCP 传输给其他的边界网关。其他边界网关上的这个小程序,则会对收到的这些数据进行分析,然后将需要的信息添加到自己的路由表里

2、Calico的实现(节点在一个二层网络)

BGP 的这个能力,正好可以取代 Flannel 维护主机上路由表的功能。而且,BGP 这种原生就是为大规模网络环境而实现的协议,其可靠性和可扩展性,远非 Flannel 自己的方案可比。

Calico 项目的架构由三个部分组成:

  • Calico 的 CNI 插件。这是 Calico 与 Kubernetes 对接的部分。我已经在上一篇文章中,和你详细分享了 CNI 插件的工作原理,这里就不再赘述了。
  • Felix。它是一个 DaemonSet,负责在宿主机上插入路由规则(即:写入 Linux 内核的 FIB 转发信息库),以及维护 Calico 所需的网络设备等工作。
  • BIRD。它就是 BGP 的客户端,专门负责在集群里分发路由规则信息

除了对路由信息的维护方式之外,Calico 项目与 Flannel 的 host-gw 模式的另一个不同之处,就是它不会在宿主机上创建任何网桥设备。 这时候,Calico 的工作方式,可以用一幅示意图来描述,如下所示(在接下来的讲述中,我会统一用“BGP 示意图”来指代它):

其中的绿色实线标出的路径,就是一个 IP 包从 Node 1 上的 Container 1,到达 Node 2 上的 Container 4 的完整路径。

可以看到,Calico 的 CNI 插件会为每个容器设置一个 Veth Pair 设备,然后把其中的一端放置在宿主机上(它的名字以 cali 前缀开头)。

此外,由于 Calico 没有使用 CNI 的网桥模式,Calico 的 CNI 插件还需要在宿主机上为每个容器的 Veth Pair 设备配置一条路由规则,用于接收传入的 IP 包。比如,宿主机 Node 2 上的 Container 4 对应的路由规则,如下所示:

# 即:发往 10.233.2.3 的 IP 包,应该进入 cali5863f3 设备。
10.233.2.3 dev cali5863f3 scope link

要是有网桥,那么ip包到底转发到那个veth pair设备就可以交给网桥用arp根据目的ip获取目的mac地址,再查cam表获得对应的veth pair端口,就不用像上面每个容器一个路由规则了:

10.233.2.3 dev cni0 scope link
或者
10.233.2.3 dev docker0 scope link

基于上述原因,Calico 项目在宿主机上设置的路由规则,肯定要比 Flannel 项目多得多。不过,Flannel host-gw 模式使用 CNI 网桥的主要原因,其实是为了跟 VXLAN 模式保持一致。否则的话,Flannel 就需要维护两套 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

我在前面提到过,Flannel host-gw 模式最主要的限制,就是要求集群宿主机之间是二层连通的。而这个限制对于 Calico 来说,也同样存在。因为都是通过将下一跳设置为目的容器的宿主机的ip实现的,如果学过网络课程,那么就应该知道下一跳地址必须是二层可达的,因此calico也逃不过这个限制

3、Calico交换路由信息模式

(1)Node-to-Node Mesh

需要注意的是,Calico 维护的网络在默认配置下,是一个被称为“Node-to-Node Mesh”的模式。这时候,每台宿主机上的 BGP Client 都需要跟其他所有节点的 BGP Client 进行通信以便交换路由信息。但是,随着节点数量 N 的增加,这些连接的数量就会以 N²的规模快速增长,从而给集群本身的网络带来巨大的压力。这个一般推荐用在少于 100 个节点的集群里。而在更大规模的集群中,你需要用到的是一个叫作 Route Reflector 的模式

(2)Route Reflector(Calico推荐)

在这种模式下,Calico 会指定一个或者几个专门的节点,来负责跟所有节点建立 BGP 连接从而学习到全局的路由规则,从而把 BGP 连接的规模控制在 N 的数量级上。。而其他节点,只需要跟这几个专门的节点交换路由信息,就可以获得整个集群的路由规则信息了。

4、K8s节点不在一个二层网络怎么办?

假如我们有两台处于不同子网的宿主机 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 包发送到下一跳地址

(1)IPIP 隧道模式

我把这个模式下容器通信的原理,总结成了一张图片,如下所示(接下来我会称之为:IPIP 示意图):

在 Calico 的 IPIP 模式下,Felix 进程在 Node 1 上添加的路由规则,会稍微不同,如下所示:

10.233.2.0/24 via 192.168.2.2 tunl0

可以看到,尽管这条规则的下一跳地址仍然是 Node 2 的 IP 地址,但这一次,要负责将 IP 包发出去的设备,变成了 tunl0,这个 tunl0 设备,是一个 IP 隧道(IP tunnel)设备。。注意,是 T-U-N-L-0,而不是 Flannel UDP 模式使用的 T-U-N-0(tun0),这两种设备的功能是完全不一样的。

IP 包进入 IP 隧道设备之后,就会被 Linux 内核的 IPIP 驱动接管。IPIP 驱动会将这个 IP 包直接封装在一个宿主机网络的 IP 包中,如下所示:

其中,经过封装后的新的 IP 包的目的地址(图 5 中的 Outer IP Header 部分),正是原 IP 包的下一跳地址,即 Node 2 的 IP 地址:192.168.2.2。源ip地址自然是本地宿主机ip。而原 IP 包本身,则会被直接封装成新 IP 包的 Payload。这样,原先从容器到 Node 2 的 IP 包,就被伪装成了一个从 Node 1 到 Node 2 的 IP 包

由于宿主机之间已经使用路由器配置了三层转发,也就是设置了宿主机之间的“下一跳”。所以这个 IP 包在离开 Node 1 之后,就可以经过路由器,最终“跳”到 Node 2 上。

这时,Node 2 的网络内核栈会使用 IPIP 驱动进行解包,从而拿到原始的 IP 包。然后,原始 IP 包就会经过路由规则和 Veth Pair 设备到达目的容器内部

不难看到,当 Calico 使用 IPIP 模式的时候,集群的网络性能会因为额外的封包和解包工作而下降。在实际测试中,Calico IPIP 模式与 Flannel VXLAN 模式的性能大致相当。所以,在实际使用时,如非硬性需求,我建议你将所有宿主机节点放在一个子网里,避免使用 IPIP

(2)网关加入BGP Peer

如果 Calico 项目能够让宿主机之间的路由设备(也就是网关),也通过 BGP 协议“学习”到 Calico 网络里的路由规则,那么从容器发出的 IP 包,不就可以通过这些设备路由到目的宿主机了么?也就不用额外的封包、解包操作了。

遗憾的是,在 Kubernetes 被广泛使用的公有云场景里,却完全不可行。这里的原因在于:公有云环境下,宿主机之间的网关,肯定不会允许用户进行干预和设置。

当然,在大多数公有云环境下,宿主机(公有云提供的虚拟机)本身往往就是二层连通的,所以这个需求也不强烈。

不过,在私有部署的环境下,宿主机属于不同子网(VLAN)反而是更加常见的部署状态。这时候,想办法将宿主机网关也加入到 BGP Mesh 里从而避免使用 IPIP,就成了一个非常迫切的需求。

而在 Calico 项目中,它已经为你提供了两种将宿主机网关设置成 BGP Peer 解决方案

  • 就是所有宿主机都跟宿主机网关建立 BGP Peer 关系。这种方案下,Node 1 和 Node 2 就需要主动跟宿主机网关 Router 1 和 Router 2 建立 BGP 连接。从而将类似于 10.233.2.0/24 这样的路由信息同步到网关上去。

需要注意的是,这种方式下,Calico 要求宿主机网关必须支持一种叫作 Dynamic Neighbors 的 BGP 配置方式。这是因为,在常规的路由器 BGP 配置里,运维人员必须明确给出所有 BGP Peer 的 IP 地址。考虑到 Kubernetes 集群可能会有成百上千个宿主机,而且还会动态地添加和删除节点,这时候再手动管理路由器的 BGP 配置就非常麻烦了。而 Dynamic Neighbors 则允许你给路由器配置一个网段,然后路由器就会自动跟该网段里的主机建立起 BGP Peer 关系

不过,相比上面这种方案,我更愿意推荐第二种方案。

  • 使用一个或多个独立组件负责搜集整个集群里的所有路由信息,然后通过 BGP 协议同步给网关。而我们前面提到,在大规模集群中,Calico 本身就推荐使用 Route Reflector 节点的方式进行组网。所以,这里负责跟宿主机网关进行沟通的独立组件,直接由 Route Reflector 兼任即可。更重要的是,这种情况下网关的 BGP Peer 个数是有限并且固定的。所以我们就可以直接把这些独立组件配置成网关路由器的 BGP Peer,而无需 Dynamic Neighbors 的支持

三、Cilium