7.7 开源容器网络方案

Kubernetes的网络模型假定了所有Pod都在一个可以直接连通的扁平网络空间中。这在GCE里面是现成的网络模型,Kubernetes假定这个网络已经存在。而在私有云里搭建Kubernetes集群,就不能假定这种网络已经存在了。我们需要自己实现这个网络假设,将跨主机容器网络部署完成,再运行容器应用。

目前已经有多个开源组件支持容器网络模型。本节介绍几种使用不同技术实现的网络组件及其安装配置方法,包括Flannel、Open vSwitch、直接路由和Calico。

7.7.1 Flannel插件的原理和部署示例

Flannel之所以可以搭建Kubernetes依赖的底层网络,是因为它能实现以下两点。

(1)它能协助Kubernetes,给每一个Node上的Docker容器都分配互不冲突的IP地址。

(2)它能在这些IP地址之间建立一个覆盖网络(Overlay Network),通过这个覆盖网络,将数据包原封不动地传递到目标容器内。

现在通过图7.19看看Flannel是如何实现这两点的。

图7.19 Flannel的实现

可以看到,Flannel首先创建了一个名为flannel0的网桥,而且这个网桥的一端连接docker0网桥,另一端连接一个叫作flanneld的服务进程。

flanneld进程并不简单,它上连etcd,利用etcd来管理可分配的IP地址段资源,同时监控etcd中每个Pod的实际地址,并在内存中建立了一个Pod节点路由表;它下连docker0和物理网络,使用内存中的Pod节点路由表,将docker0发给它的数据包包装起来,利用物理网络的连接将数据包投递到目标flanneld上,从而完成Pod到Pod之间的直接地址通信。

Flannel之间底层通信协议的可选技术包括UDP、VxLan、AWS VPC等多种方式。通过源flanneld封包、目标flanneld解包,docker0最终收到的就是原始数据,对容器应用来说是透明的,应感觉不到中间Flannel的存在。

我们看一下Flannel是如何做到为不同Node上的Pod分配的IP不产生冲突的。其实想到Flannel使用了集中的etcd存储就很容易理解了。它每次分配的地址段都在同一个公共区域获取,这样大家自然能够相互协调,不产生冲突了。而且在Flannel分配好地址段后,后面的事情是由Docker完成的,Flannel通过修改Docker的启动参数将分配给它的地址段传递进去:

通过这些操作,Flannel就控制了每个Node上的docker0地址段的地址,也就保障了所有Pod的IP地址都在同一个水平网络中且不产生冲突了。

Flannel完美地实现了对Kubernetes网络的支持,但是它引入了多个网络组件,在网络通信时需要转到flannel0网络接口,再转到用户态的flanneld程序,到对端后还需要走这个过程的反过程,所以也会引入一些网络的时延损耗。

另外,Flannel模型默认采用了UDP作为底层传输协议,UDP本身是非可靠协议,虽然两端的TCP实现了可靠传输,但在大流量、高并发的应用场景下还需要反复测试,确保没有问题。

Flannel的安装和配置如下。

1)安装etcd

由于Flannel使用etcd作为数据库,所以需要预先安装好etcd,此处不再赘述。

2)安装Flannel

需要在每个Node上都安装Flannel。首先到Flannel的GitHub官网下载软件包,文件名如flannel-<version>-linux-amd64.tar.gz,解压缩后将二进制文件flanneld和mk-docker-opts.sh复制到/usr/bin(或其他PATH环境变量中的目录)下,即可完成Flannel的安装。

3)配置Flannel

此处以使用systemd系统为例对flanneld服务进行配置。编辑服务配置文件/usr/lib/systemd/system/flanneld.service:

编辑配置文件/etc/sysconfig/flannel,设置etcd的URL地址:

在启动flanneld服务前,需要在etcd中添加一条网络配置记录,该配置用于flanneld分配给每个Docker的虚拟IP地址段:

由于Flannel将覆盖docker0网桥,所以如果Docker服务已启动,则需要停止Docker服务。

4)启动flanneld服务

