你是否之前看过 k8s 的网络部分,第一次看是否会觉得很困难?或者说你有没有想过为什么 k8s 要这样设计它的网络,跨主机之间的网络通信究竟是怎么实现的?今天就来搞一篇干货,其实想写这个很久了,但是一直拖延症,这次正好碰到了一个新的点想让我仔细重新审视一下。

本文可能需要你有以下知识基础:

  • docker基本原理
  • k8s基本架构
  • 网络基础知识

本文不想引出过多细节的概念,因为网络本身确实有很多细节,每一个细节其实都可以写一篇,如果篇幅过长就会让人觉得没有重点,于是本文的重点将会放在从外部的大视角来看跨主机的网络通信,其中的细节先挖坑,后面慢慢填。

引子问题

我们知道 k8s 往往会有很多主机进行集群的部署,k8s 要管理很多 pod,而这些 pod 里面有很多容器,每个容器都是一个小的服务,服务与服务之间往往需要互相访问,而 pod 并不总是在同一宿主机上,那么问题来了:k8s 是如何做到让服务之间能够互相访问的呢?这里网络的链路到底是怎么走的?

image-20210820002120656

时刻记住,本文将围绕这个问题展开。

假设和思考

如果说每个容器都绑定一个宿主机的端口来进行通信,那么一旦容器很多就要占用非常多的宿主机的端口,这样肯定不合适。看来要解决这个大问题,我们要一步步来,先拆分问题然后一步步思考。

问题 1:如何解决同一台物理机上两个容器之间的通信问题

先把问题的规模变小,想想同一台物理机上的两个容器之间应该是如何通信的。我们可以把两个容器看做是两个实际的主机,那么两台主机要通信,如果直接两台主机之间直接连网线可以吗?显然不行,中间至少要个路由器或者交换机对吧,所以容器也是一样的。

那么我现在就需要两个设备,“网线”、“交换机/路由器”

Veth Pair

首先认识 Veth Pair 设备,veth 是虚拟一台网卡(Virtual Ethernet) 的缩写,它总是成对出现,就如同我们生活中的网线一样连接着两边的设备。它经常就用作跨 namespace 通信(这里的 namespace 不是 k8s 的 namespace,而是 linux 的 network namespace)

docker0

我们知道 Linux 中有 bridge 也就说我们常说的网桥,它的作用就好像生活中的交换机。任意的设备都可以连接上去,而且有多个端口,数据可以从任意端口进来,然后根据 mac 地址到对应的端口出去。而 docker0 就是这样一个网桥

image-20210820002204449

问题 2:如何解决容器访问外网

第一个问题解决了,这个问题就不难了。因为我们正常在一个主机上想要访问外网只需要通过网卡,也就常见的 eth0 这样的。所以在容器访问的时候也是一样:

image-20210820002243425

问题解决了吗?

那么理论上来说只要能访问外网就能访问别的宿主机,但是访问别的宿主机里面的容器呢?

我们来看看路能不能通

image-20210820002313977

其实问题已经很明显了,问题就在于外部的设备并没有办法知道其他宿主机的容器的 ip 和对应 mac 地址的配置。我要访问 172.16.2.101 这个容器,但是外部的交换机只认宿主机的 ip,我不知道这个 ip 是对应的那个一个,所以没有办法帮你路由到对应的宿主机上,那怎么办?

这时就引出了我们今天的主角 Overlay Network 网络

Overlay Network

我们称底层的物理网络为 underlay,然后如果我们通过某种手段在这个网络之上再叠加一层网络,那么我们就可以称叠加在这之上的是 overlay network。

那么我们如何理解这里的叠加呢?

我们知道网络中传输的是打包好的一个个网络包,数据通过不同层的协议封装之后得到。而 overlay network 其实简单理解就是在原有的协议包装之上,再包装了一次,而这些额外的包装信息就可以让发送端和接收端认识彼此。

如果你不理解,可以先记下,等下面看完之后回过来再想想。

Flannel 的实现

Flannel 项目是 CoreOS 公司主推的容器网络方案,它有好多的实现方式,为了解决的问题就是跨主机网络的问题。它将是我们入手的第一件兵器。

UDP 模式

首先我们来看看 UDP 模式的 Flannel 是如何实现的。这里我们引入一个 flannel0 设备,它是一个 TUN 设备,工作在三层的虚拟网络设备。它的功能:在操作系统内核和用户应用程序之间传递 IP 包。

然后让我们直接上图:

image-20210820004421597

