Kubernetes 网络权威指南:基础、原理与实践

1、Docker 原生网络的不足

Docker 自己的网络方案比较简单,就是每个宿主机上会跑一个非常纯粹的 Linux bridge,这个 bridge 可以认为是一个二层的交换机,但它的能力有限,只能做一些简单的学习和转发。出网桥的流量会经过 iptables,经过NAT,最后通过路由转发在宿主之间进行通信。

当真正用 Docker 原生的网络模型部署一个比较复杂的业务时,会遇到诸如:容器重启之后 IP 就变了;每台宿主机会分配固定的网段,因此同一个容器迁到不同宿主机时,除了 IP 发生变化,网段也会变化,随之而来的网络策略都需要调整等问题。另外 NAT 的存在会造成两端在通信时看到对方的地址是不真实的,而且 NAT 本身也有性能损耗。这些问题都对 Docker 自身网络方案的应用造成了障碍。

2、Flannel

flannel 可以为容器提供跨节点网络服务,其模型为集群内所有容器使用一个网络,然后在每个主机上从该网络中划分一个子网。flannel为主机上的容器创建网络时,从子网中划分一个 IP 给容器。根据 Kubernetes 的模型,为每个 Pod 提供一个 IP,flannel 的模型正好与之契合。

flannel 几乎是最早的跨节点容器通信解决方案,如果仔细观察会发现其他网络插件都有 flannel 的身影。其他的方案都可以看作 flannel 的某种改进版!

  • 容器 IP 地址的重复问题。

由于 Docker 等容器工具只是利用 Linux内核的 network namespace 实现了网络隔离,各个节点上的容器 IP 地址是在所属节点上自动分配的,从全局来看,这种局部地址就像是不同小区里的门牌号,一旦拿到一个更大的范围上看,就可能是重复的。为了解决这个问题,flannel 设计了一种全局的网络地址分配机制,即使用 etcd 存储网段和节点之间的关系,然后 flannel 配置各个节点上的 Docker(或其他容器工具),只在分配到当前节点的网段里选择容器 IP 地址。这样就确保了 IP 地址分配的全局唯一性。

  • 容器 IP 地址路由问题

是不是地址不重复网络就可以联通了呢?这里还有一个问题,因为通常虚拟网络的 IP 和 MAC 地址在物理网络上是不认识的,所以数据包即使被发送到网络中,也会因为无法进行路由而被丢掉。虽然地址唯一了,但是依然无法实现真正的网络通信。 flannel 早期用得比较多的一种方式是 overlay 网络,其实就是个隧道网络。后来, flannel 也开发了另外几种处理方法,对应于 flannel 的几种网络模式。

overlay 网络下,所有被发送到网络中的数据包会被添加上额外的包头封装。这些包头里通常包含了主机本身的 IP 地址,因为只有主机的 IP 地址是原本就可以在网络里路由传播的。根据不同的封包方式, flannel 提供了 UDP 和 VXLAN 两种传输方法。 UDP 封包使用了 flannel 自定义的一种包头协议,数据是在 Linux 的用户态进行封包和解包的,因此当数据进入主机后,需要经历两次内核态到用户态的转换。 VXLAN 封包采用的是内置在 Linux 内核里的标准协议,因此虽然它的封包结构比 UDP 模式复杂, 但所有的数据装、解包过程均在内核中完成,实际的传输速度要比 UDP 模式快许多。

路由是第二种解决容器网络地址路由的方法,在 flannel 中就是 Host-Gateway 模式。从上面的讨论我们得知,容器网络无法进行路由是因为宿主机之间没有路由信息,但 flannel 是知道这个信息的,因此一个直观的想法是能不能把这个信息告诉网络上的节点呢?在 Host-Gateway 模式下, flannel 通过在各个节点上运行的 agent 将容器网络的路由信息刷到主机的路由表上,这样一来,所有的主机就都有整个容器网络的路由数据了。 Host-Gateway 的方式没有引入 overlay 额外封包和解包操作,完全是普通的网络路由机制,通信效率与裸机直连相差无几。事实上, flannel 的 Gateway 模式的性能甚至要比 Calico 好。然而,由于 flannel只能修改各个主机的路由表,一旦主机之间隔了其他路由设备,比如三层路由器,这个包就会在路由设备上被丢掉。这样一来,Host-Gateway 模式就只能用于二层直接可达的网络,由于广播风暴的问题,这种网络通常是比较小规模的。

