在 底层原理篇 中带大家从零开始搭建了一套容器网络(如下图所示),希望大家不要误以为容器网络就这点内容,真实的情况是这里面的水深得很!所以,请耐着性子,且听我娓娓道来。
当遇到容器连不通“外网”的时候,我们根据上图所示,可以这么做:
docker0
网桥能不能ping通docker0
和 Veth Pair
设备(如何判断成对的veth设备请出门右转看 这里)相关的 iptables
规则是不是有异常多说一嘴,建议大家好好学学iptables,不难的。
接下来我们一起深入聊聊“跨主机通信”这个问题。
上面那张图中,同一个宿主机上有两个容器 Container1 和 Container2,这俩都连接docker0,也就是说,同一个宿主机上的不同容器可以通过docker0网桥进行通信。
如果对数据包的传输过程感兴趣,可以打开iptables的TRACE功能:
# 在宿主机上执行
iptables -t raw -A OUTPUT -p icmp -j TRACE
iptables -t raw -A PREROUTING -p icmp -j TRACE
然后就可以在 /var/log/syslog
中看到数据包传输的日志了。
如果容器在不同的宿主机上呢?默认情况下,不同宿主机上的docker0网桥是不连通的。既然docker0不连通,那么我们就简单粗暴点,让其连通不就可以了吗,于是诞生了整个集群公共的网桥,如下图所示:
这就是 Overlay Network(覆盖网络),构建这种容器网络的核心在于:在已有的宿主机网络上,再通过软件构建一个覆盖在已有宿主机网络之上的、可以把所有容器连通在一起的虚拟网络。
甚至每台宿主机上都不需要有一个这种特殊的网桥,而仅仅通过某种方式配置宿主机的路由表,就能够把数据包转发到正确的宿主机上。
为了解决这个容器跨主机通信的问题,社区里出现了很多容器网络方案。为了讲清楚跨主机通信的原理,我们先从最具有代表性的Flannel项目说起。
Flannel项目是CoreOS公司主推的容器网络方案,有3种代表性的实现:
相比于两台宿主机之间的直接通信,基于 Flannel UDP 模式的容器通信多了一个额外的步骤,即 flanneld
的处理过程。而这个过程使用到了 flannel0
这个TUN设备,仅在发出IP包的过程中,就需要经过三次用户态与内核态之间的数据拷贝,如下图所示:
这里重点说明两个关键概念:
100.96.1.0/24
,container-1 的IP地址是 100.96.1.2
,属于这个子网简单来说,Flannel UDP 模式提供的是一个三层的Overlay网络,即:
使用Flannel在不同宿主机上的两个容器之间打通了一条“隧道”,使得这两个容器可以直接使用IP地址进行通信,而无需关心容器和宿主机的分布情况。但是这种模式有严重的性能问题,所以已经被废弃了。
VXLAN,即 Virtual Extensible LAN(虚拟可扩展局域网),是Linux内核支持的一种网络虚似化技术。
VXLAN的覆盖网络的设计思想是:在现有的三层网络之上,“覆盖”一层虚拟的、由内核VXLAN模块负责维护的二层网络,使得连接在这个VXLAN二层网络上的“主机”(虚拟机或者容器都可以)之间,可以像在同一个局域网里那样自由通信。
VXLAN具体实现方式为:将虚拟网络的数据帧添加VXLAN首部后,封装在物理网络中的UDP报文中,然后以传统网络的通信方式传送该UDP报文,到达目的主机后,去掉物理网络报文的头部信息以及VXLAN首部,将报文交付给目的终端。如下图所示:
为了能够在二层网络上打通“隧道”,上图中用到了一个特殊的网络设备 VTEP
,即:VXLAN Tunnel End Point(虚拟隧道端点)。
VTEP设备的作用其实跟前面的flanneld进程非常相似,只不过它进行封装和解封装的对象是二层数据帧(Ethernet frame),而且这个工作的执行流程,全部是在内核里完成的。
简单来说,VXLAN可以完全在内核态实现上述封装和解封装的工作,从而实现与UDP模式类似的“隧道”机制,构建出覆盖网络(Overlay Network)。
host-gw模式的工作原理是将每个Flannel子网的“下一跳”设置成该子网对应的宿主机的IP地址,在通信过程中就减少了封包和解包的性能损耗。
如上图所示,Node1上的Infra-container-1要访问Node2上的Infra-container-2,过程如下:
10.168.0.3
10.244.1.0/24
对应的路由规则)进入 cni0
网桥host-gw模式相比上面提到的两种模式有性能优势,不过需要注意的是:
Calico项目提供的网络解决方案,与Flannel的host-gw模式几乎是完全一样的,主要有两点不同:
BGP的全称是 Border Gateway Protocol,即边界网关协议。它是一个Linux内核原生就支持的、专门用在大规模数据中心里维护不同的“自治系统”之间路由信息的、无中心的路由协议。
简单来说,BGP就是在大规模网络中实现节点路由信息共享的一种协议。
在使用了BGP之后,我们可以简单地想象一下,在每个边界网关上都会运行着一个小程序,它们会将各自的路由表信息,通过TCP传输给其他的边界网关。而其他边界网关上的这个小程序,则会对收到的这些数据进行分析,然后将需要的信息添加到自己的路由表里。
Calico项目的架构主要由三个部分组成:
FDB
转发信息库,以及维护Calico所需的网络设备等工作需要注意的是,Calico默认使用 Node-to-Node Mesh
模式,在这种模式下,每台宿主机上的 BGP Client
都需要跟其他所有节点的 BGP Client
进行通信以便交换路由信息。
也就是说,随着节点数量N的增加,这些连接的数量就会以N的平方规模快速增长,因此会给集群本身的网络带来巨大的压力。
所以,Node-to-Node Mesh
模式一般推荐用在少于100个节点的集群里。而在更大规模的集群中,需要用到 Route Reflector
模式,简单来说,Calico会指定一个或者几个专门的节点(即中间代理),来负责跟所有节点建立BGP连接从而学习到全局的路由规则。
实际上,Calico也要求集群宿主机之间是二层连通的,为了解决这一问题,引入了IPIP模式,其通信的原理如下图所示:
在IPIP模式下,Felix进程会在节点上添加如下路由规则:
上面的 tunl0
设备不同于 Flannel UDP 模式使用的TUN设备,其本质是一个IP隧道(IP tunnel)设备。IP包进入IP隧道设备之后,就会被Linux内核的IPIP驱动接管,IPIP驱动会将原IP包直接封装在一个宿主机网络的IP包中,作为新IP包的 Payload
。
拿上面的图来举例,从Node1上的Container1发往Node2的Container3的IP包,过程如下:
看完上面介绍的容器跨主机网络通信方案,我们再来说说容器网络插件。简单概括一下,网络插件在宿主机上会创建一个特殊的设备,然后通过某种方法,把不同宿主机上的特殊设备连通,从而达到容器跨主机通信的目的。这其实就是Kubernetes对容器网络的主要处理方法。
只不过,Kubernetes是通过一个叫作 CNI
的接口,维护了一个单独的 cni0
网桥来代替 docker0
。
在部署Kubernetes的时候,有一个步骤是安装 kubernetes-cni
包,它的目的就是在宿主机上安装CNI插件所需的基础可执行文件,这些可执行文件放在 /opt/cni/bin
目录下:
要实现一个Kubernetes能用的容器网络方案,需要做两部分工作,以Flannel项目为例:
flannel.1
设备、配置宿主机路由、配置ARP和FDB表里的信息等等Infra
容器里面的网络栈,并把它连接在CNI网桥上另外,需要注意以下两点:
CRI
(Container Runtime Interface,容器运行时接口)实现里完成的,比如当底层使用的是Docker,对应的CRI实现叫作 dockershim
Hairpin Mode
才可以
docker run -p 8080:80
,然后在容器里面访问宿主机的8080端口在Kubernetes里,对网络隔离能力的定义是依靠一种专门的API对象来描述的,即NetworkPolicy。
在具体实现上,凡是支持NetworkPolicy的CNI网络插件,都维护着一个 NetworkPolicy Controller
,通过控制循环的方式对NetworkPolicy对象的增删改查做出响应,然后在宿主机上完成iptables规则的配置工作。
为了方便大家理解,可以把NetworkPolicy定义的规则当作“白名单”,涉及到3个关键字段 podSelector
、namespaceSelector
和 ipBlock
,具体配置看 官方文档。
在CNI网络插件中,网络隔离主要通过设置下面两组iptables规则来实现:
Kubernetes之所以需要Service,一方面是因为Pod的IP不是固定的,另一方面则是因为一组Pod实例之间总会有负载均衡的需求。
一般情况下,Service是由 kube-proxy
组件加上iptables来共同实现的。但是,过多的iptables规则,制约着Kubernetes项目承载更多的Pod。
针对这个问题,一个行之有效的方法是使用IPVS模式的Service。IPVS模式的工作原理如下:
kube-ipvs0
,并为它分配 Service VIP 作为IP地址ipvsadm
查看到这个设置,如下图所示:需要注意的是,IPVS模块只负责上述的负载均衡和代理功能,而一个完整的Service流程正常工作所需要的包过滤、SNAT等操作,还是要靠iptables来实现,只不过这些辅助性的iptables规则数量有限。
最后我们一起看看与Service相关的问题:
kubernetes.default
返回的值都有问题,就需要检查 kube-dns
的运行状态和日志KUBE-SERVICES
或者 KUBE-NODEPORTS
规则对应的Service的入口链,这个规则应该与VIP和Service端口一一对应KUBE-SEP-(hash)
规则对应的DNAT链,这些规则应该与Endpoints 一一对应KUBE-SVC-(hash)
规则对应的负载均衡链,这些规则的数目应该与Endpoints数目一致NodePort
模式,还有POSTROUTING处的SNAT链要检查hairpin-mode
设置为 hairpin-veth
或者 promiscuous-bridge
即可Ingress实际上是Kubernetes对“反向代理”的抽象。
Kubernetes提出Ingress概念的原因其实也非常容易理解,有了Ingress这个抽象,用户就可以根据自己的需求来自由选择 Ingress Controller
,如果没有合适的还可以实现一个自定义的。
更多关于Ingress的内容请看 官方文档。
最后附上一张DHCP流程图,希望骚年的你能够体会到里面的精髓: