在 Docker 的默认配置下,不同宿主机上的容器通过 IP 地址进行互相访问是根本做不到的。而正是为了解决这个容器“跨主通信”的问题,社区里才出现了那么多的容器网络方案。而且,相 信你一直以来都有这样的疑问:这些网络方案的工作原理到底是什么?Flannel 项目是 CoreOS 公司主推的容器网络方案。事实上,Flannel 项目本身只是一个框架,真 正为我们提供容器网络功能的,是 Flannel 的后端实现。目前,Flannel 支持三种后端实现,分别是:
udp模式下的通信,如上图所示,其中container-1与container-2的通信过程是这样的:
$ 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 设备发送了一个 IP 包,那么这个 IP 包就会出现在宿主机网络 栈中,然后根据宿主机的路由表进行下一步处理。这是一个从用户态向内核态的流动方向。所以,当 IP 包从容器经过 docker0 出现在宿主机,然后又根据路由表进入 flannel0 设备后,宿主 机上的flanneld 进程(Flannel 项目在每个宿主机上的主进程),就会收到这个 IP 包。
flannel0设备是一个TUN设备,有关tun设备的概念,可以参考这篇博客。Linux虚拟网络设备之tun/tap。看完以上概念,你就应该知道,tun/tap设备的用处是将协议栈中的部分数据包转发给用户空间的应用程序,给用户空间的程序一个处理数据包的机会。
$ etcdctl get /coreos.com/network/subnets/100.96.2.0-24
{"PublicIP":"10.168.0.3"}
flannel项目有子网这个概念,一台宿主机上的所有容器都在该宿主机上的某个子网中。而这些子网与宿主机的对应关系,正是保存在 Etcd 当中,如下所示:>flannel项目有子网这个概念,一台宿主机上的所有容器都在该宿主机上的某个子网中。而这些子网与宿主机的对应关系,正是保存在 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
# 在 Node 2 上
$ ip route
default via 10.168.0.1 dev eth0
100.96.0.0/16 dev flannel0 proto kernel scope link src
100.96.2.0 100.96.2.0/24 dev docker0 proto kernel scope link src 100.96.2.1
10.168.0.0/24 dev eth0 proto kernel scope link src 10.168.0.3
数据包的封装和拆解,依靠的是Flannel进程,而将数据包从内核转移到用户空间,则需要tun设备的支持,也就是flannel0设备。
UDP模式存在的问题就是巨大的资源浪费和消耗。
UDP 模式提供的其实是一个三层的 Overlay 网络,即:它首先对发出端的 IP 包进行 UDP 封装,然后在接收端进行解封装拿到原始的 IP 包,进而把这个 IP 包转发给目标容 器。这就好比,Flannel 在不同宿主机上的两个容器之间打通了一条“隧道”,使得这两个容器可 以直接使用 IP 地址进行通信,而无需关心容器和宿主机的分布情况。
UDP模式存在着严重的性能问题,其性能问题主要在于用户态和内核态之间的数据拷贝,如下图所示:
性能代价
第一次:用户态的容器进程发出的 IP 包经过 docker0 网桥进入内核态;
第二次:IP 包根据路由表进入 TUN(flannel0)设备,从而回到用户态的 flanneld 进程;
第三次:flanneld 进行 UDP 封包之后重新进入内核态,将 UDP 包通过宿主机的 eth0 发出去
此外,Flannel 进行 UDP 封装(Encapsulation)和解封装(Decapsulation) 的过程,也都是在用户态完成的。在 Linux 操作系统中,上述这些上下文切换和用户态操作的代价 其实是比较高的,这也正是造成 Flannel UDP 模式性能不好的主要原因。
名词解释
VXLAN:即 Virtual Extensible LAN(虚拟可扩展局域网),是 Linux 内核本身就支持的一种网络 虚似化技术。所以说,VXLAN 可以完全在内核态实现上述封装和解封装的工作,从而通过与前面相似的“隧道”机制,构建出覆盖网络(Overlay Network)。VXLAN 的覆盖网络的设计思想是:在现有的三层网络之上,“覆盖”一层虚拟的、由内核 VXLAN 模块负责维护的二层网络,使得连接在这个 VXLAN 二层网络上的“主机”(虚拟机或者容器都可 以)之间,可以像在同一个局域网(LAN)里那样自由通信。当然,实际上,这些“主机”可能分布在不同的宿主机上,甚至是分布在不同的物理机房里。
VTEP:为了能够在二层网络上打通“隧道”,VXLAN 会在宿主机上设置一个特殊的网络设备作为“隧道”的两端。这个设备就叫作 VTEP,即:VXLAN Tunnel End Point(虚拟隧道端点)。flannel.1就是VTEP设备。而 VTEP 设备的作用,其实跟前面的 flanneld 进程非常相似。只不过,它进行封装和解封装的对象,是二层数据帧(Ethernet frame);而且这个工作的执行流程,全部是在内核里完成的(因为VXLAN 本身就是 Linux 内核中的一个模块)。
在VXLAN模式下,容器之间通信的过程如上图所示:
# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
...
10.244.0.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0
10.244.1.0 10.244.1.0 255.255.255.0 UG 0 0 0 flannel.1
10.244.2.0 10.244.2.0 255.255.255.0 UG 0 0 0 flannel.1
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
# ip neigh show dev flannel.1
10.244.1.0 lladdr 42:40:c7:4c:be:19 PERMANENT
10.244.2.0 lladdr 76:c5:dd:8c:d9:a9 PERMANENT
通过以上步骤得到的MAC帧,并不能够直接在网络中传输,因此需要把内部数据帧进一步封装成为宿主机网络里的普通数据帧,通过宿主机网络进行传输。要封装出来的、宿主机对应的数据帧称为“外部数据帧”(Outer Ethernet Frame)。
封装VXLAN头部。Linux 内核会在“内部数据帧”前面,加上一个特殊的 VXLAN 头,用来表示这个“乘客”实际上是一个 VXLAN 要使用的数据帧。而这个 VXLAN 头里有一个重要的标志叫作VNI,它是 VTEP 设备识别某个数据帧是不是应该归自 己处理的重要标识。而在 Flannel 中,VNI 的默认值是 1,这也是为何,宿主机上的 VTEP 设备都 叫作 flannel.1 的原因,这里的“1”,其实就是 VNI 的值。
封装UDP头部。flannel.1 设备实际上要扮演一个“网桥”的角色,在二层网络进行 UDP 包的转 发。而在 Linux 内核里面,“网桥”设备进行转发的依据,来自于一个叫作 FDB(Forwarding Database)的转发数据库。
不难想到,这个 flannel.1“网桥”对应的 FDB 信息,也是 flanneld 进程负责维护的。它的内容可以通过 bridge fdb 命令查看到,如下所示:
# bridge fdb show |grep 42:40:c7:4c:be:19
42:40:c7:4c:be:19 dev flannel.1 dst 10.1.18.108 self permanent
UDP 包是一个四层数据包,所以 Linux 内核会在它前面加上一个 IP 头,即原理图中的 Outer IP Header,组成一个 IP 包。并且,在这个 IP 头里,会填上前面通过 FDB 查询出来的目的 主机的 IP 地址,即 Node 2 的 IP 地址 10.1.18.108。
Linux 内核再在这个 IP 包前面加上二层数据帧头,即Outer Ethernet Header, 并把 Node 2 的 MAC 地址填进去。整个包结构如下所示。
Node 1 上的 flannel.1 设备就可以把这个数据帧从 Node 1 的 eth0 网卡发出去。显然, 这个帧会经过宿主机网络来到 Node 2 的 eth0 网卡。
Node 2 的内核网络栈会发现这个数据帧里有 VXLAN Header,并且 VNI=1。所以 Linux 内核会对它进行拆包,拿到里面的内部数据帧,然后根据 VNI 的值,把它交给 Node 2 上的 flannel.1 设备。
而 flannel.1 设备检查数据包的目的mac地址是否为自己的mac地址,若是,则会进一步拆包,取出“原始 IP 包”。该包的目的IP地址是(10.244.1.3),是一条直连路由,将通过路由表转发到cni0上。路由信息如下。
# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
...
10.244.1.0 0.0.0.0 255.255.255.0 U 0 0 0 cni0
10.244.2.0 10.244.2.0 255.255.255.0 UG 0 0 0 flannel.1
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
...
上述报文的封装和拆解工作,实际上是依靠内核机制,也就是VTEP设备实现的。
Flannel是跨节点容器网络方案之一,它提供的Overlay方案主要有两种方式,一种是UDP在用户态封装,一种是VXLAN在内核态封装,而VXLAN的性能更好一些。这两种模式都可以称作隧道机制。
背景知识:
首先,这里有一个网络常识知识,网关一定和局域网的主机在同一个网段嘛?
举个例子,日常我们所处的网络,比如我现在的ip为,10.1.18.11/24。网关为10.1.18.254。那么问题来了,是不是所有的网关都跟局域网的网络处在同一个网段?我觉得不是的。
网络通信过程:
要深入理解这个问题,首先要搞清楚网络通讯的原理,网络上通讯工作在物理层和数据链路层,源地址和目标地址是通过源和目的的mac地址进行通讯的。
当源主机访问目标主机时,首先看两者的IP在不在同一网段,结果是
1 两者在同一网段,就会直接把包发向目标IP,这时要做:
1.1 查本地arp缓存,看看是否有IP和Mac的对应表.
1.1.1 有,直接向网络上发包,包中包括原mac及目标mac 。
1.1.2 没有,则向网络发arp 广播,用来查找与目标IP对应的mac地址。
1.1.2.1 如果查到了,则向网络发包。
1.1.2.2 没查到,则不通讯。
2 两者不在同一网段,直接向网关发送数据包,然后查找本地arp缓存,继续1.1 。
由此可以看出,源主机和网关的通讯过程中,并不会检查两者是不是同一网段,而是直接去查arp缓存。所以是可能通讯的。
Flannel的host-gw模式就是借助于这一点实现的。Node1上的container-1访问Node2上的container-2其过程如下图所示:
$ ip route
...
10.244.1.0/24 via 10.168.0.3 dev eth0
其含义是,目的 IP 地址属于 10.244.1.0/24 网段的 IP 包,应该经过本机的 eth0 设备发出去(即:dev eth0);并且,它下一跳地址(next-hop)是 10.168.0.3(也就是目的宿主机Node2的ip地址)
以上就是host-gw模式的通信原理。其核心就是将每个 Flannel 子网(Flannel Subnet,比 如:10.244.1.0/24)的“下一跳”,设置成了该子网对应的宿主机的 IP 地址。也就是说,这台“主机”(Host)会充当这条容器通信路径里的“网关”(Gateway)。这也 正是“host-gw”的含义。
当然,Flannel 子网和主机的信息,都是保存在 Etcd 当中的。flanneld 只需要 WACTH 这些数 据的变化,然后实时更新路由表即可。
host-gw 模式能够正常工作的核心,就在于 IP 包在封 装成帧发送出去的时候,会使用路由表里的“下一跳”来设置目的 MAC 地址。这样,它就会经 过二层网络到达目的宿主机。这就要求集群宿主机之间必须要二层连通的。
修改flannel网络的配置文件两种方式:
1.直接编辑
[root@master flannel]# kubectl edit configmap kube-flannel-cfg -n kube-system
2.下载相关配置文件,然后修改:
[root@master manifests]# mkdir flannel
[root@master manifests]# cd flannel/
[root@master flannel]# wget https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
[root@master flannel]# vim kube-flannel.yml
结合上面这张图,我们来过一遍 Calico 的核心组件:
通过将整个互联网的可扩展 IP 网络原则压缩到数据中心级别,Calico 在每一个计算节点利用Linux kernel实现了一个高效的vRouter来负责数据转发而每个vRouter通过BGP协议负责把自己上运行的 workload 的路由信息像整个 Calico 网络内传播 - 小规模部署可以直接互联,大规模下可通过指定的BGP route reflector 来完成。 这样保证最终所有的 workload 之间的数据流量都是通过 IP 包的方式完成互联的。
calico中 caontainer1和container4通信过程:
default via 169.254.1.1 dev eth0
169.254.1.1 dev eth0 scope link
10.233.1.3 dev calib4963f3 scope link
10.233.2.0/24 via 192.168.1.3 dev eth0 proto bird onlink
对比:
calico与flannel的host-gw模式对比,最大的区别在于,flannel网络中的路由是通过flanneld进程监控etcd中的信息,然后不断添加的。而cailico中的路由,则是通过BGP路由协议自动学习的。而且,calico中并没有使用cni0网卡。
上文中的BGP模式,直接认定A和B是在同一个网段的,倘若A和B不在同一个网段,那么其通信方式将会有所区别。
容器A1访问容器B2,IPIP模式下的通信方式与上文大致一样,不通点在于数据包到达宿主机之后,其IP路由与之前的不太一样。路由表的内容如下所示:
172.17.9.0/24 via 192.168.200.101 dev tun0 proto bird onlink
下一跳地址变为了,192.18.200.101与主机A不在同一个网络,因此需要通过tun设备来构建隧道,来打通网络。这样原来就和上文所描述的flannel的udp模式有些类似。
Container Network Interface (CNI) 最早是由CoreOS发起的容器网络规范,是Kubernetes网络插件的基础。其基本思想为:Container Runtime在创建容器时,先创建好network namespace,然后调用CNI插件为这个netns配置网络,其后再启动容器内的进程。
CNI是Container Network Interface的是一个标准的,通用的接口。现在容器平台:docker,kubernetes,mesos,容器网络解决方案:flannel,calico,weave。只要提供一个标准的接口,就能为同样满足该协议的所有容器平台提供网络功能,而CNI正是这样的一个标准接口协议。
Kubernetes 是通过一个叫作 CNI 的接口,维护了一个单独的网桥来代替 docker0。这个网桥 的名字就叫作:CNI 网桥,它在宿主机上的设备名称默认是:cni0。
我们在部署 Kubernetes 的时候,有一个步骤是安装 kubernetes-cni 包,它的目的就是在宿主机上安装CNI 插件所需的基础可执行文件。所有主机上都有安装有这个文件。
在安装完成后,你可以在宿主机的 /opt/cni/bin 目录下看到它们,如下所示:
[root@master net.d]# ls /opt/cni/bin/
bridge dhcp flannel host-local ipvlan loopback macvlan portmap ptp sample tuning vlan
这些 CNI 的基础可执行文件,按照功能可以分为三类:
CNI配置文件目录:
[root@master net.d]# ls /etc/cni/net.d/
10-flannel.conflist
Main 插件,它是用来创建具体网络设备的二进制文件:
IPAM(IP Address Management)插件,它是负责分配 IP 地址的二进制文件:
meta:由 CNI 社区维护的内置 CNI 插件
从这些二进制文件中,我们可以看到,如果要实现一个给 Kubernetes 用的容器网络方案,其实需要做两部分工作,以 Flannel 项目为例: