文章目录
- 一、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 的支持
。