2.1、Flannel 简介

在默认的 Docker 配置中,每个节点上的 Docker 服务会分别负责所在节点容器的 IP 分配。这样导致的一个问题是,不同节点上的容器可能获得相同的 IP 地址。

而在 Kubernetes 的网络模型中,假设了每个物理节点应该具备一段 “属于同一个内网 IP 段内”的“专用的子网 IP”。例如:

Node A : 10.0.1.0/24
Node B : 10.0.2.0/24
Node C : 10.0.3.0/24

flannel 最早由 CoreOS 开发,它是容器编排系统中最成熟的网络插件示例之一。随着 CNI 概念的兴起, flannel 也是最早实现 CNI 标准的网络插件(CNI 标准也是由 CoreOS 提出的)。flannel 的功能非常简单明确,解决的就是上文我们提到的容器跨节点访问的两个问题(IP 路由和重复)。

flannel 的设计目的是为集群中的所有节点重新规划 IP 地址的使用规则,从而使得集群中的不同节点主机创建的容器都具有全集群 “唯一”且 “可路由的 IP 地址”,并让属于不同节点上的容器能够直接通过内网 IP 通信。那么节点是如何知道哪些 IP 可用,哪些不可用,即其他节点已经使用了哪些网段的呢?flannel 用到了 etcd 的分布式协同功能。

E T C D

/ | \
flanneld flanneld flanneld

flannel 在架构上分为管理面和数据面。管理面主要包含一个 etcd,用于协调各个节点上容器分配的网段,数据面即在每个节点上运行一个 flanneld 进程。与其他网络方案不同的是,flannel 采用的是 no server 架构, 即不存在所谓的控制节点,简化了 flannel 的部署与运维。

集群内所有 flannel 节点共享一个大的容器地址段(在我们的例子中就是10.0.0.0/16),flanneld一启动便会观察 etcd,从 etcd 得知其他节点上的容器已占用的网段信息,然后向 etcd 申请该节点可用的 IP 地址段(在大网段中划分一个,例如 10.0.2.0/24),并把该网段和主机 IP 地址等信息都记录在 etcd 中。

flannel 通过 etcd 分配了每个节点可用的 IP 地址段后,修改了 Docker 的启动参数,例如 --bip=172.17.18.1/24 限制了所在节点容器获得的 IP 范围,以确保每个节点上的 Docker 会使用不同的 IP 地址段。

由于 flannel 没有 Master 和 slave 之分,每个节点上都安装一个 agent,即 flanneld。我们可以使用 Kubernetes 的DaemonSet 部署 flannel 以达到每个节点部署一个 flanneld 实例的目的。在每一个 flannel 的 Pod 中都运行了一个 flanneld 进程,且 flanneld 的配置文件以 ConfigMap 的形式挂载到容器内的 /etc/kube-flannel/ 目录,供 flanneld 使用。

2.2、Flannel backend

flannel 通过在每一个节点上启动一个叫 flanneld 的进程,负责每一个节点上的子网划分,并将相关的配置信息(如各个节点的子网网段、外部 IP 等)保存到 etcd 中,而具体的网络包转发交给具体的 backend 实现。

flanneld 可以在启动时通过配置文件指定不同的 backend 进行网络通信,目前比较成熟的 backend 有 UDP、VXLAN和Host Gateway 三种,VXLAN是官方最推崇的一种 backend 实现方式; Host Gateway 一般用于对网络性能要求比较高的场景,但需要基础网络架构的支持; UDP 则用于测试及一些比较老的不支持 VXLAN 的 Linux 内核。

2.2.1、UDP

当采用 UDP 模式时,flanneld 进程在启动时会通过打开/dev/net/tun的方式生成一个 TUN 设备,TUN 设备可以简单理解为 Linux 当中提供的一种内核网络与用户空间(应用程序)通信的一种机制,即应用可以通过直接读写 tun 设备的方式收发 RAW IP 包。

flanneld 进程启动后通过 ip a 命令可以发现节点当中已经多了一个叫 flannel0 的网络接口。flannel0 网络接口上的 MTU 是1472,相 比宿主机网络接口 eth0 少了28个字节,为什么呢?

容器跨节点通信实现流程:

假设在节点 A 上有容器 A(10.244.1.96),在节点 B 上有容器 B(10.244.2.194),此时容器 A 向容器 B 发送一个 ICMP 请求报文(ping),我们来逐步分析一下 ICMP 报文是如何从容器 A 到达容器 B 的。


1、容器 A 当中发出 ICMP 请求报文,通过 IP 封装后形式为:10.244.1.96 -> 10.244.2.194,此时通过容器 A 内的路由表匹配到应该将 IP 包发送到网关 10.244.1.1(cni0网桥)。

2、此时到达 cni0 的 IP 包目的地 IP 10.244.2.194匹配到节点 A 上第一条路由规则(10.244.0.0),内核将 RAW IP 包发送给 flannel0 接口。

3、flannel0为 tun 设备,发送给 flannel0 接口的RAW IP包(无MAC信息)将被 flanneld 进程接收到,flanneld 进程接收到 RAW IP 包后在原有的基础上进行 UDP 封包,UDP 封包的形式为:172.16.130.140:src port -> 172.16.130.164:8285。

这里有一个问题就是 flanneld 怎么知道 10.244.2.194 这个容器到底是在哪个节点上呢?

flanneld 在启动时会将该节点的网络信息通过 api-server 保存到 etcd 当中,故在发送报文时可以通过查询 etcd 得到 10.244.2.194 这个容器的 IP 属于 host B,且 host B 的 IP 为172.16.130.164。

4、flanneld将封装好的 UDP 报文经 eth1 发出,从这里可以看出网络包在通过 eth1 发出前先是加上了 UDP 头(8个字节),再然后加上了 IP 头(20个字节)进行封装,这也是为什么 flannel0 的 MTU 要比 eth1 的 MTU 小28个字节的原因(防止封装后的以太网帧超过 eth1 的 MTU 而在经过 eth1 时被丢弃)。

此时完整的以太网帧格式为:

5、网络包经节点 A 和节点 B 之间的网络连接到达 host B。

6、host B 收到 UDP 报文后经 Linux 内核通过 UDP 端口号 8285 将包交给正在监听的应用 flanneld。

7、运行在 host B 当中的 flanneld 将 UDP 包解包后得到 RAW IP 包:10.244.1.96 -> 10.244.2.194。

8、解封后的 RAW IP 包匹配到 host B上的路由规则(10.244.2.0),内核将 RAW IP 包发送到 cni0。

此时的完整的以太网帧格式为:

9、cni0 将 IP 包转发给连接在 cni0 网桥上的 container B,而flanneld在整个过程中主要主要负责两个工作:

  • UDP 封包解包

  • 节点上的路由表的动态更新

从上面虚线部分就可以看到 container A 和 container B 虽然在物理网络上并没有直接相连,但在逻辑上就好像是处于同一个三层网络当中,这种基于底下的物理网络设备通过 Flannel 等软件定义网络技术实现的网络我们称之为 Overlay 网络。

UDP 这种 Backend 最明显的问题就是,网络数据包先是通过 tun 设备从内核当中复制到用户态的应用,然后再由用户态的应用复制到内核,仅一次网络传输就进行了两次用户态和内核态的切换,显然这种效率是不会很高的。

那么有没有高效一点的办法呢?当然,最简单的方式就是把封包解包这些事情都交给内核去干好了,事实上 Linux 内核本身也提供了比较成熟的网络封包解包(隧道传输)实现方案 VXLAN。