如图所示,IP 包走的路线如下:

  1. 从容器 1 开始发送
  2. 然后通过 veth pair 走到了 docker0 网桥
  3. 然后 docker0 网桥发送给了 flannel0 设备(flannel 在宿主机配置了对应的路由规则)
  4. flannel0 就会将 IP 包交给 flanneld 进程(这里原来是内核态的网络调用栈上,然后切换到了用户态的 Flannel 进程)
  5. 然后 flanneld 进程通过 eth0 网卡发送到对面的宿主机(这里的路由规则是根据在 etcd 中保存的子网和宿主机的关系)同样的这里这次发送是从用户态切换到了内核态
  6. Node2 上的 flanneld 进程监听 8285 端口,收到之后进行拆包,然后依次丢给下面的 flannel0
  7. ….

所以其实它的关键就是通过 flannel 作为一个包装器和拆包器,发送的时候进行包装,然后在对面进行拆包,并且 flannel 维护了子网和宿主机的关系,其实它又扮演了一层路由器的作用,它知道哪个子网对应哪个宿主机,所以就能正确的进行发送

它的问题

image-20210820004443496

从这个图上就很明显:发送时,一个 IP 包必须先从内核态切换到用户态,然后再从用户态切换到内核态,这很显然导致就导致了性能问题,于是就提出了 VXLAN 模式。

VXLAN 模式

VXLAN 全称是 Virtual eXtensible Local Area Network,虚拟可扩展的局域网。它是一种 overlay 技术,通过三层的网络来搭建虚拟的二层网络。

A framework for overlaying virtualized layer 2 networks over lay 3 networks.

下面说说其中几个重要概念

  • VTEP(VXLAN Tunnel Endpoints):vxlan 网络的边缘设备,用来进行 vxlan 报文的处理(封包和解包)。vtep 可以是网络设备(比如交换机),也可以是一台机器(比如虚拟化集群中的宿主机)
  • VNI(VXLAN Network Identifier):VNI 是每个 vxlan 的标识,是个 24 位整数,一共有 2^24 = 16,777,216(一千多万),一般每个 VNI 对应一个租户,也就是说使用 vxlan 搭建的公有云可以理论上可以支撑千万级别的租户
  • Tunnel:隧道是一个逻辑上的概念,在 vxlan 模型中并没有具体的物理实体想对应。隧道可以看做是一种虚拟通道,vxlan 通信双方(图中的虚拟机)认为自己是在直接通信,并不知道底层网络的存在。从整体来说,每个 vxlan 网络像是为通信的虚拟机搭建了一个单独的通信通道,也就是隧道

有了 UDP 的认识,其实它就很好理解了,VTEP 的作用就和 flanneld 的作用类似,也是进行 IP 包的封装和解封,但是因为 VXLAN 就是内核中的一个模块,所以省掉了内核态到用户态相互切换的过程

但是相对应的问题也就来了,既然是内核中的一个模块,我怎么知道对面我需要访问的 VTEP 设备的 MAC 地址是什么呢?那就是 ARP 开始表现的时候了。

在每台节点启动时把它的 VTEP 设备对应的 ARP 记录,直接下放到其他每台宿主机上。

但是这样还不够,我就算知道了 MAC 地址,我不知道宿主机 IP 也是没有用的。所以还是得依赖 flanneld,需要从它的 FDB 转发数据库中找到对应的 mac 地址对应的宿主机 IP。

image-20210820005306857

这里的路径和之前类似我就不多说了

如何包装

然后我们来看看包装的 IP 包大概长什么样子,其实没有想象中的那么复杂。最外面还是目标主机的 IP 和 MAC 地址,因为这个包还是在网络中传输的,所以这个部分肯定会包装上,然后就是 UDP 的头部和 VXLAN 的头部信息了,最后不能忘记我们还需要目的的 VTEP 的 mac 地址和最终我们需要访问的容器 IP 地址

image-20210820005229969

总结

最后,我们之前说的都是 docker 下的情况,那么在 k8s 中呢?其实就是将 docker0 网桥换成了 cni 网桥而已,默认叫 cni0,这下是不是所有的都能串起来了。下面总结一下几个要点:

  1. 容器之间跨主机的通信的主要难点在于我不知道你在哪
  2. 通过协议的封装就可以实现 Overlay 的网络
  3. 网络协议的本质就是封装

当然对于 k8s 要解决的网络问题当然还不止这些,当前我们只是解决了通不通的问题;当然还有为什么 k8s 需要设计 CNI,为什么不用 docker 那一套就可以了?后面的问题挖个坑,有空了我继续填。