5)设置docker0网桥的IP地址

完成后确认网络接口docker0的IP地址属于flannel0的子网:

6)重新启动Docker服务

至此就完成了Flannel覆盖网络的设置。

使用ping命令验证各Node上docker0 之间的相互访问。例如在Node1(docker0 IP=10.1.10.1)机器上ping Node2的docker0(docker0's IP=10.1.30.1),通过Flannel能够成功连接其他物理机的Docker网络:

我们也可以在etcd中查看Flannel设置的flannel0地址与物理机IP地址的对应规则:

本例使用二进制方式部署Flannel,它也能以DaemonSet的形式部署,有兴趣的读者可以参考Flannel官网的说明。

7.7.2 Open vSwitch插件的原理和部署示例

在了解了Flannel后,我们再看看Open vSwitch是怎么解决上述两个问题的。

Open vSwitch是一个开源的虚拟交换机软件,有点儿像Linux中的bridge,但是功能要复杂得多。Open vSwitch的网桥可以直接建立多种通信通道(隧道),例如Open vSwitch with GRE/VxLAN。这些通道的建立可以很容易地通过OVS的配置命令实现。在Kubernetes、Docker场景下,我们主要是建立L3到L3的隧道。举个例子来看看Open vSwitch with GRE/VxLAN的网络架构,如图7.20所示。

图7.20 Open vSwitch with GRE/VxLAN的网络架构

首先,为了避免Docker创建的docker0地址产生冲突(因为Docker Daemon启动且给docker0选择子网地址时只有几个备选列表,很容易产生冲突),我们可以将docker0网桥删除,手动建立一个Linux网桥,然后手动给这个网桥配置IP地址范围。

其次,建立Open vSwitch的ovs网桥,使用ovs-vsctl命令给ovs网桥增加gre端口,在添加gre端口时要将目标连接的NodeIP地址设置为对端的IP地址。对每一个对端IP地址都需要这么操作(对于大型集群网络,这可是个体力活,要做自动化脚本来完成)。

最后,将ovs网桥作为网络接口,加入Docker网桥上(docker0或者自己手工建立的新网桥)。

重启ovs网桥和Docker网桥,并添加一个Docker的地址段到Docker网桥的路由规则项,就可以将两个容器的网络连接起来了。

当容器内的应用访问另一个容器的地址时,数据包会通过容器内的默认路由发送给docker0网桥。ovs网桥是作为docker0网桥的端口存在的,它会将数据发送给ovs网桥。ovs网络已经通过配置建立了与其他ovs网桥连接的GRE/VxLAN隧道,自然能将数据送达对端的Node,并送往docker0及Pod。通过新增的路由项,Node本身的应用数据也被路由到docker0网桥上,和刚才的通信过程一样,也可以访问其他Node上的Pod。

OVS的优势是,作为开源的虚拟交换机软件,相对成熟和稳定,而且支持各类网络隧道协议,通过了OpenStack等项目的考验。在前面介绍Flannel时可知,Flannel除了支持建立覆盖网络,保证Pod到Pod的无缝通信,还和Kubernetes、Docker架构体系紧密结合。Flannel能够感知Kubernetes的Service,动态维护自己的路由表,还通过etcd来协助Docker对整个Kubernetes集群中docker0的子网地址分配。而我们在使用OVS时,很多事情就需要手工完成了。无论是OVS还是Flannel,通过覆盖网络提供的Pod到Pod的通信都会引入一些额外的通信开销,如果是对网络依赖特别重的应用,则需要评估对业务的影响。

Open vSwitch的安装和配置过程如下。以两个Node为例,目标网络拓扑如图7.21所示。需要先确保节点192.168.18.128的Docker0采用了172.17.43.0/24网段,而192.168.18.131的Docker0采用了172.17.42.0/24网段,对应的参数为docker daemon的启动参数--bip设置的值。

图7.21 目标网络拓扑

1)在两个Node上安装ovs

使用yum install命令在两个Node上安装ovs:

禁用selinux,配置后重启Linux:

查看Open vSwitch的服务状态,应该启动ovsdb-server与ovs-vswitchd两个进程:

查看Open vSwitch的相关日志,确认没有异常:

注意,上述操作需要在两个节点机器上分别执行完成。

2)创建网桥和GRE隧道

在每个Node上都建立ovs的网桥br0,然后在网桥上创建一个GRE隧道连接对端网桥,最后把ovs网桥br0作为一个端口连接到docker0这个Linux网桥上(可以认为是交换机互联),这样一来,两个节点机器上的docker0网段就能互通了。

下面以节点机器192.168.18.131为例,具体的操作流程如下。

(1)创建ovs网桥:

(2)创建GRE隧道连接对端,remote_ip为对端eth0的网卡地址:

(3)添加br0到本地docker0,使得容器流量通过OVS流经tunnel:

(4)启动br0与docker0网桥:

(5)添加路由规则。由于192.168.18.128与192.168.18.131的docker0网段分别为172.17.43.0/24与172.17.42.0/24,这两个网段的路由都需要经过本机的docker0网桥路由,其中一个24网段是通过OVS的GRE隧道到达对端的,因此需要在每个Node上都添加通过docker0网桥转发的172.17.0.0/16网段的路由规则:

(6)清空Docker自带的iptables规则及Linux的规则,后者存在拒绝icmp报文通过防火墙的规则:

在192.168.18.131上完成上述步骤后,在192.168.18.128节点执行同样的操作,注意,GRE隧道里的IP地址要改为对端节点(192.168.18.131)的IP地址。

配置完成后,192.168.18.131的IP地址、docker0的IP地址及路由等重要信息显示如下:

同样,192.168.18.128节点的重要信息显示如下:

3)两个Node上容器之间的互通测试

首先,在192.168.18.128节点上ping 192.168.18.131上的docker0地址172.17.42.1,验证网络的互通性:

下面通过tshark抓包工具来分析流量走向。首先,在192.168.18.128节点监听在br0上是否有GRE报文,运行下面的命令,我们发现在br0上并没有GRE报文:

在eth0上抓包,则发现了GRE封装的ping包报文通过,说明GRE是在物理网络上完成的封包过程:

至此,基于OVS的网络搭建成功,由于GRE是点对点的隧道通信方式,所以如果有多个Node,则需要建立N×(N-1)条GRE隧道,即所有Node组成一个网状网络,实现了全网互通。

7.7.3 直接路由的原理和部署示例

我们知道,docker0网桥上的IP地址在Node网络上是看不到的。从一个Node到一个Node内的docker0是不通的,因为它不知道某个IP地址在哪里。如果能够让这些机器知道对端docker0地址在哪里,就可以让这些docker0相互通信了。这样,在所有Node上运行的Pod就都可以相互通信了。

我们可以通过部署MultiLayer Switch(MLS)实现这一点,在MLS中配置每个docker0子网地址到Node地址的路由项,通过MLS将docker0的IP寻址定向到对应的Node上。

另外,我们可以将这些docker0和Node的匹配关系配置在Linux操作系统的路由项中,这样通信发起的Node就能够根据这些路由信息直接找到目标Pod所在的Node,将数据传输过去了,如图7.22所示。

我们在每个Node的路由表中增加对方所有docker0的路由项。

例如,Pod1所在docker0网桥的IP子网是10.1.10.0,Node的地址为192.168.1.128;而Pod2所在docker0网桥的IP子网是10.1.20.0,Node的地址是192.168.1.129。

图7.22 直接路由Pod到Pod通信

在Node1上用route add命令增加一条到Node2上docker0的静态路由规则:

同样,在Node2上增加一条到Node1上docker0的静态路由规则:

在Node1上通过ping命令验证到Node2上docker0的网络连通性。这里10.1.20.1为Node2上docker0网桥自身的IP地址:

可以看到,路由转发规则生效,Node1可以直接访问Node2上的docker0网桥,进一步就可以访问属于docker0网段的容器应用了。

