gitbook/深入剖析Kubernetes/docs/67775.md
2022-09-03 22:05:03 +08:00

20 KiB
Raw Permalink Blame History

35 | 解读Kubernetes三层网络方案

你好我是张磊。今天我和你分享的主题是解读Kubernetes三层网络方案。

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

我们先来看一下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.3via 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这些数据的变化然后实时更新路由表即可。

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

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

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

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

需要注意的是宿主机之间二层不连通的情况也是广泛存在的。比如宿主机分布在了不同的子网VLAN里。但是在一个Kubernetes集群里宿主机之间必须可以通过IP地址进行通信也就是说至少是三层可达的。否则的话你的集群将不满足上一篇文章中提到的宿主机之间IP互通的假设Kubernetes网络模型。当然“三层可达”也可以通过为几个子网设置三层转发来实现。

而在容器生态中要说到像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简称为ASAS 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对接的部分。我已经在上一篇文章中和你详细分享了CNI插件的工作原理这里就不再赘述了。

  2. Felix。它是一个DaemonSet负责在宿主机上插入路由规则写入Linux内核的FIB转发信息库以及维护Calico所需的网络设备等工作。

  3. 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 dev cali5863f3 scope link

发往10.233.2.3的IP包应该进入cali5863f3设备。

基于上述原因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。

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

所以Node-to-Node Mesh模式一般推荐用在少于100个节点的集群里。而在更大规模的集群中你需要用到的是一个叫作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示意图

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

10.233.2.0/24 via 192.168.2.2 tunl0

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

Calico使用的这个tunl0设备是一个IP隧道IP tunnel设备。

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

图5 IPIP封包方式

其中经过封装后的新的IP包的目的地址图5中的Outer IP Header部分正是原IP包的下一跳地址即Node 2的IP地址192.168.2.2。

而原IP包本身则会被直接封装成新IP包的Payload。

这样原先从容器到Node 2的IP包就被伪装成了一个从Node 1到Node 2的IP包。

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

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

以上就是Calico项目主要的工作原理了。

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

不过通过上面对Calico工作原理的讲述你应该能发现这样一个事实

如果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 2192.168.2.1了。以此类推我们可以继续在Router 2上添加“下一条”路由最终把IP包转发到Node 2上。

遗憾的是上述流程虽然简单明了但是在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的支持。

当然这些独立组件的工作原理也很简单它们只需要WATCH Etcd里的宿主机和对应网段的变化信息然后把这些信息通过BGP协议分发给网关即可。

总结

在本篇文章中我为你详细讲述了Fannel host-gw模式和Calico这两种纯三层网络方案的工作原理。

需要注意的是,在大规模集群里,三层网络方案在宿主机上的路由规则可能会非常多,这会导致错误排查变得困难。此外,在系统故障的时候,路由规则出现重叠冲突的概率也会变大。

基于上述原因如果是在公有云上由于宿主机网络本身比较“直白”我一般会推荐更加简单的Flannel host-gw模式。

但不难看到在私有部署环境里Calico项目才能够覆盖更多的场景并为你提供更加可靠的组网方案和架构思路。

思考题

你能否能总结一下三层网络方案和“隧道模式”的异同,以及各自的优缺点?

感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。