WIP #
Kubernetes 作为容器编排系统,引入了容器虚拟化、pod、编排机制 等概念,网络通信在引入的同时也变得异常复杂。简单来讲,Kubernetes 的网络可以划分为 4 个维度
- 同一个 pod 内容器间的网络通信
- 同一个宿主机下,不同 pod 间的网络通信
- 不同宿主机间,容器的网络通信
- 外部与集群容器之间的网络通信
同一个 pod 内容器间的通信 #
Pod 为 Kubernetes 中的最小可部署对象,其中可以包含一组容器。正常情况下,同一个物理机上的两个进程是不能直接通信的,需要依赖网卡、回环设备、路由表等网络设施。
docker 容器主要基于 Linux 的 Namespace 技术和 Cgroup 技术进行逻辑层的虚拟化,分别实现了资源的隔离和限制。
容器启动时,通过对容器进程的 PID Namespace、Net Namespace、mnt Namespace 等配置,实现了对进程、网络、文件系统等资源的隔离。而对于同一个 Pod 内的不同容器来说,他们的 Namespace 进行了修改定制,共享同一个 Net Namespace,所以可以使用同一套网卡、回环设备、路由表等网络设施,因此容器间的通信也得到解决。
同一宿主机上的不同 pod 间的通信 #
由于网络和文件系统等 Namespace 的隔离,不同 Pod 之间的网络通信无法再依赖同一个 namespace 下的网络设施。实际上,我们可以将每一个容器看作一台物理机,其都有自己独立的网络栈,而实现网络通信最简单的方式,就是将两个容器连接到同一个网桥上。
**网桥(Bridge)**是 Linux 中能够起到虚拟交换机作用的网络设备,它是一个工作在数据链路层(Data Link)的设备,可以根据 MAC 地址学习来将数据包转发到网桥的不同端口(Port)上。而 Docker 项目会在宿主机上创建一个 docker0 网桥,将容器连接在 docker0 网桥上,就可以进行通信。
Veth Pair 设备常被用作连接不同 Net Namespace 的”网线“,当创建 Veth Pair 设备后,会出现两张(成对的)虚拟网卡(Veth Pair),从一张网卡上发出的数据包,会直接出现在另一张网卡上。当容器启动后,容器中 eth0 网卡正是 Veth Pair 设备在容器中的一端,而它的另一端,就可以在宿主机上查看到。当虚拟网卡插在宿主机上的网桥上时,就会降级为网桥的从设备,不再调用网络协议栈自行处理数据包,而是成为网桥的一个端口。所有数据包的处理全部交给对应的网桥。
当我们从一个容器访问宿主机上的另一个容器的时候,通过路由规则,经过虚拟网卡,数据包被发往对应的容器。首先容器通过 ARP 协议获取目标容器的 MAC 地址,将添加了 MAC 地址的数据包发往 docker0 网桥,随后网桥将数据包转发给目标容器,从而实现了容器间的通信。
不同宿主机上的容器间通信 #
Flannel 项目是一种容器网络解决方案,其支持三种后端实现来支持容器跨主网络
基于 UDP 的实现 #
UDP 模式是三种实现中最容易理解的方案,但也存在性能问题,目前已经被弃用。其实现基于在宿主机上部署的 flanneld 进程。
Flannel 进程会创建一个 flannel0 设备。flannel0 就是一个 TUN 设备,当系统将 IP 包发送给 flannel0 设备后,flannal0 就会将这个数据包发送给创建该设备的进程,即 Flannel 进程。这是一个数据包从内核态发往用户态的过程。
- 容器中发起的 IP 包,由于目标 IP 地址不在 docker0 网桥的网段内,匹配到默认的路有规则,通过容器网关进入 docker0 网桥,流向宿主机。(用户态 -> 内核态)
- 经过路由规则,进入 flannel0 设备
- flannel0 设备将数据包转交给宿主机上的 Flanneld 进程(内核态 -> 用户态)
- Flanneld 根据目标 IP 地址,匹配到对应目标的子网,通过 ETCD 查询到子网对应的宿主机地址。
- Flanneld 将数据包封装成 UDP 包,然后通过 UDP 协议目标的宿主机。(用户态 -> 内核态)
- 目标宿主机和容器通过相反的流程进行数据包接受和解析
UDP 模式实际是一种三层的 Overlay 网络,其首先对发出端的 IP 包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容器。但是因为存在三次用户态和内核态之间的数据拷贝,所以存在比较严重的性能问题。
基于 VXLAN 的实现 #
VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络虚似化技术,从而可以在内核实现 UDP 的封装和解封装工作,减少了用户态内核态数据拷贝的开销
flannel.1 设备,就是 VXLAN 所需的 VTEP 设备,处于内核态,它既有 IP 地址,也有 MAC 地址。
目标地址的设备信息,同样由位于宿主机的 flanneld 进程维护。当新的 Node 启动并加入到集群网络时,flanneld 进程就会添加一条路有规则,如下,同时也会在 ARP 表中添加对应的 mac 地址
$ 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 的 VTEP(即 flannel.1)设备的 ip 地址
整体链路如下:
- 数据包经过 docker0 网桥,路由至 flannel.1 设备进行处理
- 内核根据 flanneld 进程得到目标地址的 mac 地址信息,将数据封装成二层数据帧,并添加一个特殊的 VXLAN 头
VXLAN 头里有一个重要的标志叫作 VNI,VTEP 设备用这个标志识别某个数据帧是不是应该归自己处理。而在 Flannel 中,VNI 的默认值是 1,这也是为何,宿主机上的 VTEP 设备都叫作 flannel.1 的原因,这里的“1”,其实就是 VNI 的值。
- linux 内核将这个二层数据帧封装成 udp 包发出去(一般 4789 作为 VTEP 设备的 UDP 端口)
- 正常的数据封包流程,内核在二层数据帧外再添加上 UDP header,再加上 IP 头,组成四层数据包,再添加二层数据帧头,最终发往目标主机。最终组成的包格式如下图: 注意,flannel.1 的二层数据帧只有目标 VXLAN 设备的 MAC 地址,不能直接发出去,还得封装成 UDP 包在四层网络传输,所以必须要知道目标主机的 IP。目标 VTEP 设备的MAC 地址和目标主机 IP 的映射关系保存在 FDB(Forwarding Database)转发数据库里,由 flanneld 维护
基于 host-gw 的实现 #
host-gw 模式工作原理,即将每个 Flannel 子网(Flannel Subnet)的下一跳,设置为该子网对应的宿主机的 IP 地址。host 即为通信链路的网关,Flannel 子网和主机的信息全部储存在 ETCD 中,flanneld watch 这些数据变化并对路由表进行更新。
Flanenel 使用该模式时,会在宿主机上添加一条路由规则,如下
$ ip route
...
10.244.1.0/24 via 10.168.0.3 dev eth0
表示,目的 IP 属于 10.244.1.0/24 网段的 IP 包,都需要经过本机的 eth0 设备发出,且下一跳(next-hop)为 10.168.0.3,即目的宿主机的地址。之后 IP 包由网络层进入链路层,封装成帧,eht0 设备就会使用下一跳地址对应的 mac 地址作为数据帧的目的 mac 地址,从而发往目标宿主机。
目的宿主机拿到 IP 包后,根据 IP 包的目的 IP 地址,匹配到对应路由规则,将数据包发往 cni0 网桥,从而进入容器中。
这种模式不需要额外的封包和解包带来的性能损耗,性能损失很小,但是也要求集群的宿主机之间是二层相通的。但是实际上,集群大部分是三层相通,即 IP 互通,但是宿主机可能分布在了不同的子网,造成了二层不通的情况。
Calico #
Calico 的网络方案类似 Flannel 的 host-gw 模式,同样是在每台宿主机上添加一个下一跳到目标宿主机的路由规则。但是路由信息的维护不同于 flanneld 的 etcd,calico 采用 BGP(边界网关协议,Border Gateway Protocol)。
BGP 是 linux 内核原生支持的,专门用在大规模数据中心维护不同的“自治系统”之间路由信息的、无中心的复杂路由协议。简单来说,BGP 可以实现在大规模网络中的节点路由信息共享。
Calico 项目有三部分组成
- Calico 的 CNI 插件
- Felix。DaemonSet,负责在宿主机上插入路由规则(写入 linux 内核的 FIB 转发信息库),以及维护 Calico 所需的网络设备
- BIRD。即 BGP 的客户端,负责在集群里分发路由规则信息。
Calico 的“下一跳”路由规则,就是由 Calico 的 Felix 进程负责维护。这些路由规则信息,由 BIRD 组件(BGP Client)通过 BGP 协议传输来。Calico 项目将集群上所有的节点当做边界路由器来处理,组成了一个全联通的网络,节点之间通过 BGP 交换路由规则。这些节点,我们称为 BGP Peer。
Calico 的 CNI 插件会为每个容器设置一个 Veth Pair 设备,并将其一端放置在宿主机上(cali 前缀),这一点不同于 Flannel 在宿主机上创建网桥设备,Calico 的 CNI 插件需要在宿主机上为每个容器的 Veth Pair 设备配置一条路由规则用于接收传入的 IP 包。如 Node2 上的 Container4 对应的路由规则:
10.233.2.3 dev cali5863f3 scope link # 发往 10.233.2.3 的 IP 包应该进入 cali5863f3 设备
- 容器发出 IP 包,经过 Veth Pair 设备,发到宿主机。
- 宿主机根据路由规则的下一跳 IP 地址,转发到正确网关
- 后续同 Flannel 的 host-gw 模式
Calico 默认配置下,是 Node-to-Node Mesh 模式,每个宿主机的 BGP Client 都需要和其他所有节点的 BGP Client 通信交换路由信息,如果节点增加,连接数量会以 N^2 规模增长。一般推荐默认模式用于少于 100 个节点的集群。
而更大规模的集群,推荐使用 Route Reflector 模式。该模式下 Calico 会指定一个或几个专门的节点用于与所有的节点建立 BGP 连接,学习到全局的路由规则,同时充当中间代理,而其他节点只需跟这几个专门的节点交换路由信息,就可以获取全局的路由信息。此时 BGP 的连接规模增长控制在了 N。
IPIP 模式 #
基于以上,同样,Calico 也是建立在二层连通的规则上。如果 node 在不同的子网中,则需要使用 IPIP 模式。
ipip 模式下,Felix 进程在 Node1 上添加的路由规则如下:
10.233.2.0/24 via 192.168.2.2 tunl0
与之前不同的是,负责将 IP 包发出的设备变味了 tunl0,这是一个 IP 隧道(IP tunnel)设备,IP 包进入该设备后,会被 linux 内核的 IPIP 驱动接管,将这个 IP 包直接封装在一个宿主机网络的 IP 包中。如图,原 IP 包被封装成了新的 IP 包的 Payload,而新 IP 包的目的地址则为 Node2 的 IP 地址,即 192.168.2.2,离开 Node1 之后经过路由器到达 Node2。
IPIP 模式下由于多了额外的封包、解包工作,性能相较默认有所下降,可能与 Flannel VXLAN 模式性能相当。实际使用中,可以根据实际情况选择合适的网络方案,尽量避免使用 IPIP。
私有云网络方案 #
私有云环境在,宿主机之间的网关存在允许干预和定制的情况,这种情况下,将宿主机网关加入到 BGP Mash,从而避免使用 IPIP,是一种迫切的需求。Calico 项目提供了两种将宿主机网关设置为 BGP Peer 的方案:
-
所有宿主机都主动和宿主机网关建立 BGP Peer 关系,将路由信息同步到网管上。这种方案下,Calico 要求宿主机网关支持 Dynamic Neighbors 的 BGP 配置方式,允许你给路由器配置一个网段,然后路由器就会自动跟该网段里的主机建立起 BGP Peer 关系。
-
使用一个或多个独立组件负责搜集整个集群里的所有路由信息,然后通过 BGP 协议同步给网关。而我们前面提到,在大规模集群中,Calico 本身就推荐使用 Route Reflector 节点的方式进行组网。所以,这里负责跟宿主机网关进行沟通的独立组件,直接由 Route Reflector 兼任即可。这种方案网关的 BGP Peer 个数有限且固定,可以直接将这些独立组件配置成路由器的 BGP Peer,无需 Dynamic Neighbors。独立组件只需 watch ETCD 中宿主机和对应网段的变化,通过 BGP 协议分发给网关。