第7章 网络原理
7.1 Kubernetes网络模型
7.2 Docker网络基础
7.2.1 网络命名空间
7.2.2 Veth设备对
7.2.3 网桥
7.2.4 iptables和Netfilter
7.2.5 路由
7.3 Docker的网络实现
7.4 Kubernetes的网络实现
7.4.1 容器到容器的通信
7.4.2 Pod之间的通信
7.5 Pod和Service网络实战
7.6 CNI网络模型
7.6.1 CNM模型
7.6.2 CNI模型
7.6.3 在Kubernetes中使用网络插件
7.7 Kubernetes网络策略
7.7.1 网络策略配置说明
7.7.2 在Namespace级别设置默认的网络策略
7.7.3 NetworkPolicy的发展
7.8 开源的网络组件
7.8.1 Flannel
7.8.2 Open vSwitch
7.8.3 直接路由
7.8.4 Calico容器网络和网络策略实战
关于Kubernetes网络,我们通常有如下问题需要回答。
Kubernetes的网络模型是什么?
Docker背后的网络基础是什么?
Docker自身的网络模型和局限是什么?
Kubernetes的网络组件之间是怎么通信的?
外部如何访问Kubernetes集群?
有哪些开源组件支持Kubernetes的网络模型?
本章分别回答这些问题,然后通过一个具体的实验将这些相关的知识点串联成一个整体。
7.1 Kubernetes网络模型
Kubernetes网络模型设计的一个基础原则是:每个Pod都拥有一个独立的IP地址,并假定所有Pod都在一个可以直接连通的、扁平的网络空间中。
所以不管它们是否运行在同一个Node(宿主机)中,都要求它们可以直接通过对方的IP进行访问。
设计这个原则的原因是,用户不需要额外考虑如何建立Pod之间的连接,也不需要考虑如何将容器端口映射到主机端口等问题。
实际上,在Kubernetes的世界里,IP是以Pod为单位进行分配的。
一个Pod内部的所有容器共享一个网络堆栈(相当于一个网络命名空间,它们的IP地址、网络设备、配置等都是共享的)。
按照这个网络原则抽象出来的为每个Pod都设置一个IP地址的模型也被称作IP-per-Pod模型。
由于Kubernetes的网络模型假设Pod之间访问时使用的是对方Pod的实际地址,所以一个Pod内部的应用程序看到的自己的IP地址和端口与集群内其他Pod看到的一样。
它们都是Pod实际分配的IP地址。
将IP地址和端口在Pod内部和外部都保持一致,也就不需要使用NAT来进行地址转换了。
Kubernetes的网络之所以这么设计,主要原因就是可以兼容过去的应用。
当然,我们使用Linux命令“ip addr show”也能看到这些地址,和程序看到的没有什么区别。
所以这种IP-per-Pod的方案很好地利用了现有的各种域名解析和发现机制。
为每个Pod都设置一个IP地址的模型还有另外一层含义,那就是同一个Pod内的不同容器会共享同一个网络命名空间,也就是同一个Linux网络协议栈。
这就意味着同一个Pod内的容器可以通过localhost来连接对方的端口。
这种关系和同一个VM内的进程之间的关系是一样的,看起来Pod内容器之间的隔离性减小了,而且Pod内不同容器之间的端口是共享的,就没有所谓的私有端口的概念了。
如果你的应用必须要使用一些特定的端口范围,那么你也可以为这些应用单独创建一些Pod。
反之, 对那些没有特殊需要的应用,由于Pod内的容器是共享部分资源的,所以可以通过共享资源互相通信,这显然更加容易和高效。
针对这些应用,虽然损失了可接受范围内的部分隔离性,却也是值得的。
IP-per-Pod模式和Docker原生的通过动态端口映射方式实现的多节点访问模式有什么区别呢?
主要区别是后者的动态端口映射会引入端口管理的复杂性,而且访问者看到的IP地址和端口与服务提供者实际绑定的不同
(因为NAT的缘故,它们都被映射成新的地址或端口了),这也会引起应用配置的复杂化。
同时,标准的DNS等名字解析服务也不适用了,甚至服务注册和发现机制都将迎来挑战,
因为在端口映射情况下,服务自身很难知道自己对外暴露的真实的服务IP和端口,外部应用也无法通过服务所在容器的私有IP地址和端口来访问服务。
总的来说,IP-per-Pod模型是一个简单的兼容性较好的模型。
从该模型的网络的端口分配、域名解析、服务发现、负载均衡、应用配置和迁移等角度来看,Pod都能够被看作一台独立的虚拟机或物理机。
按照这个网络抽象原则,Kubernetes对网络有什么前提和要求呢?
Kubernetes对集群网络有如下要求。
(1)所有容器都可以在不用NAT的方式下同别的容器通信。
(2)所有节点都可以在不用NAT的方式下同所有容器通信,反之 亦然。
(3)容器的地址和别人看到的地址是同一个地址。
这些基本要求意味着并不是只要两台机器都运行Docker,Kubernetes就可以工作了。
具体的集群网络实现必须满足上述基本要求,原生的Docker网络目前还不能很好地支持这些要求。
实际上,这些对网络模型的要求并没有降低整个网络系统的复杂度。
如果你的程序原来在VM上运行,而那些VM拥有独立IP,并且它们之间可以直接透明地通信,那么Kubernetes的网络模型就和VM使用的网络模型一样。
所以使用这种模型可以很容易地将已有的应用程序从VM 或者物理机迁移到容器上。
当然,谷歌设计Kubernetes的一个主要运行基础就是其公有云GCE,GCE默认支持这些网络要求。
另外,常见的其他公有云服务商如亚马逊等,其公有云环境也支持这些网络要求。
由于部署私有云的场景也非常普遍,所以在私有云中运行Kubernetes+Docker集群之前,需要自己搭建出符合Kubernetes要求的网络环境。
有很多开源组件可以帮助我们打通Docker容器和容器之间的网络,实现满足Kubernetes要求的网络模型。
当然,每种方案都有适合的场景,我们要根据自己的实际需要进行选择。
在后面的章节中会对常见的开源方案进行介绍。
Kubernetes的网络依赖于Docker,Docker的网络又离不开Linux操作系统内核特性的支持,所以我们有必要先深入了解Docker背后的网络原理和基础知识。
接下来一起深入学习必要的Linux网络知识。
7.2 Docker网络基础
Docker本身的技术依赖于近年来Linux内核虚拟化技术的发展,所 以Docker对Linux内核的特性有很强的依赖。
这里将Docker使用到的与 Linux网络有关的主要技术进行简要介绍,这些技术有:网络命名空间 (Network Namespace)、Veth设备对、网桥、ipatables和路由。
7.2.1 网络命名空间
为了支持网络协议栈的多个实例,Linux在网络栈中引入了网络命名空间,这些独立的协议栈被隔离到不同的命名空间中。
处于不同命名空间中的网络栈是完全隔离的,彼此之间无法通信,就好像两个“平行宇宙”。
通过对网络资源的隔离,就能在一个宿主机上虚拟多个不同的网络环境。
Docker正是利用了网络的命名空间特性,实现了不同容器之间的网络隔离。
在Linux的网络命名空间中可以有自己独立的路由表及独立的iptables设置来提供包转发、NAT及IP包过滤等功能。
为了隔离出独立的协议栈,需要纳入命名空间的元素有进程、套接字、网络设备等。
进程创建的套接字必须属于某个命名空间,套接字的操作也必须在命名空间中进行。
同样,网络设备也必须属于某个命名空间。
因为网络设备属于公共资源,所以可以通过修改属性实现在命名空间之间移动。
当然,是否允许移动与设备的特征有关。
让我们稍微深入Linux操作系统内部,看它是如何实现网络命名空间的,这也会对理解后面的概念有帮助。
1.网络命名空间的实现
Linux的网络协议栈是十分复杂的,为了支持独立的协议栈,相关的这些全局变量都必须被修改为协议栈私有。
最好的办法就是让这些全局变量成为一个Net Namespace变量的成员,然后为协议栈的函数调用加入一个Namespace参数。
这就是Linux实现网络命名空间的核心。
同时,为了保证对已经开发的应用程序及内核代码的兼容性,内核代码隐式地使用了命名空间中的变量。
程序如果没有对命名空间有特殊需求,就不需要编写额外的代码,网络命名空间对应用程序而言是透明的。
在建立了新的网络命名空间,并将某个进程关联到这个网络命名空间后,就出现了类似于如图7.1所示的内核数据结构,所有网站栈变量都被放入了网络命名空间的数据结构中。
这个网络命名空间是其进程组私有的,和其他进程组不冲突。
在新生成的私有命名空间中只有回环设备(名为“lo”且是停止状态),其他设备默认都不存在,如果我们需要,则要一一手工建立。
Docker容器中的各类网络栈设备都是Docker Daemon在启动时自动创建和配置的。
所有的网络设备(物理的或虚拟接口、桥等在内核里都叫作Net Device)都只能属于一个命名空间。
当然,物理设备(连接实际硬件的设备)通常只能关联到root这个命名空间中。
虚拟的网络设备(虚拟的以太网接口或者虚拟网口对)则可以被创建并关联到一个给定的命名空间中,而且可以在这些命名空间之间移动。
前面提到,由于网络命名空间代表的是一个独立的协议栈,所以它 们之间是相互隔离的,彼此无法通信,在协议栈内部都看不到对方。
那么有没有办法打破这种限制,让处于不同命名空间的网络相互通信,甚至和外部的网络进行通信呢?
答案就是“有,应用Veth设备对即可”。
Veth设备对的一个重要作用就是打通互相看不到的协议栈之间的壁垒,它就像一条管子,一端连着这个网络命名空间的协议栈,一端连着另一个网络命名空间的协议栈。
所以如果想在两个命名空间之间通信,就必须有一个Veth设备对。
后面会介绍如何操作Veth设备对来打通不同命名空间之间的网络。
2.网络命名空间的操作
下面列举网络命名空间的一些操作。
我们可以使用Linux iproute2系列配置工具中的IP命令来操作网络命名空间。
注意,这个命令需要由root用户运行。
创建一个命名空间: ip netns add <name>
在命名空间中执行命令: ip netns exec <name> <command>
也可以先通过bash命令进入内部的shell界面,然后执行各种命令:ip netns exec <name> bash
退出到外面的命名空间时,请输入“exit”。
3.网络命名空间的实用技巧
操作网络命名空间时的一些实用技巧如下。
我们可以在不同的网络命名空间之间转移设备,例如下面会提到的Veth设备对的转移。
因为一个设备只能属于一个命名空间,所以转移后在这个命名空间中就看不到这个设备了。
具体哪些设备能被转移到不同的命名空间呢?
在设备里面有一个重要的属性:NETIF_F_ETNS_LOCAL,如果这个属性为on,就不能被转移到其他命名空间中。
Veth设备属于可以转移的设备,而很多其他设备如lo设备、vxlan设备、ppp设备、bridge设备等都是不可以转移的。
将无法转移的设备移动到别的命名空间时,会得到无效参数的错误提示。
# ip link set br0 netns ns1
如何知道这些设备是否可以转移呢?
可以使用ethtool工具查看: # ethtool -k br0
netns-local的值是on,说明不可以转移,否则可以转移。
7.2.2 Veth设备对
引入Veth设备对是为了在不同的网络命名空间之间通信,利用它可以直接将两个网络命名空间连接起来。
由于要连接两个网络命名空间,所以Veth设备都是成对出现的,很像一对以太网卡,并且中间有一根直连的网线。
既然是一对网卡,那么我们将其中一端称为另一端的peer。
在Veth设备的一端发送数据时,它会将数据直接发送到另一端,并触发另一端的接收操作。
整个Veth的实现非常简单,有兴趣的读者可以参考源代码“drivers/net/veth.c”的实现。
如图7.2所示是Veth设备对的示意图。
1.Veth设备对的操作命令
接下来看看如何创建Veth设备对,如何连接到不同的命名空间,并设置它们的地址,让它们通信。
创建Veth设备对: ip link add veth0 type veth peer name veth1
创建后,可以查看Veth设备对的信息。使用ip link show命令查看所有网络接口:# ip link show
看到了吧,有两个设备生成了,一个是veth0,它的peer是veth1。
现在这两个设备都在自己的命名空间中,那怎么能行呢?
好了,如果将Veth看作有两个头的网线,那么我们将另一个头甩给另一个命名空间:
ip link set veth1 netns netns1
这时可在外面这个命名空间中看两个设备的情况: # ip link show
只剩一个veth0设备了,已经看不到另一个设备了,另一个设备已经被转移到另一个网络命名空间中了。
在netns1网络命名空间中可以看到veth1设备了,符合预期:
# ip netns exec netns1 ip link show
现在看到的结果是,两个不同的命名空间各自有一个Veth的“网线头”,各显示为一个Device(在Docker的实现里面,它除了将Veth放入容器内,还将它的名字改成了eth0,简直以假乱真,你以为它是一个本地 网卡吗)。
现在可以通信了吗?不行,因为它们还没有任何地址,我们现在给它们分配IP地址:
ip netes exec ip addr add 10.1.1.1/24 dev veth1
ip addr add 10.1.1.2/24 dev veth0
再启动它们:
ip netes exec netes1 ip link set dev veth1 up
ip link set dev veth0 up
现在两个网络命名空间可以互相通信了:
# ping 10.1.1.1
# ip netes exec netns1 ping 10.1.1.2
至此,我们就能够理解Veth设备对的原理和用法了。
在Docker内部,Veth设备对也是连通容器与宿主机的主要网络设备,离开它是不行的。
2.Veth设备对如何查看对端
我们在操作Veth设备对时有一些实用技巧,如下所示。
一旦将Veth设备对的对端放入另一个命名空间,在本命名空间中就看不到它了。
那么我们怎么知道这个Veth设备的对端在哪里呢,也就是说它到底连接到哪个命名空间呢?
可以使用ethtool工具来查看(当网络命名空间特别多时,这可不是一件很容易的事情)。
首先,在命名空间netns1中查询Veth设备对端接口在设备列表中的序列号:
# ip netns exec netns1 ethtool -S veth1
得知另一端的接口设备的序列号是5(peer_ifindex:5),我们再到命名空间netns2中查看序列号5代表什么设备:
# ip netns exec netns2 ip link | grep 5
好了,我们现在就找到序列号为5的设备了,它是veth0,它的另一端自然就是命名空间netns1中的veth1了,因为它们互为peer。
7.2.3 网桥
Linux可以支持多个不同的网络,它们之间能够相互通信,如何将这些网络连接起来并实现各网络中主机的相互通信呢?可以用网桥。
网桥是一个二层的虚拟网络设备,把若干个网络接口“连接”起来,以使得网络接口之间的报文能够互相转发。
网桥能够解析收发的报文,读取目标MAC地址的信息,和自己记录的MAC表结合,来决策报文的转发目标网络接口。
为了实现这些功能,网桥会学习源MAC地址(二层网桥转发的依据就是MAC地址)。
在转发报文时,网桥只需要向特定的网口进行转发,来避免不必要的网络交互。
如果它遇到一个自己从未学习到的地址,就无法知道这个报文应该向哪个网络接口转发,就将报文广播给所有的网络接口(报文来源的网络接口除外)。
在实际的网络中,网络拓扑不可能永久不变。
设备如果被移动到另一个端口上,却没有发送任何数据,网桥设备就无法感知到这个变化,网桥还是向原来的端口转发数据包,在这种情况下数据就会丢失。
所以网桥还要对学习到的MAC地址表加上超时时间(默认为5min)。
如果网桥收到了对应端口MAC地址回发的包,则重置超时时间,否则过了超时时间后,就认为设备已经不在那个端口上了,它就会重新广播发送。
在Linux的内部网络栈里实现的网桥设备,作用和上面的描述相同。
过去Linux主机一般都只有一个网卡,现在多网卡的机器越来越多,而且有很多虚拟的设备存在,所以Linux的网桥提供了在这些设备之间互相转发数据的二层设备。
Linux内核支持网口的桥接(目前只支持以太网接口)。
但是与单纯的交换机不同,交换机只是一个二层设备,对于接收到的报文,要么转发,要么丢弃。
运行着Linux内核的机器本身就是一台主机,有可能是网络报文的目的地,其收到的报文除了转发和丢弃,
还可能被送到网络协议栈的上层(网络层),从而被自己(这台主机本身的协议栈)消化,所以我们既可以把网桥看作一个二层设备,也可以把它看作一个三层设备。
1.Linux网桥的实现
Linux内核是通过一个虚拟的网桥设备(Net Device)来实现桥接的。
这个虚拟设备可以绑定若干个以太网接口设备,从而将它们桥接起来。
如图7.3所示,这种Net Device网桥和普通的设备不同,最明显的一个特性是它还可以有一个IP地址。
如图7.3所示,网桥设备br0绑定了eth0和eth1。对于网络协议栈的上层来说,只看得到br0就行。
因为桥接是在数据链路层实现的,上层不需要关心桥接的细节,所以协议栈上层需要发送的报文被送到br0,网桥设备的处理代码判断报文该被转发到eth0还是eth1,或者两者皆转发;
反过来,从eth0或从eth1接收到的报文被提交给网桥的处理代码,在这里会判断报文应该被转发、丢弃还是被提交到协议栈上层。
而有时eth0、eth1也可能会作为报文的源地址或目的地址,直接参与报文的发送与接收,从而绕过网桥。
2.网桥的常用操作命令
Docker自动完成了对网桥的创建和维护。
为了进一步理解网桥,下面举几个常用的网桥操作例子,对网桥进行手工操作:
新增一个网桥设备:# brctl addbr xxxxx
之后可以为网桥增加网口,在Linux中,一个网口其实就是一个物理网卡。将物理网卡和网桥连接起来:
# brctl addif xxxxx ethx
网桥的物理网卡作为一个网口,由于在链路层工作,就不再需要IP地址了,这样上面的IP地址自然失效:
# ifconfig ethx 0.0.0.0
给网桥配置一个IP地址:
# ifconfig brxxx xxx.xxx.xxx.xxx
这样网桥就有了一个IP地址,而连接到上面的网卡就是一个纯链路层设备了。
7.2.4 iptables和Netfilter
我们知道,Linux网络协议栈非常高效,同时比较复杂。
如果我们希望在数据的处理过程中对关心的数据进行一些操作,则该怎么做呢?Linux提供了一套机制来为用户实现自定义的数据包处理过程。
在Linux网络协议栈中有一组回调函数挂接点,通过这些挂接点挂接的钩子函数可以在Linux网络栈处理数据包的过程中对数据包进行一 些操作,例如过滤、修改、丢弃等。
整个挂接点技术叫作Netfilter和iptables。
Netfilter负责在内核中执行各种挂接的规则,运行在内核模式中;而iptables是在用户模式下运行的进程,负责协助和维护内核中Netfilter的各种规则表。
二者互相配合来实现整个Linux网络协议栈中灵活的数据包处理机制。
Netfilter可以挂接的规则点有5个,如图7.4中的深色椭圆所示。
1.规则表Table
这些挂接点能挂接的规则也分不同的类型(也就是规则表Table),我们可以在不同类型的Table中加入我们的规则。
目前主要支持的Table类型有:RAW、MANGLE、NAT和FILTER。
上述4个Table(规则链)的优先级是RAW最高,FILTER最低。
在实际应用中,不同的挂接点需要的规则类型通常不同。
例如,在Input的挂接点上明显不需要FILTER过滤规则,因为根据目标地址已经选择好本机的上层协议栈了,所以无须再挂接FILTER过滤规则。
目前Linux系统支持的不同挂接点能挂接的规则类型如图7.5所示。
当Linux协议栈的数据处理运行到挂接点时,它会依次调用挂接点上所有的挂钩函数,直到数据包的处理结果是明确地接受或者拒绝。
2.处理规则
每个规则的特性都分为以下几部分:
表类型(准备干什么事情)。
什么挂接点(什么时候起作用)。
匹配的参数是什么(针对什么样的数据包)。
匹配后有什么动作(匹配后具体的操作是什么)。
前面已经介绍了表类型和挂接点,接下来看看匹配的参数和匹配后的动作。
(1)匹配的参数
匹配的参数用于对数据包或者TCP数据连接的状态进行匹配。
当有多个条件存在时,它们一起发挥作用,来达到只针对某部分数据进行修改的目的。
常见的匹配参数如下:流入/流出的网络接口、来源/目的地址、协议类型、来源/目的端口。
(2)匹配后的动作
一旦有数据匹配,就会执行相应的动作。
动作类型既可以是标准的预定义的几个动作,也可以是自定义的模块注册的动作,或者是一个新的规则链,以便更好地组织一组动作。
3.iptables命令
iptables命令用于协助用户维护各种规则。
我们在使用Kubernetes、Docker的过程中,通常都会去查看相关的Netfilter配置。
这里只介绍如何查看规则表,详细的介绍请参照Linux的iptables帮助文档。
查看系统中已有规则的方法如下:
iptables-save:按照命令的方式打印iptables的内容。
iptables-vnL:以另一种格式显示Netfilter表的内容。
7.2.5 路由
Linux系统包含一个完整的路由功能。
当IP层在处理数据发送或者转发时,会使用路由表来决定发往哪里。
在通常情况下,如果主机与目的主机直接相连,那么主机可以直接发送IP报文到目的主机,这个过程比较简单。
例如,通过点对点的链接或网络共享,如果主机与目的主机没有直接相连,那么主机会将IP报文发送给默认的路由器,然后由路由器来决定往哪里发送IP报文。
路由功能由IP层维护的一张路由表来实现。
当主机收到数据报文时,它用此表来决策接下来应该做什么操作。
当从网络侧接收到数据报文时,IP层首先会检查报文的IP地址是否与主机自身的地址相同。
如果数据报文中的IP地址是主机自身的地址,那么报文将被发送到传输层相应的协议中。
如果报文中的IP地址不是主机自身的地址,并且主机配置了路由功能,那么报文将被转发,否则,报文将被丢弃。
路由表中的数据一般是以条目形式存在的。一个典型的路由表条目通常包含以下主要的条目项:
(1)目的IP地址:此字段表示目标的IP地址。
这个IP地址可以是某主机的地址,也可以是一个网络地址。
如果这个条目包含的是一个主机地址,那么它的主机ID将被标记为非零;如果这个条目包含的是一个网络地址,那么它的主机ID将被标记为零。
(2)下一个路由器的IP地址:这里采用“下一个”的说法,是因为下一个路由器并不总是最终的目的路由器,它很可能是一个中间路由器。
条目给出的下一个路由器的地址用来转发在相应接口接收到的IP数据报文。
(3)标志:这个字段提供了另一组重要信息,例如,目的IP地址是一个主机地址还是一个网络地址。
此外,从标志中可以得知下一个路由器是一个真实路由器还是一个直接相连的接口。
(4)网络接口规范:为一些数据报文的网络接口规范,该规范将与报文一起被转发。
在通过路由表转发时,如果任何条目的第1个字段完全匹配目的IP地址(主机)或部分匹配条目的IP地址(网络),它将指示下一个路由器的IP地址。
这是一个重要的信息,因为这些信息直接告诉主机(具备路由功能的)数据包应该被转发到哪个路由器。
而条目中的所有其他字段将提供更多的辅助信息来为路由转发做决定。
如果没有找到一个完全匹配的IP,就接着搜索相匹配的网络ID。
如果找到,那么该数据报文会被转发到指定的路由器上。
可以看出,网络上的所有主机都通过这个路由表中的单个(这个)条目进行管理。
如果上述两个条件都不匹配,那么该数据报文将被转发到一个默认的路由器上。
如果上述步骤都失败,默认路由器也不存在,那么该数据报文最终无法被转发。
任何无法投递的数据报文都将产生一个ICMP主机不可达或ICMP网络不可达的错误,并将此错误返回给生成此数据报文的应用程序。
1.路由表的创建
Linux的路由表至少包括两个表(当启用策略路由时,还会有其他表):一个是LOCAL,另一个是MAIN。
在LOCAL表中会包含所有的本地设备地址。LOCAL路由表是在配置网络设备地址时自动创建的。
LOCAL表用于供Linux协议栈识别本地地址,以及进行本地各个不同网络接口之间的数据转发。
可以通过下面的命令查看LOCAL表的内容:
# ip route show table local type local
MAIN表用于各类网络IP地址的转发。它的建立既可以使用静态配置生成,也可以使用动态路由发现协议生成。
动态路由发现协议一般使用组播功能来通过发送路由发现数据,动态地交换和获取网络的路由信息,并更新到路由表中。
Linux下支持路由发现协议的开源软件有许多,常用的有Quagga、Zebra等。
7.8节会介绍如何使用Quagga动态容器路由发现的机制来实现Kubernetes的网络组网。
2.路由表的查看
我们可以使用ip route list命令查看当前的路由表:
# ip route list
在上面的例子代码中只有一个子网的路由,源地址是192.168.6.140(本机),目标地址在192.168.6.0/24网段的数据包都将通过eno16777736接口发送出去。
Netstat -rn是另一个查看路由表的工具:
# netstat -rn
在它显示的信息中,如果标志是U,则说明是可达路由;如果标志是G,则说明这个网络接口连接的是网关,否则说明这个接口直连主机。
7.3 Docker的网络实现
标准的Docker支持以下4类网络模式:
host模式:使用--net=host指定。
container模式:使用--net=container:NAME_or_ID指定。
none模式:使用--net=none指定。
bridge模式:使用--net=bridge指定,为默认设置。
在Kubernetes管理模式下通常只会使用bridge模式,所以本节只介绍在bridge模式下Docker是如何支持网络的。
在bridge模式下,Docker Daemon第1次启动时会创建一个虚拟的网桥,默认的名称是docker0,然后按照RPC1918的模型在私有网络空间中给这个网桥分配一个子网。
针对由Docker创建的每一个容器,都会创建一个虚拟的以太网设备(Veth设备对),
其中一端关联到网桥上,另一端使用Linux的网络命名空间技术,映射到容器内的eth0设备,然后从网桥的地址段内给eth0接口分配一个IP地址。
如图7.6所示就是Docker的默认桥接网络模型。
其中ip1是网桥的IP地址,Docker Daemon会在几个备选地址段里给它选一个地址,通常是以172开头的一个地址。这个地址和主机的IP地址是不重叠的。
ip2是Docker在启动容器时,在这个地址段选择的一个没有使用的IP地址分配给容器。
相应的MAC地址也根据这个IP地址,在 02:42:ac:11:00:00和02:42:ac:11:ff:ff的范围内生成,这样做可以确保不会有ARP冲突。
启动后,Docker还将Veth对的名称映射到eth0网络接口。ip3就是主 机的网卡地址。
在一般情况下,ip1、ip2和ip3是不同的IP段,所以在默认不做任何特殊配置的情况下,在外部是看不到ip1和ip2的。
这样做的结果就是,在同一台机器内的容器之间可以相互通信,不同主机上的容器不能相互通信,实际上它们甚至有可能在相同的网络地址范围内(不同主机上的docker0的地址段可能是一样的)。
为了让它们跨节点互相通信,就必须在主机的地址上分配端口,然后通过这个端口路由或代理到容器上。
这种做法显然意味着一定要在容器之间小心谨慎地协调好端口的分配,或者使用动态端口的分配技术。
在不同应用之间协调好端口分配是十分困难的事情,特别是集群水平扩展时。
而动态的端口分配也会带来高度复杂性,例如:每个应用程序都只能将端口看作一个符号(因为是动态分配的,所以无法提前设置)。
而且API Server要在分配完后,将动态端口插入配置的合适位置,服务也必须能互相找到对方等。
这些都是Docker的网络模型在跨主机访问时面临的问题。
1.查看Docker启动后的系统情况
我们已经知道,Docker网络在bridge模式下Docker Daemon启动时创建docker0网桥,并在网桥使用的网段为容器分配IP。
让我们看看实际的操作。
在刚刚启动Docker Daemon并且还没有启动任何容器时,网络协议栈的配置情况如下:
# systemctl start docker
# ip addr
# iptables-save
可以看到,Docker创建了docker0网桥,并添加了iptables规则。docker0网桥和iptables规则都处于root命名空间中。
通过解读这些规则,我们发现,在还没有启动任何容器时,如果启动了Docker Daemon,那么它已经做好了通信准备。
对这些规则的说明如下:
(1)在NAT表中有3条记录,前两条匹配生效后,都会继续执行DOCKER链,而此时DOCKER链为空,所以前两条只是做了一个框架,并没有实际效果。
(2)NAT表第3条的含义是,若本地发出的数据包不是发往docker0的,即是发往主机之外的设备的,则都需要进行动态地址修改(MASQUERADE),
将源地址从容器的地址(172段)修改为宿主机网卡的IP地址,之后就可以发送给外面的网络了。
(3)在FILTER表中,第1条也是一个框架,因为后继的DOCKER链是空的。
(4)在FILTER表中,第3条是说,docker0发出的包,如果需要Forward到非docker0的本地IP地址的设备,则是允许的。
这样,docker0设备的包就可以根据路由规则中转到宿主机的网卡设备,从而访问外面的网络。
(5)FILTER表中,第4条是说,docker0的包还可以被中转给docker0本身,即连接在docker0网桥上的不同容器之间的通信也是允许的。
(6)FILTER表中,第2条是说,如果接收到的数据包属于以前已经建立好的连接,那么允许直接通过。
这样接收到的数据包自然又走回docker0,并中转到相应的容器。
除了这些Netfilter的设置,Linux的ip_forward功能也被Docker Daemon打开了:
# cat /proc/sys/net/ipv4/ip_forward
另外,我们可以看到刚刚启动Docker后的Route表,和启动前没有什么不同:
# ip route
2.查看容器启动后的情况(容器无端口映射)
刚才查看了Docker服务启动后的网络情况。现在启动一个Registry容器(不使用任何端口镜像参数),看一下网络堆栈部分相关的变化:
# docker run --name register -d registry
# ip addr
可以看到如下情况:
(1)宿主机器上的Netfilter和路由表都没有变化,说明在不进行端口映射时,Docker的默认网络是没有特殊处理的。相关的NAT和FILTER这两个Netfilter链还是空的。
(2)宿主机上的Veth对已经建立,并连接到容器内。
我们再次进入刚刚启动的容器内,看看网络栈是什么情况。容器内部的IP地址和路由如下:
# docker exec -it xxxx bash
# ip route
可以看到,默认停止的回环设备lo已经被启动,外面宿主机连接进来的Veth设备也被命名成了eth0,并且已经配置了地址172.17.0.10。
路由信息表包含一条到docker0的子网路由和一条到docker0的默认路由。
3.查看容器启动后的情况(容器有端口映射)
下面用带端口映射的命令启动registry:
# docker run --name register -d -p 1180:5000 registry
启动后查看iptables的变化:
# iptables-save
从新增的规则可以看出,Docker服务在NAT和FILTER两个表内添加的两个DOCKER子链都是给端口映射用的。在本例中我们需要把外面宿主机的1180端口映射到容器的5000端口。
通过前面的分析我们知道,无论是宿主机接收到的还是宿主机本地协议栈发出的,目标地址是本地IP地址的包都会经过NAT表中的DOCKER子链。
Docker为每一个端口映射都在这个链上增加了到实际容器目标地址和目标端口的转换。
经过这个DNAT的规则修改后的IP包,会重新经过路由模块的判断进行转发。
由于目标地址和端口已经是容器的地址和端口,所以数据自然就被转发到docker0上,从而被转发到对应的容器内部。
当然在Forward时,也需要在DOCKER子链中添加一条规则,如果目标端口和地址是指定容器的数据,则允许通过。
在Docker按照端口映射的方式启动容器时,主要的不同就是上述iptables部分。而容器内部的路由和网络设备,都和不做端口映射时一样,没有任何变化。
1.Docker的网络局限
我们从Docker对Linux网络协议栈的操作可以看到,Docker一开始没有考虑到多主机互联的网络解决方案。
Docker一直以来的理念都是“简单为美”,几乎所有尝试Docker的人都被它“用法简单,功能强大”的特性所吸引,这也是Docker迅速走红的一个原因。
我们都知道,虚拟化技术中最为复杂的部分就是虚拟化网络技术,即使是单纯的物理网络部分,也是一个门槛很高的技能领域,通常只被少数网络工程师所掌握,
所以我们可以理解结合了物理网络的虚拟网络技术有多难。
在Docker之前,所有接触过OpenStack的人都对其网络问题讳莫如深,
Docker明智地避开这个“雷区”,让其他专业人员去用现有的虚拟化网络技术解决Docker主机的互联问题,以免让用户觉得Docker太难,从而放弃学习和使用Docker。
Docker成名以后,重新开始重视网络解决方案,收购了一家Docker网络解决方案公司—Socketplane,原因在于这家公司的产品广受好评,
但有趣的是Socketplane的方案就是以Open vSwitch为核心的,其还为Open vSwitch提供了Docker镜像,以方便部署程序。
之后,Docker开启了一个宏伟的虚拟化网络解决方案—Libnetwork,如图7.7所示是其概念图。
这个概念图没有了IP,也没有了路由,已经颠覆了我们的网络常识,对于不怎么懂网络的大多数人来说,它的确很有诱惑力,
未来是否会对虚拟化网络的模型产生深远冲击,我们还不得而知,但它仅仅是Docker官方当前的一次“尝试”。
针对目前Docker的网络实现,Docker使用的Libnetwork组件只是将Docker平台中的网络子系统模块化为一个独立库的简单尝试,离成熟和完善还有一段距离。
7.4 Kubernetes的网络实现
在实际的业务场景中,业务组件之间的关系十分复杂,特别是随着微服务理念逐步深入人心,应用部署的粒度更加细小和灵活。
为了支持业务应用组件的通信,Kubernetes网络的设计主要致力于解决以下问题:
(1)容器到容器之间的直接通信。
(2)抽象的Pod到Pod之间的通信。
(3)Pod到Service之间的通信。
(4)集群外部与内部组件之间的通信。
其中第3条、第4条在之前的章节里都有所讲解,本节对更为基础的第1条与第2条进行深入分析和讲解。
7.4.1 容器到容器的通信
同一个Pod内的容器(Pod内的容器是不会跨宿主机的)共享同一个网络命名空间,共享同一个Linux协议栈。
所以对于网络的各类操作,就和它们在同一台机器上一样,它们甚至可以用localhost地址访问彼此的端口。
这么做的结果是简单、安全和高效,也能减小将已经存在的程序从物理机或者虚拟机移植到容器下运行的难度。
其实,在容器技术出来之前,大家早就积累了如何在一台机器上运行一组应用程序的经验,例如,如何让端口不冲突,以及如何让客户端发现它们等。
我们来看一下Kubernetes是如何利用Docker的网络模型的。
如图7.8中的阴影部分所示,在Node上运行着一个Pod实例。在我们的例子中,容器就是图7.8中的容器1和容器2。
容器1和容器2共享一个网络的命名空间,共享一个命名空间的结果就是它们好像在一台机器上运行,它们打开的端口不会有冲突,
可以直接使用Linux的本地IPC进行通信(例如消息队列或者管道)。
其实,这和传统的一组普通程序运行的环境是完全一样的,传统程序不需要针对网络做特别的修改就可以移植了,它们之间的互相访问只需要使用localhost就可以。
例如,如果容器2运行的是MySQL,那么容器1使用localhost:3306就能直接访问这个运行在容器2上的MySQL了。
7.4.2 Pod之间的通信
我们看了同一个Pod内的容器之间的通信情况,再看看Pod之间的通信情况。
每一个Pod都有一个真实的全局IP地址,同一个Node内的不同Pod之间可以直接采用对方Pod的IP地址通信,而且不需要采用其他发现机制,例如DNS、Consul或者etcd。
Pod容器既有可能在同一个Node上运行,也有可能在不同的Node上运行,所以通信也分为两类:同一个Node内Pod之间的通信和不同Node上Pod之间的通信。
1.同一个Node内Pod之间的通信
我们看一下同一个Node内两个Pod之间的关系,如图7.9所示。
可以看出,Pod1和Pod2都是通过Veth连接到同一个docker0网桥上的,它们的IP地址IP1、IP2都是从docker0的网段上动态获取的,它们和网桥本身的IP3是同一个网段的。
另外,在Pod1、Pod2的Linux协议栈上,默认路由都是docker0的地址,也就是说所有非本地地址的网络数据,都会被默认发送到docker0网桥上,由docker0网桥直接中转。
综上所述,由于它们都关联在同一个docker0网桥上,地址段相同,所以它们之间是能直接通信的。
2.不同Node上Pod之间的通信
Pod的地址是与docker0在同一个网段的,我们知道docker0网段与宿主机网卡是两个完全不同的IP网段,并且不同Node之间的通信只能通过宿主机的物理网卡进行,
因此要想实现不同Node上Pod容器之间的通信,就必须想办法通过主机的这个IP地址进行寻址和通信。
另一方面,这些动态分配且藏在docker0之后的所谓“私有”IP地址也是可以找到的。
Kubernetes会记录所有正在运行的Pod的IP分配信息,并将这些信息保存在etcd中(作为Service的Endpoint)。
这些私有IP信息对于Pod到Pod的通信也是十分重要的,因为我们的网络模型要求Pod到Pod使用私有IP进行通信。所以首先要知道这些IP是什么。
之前提到,Kubernetes的网络对Pod的地址是平面的和直达的,所以这些Pod的IP规划也很重要,不能有冲突。只要没有冲突,我们就可以想办法在整个Kubernetes的集群中找到它。
综上所述,要想支持不同Node上Pod之间的通信,就要满足两个条件:
(1)在整个Kubernetes集群中对Pod的IP分配进行规划,不能有冲突;
(2)找到一种办法,将Pod的IP和所在Node的IP关联起来,通过这个关联让Pod可以互相访问。
根据条件1的要求,我们需要在部署Kubernetes时对docker0的IP地址进行规划,保证每个Node上的docker0地址都没有冲突。
我们可以在规划后手工配置到每个Node上,或者做一个分配规则,由安装的程序自己去分配占用。例如,Kubernetes的网络增强开源软件Flannel就能够管理资源池的分配。
根据条件2的要求,Pod中的数据在发出时,需要有一个机制能够知道对方Pod的IP地址挂在哪个具体的Node上。
也就是说先要找到Node对应宿主机的IP地址,将数据发送到这个宿主机的网卡,然后在宿主机上将相应的数据转发到具体的docker0上。
一旦数据到达宿主机Node,则那个Node内部的docker0便知道如何将数据发送到Pod。如图7.10所示。
在图7.10中,IP1对应的是Pod1,IP2对应的是Pod2。
Pod1在访问Pod2时,首先要将数据从源Node的eth0发送出去,找到并到达Node2的 eth0。即先是从IP3到IP4的递送,之后才是从IP4到IP2的递送。
在谷歌的GCE环境中,Pod的IP管理(类似docker0)、分配及它们之间的路由打通都是由GCE完成的。
Kubernetes作为主要在GCE上面运行的框架,它的设计是假设底层已经具备这些条件,所以它分配完地址并将地址记录下来就完成了它的工作。
在实际的GCE环境中,GCE的网络组件会读取这些信息,实现具体的网络打通。
而在实际生产环境中,因为安全、费用、合规等种种原因,Kubernetes的客户不可能全部使用谷歌的GCE环境,
所以在实际的私有云环境中,除了需要部署Kubernetes和Docker,还需要额外的网络配置,甚至通过一些软件来实现Kubernetes对网络的要求。
做到这些后,Pod和Pod之间才能无差别地进行透明通信。
为了达到这个目的,开源界有不少应用增强了Kubernetes、Docker的网络,在后面的章节中会介绍几个常用的组件及其组网原理。
7.5 Pod和Service网络实战
Docker给我们带来了不同的网络模式,Kubernetes也以一种不同的方式来解决这些网络模式的挑战,
但其方式有些难以理解,特别是对于刚开始接触Kubernetes的网络的开发者来说。
我们在前面学习了Kubernetes、Docker的理论,本节将通过一个完整的实验,
从部署一个Pod开始,一步一步地部署那些Kubernetes的组件,来剖析Kubernetes在网络层是如何实现及工作的。
这里使用虚拟机来完成实验。如果要部署在物理机器上或者云服务商的环境中,则涉及的网络模型很可能稍微有所不同。
不过,从网络角度来看,Kubernetes的机制是类似且一致的。
好了,来看看我们的实验环境,如图7.11所示。
Kubernetes的网络模型要求每个Node上的容器都可以相互访问。
默认的Docker网络模型提供了一个IP地址段是172.17.0.0/16的docker0网桥。
每个容器都会在这个子网内获得IP地址,并且将docker0网桥的IP地址(172.17.42.1)作为其默认网关。
需要注意的是,Docker宿主机外面的网络不需要知道任何关于这个172.17.0.0/16的信息或者知道如何连接到其内部,
因为Docker的宿主机针对容器发出的数据,在物理网卡地址后面都做了IP伪装MASQUERADE(隐含NAT)。
也就是说,在网络上看到的任何容器数据流都来源于那台Docker节点的物理IP地址。
这里所说的网络都指连接这些主机的物理网络。
这个模型便于使用,但是并不完美,需要依赖端口映射的机制。
在Kubernetes的网络模型中,每台主机上的docker0网桥都是可以被路由到的。
也就是说,在部署了一个Pod时,在同一个集群内,各主机都可以访问其他主机上的Pod IP,并不需要在主机上做端口映射。
综上所述,我们可以在网络层将Kubernetes的节点看作一个路由器。
如果将实验环境改画成一个网络图,那么它看起来如图7.12所示。
为了支持Kubernetes网络模型,我们采取了直接路由的方式来实现,在每个Node上都配置相应的静态路由项,
例如在192.168.1.129这个Node上配置了两个路由项:
# route add -net 10.1.20.0 netmask 255.255.255.0 gw 192.168.1.130
# route add -net 10.1.30.0 netmask 255.255.255.0 gw 192.168.1.131
这意味着,每一个新部署的容器都将使用这个Node(docker0的网桥IP)作为它的默认网关。
而这些Node(类似路由器)都有其他docker0的路由信息,这样它们就能够相互连通了。
接下来通过一些实际的案例,来看看Kubernetes在不同的场景下其网络部分到底做了什么。
第1步:部署一个RC/Pod
部署的RC/Pod描述文件如下(frontend-controller.yaml):
apiVersion: v1
kind: ReplicationController
metadata:
name: frontend
labels:
name: frontend
spec:
replicas: 1
selector:
name: frontend
template:
metadata:
labels:
name: frontend
spec:
containers:
- name: php-redis
image: kubeguide/example-guestbook-php-redis
env:
- name: GET_HOSTS_FROM
value: env
ports:
- containerPort: 80
hostPort: 80
为了便于观察,我们假定在一个空的Kubernetes集群上运行,提前清理了所有Replication Controller、Pod和其他Service:
# kubectl get rc
# kubectl get services
# kubectl get pods
让我们检查一下此时某个Node上的网络接口都有哪些。Node1的状态是:
# ifconfig
可以看出,有一个docker0网桥和一个本地地址的网络端口。
现在部署一下我们在前面准备的RC/Pod配置文件,看看发生了什么:
# kubectl create -f frontend-controller.yaml
# kubectl get pods
可以看到一些有趣的事情。Kubernetes为这个Pod找了一个主机192.168.1.130(Node2)来运行它。
另外,这个Pod获得了一个在Node2的docker0网桥上的IP地址。
我们登录Node2查看正在运行的容器:
# docker ps
在Node2上现在运行了两个容器,在我们的RC/Pod定义文件中仅仅包含了一个,那么这第2个是从哪里来的呢?
第2个看起来运行的是一个叫作google_containers/pause:latest的镜像,而且这个容器已经有端口映射到它上面了,为什么是这样呢?
让我们深入容器内部去看一下具体原因。
使用Docker的inspect命令来查看容器的详细信息,特别要关注容器的网络模型:
# docker inspect xxxxx(google_containers/pause:latest镜像对应容器) | grep NetworkMode
# docker inspect xxxxx(php-redis容器) | grep NetworkMode
有趣的结果是,在查看完每个容器的网络模型后,我们可以看到这样的配置:
我们检查的第1个容器是运行 了“google_containers/pause:latest”镜像的容器,它使用了Docker默认的网络模型bridge;
而我们检查的第2个容器,也就是在RC/Pod中定义运行的php-redis容器,使用了非默认的网络配置和映射容器的模型,指定了映射目标容器为“google_containers/ pause:latest”。
一起来仔细思考这个过程,为什么Kubernetes要这么做呢?
首先,一个Pod内的所有容器都需要共用同一个IP地址,这就意味着一定要使用网络的容器映射模式。
然而,为什么不能只启动第1个Pod 中的容器,而将第2个Pod中的容器关联到第1个容器呢?
我们认为Kubernetes是从两方面来考虑这个问题的:
首先,如果在Pod内有多个容器的话,则可能很难连接这些容器;
其次,后面的容器还要依赖第1个被关联的容器,如果第2个容器关联到第1个容器,且第1个容器死掉的话,第2个容器也将死掉。
启动一个基础容器,然后将Pod内的所有容器都连接到它上面会更容易一些。
因为我们只需要为基础的这个Google_containers/pause容器执行端口映射规则,这也简化了端口映射的过程。
所以我们启动Pod后的网络模型类似于图7.13。
在这种情况下,实际Pod的IP数据流的网络目标都是这个google_containers/pause容器。
图7.13有点儿取巧地显示了是google_containers/pause容器将端口80的流量转发给了相关的容器。
而pause容器只是看起来转发了网络流量,但它并没有真的这么做。
实际上,应用容器直接监听了这些端口,和google_containers/pause容器共享了同一个网络堆栈。
这就是为什么在Pod内部实际容器的端口映射都显示到google_containers/pause容器上了。
我们可以通过docker port命令来检验一下:
# docker ps
综上所述,google_containers/pause容器实际上只是负责接管这个Pod的Endpoint,并没有做更多的事情。
那么Node呢?它需要将数据流传给google_containers/pause容器吗?我们来检查一下iptables的规则,看看有什么发现:
# iptables-save
上面的这些规则并没有被应用到我们刚刚定义的Pod上。
当然,Kubernetes会给每一个Kubernetes节点都提供一些默认的服务,上面的规则就是Kubernetes的默认服务所需要的。
关键是,我们没有看到任何IP伪装的规则,并且没有任何指向Pod 10.1.20.4内部的端口映射。
第2步:发布一个服务
我们已经了解了Kubernetes如何处理最基本的元素即Pod的连接问题,接下来看一下它是如何处理Service的。
Service允许我们在多个Pod之间抽象一些服务,而且服务可以通过提供在同一个Service的多个Pod之间的负载均衡机制来支持水平扩展。
我们再次将环境初始化,删除刚刚创建的RC或Pod来确保集群是空的:
# kubectl stop rc frontend
# kubectl get rc
# kubectl get services
# kubectl get pods
然后准备一个名为frontend的Service配置文件(frontend-service.yaml):
apiVersion: v1
kind: Service
metadata:
name: frontend
labels:
name: frontend
spec:
ports:
- port: 80
# nodePort: 30001
selector:
name: frontend
# type:
# NodePort
接着在Kubernetes集群中定义这个服务:
# kubectl create -f frontend-service.yaml
# kubectl get services
在服务正确创建后,可以看到Kubernetes集群已经为这个服务分配了一个虚拟IP地址20.1.244.75,这个IP地址是在Kubernetes的Portal Network中分配的。
而这个Portal Network的地址范围是我们在Kubmaster上启动API服务进程时,使用--service-cluster-ip-range=xx命令行参数指定的:
# cat /etc/kubernetes/apiserver
KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=20.1.0.0/16"
这个IP段可以是任何段,只要不和docker0或者物理网络的子网冲突就可以。
选择任意其他网段的原因是这个网段将不会在物理网络和docker0网络上进行路由。
这个Portal Network针对每一个Node都有局部的特殊性,实际上它存在的意义是让容器的流量都指向默认网关(也就是docker0网桥)。
在继续实验前,先登录到Node1上看一下在我们定义服务后发生了什么变化。
首先检查一下iptables或Netfilter的规则:
# iptables-save
-A KUBE-PORTALS-CONTAINER -d 20.1.244.75/32 -p tcp -m comment --comment "default/frontend:" -m tcp -dport 80 -j REDIRECT --to-ports 59528
-A KUBE-PORTALS-HOST -d 20.1.244.75/32 -p tcp -m comment --comment "default/kubernetes:" -m tcp -dport 80 -j DNAT --to-destination 192.168.1.131:59528
第1行是挂在PREROUTING链上的端口重定向规则,所有进入的流量如果满足20.1.244.75: 80,则都会被重定向到端口33761。
第2行是挂在OUTPUT链上的目标地址NAT,做了和上述第1行规则类似的工作,但针对的是当前主机生成的外出流量。
所有主机生成的流量都需要使用这个DNAT规则来处理。
简而言之,这两个规则使用了不同的方式做了类似的事情,就是将所有从节点生成的发送给20.1.244.75:80的流量重定向到本地的33761端口。
至此,目标为Service IP地址和端口的任何流量都将被重定向到本地的33761端口。
这个端口连到哪里去了呢?这就到了kube-proxy发挥作用的地方了。
这个kube-proxy服务给每一个新创建的服务都关联了一个随机的端口号,并且监听那个特定的端口,为服务创建相关的负载均衡对象。
在我们的实验中,随机生成的端口刚好是33761。
通过监控Node1上的Kubernetes-Service的日志,在创建服务时可以看到下面的记录:
Opened iptables from-containers portal for service "default/frontend:" on TCP 20.1.244.75:80
Opened iptables from-host portal for service "default/frontend:" on TCP 20.1.244.75:80
现在我们知道,所有流量都被导入kube-proxy中了。
我们现在需要它完成一些负载均衡的工作,创建Replication Controller并观察结果,下 面是Replication Controller的配置文件(frontend-controller.yaml):
apiVersion: v1
kind: ReplicationController
metadata:
name: frontend
labels:
name: frontend
spec:
replicas: 3
selector:
name: frontend
template:
metadata:
labels:
name: frontend
spec:
containers:
- name: php-redis
image: kubeguide/example-guestbook-php-redis
env:
- name: GET_HOSTS_FROM
value: env
ports:
- containerPort: 80
# hostPort: 80
在集群发布上述配置文件后,等待并观察,确保所有Pod都运行起来了:
# kubectl create -f frontend-controller.yaml
# kubectl get pods -o wide
现在所有的Pod都运行起来了,Service将会把客户端请求负载分发到包含“name=frontend”标签的所有Pod上。
现在的实验环境如图7.14所示。
Kubernetes的kube-proxy看起来只是一个夹层,但实际上它只是在Node上运行的一个服务。
上述重定向规则的结果就是针对目标地址为服务IP的流量,将Kubernetes的kube-proxy变成了一个中间的夹层。
为了查看具体的重定向动作,我们会使用tcpdump来进行网络抓包操作。
首先,安装tcpdump:
# yum -y install tcpdump
安装完成后,登录Node1,运行tcpdump命令:
# tcpdump -nn -q -i eno16777736 port 80
需要捕获物理服务器以太网接口的数据包,Node1机器上的以太网接口名字叫作eno16777736。
再打开第1个窗口运行第2个tcpdump程序,不过我们需要一些额外的信息去运行它,即挂接在docker0桥上的虚拟网卡Veth的名称。
我们看到只有一个frontend容器在Node1主机上运行,所以可以使用简单的“ip addr”命令来查看唯一的Veth网络接口:
# ip addr
复制这个接口的名字,在第2个窗口中运行tcpdump命令:
# tcpdump -nn -q -i veth0558bfa host 20.1.244.75
同时运行这两个命令,并且将窗口并排放置,以便同时看到两个窗口的输出:
# tcpdump -nn -q -i eno16777736 port 80
# tcpdump -nn -q -i veth0558bfa host 20.1.244.75
好了,我们已经在同时捕获两个接口的网络包了。
这时再启动第3个窗口,运行一个“docker exec”命令来连接到我们的frontend容器内部 (你可以先执行docker ps来获得这个容器的ID):
# docker ps
执行命令进入容器内部:
# docker exec -it 容器ID bash
一旦进入运行的容器内部,我们就可以通过Pod的IP地址来访问服务了。
使用curl来尝试访问服务:
# curl 20.1.244.75
在使用curl访问服务时,将在抓包的两个窗口内看到:
IP 192.168.1.29.57452 > 10.1.30.8.8080: tcp 0
IP 10.1.30.8.8080 > 192.168.1.29.57452: tcp 0
这些信息说明了什么问题呢?让我们在网络图上用实线标出第1个窗口中网络抓包信息的含义(物理网卡上的网络流量),
并用虚线标出第2个窗口中网络抓包信息的含义(docker0网桥上的网络流量),如图 7.15所示。
注意,在图7.15中,虚线绕过了Node3的kube-proxy,这么做是因为Node3上的kube-proxy没有参与这次网络交互。
换句话说,Node1的kube-proxy服务直接和负载均衡到的Pod进行网络交互。
在查看第2个捕获包的窗口时,我们能够站在容器的视角看这些流量。
首先,容器尝试使用20.1.244.75:80打开TCP的Socket连接。同时,我们可以看到从服务地址20.1.244.75返回的数据。
从容器的视角来看,整个交互过程都是在服务之间进行的。
但是在查看一个捕获包的窗口时(上面的窗口),我们可以看到物理机之间的数据交互,
可以看到一个TCP连接从Node1的物理地址(192.168.1.129)发出,直接连接到运行Pod的主机Node3(192.168.1.131)。
总而言之,Kubernetes的kube-proxy作为一个全功能的代理服务器管理了两个独立的TCP连接:一个是从容器到kube-proxy:另一个是从kube-proxy到负载均衡的目标Pod。
如果清理一下捕获的记录,再次运行curl,则还可以看到网络流量被负载均衡转发到另一个节点Node2上了:
IP 192.168.1.29.57485 > 10.1.20.6.8080: tcp 0
IP 10.1.20.6.8080 > 192.168.1.29.57485: tcp 0
这一次,Kubernetes的Proxy将选择运行在Node2(10.1.20.1)上的Pod作为目标地址。网络流动图如图7.16所示。
到这里,你肯定已经知道另一个可能的负载均衡的路由结果了。
7.6 CNI网络模型
随着容器技术在企业生产系统中的逐步落地,用户对容器云的网络特性要求也越来越高。
跨主机容器间的网络互通已经成为基本要求,更高的要求包括容器固定IP地址、一个容器多个IP地址、多个子网隔离、 ACL控制策略、与SDN集成等。
目前主流的容器网络模型主要有Docker公司提出的Container Network Model(CNM)模型和CoreOS公司提出的Container Network Interface(CNI)模型。
7.6.1 CNM模型
CNM模型是由Docker公司提出的容器网络模型,现在已经被Cisco Contiv、Kuryr、Open Virtual Networking(OVN)、Project Calico、VMware、Weave和Plumgrid等项目所采纳。
另外,Weave、Project Calico、Kuryr和Plumgrid等项目也为CNM提供了网络插件的具体实现。
CNM模型主要通过Network Sandbox、Endpoint和Network这3个组件进行实现,如图7.17所示。
Network Sandbox:容器内部的网络栈,包括网络接口、路由表、DNS等配置的管理。
Sandbox可用Linux网络命名空间、FreeBSD Jail等机制进行实现。一个Sandbox可以包含多个Endpoint。
Endpoint:用于将容器内的Sandbox与外部网络相连的网络接口。
可以使用veth对、Open vSwitch的内部port等技术进行实现。一个Endpoint仅能够加入一个Network。
Network:可以直接互连的Endpoint的集合。可以通过Linux网 桥、VLAN等技术进行实现。一个Network包含多个Endpoint。
7.6.2 CNI模型
CNI是由CoreOS公司提出的另一种容器网络规范,现在已经被Kubernetes、rkt、Apache Mesos、Cloud Foundry和Kurma等项目采纳。
另外,Contiv Networking, Project Calico、Weave、SR-IOV、Cilium、Infoblox、Multus、Romana、Plumgrid和Midokura等项目也为CNI提供网络插件的具体实现。
图7.18描述了容器运行环境与各种网络插件通过CNI进行连接的模型。
CNI定义的是容器运行环境与网络插件之间的简单接口规范,通过一个JSON Schema定义CNI插件提供的输入和输出参数。
一个容器可以通过绑定多个网络插件加入多个网络中。
本节将对Kubernetes如何实现CNI模型进行详细说明。
1.CNI规范概述
CNI提供了一种应用容器的插件化网络解决方案,定义对容器网络进行操作和配置的规范,通过插件的形式对CNI接口进行实现。
CNI是由rkt Networking Proposal发展而来的,试图提供一种普适的容器网络解决方案。
CNI仅关注在创建容器时分配网络资源,和在销毁容器时删除网络资源,这使得CNI规范非常轻巧、易于实现,得到了广泛的支持。
在CNI模型中只涉及两个概念:容器和网络。
容器(Container):是拥有独立Linux网络命名空间的环境,例如使用Docker或rkt创建的容器。
关键之处是容器需要拥有自己的Linux网络命名空间,这是加入网络的必要条件。
网络(Network):表示可以互连的一组实体,这些实体拥有各自独立、唯一的IP地址,可以是容器、物理机或者其他网络设备(比 如路由器)等。
对容器网络的设置和操作都通过插件(Plugin)进行具体实现,CNI插件包括两种类型:CNI Plugin和IPAM(IP Address Management)Plugin。
CNI Plugin负责为容器配置网络资源,IPAM Plugin负责对容器的IP地址进行分配和管理。
IPAM Plugin作为CNI Plugin的一部分,与CNI Plugin一起工作。
2.CNI Plugin插件详解
CNI Plugin包括3个基本接口的定义:添加(ADD)、删除(DELETE)、检查(CHECK)和版本查询(VERSION)。
这些接口的具体实现要求插件提供一个可执行的程序,在容器网络添加或删除时进行调用,以完成具体的操作。
(1)添加:将容器添加到某个网络。
主要过程为在Container Runtime创建容器时,先创建好容器内的网络命名空间(Network Namespace),然后调用CNI插件为该netns进行网络配置,最后启动容器内的进程。
添加接口的参数如下:
Version:CNI版本号。
Container ID:容器ID。
Network namespace path:容器的网络命名空间路径,例如/proc/[pid]/ns/net。
Network configuration:网络配置JSON文档,用于描述容器待加入的网络。
Extra arguments:其他参数,提供基于容器的CNI插件简单配置机制。
Name of the interface inside the container:容器内的网卡名。
返回的信息如下:
Interfaces list:网卡列表,根据Plugin的实现,可能包括Sandbox Interface名称、主机Interface名称、每个Interface的地址等信 息。
IPs assigned to the interface:IPv4或者IPv6地址、网关地址、路 由信息等。
DNS information:DNS相关的信息。
(2)删除:容器销毁时将容器从某个网络中删除。
删除接口的参数如下。
Version:CNI版本号。
Container ID:容器ID。
Network namespace path:容器的网络命名空间路径,例如/proc/[pid]/ns/net。
Network configuration:网络配置JSON文档,用于描述容器待加入的网络。
Extra arguments:其他参数,提供基于容器的CNI插件简单配 置机制。
Name of the interface inside the container:容器内的网卡名。
(3)检查:检查容器网络是否正确设置。
检查接口的参数如下:
Container ID:容器ID。
Network namespace path:容器的网络命名空间路径,例如/proc/[pid]/ns/net。
Network configuration:网络配置JSON文档,用于描述容器待加入的网络。
Extra arguments:其他参数,提供基于容器的CNI插件简单配置机制。
Name of the interface inside the container:容器内的网卡名。
(4)版本查询:查询网络插件支持的CNI规范版本号。
无参数,返回值为网络插件支持的CNI规范版本号。
CNI插件应能够支持通过环境变量和标准输入传入参数。
可执行文件通过网络配置参数中的type字段标识的文件名在环境变量CNI_PATH设定的路径下进行查找。
一旦找到,容器运行时将调用该可执行程序,并传入以下环境变量和网络配置参数,供该插件完成容器网络资源和参数的设置。
环境变量参数如下:
CNI_COMMAND:接口方法,包括ADD、DEL和VERSION。
CNI_CONTAINERID:容器ID。
CNI_NETNS:容器的网络命名空间路径,例如/proc/[pid]/ns/net。
CNI_IFNAME:待设置的网络接口名称。
CNI_ARGS:其他参数,为key=value格式,多个参数之间用分号分隔,例如"FOO=BAR; ABC=123"。
CNI_PATH:可执行文件的查找路径,可以设置多个。
网络配置参数则由一个JSON报文组成,以标准输入(stdin)的方式传递给可执行程序。
网络配置参数如下:
cniVersion(string):CNI版本号。
name(string):网络名称,应在一个管理域内唯一。
type(string):CNI插件的可执行文件的名称。
args(dictionary):其他参数。
ipMasq(boolean):是否设置IP Masquerade(需插件支持),适用于主机可作为网关的环境中。
ipam:IP地址管理的相关配置。
- type(string):IPAM可执行的文件名。
dns:DNS服务的相关配置。
- nameservers(list of strings):名字服务器列表,可以使用IPv4或IPv6地址。
- domain(string):本地域名,用于短主机名查询。
- search(list of strings):按优先级排序的域名查询列表。
- options(list of strings):传递给resolver的选项列表。
下面的例子定义了一个名为dbnet的网络配置参数,IPAM使用host-local进行设置:
{
“cniVersion”: "0.4.0",
“name”: "dbnet",
“type”: "bridge",
“bridge”: "cni0",
“ipm”: {
“type”: "host-local",
“subnet”: "10.1.0.0/16",
“gateway”: "10.1.0.1"
}
“dns”: {
“nameservers”: ["10.1.0.1"]
}
}
3.IPAM Plugin插件详解
为了减轻CNI Plugin对IP地址管理的负担,在CNI规范中设置了一个新的插件专门用于管理容器的IP地址(还包括网关、路由等信息),被称为IPAM Plugin。
通常由CNI Plugin在运行时自动调用IPAM Plugin完成容器IP地址的分配。
IPAM Plugin负责为容器分配IP地址、网关、路由和DNS,典型的实现包括host-local和dhcp。
与CNI Plugin类似,IPAM插件也通过可执行程序完成IP地址分配的具体操作。
IPAM可执行程序也处理传递给CNI插件的环境变量和通过标准输入(stdin)传入的网络配置参数。
如果成功完成了容器IP地址的分配,则IPAM插件应该通过标准输 出(stdout)返回以下JSON报文。
其中包括ips、routes和dns三段内容。
ips段:分配给容器的IP地址(也可能包括网关)。
routes段:路由规则记录。
dns段:DNS相关的信息。
4.多网络插件
在很多情况下,一个容器需要连接多个网络,CNI规范支持为一个容器运行多个CNI Plugin来实现这个目标。
多个网络插件将按照网络配置列表中的顺序执行,并将前一个网络配置的执行结果传递给后面的网络配置。
多网络配置用JSON报文进行配置,包括如下信息:
cniVersion(string):CNI版本号。
name(string):网络名称,应在一个管理域内唯一,将用于下面的所有Plugin。
plugins(list):网络配置列表。
下面的例子定义了两个网络配置参数,分别作用于两个插件,第1个为bridge,第2个为tuning。
CNI将首先执行第1个bridge插件设置容器的网络,然后执行第2个tuning插件:
{
“cniVersion”: "0.4.0",
“name”: "dbnet",
“plugins”: [
{
“type”: "bridge",
“bridge”: "cni0",
“args”: {
“labels”: {
“appVersion”: "1.0"
}
},
“ipm”: {
“type”: "host-local",
“subnet”: "10.1.0.0/16",
“gateway”: "10.1.0.1"
},
“dns”: {
“nameservers”: ["10.1.0.1"]
}
},
{
“type”: "tuning",
"sysctl": {
“net.core.somaxconn”: "500"
}
}
]
}
在容器运行且执行第1个bridge插件时,网络配置参数将被设置为:
{
“cniVersion”: "0.4.0",
“name”: "dbnet",
“type”: "bridge",
“bridge”: "cni0",
“args”: {
“labels”: {
“appVersion”: "1.0"
}
},
“ipm”: {
“type”: "host-local",
“subnet”: "10.1.0.0/16",
“gateway”: "10.1.0.1"
},
“dns”: {
“nameservers”: ["10.1.0.1"]
}
}
接下来执行第2个tuning插件,网络配置参数将被设置为:
{
“cniVersion”: "0.4.0",
“name”: "dbnet",
“type”: "tuning",
"sysctl": {
“net.core.somaxconn”: "500"
},
"prevResult": {
“ips”: [
{
“version”: "4",
“address”: "10.0.0.5/32",
“interface”: 2
}
],
“ips”: [
{
“version”: "4",
“address”: "10.0.0.5/32",
“interface”: 2
}
],
}
}
其中,prevResult字段包含的信息为上一个bridge插件执行的结果。
在删除多个CNI Plugin时,则以逆序执行删除操作,以上例为例,将先删除tuning插件的网络配置,其中prevResult字段包含的信息为新增操作(ADD)时补充的信息:
然后删除bridge插件的网络配置,其中prevResult字段包含的信息也是在新增操作(ADD)时补充的信息:
1.命令返回信息说明
对于ADD或DELETE操作,返回码为0表示执行成功,非0表示失败,并以JSON报文的格式通过标准输出(stdout)返回操作的结果。
以ADD操作为例,成功将容器添加到网络的结果将返回以下JSON 报文。
其中ips、routes和dns段的信息应该与IPAM Plugin(IPAM Plugin的说明详见下节)返回的结果相同,重要的是interfaces段,应通过CNI Plugin进行设置并返回:
接口调用失败时,返回码不为0,应通过标准输出返回包含错误信息的如下JSON报文:
错误码包括如下内容。
CNI版本不匹配。
在网络配置中存在不支持的字段,详细信息应在msg中说明。
7.6.3 在Kubernetes中使用网络插件
Kubernetes目前支持两种网络插件的实现。
CNI插件:根据CNI规范实现其接口,以与插件提供者进行对接。
kubenet插件:使用bridge和host-local CNI插件实现一个基本的cbr0。
为了在Kubernetes集群中使用网络插件,需要在kubelet服务的启动参数上设置下面两个参数。
--network-plugin-dir:kubelet启动时扫描网络插件的目录。
--network-plugin:网络插件名称,对于CNI插件,设置为cni即可,无须关注--network-plugin-dir的路径。
对于kubenet插件,设置为kubenet,目前仅实现了一个简单的cbr0 Linux网桥。
在设置--network-plugin="cni"时,kubelet还需设置下面两个参数。
--cni-conf-dir:CNI插件的配置文件目录,默认为/etc/cni/net.d。该目录下配置文件的内容需要符合CNI规范。
--cni-bin-dir:CNI插件的可执行文件目录,默认为/opt/cni/bin。
目前已有多个开源项目支持以CNI网络插件的形式部署到Kubernetes集群中,进行Pod的网络设置和网络策略的设置,
包括:Calico、Canal、Cilium、Contiv、Flannel、Romana、Weave Net等。
7.7 Kubernetes网络策略
为了实现细粒度的容器间网络访问隔离策略,Kubernetes从1.3版本开始,由SIG-Network小组主导研发了Network Policy机制,目前已升级为networking.k8s.io/v1稳定版本。
Network Policy的主要功能是对Pod间的网络通信进行限制和准入控制,设置方式为将Pod的Label作为查询条件,设置允许访问或禁止访问的客户端Pod列表。
目前查询条件可以作 用于Pod和Namespace级别。
为了使用Network Policy,Kubernetes引入了一个新的资源对象NetworkPolicy,供用户设置Pod间网络访问的策略。
但仅定义一个网络策略是无法完成实际的网络隔离的,还需要一个策略控制器(Policy Controller)进行策略的实现。
策略控制器由第三方网络组件提供,目前Calico、Cilium、Kube-router、Romana、Weave Net等开源项目均支持网络策略的实现。
Network Policy的工作原理如图7.19所示,
policy controller需要实现一个API Listener,监听用户设置的NetworkPolicy定义,并将网络访问规则通过各Node的Agent进行实际设置(Agent则需要通过CNI网络插件实现)。
7.7.1 网络策略配置说明
网络策略的设置主要用于对目标Pod的网络访问进行限制,
在默认情况下对所有Pod都是允许访问的,在设置了指向Pod的NetworkPolicy网络策略之后,到Pod的访问才会被限制。
下面通过一个例子对NetworkPolicy资源对象的使用进行说明:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: test-network-policy
namespace: default
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
- Egress
ingress:
- from:
- ipBlock:
cidr: 172.17.0.0/16
except:
- 172.17.1.0/24
- namespaceSelector:
matchLabels:
priject: myproject
- podSelector:
matchLabels:
priject: frontend
ports:
- protocol: TCP
port: 6379
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/24
ports:
- protocol: TCP
port: 5978
主要的参数说明如下:
podSelector:用于定义该网络策略作用的Pod范围,本例的选择条件为包含“role=db”标签的Pod。
policyTypes:网络策略的类型,包括ingress和egress两种,用于设置目标Pod的入站和出站的网络限制。
ingress:定义允许访问目标Pod的入站白名单规则,满足from条件的客户端才能访问ports定义的目标Pod端口号。
- from:对符合条件的客户端Pod进行网络放行,规则包括基于客户端Pod的Label、基于客户端Pod所在的Namespace的Label或者客户端的IP范围。
- ports:允许访问的目标Pod监听的端口号。
egress:定义目标Pod允许访问的“出站”白名单规则,目标Pod 仅允许访问满足to条件的服务端IP范围和ports定义的端口号。
- to:允许访问的服务端信息,可以基于服务端Pod的Label、基于服务端Pod所在的Namespace的Label或者服务端IP范围。
- ports:允许访问的服务端的端口号。
通过本例的NetworkPolicy设置,对目标Pod的网络访问的效果如下:
该网络策略作用于Namespace“default”中含有“role=db”Label的全部Pod。
允许与目标Pod在同一个Namespace中的包含“role=frontend”Label的客户端Pod访问目标Pod。
允许属于包含“project=myproject”Label的Namespace的客户端Pod访问目标Pod。
允许从IP地址范围“172.17.0.0/16”的客户端Pod访问目标Pod,但是不包括IP地址范围“172.17.1.0/24”的客户端。
允许目标Pod访问IP地址范围“10.0.0.0/24”并监听5978端口的服务。
这里是关于namespaceSelector和podSelector的说明:在from或to的配置中,namespaceSelector和podSelector可以单独设置,也可以组合配置。
如果仅配置podSelector,则表示与目标Pod属于相同的Namespace,而组合设置则可以设置Pod所属的Namespace,例如:
- from:
- namespaceSelector:
matchLabels:
priject: myproject
- podSelector:
matchLabels:
priject: frontend
表示允许访问目标Pod的来源客户端Pod应具有如下属性:属于有“project=myproject”标签的Namespace,并且有“role=frontend”标签。
7.7.2 在Namespace级别设置默认的网络策略
在Namespace级别还可以设置一些默认的全局网络策略,以方便管理员对整个Namespace进行统一的网络策略设置。
默认禁止任何客户端访问该Namespace中的所有Pod:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {}
policyTypes:
- Ingress
默认允许任何客户端访问该Namespace中的所有Pod:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all
spec:
podSelector: {}
ingress:
- {}
policyTypes:
- Ingress
默认禁止该Namespace中的所有Pod访问外部服务:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {}
policyTypes:
- Egress
默认允许该Namespace中的所有Pod访问外部服务:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all
spec:
podSelector: {}
egress:
- {}
policyTypes:
- Egress
默认禁止任何客户端访问该Namespace中的所有Pod,同时禁止访问外部服务:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
7.7.3 NetworkPolicy的发展
Kubernetes从1.12版本开始,引入了对SCTP协议的支持,目前为Alpha版本的功能,可以通过打开--feature-gates=SCTPSupport=true特性开关启用。
开启之后,可以在NetworkPolicy资源对象中设置protocol字段的值为SCTP,启用对SCTP协议的网络隔离设置。
这要求CNI插件提供对SCTP协议的支持。
7.8 开源的网络组件
Kubernetes的网络模型假定了所有Pod都在一个可以直接连通的扁平网络空间中。这在GCE里面是现成的网络模型,Kubernetes假定这个网络已经存在。
而在私有云里搭建Kubernetes集群,就不能假定这种网络已经存在了。
我们需要自己实现这个网络假设,将不同节点上的Docker容器之间的互相访问先打通,然后运行Kubernetes。
目前已经有多个开源组件支持容器网络模型。
本节介绍几个常见的网络组件及其安装配置方法,包括Flannel、Open vSwitch、直接路由和Calico。
7.8.1 Flannel
Flannel之所以可以搭建Kubernetes依赖的底层网络,是因为它能实现以下两点。
(1)它能协助Kubernetes,给每一个Node上的Docker容器都分配互相不冲突的IP地址。
(2)它能在这些IP地址之间建立一个覆盖网络(Overlay Network),通过这个覆盖网络,将数据包原封不动地传递到目标容器内。
现在,通过图7.20来看看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的启动参数将分配给它的地址段传递进去:
--bip=172.17.18.1/24
通过这些操作,Flannel就控制了每个Node上的docker0地址段的地址,也就保障了所有Pod的IP地址在同一个水平网络中且不产生冲突了。
Flannel完美地实现了对Kubernetes网络的支持,但是它引入了多个网络组件,
在网络通信时需要转到flannel0网络接口,再转到用户态的flanneld程序,到对端后还需要走这个过程的反过程,所以也会引入一些网络的时延损耗。
另外,Flannel模型默认采用了UDP作为底层传输协议,UDP本身是非可靠协议,虽然两端的TCP实现了可靠传输,但在大流量、高并发的应用场景下还需要反复测试,确保没有问题。
Flannel的安装和配置如下。
1)安装etcd
由于Flannel使用etcd作为数据库,所以需要预先安装好etcd,此处不再赘述。
2)安装Flannel
需要在每个Node上都安装Flannel。
Flannel软件的下载地址为https://github.com/coreos/flannel/releases。
将下载的压缩包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:
[Unit]
Description=flanneld overlay address etcd agent
After=network.target
Before=docker.service
[Service]
Type=notify
EnvironmentFile=/etc/sysconfig/flanneld
ExecStart=/usr/bin/flanneld -etcd-endpoints=${FLANNEL_ETCD} $FLANNEL_OPTIONS
[Install]
RequiredBy=docker.service
WantedBy=multi-user.target
编辑配置文件/etc/sysconfig/flannel,设置etcd的URL地址:
FLANNEL_ETCD="http://192.168.18.3:2379"
FLANNEL_ETCD_KEY="/coreos.com/network"
在启动flanneld服务之前,需要在etcd中添加一条网络配置记录,这个配置将用于flanneld分配给每个Docker的虚拟IP地址段:
# etcdctl set /coreos.com/network/config '{ "Network": "10.1.0.0/16" }'
由于Flannel将覆盖docker0网桥,所以如果Docker服务已启动,则需要停止Docker服务。
4)启动flanneld服务
# systemctl restart flanneld
5)设置docker0网桥的IP地址
# mk-docker-opts.sh -i
# source /run/flannel/subnet.env
# ifconfig docker0 ${FLANNEL_SUBNET}
完成后确认网络接口docker0的IP地址属于flannel0的子网:
# ip addr
6)重新启动Docker服务
# systemctl restart docker
至此就完成了Flannel覆盖网络的设置。
使用ping命令验证各Node上docker0之间的相互访问。
例如在Node1(docker0 IP=10.1.10.1)机器上ping Node2的docker0(docker0's IP=10.1.30.1),通过Flannel能够成功连接其他物理机的Docker网络:
# ping 10.1.30.1
我们也可以在etcd中查看Flannel设置的flannel0地址与物理机IP地址的对应规则:
# etcdctl ls /coreos.com/network/subnets
# etcdctl get /coreos.com/network/subnets/10.1.10.0-24
# etcdctl get /coreos.com/network/subnets/10.1.20.0-24
# etcdctl get /coreos.com/network/subnets/10.1.30.0-24
7.8.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.21所示。
首先,为了避免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网桥的路由规则项,就可以将两个容器的网络连接起来了。
1.网络通信过程
当容器内的应用访问另一个容器的地址时,数据包会通过容器内的默认路由发送给docker0网桥。
ovs的网桥是作为docker0网桥的端口存在的,它会将数据发送给ovs网桥。
ovs网络已经通过配置建立了和其他ovs网桥的GRE/VxLAN隧道,自然能将数据送达对端的Node,并送往docker0及Pod。
通过新增的路由项,Node本身的应用数据也被路由到docker0网桥上,和刚才的通信过程一样,自然也可以访问其他Node上的Pod。
2.OVS with GRE/VxLAN组网方式的特点
OVS的优势是,作为开源虚拟交换机软件,它相对成熟和稳定,而且支持各类网络隧道协议,通过了OpenStack等项目的考验。
另一方面,在前面介绍Flannel时可知,Flannel除了支持建立覆盖网络,保证Pod到Pod的无缝通信,还和Kubernetes、Docker架构体系紧密结合。
Flannel能够感知Kubernetes的Service,动态维护自己的路由表,还通过etcd来协助Docker对整个Kubernetes集群中docker0的子网地址分配。
而我们在使用OVS时,很多事情就需要手工完成了。
无论是OVS还是Flannel,通过覆盖网络提供的Pod到Pod通信都会引入一些额外的通信开销,如果是对网络依赖特别重的应用,则需要评估对业务的影响。
Open vSwitch的安装和配置如下。 以两个Node为例,目标网络拓扑如图7.22所示。
首先,确保节点192.168.18.128的Docker0采用了172.17.43.0/24网段,而192.168.18.131的Docker0采用了172.17.42.0/24网段,对应的参数为docker daemon的启动参数“--bip”设置的值。
1.在两个Node上安装ovs
# yum install openvswitch-2.4.0-1.x86_64.rpm
禁止selinux,配置后重启Linux:
# vi /etc/selinux/config
SELINUX=disabled
查看Open vSwitch的服务状态,应该启动ovsdb-server与ovs-vswitchd两个进程:
# service openvswitch status
查看Open vSwitch的相关日志,确认没有异常:
# more /var/log/messages | grep openv
注意,上述操作需要在两个节点机器上分别执行完成。
2.创建网桥和GRE隧道
接下来需要在每个Node上都建立ovs的网桥br0,
然后在网桥上创建一个GRE隧道连接对端网桥,
最后把ovs的网桥br0作为一个端口连接到docker0这个Linux网桥上(可以认为是交换机互联),
这样一来,两个节点机器上的docker0网段就能互通了。
下面以节点机器192.168.18.131为例,具体的操作步骤如下。
(1)创建ovs网桥:
# ovs-vsctl add-br br0
(2)创建GRE隧道连接对端,remote_ip为对端eth0的网卡地址:
# ovs-vsctl add-port br0 grel -- set interface grel type=gre option:remote_ip=192.168.18.128
(3)添加br0到本地docker0,使得容器流量通过OVS流经tunnel:
# brctl addif docker0 br0
(4)启动br0与docker0网桥:
# ip link set dev br0 up
# ip link set dev docker0 up
(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段的路由规则:
# ip route add 172.17.0.0/16 dev docker0
(6)清空Docker自带的iptables规则及Linux的规则,后者存在拒绝icmp报文通过防火墙的规则:
# iptables -t nat -F; iptables -F
在192.168.18.131上完成上述步骤后,在192.168.18.128节点执行同样的操作,
注意,GRE隧道里的IP地址要改为对端节点(192.168.18.131)的IP地址。
配置完成后,192.168.18.131的IP地址、docker0的IP地址及路由等 重要信息显示如下:
# ip addr
同样,192.168.18.128节点的重要信息如下:
# ip addr
3.两个Node上容器之间的互通测试
首先,在192.168.18.128节点上ping 192.168.18.131上的docker0地址172.17.42.1,验证网络的互通性:
# ping 172.17.42.1
下面通过tshark抓包工具来分析流量走向。
首先,在192.168.18.128节点监听在br0上是否有GRE报文,执行下面的命令,我们发现在br0上并没有GRE报文:
# tshark -i br0 -R proto GRE
在eth0上抓包,则发现了GRE封装的ping包报文通过,说明GRE是在物理网络上完成的封包过程:
# tshark -i eth0 -R ip proto GRE
至此,基于OVS的网络搭建成功,由于GRE是点对点的隧道通信方式,
所以如果有多个Node,则需要建立N×(N-1)条GRE隧道,即所有 Node组成一个网状网络,实现了全网互通。
7.8.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.23所示。
我们在每个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。
在Node1上用route add命令增加一条到Node2上docker0的静态路由规则:
# route add -net 10.1.20.0 netmask 255.255.255.0 gw 192.168.1.129
同样,在Node2上增加一条到Node1上docker0的静态路由规则:
# route add -net 10.1.10.0 netmask 255.255.255.0 gw 192.168.1.128
在Node1上通过ping命令验证到Node2上docker0的网络连通性。这里10.1.20.1为Node2上docker0网桥自身的IP地址:
# ping 10.1.20.1
可以看到,路由转发规则生效,Node1可以直接访问Node2上的docker0网桥,进一步就可以访问属于docker0网段的容器应用了。
在大规模集群中,在每个Node上都需要配置到其他docker0/Node的路由项,这会带来很大的工作量;
并且在新增机器时,对所有Node都需要修改配置;在重启机器时,如果docker0的地址有变化,则也需要修改所有Node的配置,这显然是非常复杂的。
为了管理这些动态变化的docker0地址,动态地让其他Node都感知到它,还可以使用动态路由发现协议来同步这些变化。
在运行动态路由发现协议代理的Node时,会将本机LOCAL路由表的IP地址通过组播协议发布出去,同时监听其他Node的组播包。
通过这样的信息交换,Node上的路由规则就都能够相互学习。
当然,路由发现协议本身还是很复杂的,感兴趣的话,可以查阅相关规范。
在实现这些动态路由发现协议的开源软件中,常用的有Quagga(http://www.quagga.net)、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镜像:
# docker pull index.alauda.cn/georce/router
在运行Quagga容器之前,需要确保每个Node上docker0网桥的子网地址不能重叠,也不能与物理机所在的网络重叠,这需要网络管理员的仔细规划。
下面以3个Node为例,每个Node的docker0网桥的地址如下(前提是Node物理机的IP地址不是10.1.X.X地址段):
Node 1: # ifconfig docker0 10.1.10.1/24
Node 2: # ifconfig docker0 10.1.20.1/24
Node 3: # ifconfig docker0 10.1.30.1/24
在每个Node上启动Quagga容器。需要说明的是,Quagga需要以--privileged特权模式运行,并且指定--net=host,表示直接使用物理机的网络:
# docker run -itd --name=router --privileged --net=host index.alauda.cn/georce/router
启动成功后,各Node上的Quagga会相互学习来完成到其他机器的docker0路由规则的添加。
一段时间后,在Node1上使用route -n命令来查看路由表,可以看到Quagga自动添加了两条到Node2和到Node3上docker0的路由规则:
# route -n
在Node2上查看路由表,可以看到自动添加了两条到Node1和Node3 上docker0的路由规则:
# route -n
至此,所有Node上的docker0就都可以互联互通了。
当然,聪明的你还会有新的疑问:这样做的话,由于每个Pod的地址都会被路由发现协议广播出去,会不会存在路由表过大的情况?
实际上,路由表通常都会有高速缓存,查找速度会很快,不会对性能产生太大的影响。
当然,如果你的集群容量在数千个Node以上,则仍然需要测试和评估路由表的效率问题。
7.8.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.24所示。
Calico在小规模集群中可以直接互联,在大规模集群中可以通过额外的BGP route reflector来完成,如图7.25所示。
此外,Calico基于iptables还提供了丰富的网络策略,实现了Kubernetes的Network Policy策略,提供容器间网络可达性限制的功能。Calico的系统架构如图7.26所示。
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配置文件,下载地址为http://docs.projectcalico.org/v3.5/getting-started/kubernetes/installation/hosted/calico.yaml,
该配置文件包括启动Calico所需的全部资源对象的定义,下面对它们逐个进行说明。
(1)Calico所需的配置,以ConfigMap对象进行创建(calico-config.yaml):
对主要参数说明如下:
etcd_endpoints:Calico使用etcd来保存网络拓扑和状态,该参 数指定etcd服务的地址。
calico_backend:Calico的后端,默认为bird。
cni_network_config:符合CNI规范的网络配置。其中 type=calico表示kubelet将从/opt/cni/bin目录下搜索名为calico的可执行文 件,并调用它来完成容器网络的设置。
ipam中的type=calico-ipam表示 kubelet将在/opt/cni/bin目录下搜索名为calico-ipam的可执行文件,用于 完成容器IP地址的分配。
如果etcd服务配置了TLS安全认证,则还需指定相应的ca、cert、 key等文件。
apiVersion: v1
kind: ConfigMap
metadata:
name: calico-config
namespace: kube-system
data:
# 配置etcd的服务URL
etcd_endpoints: "http://192.168.18.3:2379"
# 如果etcd启用了HTTPS安全认证,则需要配置etcd相关证书
etcd-ca: "" # "/calico-secrets/ etcd-ca"
etcd-cert: "" # "/calico-secrets/ etcd-cert"
etcd-key: "" # "/calico-secrets/ etcd-key"
typha_service_name: "none"
# 设置calico使用的backend类型,默认为bird
calico_backend: "bird"
# 设置MTU值
veth_mtu: "1440"
# 设置CNI网络配置文件
cni_network_config: |-
{
"name": "k8s-pod-network",
"cniVersion": "0.3.0",
"plugins": [
{
"type": "calico",
"log_level": "info",
"etcd_endpoints": "__ETCD_ENDPOINTS__",
"etcd_key_file": "__ETCD_KEY_FILE__",
"etcd_cert_file": "__ETCD_CERT_FILE__",
"etcd_ca_cert_file": "__ETCD_CA_CERT_FILE__",
"mtu": "__CNI_MTU__",
"ipam": {
"type": "calico-ipam"
},
"policy": {
"type": "k8s"
},
"kubernetes": {
"kubeconfig": "__KUBECONFIG_FILEPATH__"
}
},
{
"type": "portmap",
"snat": true,
"capablilties": {"portMappings": true}
}
]
}
(2)访问etcd所需的secret(calico-etcd-secret.yaml),对于无TLS的etcd服务,将data设置为空即可:
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: calico-etcd-secret
namespace: kube-system
data:
# 如果配置了TLS,则需要设置相应的证书和秘钥文件路径
# etcd-key: null
# etcd-cert: null
# etcd-ca: null
(3)calico-node,以DaemonSet方式在每个Node上都运行一个calico-node服务和一个install-cni服务:
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
name: calico-node
namespace: kube-system
labels:
k8s-app: calico-node
spec:
......
在该Pod中包括如下两个容器:
install-cni:在Node上安装CNI二进制文件到/opt/cni/bin目录下,并安装相应的网络配置文件到/etc/cni/net.d目录下,设置为initContainers并在运行完成后退出。
calico-node:Calico服务程序,用于设置Pod的网络资源,保证Pod的网络与各Node互联互通。它还需要以hostNetwork模式运行,直接使用宿主机网络。
calico-node服务的主要参数如下:
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:日志级别。
securityContext.privileged=true:以特权模式运行。
另外,如果启用RBAC权限控制,则可以设置ServiceAccount。
IP Pool可以使用两种模式:BGP或IPIP。
使用IPIP模式时,设置CALICO_IPV4POOL_IPIP="always",不使用IPIP模式时,设置 CALICO_IPV4POOL_IPIP="off",此时将使用BGP模式。
IPIP是一种将各Node的路由之间做一个tunnel,再把两个网络连接起来的模式,如图7.27所示。
启用IPIP模式时,Calico将在各Node上创建一个名为tunl0的虚拟网络接口。
BGP模式则直接使用物理机作为虚拟路由器(vRouter),不再创建额外的tunnel。
(4)calico-kube-controllers容器,用于对接Kubernetes集群中为Pod设置的Network Policy:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: calico-kube-controllers
namespace: kube-system
labels:
k8s-app: calico-kube-controllers
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ''
spec:
......
如果启用RBAC权限控制,则可以设置ServiceAccount。
用户在Kubernetes集群中设置了Pod的Network Policy之后,calico-kube-controllers就会自动通知各Node上的calico-node服务,
在宿主机上设置相应的iptables规则,完成Pod间网络访问策略的设置。
修改好相应的参数后,创建Calico的各资源对象(configmap、secret、daemonset、deployment):
# kubectl create -f calico.yaml
确保Calico的各服务正确运行:
# kubectl get pods --namespace=kube-system -o wide
calico-node在正常运行之后,会根据CNI规范,在/etc/cni/net.d/目录下生成如下文件和目录,并在/opt/cni/bin/目录下安装二进制文件calico和calico-ipam,供kubelet调用。
10-calico.conf:符合CNI规范的网络配置,其中type=calico表示该插件的二进制文件名为calico。
calico-kubeconfig:Calico所需的kubeconfig文件。
calico-tls目录:以TLS方式连接etcd的相关文件。
查看k8s-node-1服务器的网络接口设置,可以看到一个新的名为tunl0的接口,并设置了网络地址为10.1.109.64/32:
# ip addr show
查看k8s-node-2服务器的网络接口设置,同样可以看到一个新的名为tunl0的接口,网络地址为10.1.140.64/32:
# ip addr show
这两个子网都是从calico-node设置的IP地址池(CALICO_IPV4POOL_CIDR="10.1.0.0/16")中进行分配的。
同时, docker0对于Kubernetes设置Pod的IP地址将不再起作用。
查看两台主机的路由表。
首先,查看k8s-node-1服务器的路由表, 可以看到一条到k8s-node-2的私网10.1.140.64的路由转发规则:
# ip route
然后,查看k8s-node-2服务器的路由表,可以看到一条到k8s-node-1 的私网10.1.109.64/26的路由转发规则:
# ip route
这样,通过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网卡进行转发:
# ip route
查看k8s-node-2服务器的路由表,可以看到一条到k8s-node-1的私网10.1.109.64/26的路由转发规则,将通过本机ens33网卡进行转发:
# ip route
3.Calico设置容器IP地址,跨主机容器网络连通性验证
下面创建几个Pod,验证Calico对它们的网络设置。
以第1章的mysql和myweb为例,分别创建1个Pod和两个Pod:
# kubectl create -f mysql-rc.yaml -f myweb-rc.yaml
查看各Pod的IP地址,可以看到是通过Calico设置的以10.1开头的IP地址:
# kubectl get pod -o wide
进入运行在k8s-node-2上的Pod“myweb-s86sk”:
# kubectl exec -it myweb-xxx bash
在容器内访问运行在k8s-node-1上的Pod“mysql-8cztq”的IP地址 10.1.109.71:
# ping 10.1.109.71
在容器内访问物理机k8s-node-1的IP地址192.168.18.3:
# ping 192.168.18.3
这说明跨主机容器间、容器与宿主机之间的网络都能互联互通了。
查看k8s-node-2物理机的网络接口和路由表,可以看到Calico为Pod“myweb-s86sk”新建了一个网络接口cali439924adc43,并为其设置了一条路由规则:
# ip addr show
另外,Calico为该网络接口cali439924adc43设置了一系列iptables规则:
# iptables -L
4.使用网络策略实现Pod间的访问策略
下面以一个提供服务的Nginx Pod为例,为两个客户端Pod设置不同的网络访问权限,允许包含Label“role=nginxclient”的Pod访问Nginx容器,不包含该Label的容器则拒绝访问。
为了实现这个需求,需要通过以下步骤完成。
(1)创建Nginx Pod(nginx.yaml),并添加Label“app=nginx”:
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
# kubectl create -f nginx.yaml
(2)为Nginx设置网络策略,编辑文件networkpolicy-allow-nginxclient.yaml,内容如下:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-nginxclient
spec:
podSelector:
matchLabels:
app: nginx
ingress:
- from:
- podSelector:
matchLabels:
role: nginxclient
ports:
- protocol: TCP
port: 80
目标Pod应包含Label“app=nginx”,允许访问的客户端Pod包含Label“role=nginxclient”,并允许客户端访问mysql容器的80端口。
创建该NetworkPolicy资源对象:
# kubectl create -f networkpolicy-allow-nginxclient.yaml
(3)创建两个客户端Pod,一个(client1.yaml)包含Label“role=nginxclient”,另一个(client2.yaml)无此Label。
分别进入各Pod,访问Nginx容器,验证网络策略的效果。
apiVersion: v1
kind: Pod
metadata:
name: client1
labels:
role=nginxclient
spec:
containers:
- name: client1
image: busybox
command: [ "sleep","3600" ]
apiVersion: v1
kind: Pod
metadata:
name: client2
spec:
containers:
- name: client2
image: busybox
command: [ "sleep","3600" ]
# kubectl create -f client1.yaml -f client2.yaml
登录Pod“client1”:
# kubectl exec -it client1 -- sh
尝试连接Nginx容器的80端口:
# wget 10.1.109.69
成功访问到Nginx的服务,说明NetworkPolicy生效。
登录Pod“client2”:
# kubectl exec -it client2 -- sh
尝试连接Nginx容器的80端口:
# wget --timeout=5 10.1.109.69
访问超时,说明NetworkPolicy生效,对没有Label“role=nginxclient”的客户端Pod拒绝访问。
本例中的网络策略是由calico-kube-controllers具体实现的,
calico-kube-controllers持续监听Kubernetes中NetworkPolicy的定义,与各Pod通过Label进行关联,
将允许访问或拒绝访问的策略通知到各calico-node服务,最终calico-node完成对Pod间网络访问的设置,实现应用的网络隔离。