2.2.2、VXLAN

VXLAN 全称 Virtual Extensible LAN,是一种虚拟化隧道通信技术,主要是为了突破 VLAN 的最多4096个子网的数量限制,以满足大规模云计算数据中心的需求。VLAN技术的缺陷是 VLAN Heade r预留的长度只有12 bit,故最多只能支持2的12次方即4096个子网的划分,无法满足云计算场景下主机数量日益增长的需求。当前 VXLAN 的报文 Header 内有24 bit,可以支持2的24次方个子网,并通过VNI(Virtual Network Identifier)来区分不同的子网,相当于VLAN当中的VLAN ID。

VXLAN包格式:

从 VXLAN 的包格式就可以看到原本的二层以太网帧被放在 VXLAN 包头里进行封装,VXLAN 实际实现的是一个二层网络的隧道,通过 VXLAN 让处于同一个 VXLAN 网络当中的机器看似处在同一个二层网络当中,而网络包转发的方式也类似二层网络当中的交换机。

此时容器跨节点网络通信实现流程为:

  1. 同 UDP Backend 模式,容器 A 当中的 IP 包通过容器 A 内的路由表被发送到 cni0

  2. 到达 cni0 当中的 IP 包通过匹配 host A 当中的路由表发现通往 10.244.2.194 的 IP 包应该交给 flannel.1 接口

  3. flannel.1 作为一个 VTEP 设备,收到报文后将按照 VTEP 的配置进行封包,首先通过 etcd 得知10.244.2.194属于节点 B,并得到节点 B 的 IP,通过节点 A 当中的转发表得到节点 B 对应的 VTEP 的 MAC,根据flannel.1设备创建时的设置的参数(VNI、local IP、Port)进行VXLAN封包

  4. 通过 host A 跟 host B 之间的网络连接,VXLAN 包到达 host B 的 eth1 接口

  5. 通过端口8472,VXLAN 包被转发给 VTEP 设备 flannel.1 进行解包

  6. 解封装后的 IP 包匹配 host B 当中的路由表(10.244.2.0),内核将IP包转发给cni0

  7. cni0 将 IP 包转发给连接在 cni0 上的容器 B

这么一看是不是觉得相比UDP模式单单从步骤上就少了很多步?VXLAN模式相比UDP模式高效也就不足为奇了。

2.2.3、Host Gateway

从名字中就可以想到这种方式是通过把主机当作网关来实现跨节点网络通信的。那么具体如何实现跨节点通信呢?

采用 host-gw 模式后 flanneld 的唯一作用就是负责主机上路由表的动态更新, 想一下这样会不会有什么问题?

使用 host-gw Backend 的 Flannel 网络的网络包传输过程如下图所示:

  1. 同 UDP、VXLAN 模式一致,通过容器 A 的路由表 IP 包到达 cni0

  2. 到达 cni0 的 IP 包匹配到 host A 当中的路由规则(10.244.2.0),并且网关为172.16.130.164,即 host B,所以内核将 IP 包发送给host B(172.16.130.164)

  3. IP 包通过物理网络到达 host B 的 eth1

  4. 到达 host B eth1 的 IP 包匹配到 host B 当中的路由表(10.244.2.0),IP 包被转发给 cni0

  5. cni0 将 IP 包转发给连接在 cni0 上的容器 B

host-gw 模式其中一个局限性就是,由于是通过节点上的路由表来实现各个节点之间的跨节点网络通信,那么就得保证两个节点是可以直接路由过去的。按照内核当中的路由规则,网关必须在跟主机当中至少一个 IP 处于同一网段,故造成的结果就是采用 host-gw 这种 Backend 方式时则集群中所有的节点必须处于同一个网络当中,这对于集群规模比较大时需要对节点进行网段划分的话会存在一定的局限性。另外一个则是随着集群当中节点规模的增大,flanneld 需要维护主机上成千上万条路由表的动态更新也是一个不小的压力。