在大规模集群中,在每个Node上都需要配置到其他docker0/Node的路由项,这会带来很大的工作量;并且在新增机器时,对所有Node都需要修改配置;在重启机器时,如果docker0的地址有变化,则也需要修改所有Node的配置,这显然是非常复杂的。

为了管理这些动态变化的docker0地址,动态地让其他Node都感知到它,还可以使用动态路由发现协议来同步这些变化。在运行动态路由发现协议代理的Node时,会将本机LOCAL路由表的IP地址通过组播协议发布出去,同时监听其他Node的组播包。通过这样的信息交换,Node上的路由规则就都能够相互学习了。当然,路由发现协议本身还是很复杂的,感兴趣的话,可以查阅相关规范。在实现这些动态路由发现协议的开源软件中,常用的有Quagga、Zebra等。下面简单介绍直接路由的操作过程。

首先,手工分配Docker bridge的地址,保证它们在不同的网段是不重叠的。建议最好不用Docker Daemon自动创建的docker0(因为我们不需要它的自动管理功能),而是单独建立一个bridge,给它配置规划好的IP地址,然后使用--bridge=XX来指定网桥。

然后,在每个节点上都运行Quagga。

完成这些操作后,我们很快就能得到一个Pod和Pod直接相互访问的环境了。由于路由发现能够被网络上的所有设备接收,所以如果网络上的路由器也能打开RIP协议选项,则能够学习到这些路由信息。通过这些路由器,我们甚至可以在非Node上使用Pod的IP地址直接访问Node上的Pod了。

除了在每台服务器上安装Quagga软件并启动,还可以使用Quagga容器运行(例如index.alauda.cn/georce/router)。在每个Node上下载该Docker镜像:

在运行Quagga容器前,需要确保每个Node上docker0网桥的子网地址不能重叠,也不能与物理机所在的网络重叠,这需要网络管理员的仔细规划。

下面以3个Node为例,每个Node的docker0网桥的地址如下(前提是Node物理机的IP地址不是10.1.X.X地址段):

在每个Node上启动Quagga容器。需要说明的是,Quagga需要以--privileged特权模式运行,并且指定--net=host,表示直接使用物理机的网络:

启动成功后,各Node上的Quagga会相互学习来完成到其他机器的docker0路由规则的添加。

一段时间后,在Node1上使用route-n命令来查看路由表,可以看到Quagga自动添加了两条到Node2和到Node3上docker0的路由规则:

在Node2上查看路由表,可以看到自动添加了两条到Node1和Node3上docker0的路由规则:

至此,所有Node上的docker0就都可以互联互通了。

当然,聪明的你还会有新的疑问:这样做的话,由于每个Pod的地址都会被路由发现协议广播出去,会不会存在路由表过大的情况?实际上,路由表通常都会有高速缓存,查找速度会很快,不会对性能产生太大的影响。当然,如果你的集群容量在数千个Node以上,则仍然需要测试和评估路由表的效率问题。

7.7.4 Calico插件的原理和部署示例

本节以Calico为例讲解Kubernetes中CNI插件的原理和应用。

1.Calico简介

Calico是一个基于BGP的纯三层的网络方案,与OpenStack、Kubernetes、AWS、GCE等云平台都能够良好地集成。Calico在每个计算节点都利用Linux Kernel实现了一个高效的vRouter来负责数据转发。每个vRouter都通过BGP1协议把在本节点上运行的容器的路由信息向整个Calico网络广播,并自动设置到达其他节点的路由转发规则。Calico保证所有容器之间的数据流量都是通过IP路由的方式完成互联互通的。Calico节点组网时可以直接利用数据中心的网络结构(L2或者L3),不需要额外的NAT、隧道或者Overlay Network,没有额外的封包解包,能够节约CPU运算,提高网络效率,如图7.23所示。

图7.23 Calico不使用额外的封包解包

Calico在小规模集群中可以直接互联,在大规模集群中可以通过额外的BGP route reflector来完成,如图7.24所示。

图7.24 通过BGP route reflector连接大规模网络

此外,Calico基于iptables还提供了丰富的网络策略,实现了Kubernetes的Network Policy策略,提供容器间网络可达性限制的功能。

Calico的系统架构如图7.25所示。

图7.25 Calico的系统架构

Calico的主要组件如下。

◎ Felix:Calico Agent,运行在每个Node上,负责为容器设置网络资源(IP地址、路由规则、iptables规则等),保证跨主机容器网络互通。

◎ etcd:Calico使用的后端存储。

◎ BGP Client:负责把Felix在各Node上设置的路由信息通过BGP广播到Calico网络。

◎ Route Reflector:通过一个或者多个BGP Route Reflector完成大规模集群的分级路由分发。

◎ CalicoCtl:Calico命令行管理工具。

2. 部署Calico应用

在Kubernetes中部署Calico的主要步骤如下。

(1)修改Kubernetes服务的启动参数,并重启服务。

◎ 设置Master上kube-apiserver服务的启动参数:--allow-privileged=true(因为calico-node需要以特权模式运行在各Node上)。

◎ 设置各Node上kubelet服务的启动参数:--network-plugin=cni(使用CNI网络插件)。

本例中的Kubernetes集群包括两个Node:k8s-node-1(IP地址为 192.168.18.3)和k8s-node-2(IP地址为192.168.18.4)。

(2)创建Calico服务,主要包括calico-node和calico policy controller。需要创建的资源对象如下。

◎ 创建ConfigMap calico-config,包含Calico所需的配置参数。

◎ 创建Secret calico-etcd-secrets,用于使用TLS方式连接etcd。

◎ 在每个Node上都运行calico/node容器,部署为DaemonSet。

◎ 在每个Node上都安装Calico CNI二进制文件和网络配置参数(由install-cni容器完成)。

◎ 部署一个名为calico/kube-policy-controller的Deployment,以对接Kubernetes集群中为Pod设置的Network Policy。

从Calico官网下载Calico的YAML文件calico.yaml,该配置文件包括启动Calico所需的全部资源对象的定义,下面对它们逐个进行说明。

(1)Calico所需的配置及CNI网络配置,以ConfigMap对象进行创建:

对主要参数说明如下。

◎ typha_service_name:typha服务用于大规模环境中,如需安装,则请参考官网上calico-typha.yaml的配置。

◎ veth_mtu:网络接口的MTU值,需要根据不同的网络设置进行调整。

◎ calico_backend:Calico的后端,默认为bird。

◎ cni_network_config:符合CNI规范的网络配置,将在/etc/cni/net.d目录下生成CNI网络配置文件。其中type=calico表示kubelet将从/opt/cni/bin目录下搜索名为calico的可执行文件,并调用它来完成容器网络的设置。ipam中的type=calico-ipam表示kubelet将在/opt/cni/bin目录下搜索名为calico-ipam的可执行文件,用于管理容器的IP地址。

(2)calico-node,以DaemonSet的形式在每个Node上都运行一个calico-node容器:

在该Pod中,初始化容器upgrade-ipam、install-cni、flexvol-driver分别完成了一些初始化工作。主应用容器为calico-node,用于管理Pod的网络配置,保证Pod的网络与各Node互联互通。

calico-node应用的主要参数如下。

◎ DATASTORE_TYPE:数据后端存储,默认为“kubernetes”,也可以使用“etcd”。

◎ CALICO_IPV4POOL_CIDR:Calico IPAM的IP地址池,Pod的IP地址将从该池中进行分配。

◎ CALICO_IPV4POOL_IPIP:是否启用IPIP模式。启用IPIP模式时,Calico将在Node上创建一个名为tunl0的虚拟隧道。

◎ IP_AUTODETECTION_METHOD:获取Node IP地址的方式,默认使用第1个网络接口的IP地址,对于安装了多块网卡的Node,建议使用正则表达式选择正确的网卡,例如"interface=ens.*"表示选择名称以ens开头的网卡的IP地址。

◎ FELIX_IPV6SUPPORT:是否启用IPv6。

◎ FELIX_LOGSEVERITYSCREEN:日志级别。

其中,IP Pool可以使用两种模式:BGP或IPIP。使用IPIP模式时,设置CALICO_IPV4POOL_IPIP="always";不使用IPIP模式时,设置CALICO_IPV4POOL_IPIP="off",此时将使用BGP模式。

IPIP是一种将各Node的路由之间做一个tunnel,再把两个网络连接起来的模式,如图7.26所示。启用IPIP模式时,Calico将在各Node上创建一个名为tunl0的虚拟网络接口。

图7.26 IPIP模式

BGP模式则直接使用物理机作为虚拟路由器(vRouter),不再创建额外的tunnel。

(3)calico-kube-controllers应用,用于管理Kubernetes集群中的网络策略(Network Policy):

本节将省略为calico-node和calico-kube-controllers配置的RBAC规则,以及对Calico自定义资源对象CRD的说明,详细配置请参考官方文档的说明。

修改好相应的参数后,创建Calico的各个资源对象:

确保Calico的各个服务正确运行:

calico-node在正常运行后,会根据CNI规范,在/etc/cni/net.d/目录下生成如下文件和目录,并在/opt/cni/bin/目录下安装二进制文件calico和calico-ipam,供kubelet调用。

◎ 10-calico.conflist:符合CNI规范的网络配置列表,其中type=calico表示该插件的二进制文件名为calico。示例如下:

◎ calico-kubeconfig:Calico访问Master所需的kubeconfig文件。示例如下:

在Calico正确运行后,我们看看Calico在操作系统上设置的网络配置。查看k8s-node-1服务器的网络接口设置,可以看到一个新的名为tunl0的接口,并设置了网络地址为10.1.109.64/32:

查看k8s-node-2服务器的网络接口设置,同样可以看到一个新的名为tunl0的接口,网络地址为10.1.140.64/32:

这两个子网都是从calico-node设置的IP地址池(CALICO_IPV4POOL_CIDR="10.1.0.0/16")中进行分配的。

我们再看看Calico在两台主机上设置的路由规则。首先,查看k8s-node-1服务器的路由表,可以看到一条到k8s-node-2的Calico容器网络10.1.140.64的路由转发规则:

然后,查看k8s-node-2服务器的路由表,可以看到一条到k8s-node-1的Calico容器网络10.1.109.64/26的路由转发规则:

这样,通过Calico就完成了Node间的容器网络设置。在后续的Pod创建过程中,kubelet将通过CNI接口调用Calico进行Pod网络设置,包括IP地址、路由规则、iptables规则等。

如果设置CALICO_IPV4POOL_IPIP="off",即不使用IPIP模式,则Calico将不会创建tunl0网络接口,路由规则直接使用物理机网卡作为路由器进行转发。

查看k8s-node-1服务器的路由表,可以看到一条到k8s-node-2的私网10.1.140.64的路由转发规则,将通过本机ens33网卡进行转发:

查看k8s-node-2服务器的路由表,可以看到一条到k8s-node-1的私网10.1.109.64/26的路由转发规则,将通过本机ens33网卡进行转发:

3. 跨主机Pod网络连通性验证

下面创建几个Pod,验证Calico对它们的网络设置。以第1章的mysql和myweb为例,分别创建1个Pod和两个Pod:

查看各Pod的IP地址,可以看到是通过Calico设置的以10.1开头的IP地址:

进入运行在k8s-node-2上的Pod“myweb-s86sk”:

在容器内访问运行在k8s-node-1上的Pod“mysql-8cztq”的IP地址10.1.109.71:

在容器内访问物理机k8s-node-1的IP地址192.168.18.3:

这说明跨主机容器之间、容器与宿主机之间的网络都能互联互通了。

查看k8s-node-2物理机的网络接口和路由表,可以看到Calico为Pod“myweb-s86sk”新建了一个网络接口cali439924adc43,并为其设置了一条路由规则:

另外,Calico为该网络接口cali439924adc43设置了一系列iptables规则: