学习了一段时间kubernetes,暂时觉得最让我困惑的就是网络,kubernetes各组件分工明确,可逐个学习。但网络却是无处不在的,在理解各组件的时候经常因为理不清各种通信是如何实现的而卡住。因此有必要理清楚kubernetes网络通信,首先是容器网络。
1 同宿主机内的容器通信
不同容器之间怎样通信?
容器将进程隔离在自身的Network Namespace 里,为了实现不同容器间的通信,在Docker中,默认在宿主机上创建一个名叫 docker0 的网桥,这是数据链路层的设备,可根据MAC地址将数据包转发到网桥的不同端口,凡是连接在 docker0 网桥上的容器,就可以通过它来进行通信。
容器如何连接到docker0网桥?
通过虚拟设备Veth Pair连接。Veth Pair 设备是以两张虚拟网卡(Veth Peer)的形式成对出现的。从其中一个“网卡”发出的数据包,可以直接出现在与它对应的另一张“网卡”上,即使这两个“网卡”在不同的 Network Namespace 里。
Docker会为容器创建一对Veth Pair,一端在容器上,另一端插在宿主机的docker0网桥。
一个例子
如下图,Container 1和Container 2为同一宿主机上的两个容器,其中Container 1的eth0网卡与插在docker0网桥上的veth9c02e56网卡是一对Veth Pair设备,Container 2的eth0网卡与插在docker0网桥上的2vethb4963f3网卡是一对Veth Pair设备。
当Container 1里的进程向IP地址为172.17.0.3的Container 2中的进程发送数据时,目的IP会匹配到如下第二条路由规则:
Destination Gateway Genmask Flags Metric Ref Use Ifacedefault 172.17.0.1 0.0.0.0 U G 0 0 0 eth0172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0
Gateway是0.0.0.0,说明是直连的网段,不需要走三层,直接走二层协议,即应该通过本机的eth0网卡直接发往目的主机。eth0网卡会发送ARP广播,通过目的IP地址查找对应的MAC地址,此时docker0会充当二层交换机角色,将ARP广播转发到插在其上的虚拟网卡,从而Container 2收到后会将MAC地址返回给Container 1。
有了这个MAC地址,Container 1的eth0就能将数据包发送出去,docker0网桥收到数据包后,通过其上的CAM表查找到该MAC地址对应的端口为vethb4963f3,而vethb4963f3的另一端就是Container 2的eth0网卡,于是就完成了一次跨Network Namespace的通信。
当Container 1中的进程试图与宿主机外部通信时,会匹配Container 1中的第一条default路由规则,将数据包发往docker0网桥,再由宿主机的直连路由规则转发到宿主机的eth0网卡,再由它发往外部。
2 跨宿主机容器通信
不同宿主机上的容器通过IP地址是不能相互访问到的,当容器要与不同宿主机上的容器通信时,需要在集群里设定一个互通的机制,Flannel 项目就是为了解决这个问题。Flannel支持三种方式:VXLAN、host-gw、UDP。
UDP模式
当访问另一台机器上的IP地址时,IP包会被发到docker0网桥,如果此时docker0网桥将IP包直接王eth0网卡传,那么显然eth0找不到目的IP地址对应的MAC地址,因为它是另一台机器上的一个容器的IP。
docker0网桥应该将IP包发往何处?
Flannel在宿主机上设置一个叫flannel0的TUN设备,负责在操作系统内核和用户进程间传递IP包。Flannel会维护一系列路由规则,将目的地址是集群内部容器的IP包路由到flannel0设备。而flannel0设备会将IP包发给运行在宿主机上的flanneld进程,flanneld进程负责“打通”不同容器直接的网络。
flanneld进程怎么知道这个IP地址对应的容器在哪台机器上?
Flannel 项目会为每台宿主机分配一个子网,该宿主机上所有容器都属于该子网,容器都在docker0网桥下,这就必须保证docker0网桥的地址范围必须是 Flannel 为宿主机分配的子网。将子网与具体宿主机的IP地址一一对应的信息保存在Etcd中。所以flanneld进程可以根据Etcd中的信息找到容器IP对应的宿主机IP。
同时,flanneld进程会监听本机的8285端口,因此得到目的宿主机IP后,只需要加上目的端口8285,即可把IP包封装成UDP包发送出去。目的宿主机上的flanneld进程监听到信息后,会将拆封出来的IP包根据路由规则转发给docker0,就能到达目的容器了。
Flannel 在不同宿主机上的两个容器之间打通了一条“隧道”,使得这两个容器可以直接使用 IP 地址进行通信,而无需关心容器和宿主机的分布情况。但是UDP方式由于性能太差,原因是内核态和用户态直接的转换过多,在发送时就有三次转换:
- 用户态的容器进程发出的 IP 包经过 docker0 网桥进入内核态
- IP 包根据路由表进入flannel0设备,进入用户态的 flanneld 进程
- flanneld 进行 UDP 封包之后重新进入内核态,将 UDP 包通过宿主机的 eth0 发出去。
因此,后来UDP方案被弃用,VXLAN模式成为主流方案。
VXLAN模式
VXLAN模式下,作为打通容器网络的“两端”的不是flannel0,而是VTEP(VXLAN Tunnel End Point),叫flannel.1,与UDP模式不同的是 flannel.1 设备组成的是虚拟二层网络,而不是三层。
很多资料把简单的逻辑说得很复杂,总结来说,其实就是flanneld维护了三项信息:
- 发往集群内容器网段的IP包必须经过flannel.1设备,并指定发往的目的网关,即目的VTEP设备的IP
- ARP记录:VTEP 设备的IP地址对应的MAC 地址
- VTEP设备的MAC地址对应的宿主机IP
当容器向外访问的原始IP包到达docker0后,根据flanneld维护的信息1,docker0将IP包发往flannel.1,同时得知需要发往的目的VTEP的IP地址。
flannel.1根据flanneld维护的信息2,获得VTEP的MAC地址,进而将数据封装成二层帧格式:
但这个MAC地址对于宿主机网络来说没有任何意义,宿主机网络需要知道的是目的宿主机的IP。于是,flannel.1根据flanneld维护的信息3,获得目的宿主机的IP地址,然后宿主机即可根据ARP规则获得目的宿主机的MAC地址,将IP包封装成UDP包,格式如下:
其中VXLAN Header里有一个重要的标志叫作 VNI,它是 VTEP 设备识别某个数据帧是不是应该归自己处理的标识。宿主机将UDP包发送出去,目的宿主机再反向层层解包,即可到达目的容器。
在发送数据时,与UDP模式相比,VXLAN只需要一次用户态到内核态的切换,效率会大大提升。
有个问题:
VTEP设备的MAC地址到主机的匹配规则没有使用Etcd保存,而是保存在Linux内核的FDB,那么集群每次有node加入时,都需要在其他所有node内核的FDB中添加一条规则吗?