3、Calico

Calico 作为容器网络方案和我们前面介绍的那些方案最大的不同是它没有采用 overlay 网络做报文的转发,而是提供了纯3层的网络模型。三层信模型表示每个容器都通过 IP 直接通信,中间通过路由转发找到对方。在这个过程中,容器所在的节点类似于传统的路由器,提供了路由查找的功能。要想路由能够正常工作,每个容器所在的主机节点扮演了虚拟路由器(vRouter)的功能,而且这些 vRouter必须有某种方法,能够知道整个集群的路由信息。

Calico 是一个基于 BGP 的纯三层的数据中心网络方案。之前提到 flannel 的 host-gw 模式之所以不能跨二层网络,是因为它只能修改主机的路由, Calico 把改路由表的做法换成了标准的 BGP 路由协议。相当于在每个节点上模拟出一个额外的路由器,由于采用的是标准协议, Calico 模拟路由器的路由表信息可以被传播到网络的其他路由设备中,这样就实现了在三层网络上的高速跨节点网络。

注:现实中的网络并不总是支持 BGP 路由,因此 Calico 也设计了一种 ipip 模式,使用 overlay 的方式传输数据。 ipip 的包头非常小,而且是内置在内核中的,因此它的速度理论上要比 VXLAN 快,但是安全性更差。

3.1、Calico 简介

Calico 基于 iptables 实现了 Kubernetes 的网络策略,通过在各个节点上应用 ACL(访问控制列表)提供工作负载的多租户隔离、安全组及其他可达性限制等功能。此外,Calico 还可以与服务网格 Istio 集成,以便在服务网格层和网络基础架构层中解释和实施集群内工作负载的网络策略。

Calico 在每一个计算节点利用 Linux 内核的一些能力实现了一个高效的 vRouter 负责数据转发,而每个 vRouter 通过 BGP 把自己运行的工作负载 的路由信息向整个 Calico 网络传播。小规模部署可以直接互联,大规模下 可以通过指定的 BGP Route Reflector 完成。最终保证所有的工作负载之间的数据流量都是通过 IP 路由的方式完成互联的。

名词解释:

  • Endpoint:接入 Calico 网络中的网卡(IP)

  • AS:网络自治系统,通过 BGP 与其他 AS 网络交换路由信息

  • iBGP:AS 内部的 BGP Speaker,与同一个 AS 内部的 iBGP、eBGP 交换路由信息

  • eBGP:AS 边界的 BGP Speaker,与同一个 AS 内部的 iBGP、其他 AS 的 eBGP 交换路由信息

  • workloadEndpoint:虚拟机和容器端点,一般指它们的 IP 地址

  • hostEndpoint:宿主机端点,一般指它们的 IP 地址

架构解析:

  • Orchestrator Plugin:每个主要的云编排平台都有单独的 Calico 网络插件(例如 Kubernetes、Docker Swarm 等)。这些插件的目的是将 Calico 无缝集成到编排工具中,就像它们在管理编排工具中内置的网络工具一样管理 Calico 网络。

  • etcd:以分布式、一致和容错的方式存储 Calico 网络的数据(一般至少由3个 etcd 节点组成一个集群)。确保 Calico 网络始终处于良好状态,同时允许运行 etcd 的个别机器节点失败或无法访问。 让 Calico 其他组件watch etcd 键值空间中的某些 key,确保它们能够及时响应集群状态的更新。

  • Felix:

Felix 是一个守护程序,作为 agent 运行在托管容器或虚拟机的Calico节点上。Felix 负责刷新主机路由和 ACL 规则等,以便为该主机上的 Endpoint 正常运行提供所需的网络连接和管理。进出容器、虚拟机和物理主机的所有流量都会遍历 Calico,利用 Linux 内核原生的路由和 iptables生成的规则。

Felix 一般负责以下工作:

管理网络接口, Felix 将有关网络接口的一些信息编程到内核中,使内核能够正确处理该 Endpoint 发出的流量。Felix 将确保主机正确响应来自每个工作负载的 ARP 请求,并将其管理的网卡启用 IP Forward。

