趣谈网络协议(七:容器网络)

  • 2020 年 4 月 19 日
  • AI

一,容器网络简介

虚拟机隔离性好,并且解决了基础资源层的弹性伸缩问题,但却没有解决当虚拟机批量伸缩时,可能带来的PaaS层应用快速部署的问题。于是,容器应运而生。它的目标就是要变成软件交付的集装箱,它的特点是“打包”和“标准”。即,将应用环境和软件,以一套完备的标准,按镜像的方式封装在容器里,可以自由完整的搬运部署。

容器通过2种技术实现隔离,namespace和cgroups。前者负责网络环境,用户空间,进程号等环境的隔离,让用户“看起来”是完全不同的环境。后者负责CPU,内存等资源上的隔离,让用户“用起来”是完全独立的资源。

1,namespace

在Linux环境下,很多资源都是全局的,比如进程是全局的ID,网络是全局的路由表。但如果我们希望机器上的某些进程使用独立的路由政策,就可以将它们放在一个单独的namespace里面,使用ip netns命令独立配置网络环境。

2,cgroups

全称control groups,是Linux内核提供的一种可以限制,隔离进程使用资源的机制。它有很多子系统,包括CPU,cpuset,memory,blkio和net_cls等。

具体而言,cgroups使用一个虚拟文件系统来做到这一点,这个虚拟文件系统是分组管理和子系统设置的用户接口,要使用cgroups的功能,必须挂载cgroups文件系统,位于/sys/fs/cgroup下。

3,容器网络融入物理网络

容器网络和虚拟机网络类似,容器内有一张网卡,容器外一张网卡veth,容器外的网卡和网桥docker0相连,通过这个网桥,容器间可以互通。其中,docker0和虚拟机一节中用 brctl创建的网桥是一样的效果。在虚拟机环境下,可以通过TUN/TAP设备虚拟内外网卡给虚拟机,但是容器场景下并没有这种功能。所以,容器环境使用的是veth pair技术,从一边发送包,另一边就能收到。

一台物理机内的容器互通问题解决了,那容器如何访问外网呢?

同样,可以使用类似虚拟机一节中的桥接模式或NAT模式,Docker默认使用NAT模式,如果容器内部需要访问外部网络,就需要通过Snat。即,所有从容器发出的包,都要将源私网地址转换为物理网卡的IP地址,它们共享一个出口IP地址,但是在conntrack表中,可以记录下这个转换的连接,反之亦然。如果容器内部一个应用作为服务端,提供给外部访问,还需要通过Docker的端口映射,将容器内部的端口映射到物理机上,再通过contrack表中的规则,将目标IP Dnat成容器私网的IP地址。

有2种方式来具体实现:其一,开启docker-proxy进程监听物理机端口,转换为容器80端口;其二,通过Dnat,在PREROUTING阶段新增规则,将到物理机10080端口的流量Dnat到容器私有网络。

二,容器网络之Flannel

在微服务架构中,Nat在多物理主机的场景下是有问题的。架构中会存在一个注册中心,实时接收容器内服务的注册,但每台物理机中,容器网络的网段默认都是172.17.0.0/16,那么拿着容器网络内网IP向注册中心注册时,明明是2台物理机上不同的容器服务,但因为IP和端口一致,在注册中心内部会当做是一个应用。为了解决这个问题,有一种方法是容器内的服务向注册中心注册物理机IP+物理机映射端口,但容器的目标之一就是隔离,让容器能感知到外部物理机环境,本身就是非常不好的设计。

Kubernetes作为容器调度领域的事实标准,对这个问题,提出了指导性意见:将容器网络 模型打平。在这个意见下,业界涌现出大量的实现方案,Flannel就是其中之一。

默认环境下,物理机会为docker容器分配172.17.0.0/16网段,如果所有物理机上的所有docker容器都不由分说的从头共享这个网段,当然会冲突。但这个网段非常大,每台物理机没必要都从头开始分配IP地址,可以设置每个物理机都从其中抠出一小段,排他性的独占这一段区间组建自己内部的容器网络,就不会发生容器内IP地址冲突的问题了。

接下来的问题,物理机A上的容器如何访问到物理机B上的容器呢

回忆虚拟机跨物理机互通,使用Overlay方式,虚拟机内虚拟网卡全部连到虚拟网关br0,虚拟网关再同物理网卡连接,向外网发包,这一切都是通过qemu-kvm打开一个TUN/TAP字符设备来实现的。对docker而言,没法虚拟化,容器内外的网卡通过veth pair实现,物理机上会打开docker0,作用与br0一样,所有容器与docker0连接,实现相互通信,并且每个物理机还会运行一个flannel进程,打开/dev/net/tun字符设备,模拟flannel.1网卡,功能与qemu-kvm如出一辙。

但具体实现上,物理机A上的flanel进程会将网络包读进来处理后,封装在UDP包里,再在外面包上物理机A和物理机B的IP地址,发送给物理机上的flannel进程。因为容器内的服务间已经建立TCP连接的话,flannel进程就没必要再建立连接了。

这样,在物理机容器私网IP地址分段独占+flannel进程中转的前提下,跨物理机的连通性没问题,只不过因为都在用户态,所以性能稍微差了一些。

为了提升效率,容器网络同样可以使用虚拟机网络中现成的解决方案VXLAN,它也被flannel支持。flannel通过netlink这样一个用户态和内核态的通信机制,通知内核建立一个运行VTEP的网卡flannel.1,由这张网卡接收跨物理机的网络包后,交由VTEP封装和解封装隧道协议和承载协议,通过隧道的方式,实现flannel进程相同的功能。

三,容器网络之Calico

在物理机容器私网IP地址分段独占的前提下,因为每台物理机上内容器网络的网段都各不相同,也为路由策略的实现方案提供了可能。假设,两台物理机在同一个二层网络里面,但内部容器的网段不同时,我们完全可以将两台物理机配置成路由器,并按照容器内的网段配置路由表。

这也是Calico网络的大致思路,不走Overlay(隧道VXLAN),不引入新的网络性能损耗(网桥docker0),而是将转发全部用三层的路由转发来实现,只是实现略有不同。

1,转发细节

Calico全部使用3层路由规则的话,就没必要再每台物理机上在新增网桥docker0,可以直接根据路由规则将网络包转发到veth pair在物理机这一端的网卡。同样,容器内的路由规则将外面的veth pair设置为默认网关,第一跳是外部veth的MAC地址,下一跳是物理机。物理机间正常用交换机连接。注意,容器内外的veth pair均是单点局域网。

2,拓扑架构

(1),路由配置组件Felix

在容器环境中动态的扩缩容器是家常便饭,如果所有的容器创建或删除都要手工维护路由表也不现实。在Calico中有自动配置所有容器内外路由表的agent,称为Felix。

(2),路由广播组件BGP Speaker

Felix完成自动配置本机的路由信息后,BGP Speaker将路由信息,比如“如何到达本台物理机,如何访问机器内的容器”等信息,以BGP路由协议的形式广播出去。在Calico中,每个Node上运行一个BGP Speaker,所有机器上的Speaker全互联。

(3),安全策略组件

Calico中也是通过iptables在内核对网络包的嵌入点预埋处理函数,实现网络策略的灵活配置,匹配处理规则,判断接收,转发或丢弃。

3,全连接复杂性与规模问题

BGP Speaker的全连接太复杂,于是在中间新增一个中间件BGP Router Reflector,所有的Speaker都直连Reflector,把M*N的连接变成M+N。当然,单点的Reflector肯定不行,需要多个Reflector,每个只负责一部分的Speaker连接。

在数据中心的场景下,可以将一个机架上的机器作为一个管理单元,交给一个Reflector负责,不同机架间的Reflector也直接使用路由交换。此时,一个机架作为一个AS,就像一个小规模的数据中心,Reflector就类似数据中心内的边界路由器。所以,服务器和Reflector间使用数据中心内部的iBGP协议,不同Reflector间使用数据中心之间的eBGP协议。

此时,拓扑结构为:一个机架上有多台机器,每个机器上有多个容器,每台机器都有到达容器的路由,且启动一个BGP Speaker,将这些路由规则将使用iBGP协议,上报到机架上方接入交换机里运行的BGP Route Reflector中。同时,接入交换机之间也运行BGP连接,互相通知路由,将不同机架间的网络连通。最后,核心或汇聚交换机再将接入交换机连接起来,交换路由形成更大的连通网络。

4,跨网段访问

Calico中还有一种IPIP模式,不同的物理机间用隧道协议联通。下一跳就不再是同一个网段下的物理机B,并且也不是从eth0跳,而是建立隧道端点tun0,不同的物理机在tun0上封装和解封装,最后再由eth0发出,逻辑上就好像直连一样。

四,参考文章

1,极客时间:趣谈网络协议 — 刘超:(云计算中的网络29-31讲)