04-三层网络方案

除了网桥模式的CNI插件,还有纯三层(Pure Layer3)网络方案。如Flannel的host-gw模式和Calicao项目。

0.1. Flannel的host-gw模式

工作原理如下图所示:

image

Node1的C1要访问Node2的C2,当设置Flannel使用hots-gw模式后,flanneld会在宿主机上创建如下规则:

ip route
...
10.244.1.0/24 via 10.168.0.3 dev eth0

# 目标IP地址属于10.233.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模式下,下一跳地址就是目的宿主机node2的地址。配置完成后,当IP包从网络层进入链路层封装成帧的时候,eth0设备就会使用下一跳地址对应的MAC地址(node2的MAC地址),作为该数据帧的目的地址。这样数据帧就能从node1通过宿主机的二层网络顺利到达node2上。

Node2的内核网络栈从二层数据帧里拿到IP包后,看到IP包的目的IP地址是C2的IP,根据Node2的路由表,该目的地址会匹配第二条路由规则,从而进入cni0网桥,最后进入到C2中。

host-gw模式的工作原理:将每个Flannel子网(如10.244.1.0/24)的下一跳设置成该子网对应的宿主机的IP地址。宿主机会被充当这条容器通信路径里的网关。

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

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网络模型)。三层可达也能通过为几个子网设置三层转发来实现。

0.2. Calico

Calico也会在每台宿主机上添加一个路由规则,如下所示:

< 目的容器 IP 地址段 > via < 网关的 IP 地址 > dev eth0

# 网关的IP地址,正式目的容器所在宿主机的IP地址

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

不同于Flannel通过Etcd和宿主机上的flanneld来维护路由信息,Calico使用BGP(Border Gateway Protocol,边界网关协议)来自动地在整个集群中分发路由信息。

BGP是Linux内核原生支持的、专门用在大规模数据中心里维护不同的“自治系统”之间路由信息的、无中心的路由协议。

image

图中有两个自治系统(一个组织管辖下的所有IP网络和路由器的全体):AS1、AS2,正常情况下,自治系统之间不会有任何来往。如果自治系统中的主机,要通过IP地址直接进行通信,就必须使用路由器把这两个自治系统连接起来。

  1. AS1的主机(10.10.0.2)要访问AS2的主机(172.17.0.3).发出的IP包就会先到达自治系统AS1的路由器Router1
  2. Router1的路由表里有一条规则:目的地址的(172.17.0.2)的包,应该经过Router1的C接口,发往网关Route2
  3. IP包到到达Router2上,经过路由表从接口B出来达到目的主机(172.17.0.3
  4. 当主机(172.17.0.3)要访问主机(10.10.0.2),那么这个IP包,在到达Router2之后,就不知道该去哪里了,因为在Router2的路由表里,并没有关于AS1自治系统的任何路由规则
  5. 此时应该给Router2添加一条路由规则(如:目的地址10.10.0.2的IP包,应该经过Router2的C接口,发送网关Router1)

像这样,负责把自治系统连接在一起的路由器,成为边界网关,与普通路由器的不同之处在于,路由表里拥有其他自治系统的主机路由信息

当网络拓扑结构非常复杂,每个自治系统都有成千上万个主机、无数个路由器,甚至是多个分公司,多个网络提供商、多个自治系统的复合自治系统,依靠人工来对边界网关的路由表进行配置和维护,那是不现实的。此时就需要使用BGP,BGP会在每个边界网关上运行一个小程序,他们会将各自的路由表信息、通过TCP传输给其他的边界网关,其他边界网关上的这个小程序,就会对接收到的这些数据进行分析,然后将需要的信息添加到自己的路由表中。

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

0.2.1. Calico组成

  1. Calico的CNI插件,这是Calico与kubernetes对接的部分。
  2. Felix,它是一个DaemonSet,负责在宿主机上插入路由规则(即,写入Linux内核的FIB转发信息库),以及维护Calico所需的网络设备等
  3. BIRD,是BGP的客户端,专门负责在集群里分发路由规则信息。

Calico和Flannel的host-gw的异同:

  1. Calico不会在宿主机上创建任何网桥设备
  2. 对路由信息的维护方式不同
  3. Calico在宿主机上设置的路由规则更多
  4. 都要求集群之间是二层连通

0.2.2. Calico的工作方式如下

image

绿色实线标出的路径,就是一个IP包从node1的C1到node2的C4的完整路径。Calico的CNI插件会为每个容器设置一个Veth Pair设备,然后把其中的一端放置在宿主机上(cali前缀)。

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

10.233.2.3 dev cali5863f3 scope link

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

有了Veth Pair设备之后,容器发送的IP包就会经过Veth Pair设备出现在宿主机上,然后,在宿主机网络栈就会根据路由规则的下一跳IP地址(最核心的这个路由规则,由Calico的Felix进程负责维护,这些路由规则信息,通过BGP Client也就是BIRD组件,使用BGP协议传输而来),把它们转发给正确的网关。

BGP协议传输的消息,类似如下格式:

[BGP 消息]
我是宿主机 192.168.1.3
10.233.2.0/24 网段的容器都在我这里
这些容器的下一跳地址是我

Calico项目实际上将集群里的所有节点,都当作是边界路由器来处理,他们一起组成了一个全连通的网络,互相之间通过BGP协议交换路由规则,这些节点称为BGP Peer。

0.3. 模式

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

  2. Route Reflector模式 规模比较大时,使用Route Reflector模式,在这中模式下,Calico会指定一个或者几个专门的节点,来负责跟所有节点建立BGP连接从而学习到全局的路由规则,其他节点只需要与这几个专门的节点(Route Reflector节点,扮演了中间代理的角色)交换路由信息,就能够获得整个集群的路由规则信息。这个模式可以把BGP连接的规模控制在N的数量级上。

  3. IPIP模式

    image

当两个节点在不同的子网下,节点中的容器需要通信时,如C1(192.168.1.2)与C4(192.168.2.2)进行通信,Calico会尝试在Node1上添加如下路由规则:

10.233.2.0/16 via 192.168.2.2 eth0

# 规则的下一跳地址是192.168.2.2 这个ip不在node1的子网中,没法通过二层网络把IP包发送到下一跳地址

使用IPIP模式后,可以解决这个问题,Felix进程会在Node1上添加的路由规则会有变化,如下所示:

10.233.2.0/24 via 192.168.2.2 tunl0

# 规则的下一跳地址没有变化,但是负责将IP包转发的设备变成了tunl0
# tunl0是一个IP隧道(IP tunnel设备)

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

image

  1. 经过封装后的新的IP包的目的地址(Outer IP Header部分),正是原IP包的下一跳地址(node2的ip 192.168.2.2),原IP包本身,则会被直接封装成新IP包的Payload。
  2. 原先从容器到Node2的IP包,就被伪装成一个从Node1到Node2的IP包。
  3. 宿主机之间已经使用路由器配置了三层转发(即设置了宿主机之间的下一跳),所有IP包离开Node1之后,就可以经过路由器,最终跳到Node2上。
  4. Node2的网络内核栈会使用IPIP驱动进行解包,从而拿到原始的IP包。
  5. 原始IP包经过路由规则和Veth Pair设备到达目的容器内部。

当使用IPIP模式时,集群的网络性能会因为额外的封包和解包工作而下降。性能大概和Flannel的VXLAN模式差不多。在实际使用时,尽量在一个子网中,避免使用IPIP模式。

0.4. 多次路由跳转

如果Calico项目能让宿主机之间的路由设备(网关)也通过BGP协议学习到Calico网络里的路由规则,那么从容器发出的IP包,就可以通过这些设备路由到目的宿主机。

# 在node1中添加 如下路由规则
10.233.2.0/24 via 192.168.1.1 eth0

# 在Router(192.168.1.1)上添加如下路由规则
10.233.2.0/24 via 192.168.2.1 eth0

C1发出的IP包,通过两次下一跳,到达Router2。

在公有云环境下,宿主机之间的网关,是不允许用户进行干预和配置的。在大多数公有云环境下,宿主机(公有云提供的虚拟机)本身是二层连通的。

在私有云环境下,宿主机属于不同子网很常见,想办法将宿主机网关加入到BGP Mesh里从而避免使用IPIP。Calico提供了两种将宿主机网设置成BGP Peer的解决方案。

0.4.1. 方案1

所有宿主机都与宿主机网关建立BGP Peer关系。这样每个节点都需要主动与宿主机网关建立BGP连接,从而将路由信息同步到网关上。

这种方式,Calico要求宿主机网关必须支持Dynamic Neighbors的BGP配置,在常规的BGP配置中,运维人员必须明确给出所有BGP Peer的IP地址。kubernetes集群中宿主机动态增加节点,手动管理很麻烦,Dynamic Neighbors允许给路由配置一个网段,然后路由器会自动跟给网段里的主机建立BGP Peer关系。

0.4.2. 方案2

使一个或多个独立组件搜集整个集群里所有路由信息,然后通过BGP协议同步给网关。在大规模集群中,Calico使用Router Reflector节点的方式进行组网,这些节点兼任负责与宿主机网关进行沟通的独立组件的任务。

这种情况下,BGP Peer数量有限且固定,可以直接把这些独立组件配置成路由器的BGP Peer,无需Dynamic Neighbors支持。这些独立组件只需要WATCH Etcd里的宿主机和对应网段的变化信息,然后把这些信息通过BGP协议分发给网关即可。

0.5. 总结

在大规模集群中,三层网络方案在宿主机上的路由规则可能会非常多,导致错误排除困难,系统故障时,路由规则重叠冲突的概率变大,在公有云部署,使用Flannel host-gw模式,在私有云部署,Calico能覆盖更多场景,提供更可靠的组网方案和架构思路。

上次修改: 14 April 2020