编写路由,Felix 负责将到其主机上 Endpoint 的路由编写到 Linux 内核 FIB(转发信息库)中。这可以确保那些发往目标主机的 Endpoint 的数据包被正确地转发

编写 ACL,Felix 还负责将 ACL 编程到 Linux 内核中,即 iptables 规则。这些 ACL 用于确保只在 Endpoints 之间发送有效的网络流量,并确保 Endpoint 无法绕过 Calico 的安全措施;

报告状态, Felix 负责提供有关网络健康状况的数据。例如,它将报告配置其主机时发生的错误和问题。该数据会被写入 etcd,并对网络中的其他组件可见。

  • BGP Client:

Calico 在每个运行 Felix 服务的节点上都部署一个 BGP Client(BGP 客户端)。 BGP 客户端的作用是读取 Felix 编写到内核中的路由信息,由 BGP 客户端对这些路由信息进行分发。具体来说,当 Felix 将路由插入 Linux 内核 FIB 时, BGP 客户端将接收它们,并将它们分发到集群中的其他工作节点。

  • BGP Route Reflector(BIRD)

简单的 BGP 可能成为较大规模部署的性能瓶颈,因为它要求每个 BGP 客户端连接到网状拓扑中的每一个其他 BGP 客户端。随着集群规模的增大,一些设备的路由表甚至会被撑满。

因此,在较大规模的部署中, Calico 建议使用 BGP Route Reflector(路由器反射器)。互联网中通常使用 BGP Route Reflector 充当 BGP 客户端连接的中心点,从而避免与互联网中的每个 BGP 客户端进行通信。

在 Calico 中,最常见的 BGP 组件是 BIRD,配置为 Route Reflector 运行,而非标准 BGP 客户端。

Calico 跨节点通信的模式: IPIP 模式(默认)和 BGP 模式(需要交换机支持 BGP 协议)。

3.2、Calico 隧道模式

Calico 可以创建并管理一个3层平面网络,为每个工作负载分配一个完全可路由的 IP 地址。工作负载可以在没有 IP 封装或 NAT 的情况下进行通信,以实现裸机性能,简化故障排除和提供更好的互操作性。我们称这种网络管理模式为 vRouter 模式。vRouter 模式直接使用物理机作为虚拟路由器,不再创建额外的隧道。然而在需要使用 overlay 网络的环境中, Calico也提供了 IP-in-IP(简称 ipip)的隧道技术。

类似 flannel 隧道

3.3、Calico BGP 模式

Calico 的核心设计思想就是 Router,它把每个操作系统的协议栈看成一个路由器,然后把所有的容器看成连在这个路由器上的网络终端,在路由器之间运行标准的 BGP,并让节点自己学习这个网络拓扑该如何转发。然而,当网络端点数量足够大时,自我学习和发现拓扑的收敛过程非常耗费资源和时间。

那么为什么 Calico 要选择 BGP 呢?为什么选择 BGP 而不是一个 IGP 协议(如 OSPF 或者 IS-IS)?

任何网络,尤其是大型网络,都需要处理两个不同的路由问题:

  • 发现网络中路由器之间的拓扑结构

  • 发现网络中正在工作的节点,以及可到达该网络的外部连接

IGP 需要执行大量复杂计算,才能让每台设备在同一时刻都能得到对所处网络拓扑的相同认知。这其实就限制了 IGP 所能运行的规模。

为了解决大规模网络中的路由可扩展性问题, BGP 被开发出来。 BGP 可以在一个网络中扩容到几百台路由器的规模,而如果使用 BGP Route Reflection 这一数量更是可以到达数万台。如果需要的话, BGP 可以宣告数百万条路由信息,并通过非常灵活的策略加以管理。

因此,我们就能理解为什么 Calico 使用 BGP 宣告网络端点的路由了。就是提高路由规则的可扩展性以满足大规模组网的需求。