容器跨主通信实现场景简述

以上是单机容器的实现原理,现在思考一个问题:如果有多个机器,每个机器上都部署了容器应用,这些机器上的容器应用需要进行通信,这样的通信场景需求就是K8S集群,集群中的工作节点上我们通常会跑一些容器,这些容器之间如何进行相互的通信呢?其实只需要将这些节点上虚拟出一个设备,作为公用的网桥,把集群里的所有容器都连接到这个网桥上,就可以相互通信了

这样,我们整个集群里的容器网络就会类似于下图所示

容器间通信 容器间跨宿主通信_运维

这种需要在已有的宿主机网络上,再通过软件构建一个覆盖在已有宿主机网络之上的、可以把所有容器连通在一起的虚拟网络。所以,这种技术就被称为:Overlay Network(覆盖网络)

这个 Overlay Network 本身,可以由每台宿主机上的一个“特殊网桥”共同组成。比如,当 Node X 上的 Container1 要访问 Node Y 上的 Container 3 的时候,Node X 上的“特殊网桥”在收到数据包之后,能够通过某种方式,把数据包发送到正确的宿主机,比如 Node Y 上。而 Node Y 上的“特殊网桥”在收到数据包后,也能够通过某种方式,把数据包转发给正确的容器,比如 Container 3。甚至,每台宿主机上,都不需要有一个这种特殊的网桥,而仅仅通过某种方式配置宿主机的路由表,就能够把数据包转发到正确的宿主机上。

为了解决这个容器“跨主通信”的问题,社区里出现了很多的容器网络方案

Flannel 项目是 CoreOS 公司主推的容器网络方案。事实上,Flannel 项目本身只是一个框架,真正为我们提供容器网络功能的,是 Flannel 的后端实现。目前,Flannel 支持三种后端实现,分别是:

1. VXLAN;2. host-gw;3. UDP。这三种不同的后端实现,正代表了三种容器跨主网络的主流实现方法。

UDP

先来说flannel的UDP模式,这个模式是最早采用的一个模式,但是也存在一些弊端,现在说一下UDP模式下跨主通信的过程,就会知道这个过程存在什么问题。

在flannel插件被安装之后,Flannel 就会在宿主机上创建出了一系列的路由规则,通过在宿主机上使用ip route命令,可以查看到路由规则,路由表的路由来源分为三种类型:

  1. 直连路由(直接连接到路由器的网络)
  2. 静态路由(手工构建路由表,又称最后一跳求助对象,八个0表示匹配所有。不管去往哪个目的地址,只要路由表中没有,就将包丢给默认路由)
  3. 动态路由(路由器之间动态学习到的路由表)

为了方便叙述和理解,给出一个实验环境:

宿主机 Node X 上有一个容器 container1,IP : 100.96.1.2,对应的 docker0 IP:100.96.1.1/24。

宿主机 Node Y 上有一个容器 container2,IP: 100.96.2.3,对应的 docker0 IP:100.96.2.1/24。

现在考虑container1与container2通信

如下所示,Node X路由表给出了一些路由规则,第一条是默认路由,第二条是对于源ip为100.96.1.0,目标IP为100.96.0.0/16的数据包,需要发往flannel0进行进行处理,第三条第四条不再赘述,因此container1发往container2的数据包会先发给flannel0进行处理。

$ ip route 
default via 10.168.0.1 dev eth0 
100.96.0.0/16 dev flannel0 proto kernel scope link src 100.96.1.0 
100.96.1.0/24 dev docker0 proto kernel scope link src 100.96.1.1 
10.168.0.0/24 dev eth0 proto kernel scope link src 10.168.0.2

flannel包括flannel0设备和flanneld进程,flannel0设备是一个TUN设备,flanneld监控来到flannel0设备上的数据包,根据IP 地址对应的节点 进行数据包的传输。

那么flanneld是如何找到IP地址对应的是哪一个节点上的容器呢?(比如这里的目标容器100.96.2.3,flanneld如何知道是哪一个节点的容器呢

同一台宿主机上的所有容器会被分配在一个子网内,而子网与节点的对应关系的信息被保存在Etcd中,可以通过这个命令查看

$ etcdctl ls /coreos.com/network/subnets 
/coreos.com/network/subnets/100.96.1.0-24 
/coreos.com/network/subnets/100.96.2.0-24 
/coreos.com/network/subnets/100.96.3.0-24

所以,flanneld 进程在处理由 flannel0 传入的 IP 包时,就可以根据目的 IP 的地址(比如100.96.2.3),匹配到对应的子网(比如 100.96.2.0/24),从 Etcd 中找到这个子网对应的宿主机的 IP 地址是 10.168.0.3

因此,对于flanneld进程来说,只需要关心节点间的通信即可,只要 Node X 和 Node Y 是互通的,那么 flanneld 作为 Node X 上的一个普通进程,就一定可以通过上述 IP 地址(10.168.0.3)访问到 Node Y,但需注意,每台宿主机上的 flanneld,都监听着一个 8285 端口,所以 flanneld 只要把数据包发往 Node Y 的 8285 端口即可。

补充:

在 Linux 中,TUN 设备是一种工作在三层(Network Layer)的虚拟网络设备。TUN 设备的功能非常简单,即:在操作系统内核和用户应用程序之间传递 IP 包。当操作系统将一个 IP 包发送给 flannel0 设备之后,flannel0 就会把这个IP 包,交给创建这个设备的应用程序,也就是 Flannel 进程。这是一个从内核态(Linux 操作系统)向用户态(Flannel 进程)的流动方向。反之,如果 Flannel 进程向 flannel0 设备发送了一个 IP 包,那么这个 IP 包就会出现在宿主机网络栈中,然后根据宿主机的路由表进行下一步处理。这是一个从用户态向内核态的流动方向。

因此我们得到Flannel UDP 模式的跨主通信的基本过程可以如下图所示,绿色线条表示NodeX上的Container1数据包与NodeY上Container1数据包进行通信的数据流。

容器间通信 容器间跨宿主通信_网络_02

Flannel UDP 模式提供的其实是一个三层的 Overlay 网络,即:它首先对发出端的 IP 包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容器。

同时,由于flanneld工作在用户空间,节点网卡eth0和虚拟网桥设备docker0工作在内核空间,因此数据包需要经过三次上下文切换的拷贝, 数据包传输过程具体如图示红色线条:

容器间通信 容器间跨宿主通信_容器间通信_03

因此通过上面的分析,我们可以很容易的知道,UDP模式下的缺点是多次上下文切换会造成数据包进行多次拷贝,并且数据包封装(Encapsulation)和解封装(Decapsulation)的过程,也都是在用户态完成的(flanneld完成),这些上下文切换和用户态操作的代价其实是比较高的。