编辑推荐: |
本文帮助读者系统了解容器云网络架构。从容器网络相关的基础概念入题,介绍了容器网络的协议栈、穿越方式以及隔离方式。随后介绍了Kubernetes网络的各种场景,包括容器之间、Pod之间、Pod到Service、外部到内部的这4种场景下,不同的通信模式。希望对你的学习有帮助。
本文来自于微信公众号Linux云计算网络,由火龙果软件Linda编辑、推荐。 |
|
1 容器平台网络模型
1.1 容器网络概述
与传统的虚拟化相比,容器其生命周期更短、数量密度更高、集群变更速度更快。基于这些特性,容器平台网络就必须对集群节点之间的高速通信进行充分的考量。除此之外,在企业级的容器云平台上,承载众多租户的计算负载之间资源的安全隔离,也必须要考虑到的因素。
显而易见,传统的物理网络架构无法满足容器平台高灵活性的需求,容器平台网络构建必须要有一种崭新的设计架构来满足,这便推动了容器平台网络设计的发展。
容器网络发展到目前,已经形成了Docker主导的CNM模型和Google、CoreOS、Kubernetes主导的CNI模型两种模型共存的情况。CNM和CNI并不是网络实现,他们是网络规范和网络体系。从研发的角度来看就是一些接口,主要是和网络管理相关的问题。容器平台的网络方案,通常可以从协议栈、穿越方式、隔离方法三个维度去设计方案:
图 1 网络架构示意图
协议栈:
二层(桥接、ARP+MAC)
三层(路由转发)
二层+三层(节点内部二层转发、跨节点三层转发)
穿越方式:
Overlay(隧道穿越底层基础设施)
Underlay(直接穿越底层基础设施)
隔离方法:
VLAN
VXLAN
1.2 容器网络分类介绍
1.2.1 协议栈
二层解决方案,常见于传统机房或者虚拟化场景中,针对ARP和MAC(桥接模式)学习,广播风暴是这个方案最核心要解决的问题。众所周知,二层广播,对整个集群中节点的数量也会有严格的限制。三层解决方案一般是基于BGP协议实现路由转发,通过自主学习完善整个数据中心的路由状态。这个方案的优势是IP穿透性,可以实现IP网络透传。因为基于IP,所以其优势在于规模性,具有非常优秀的扩展性。但在实际使用过程中,受限于各个企业自身网络安全的考量,例如生产网和开发测试网隔离,或者网络本身不支持BGP,那么这个方案就受限了。二层+三层的方案,集成了前面两种方案的优势(既解决了二层规模性扩展的问题,又解决三层网络隔离受限的问题),正成为容器云网络场景下首选的协议栈层级的解决方案。
1.2.2 穿越方式
Underlay网络
提到Underlay网络,就必须从以太网说起,以太网从最开始设计出来就是一个分布式网络,没有中心的控制节点,网路中的各个设备之间通过协议传递的方式学习网络的可达信息,由每台设备自己决定要如何转发,这直接导致了没有整体观念,不能从整个网络的角度对流量进行调控。由于要完成所有网络设备之间的互通,就必须使用通用的语言,这就是网络协议,RFC就是网络协议的法律,相当于国际法,各个设备供应商遵从国际法行事,就基本保证了整个网络世界的正常运行。Underlay就是当前数据中心网路基础转发架构的网络,只要数据中心网络上任意两点路由可达即可,指的是物理基础层。我们可以通过物理网络设备本身的技术改良、扩大设备数量、带宽规模等完善Underlay网络,其包含了一切现有的传统网络技术。
Overlay网络
Overlay技术可以分为网络Overlay,主机Overlay和混合式Overlay三大类。网络Overlay是指通过控制协议对边缘的网络设备进行网络构建和扩展,也就是本文所讲的Overlay网络技术。Overlay网络技术多种多样,一般采用TRILL、VXLAN、GRE、NVGRE等隧道技术。TRILL(Transparent
Interconnection of Lots of Links)技术是电信设备厂商主推的新型环网技术;NVGRE(Network
Virtualization using Generic Routing Encapsulation)STT(Stateless
Transport Tunneling Protocol)是IT厂商主推的Overlay技术;以及大家非常熟悉的LAN)等基于隧道的封装技术。由于这些也都是新增的协议,均需要升级现有网络设备才能支持。Overlay网络中应用部署的位置将不受限制,网络设备可即插即用、自动配置下发,自动运行,Overlay网络业务变化,基础网络不感知,并对传统网络改造极少,最为重要的是虚拟机和物理服务器都可以接入Overlay网络中。
1.2.3 根据基础设施划分
VLAN(Virtual Local Area Network)意为虚拟局域网,是在交换机实现过程中涉及到的概念,由802.1Q标准所定义。由于交换机是工作在链路层的网络设备,连接在同一台交换机的终端处于同一个三层网中,同时也处于同一个广播域。当交换机接入较多的终端时,任意一台终端发送广播报文时(例如:ARP请求),报文都会传遍整个网络。对于规模较大的组网场景,广播报文的泛滥对于网络通信将会造成较大的影响。VLAN技术为这一问题提供了解决方案,VLAN将同一网络划分为多个逻辑上的虚拟子网,并规定当收到广播报文时,仅仅在其所在VLAN中进行广播从而防止广播报文泛滥。VLAN技术在链路层的层次中实现了广播域的隔离。
随着大数据、云计算技术的兴起以及虚拟化技术的普及,VLAN技术的弊端逐渐显现出来,具体表现为如下3个方面:
(1) 虚拟化技术的发展促使大数据、云计算技术公司采用单个物理设备虚拟多台虚拟机的方式来进行组网,随着应用模块的增加,对于支持VLAN数目的要求也在提升,802.1Q标准中的最多支持4094个VLAN的能力已经无法满足当下需求。
(2) 公有云提供商的业务要求将实体网络租借给多个不同的用户,这些用户对于网络的要求有所不同,而不同用户租借的网络有很大的可能会出现IP地址、MAC地址的重叠,传统的VLAN仅仅解决了同一链路层网络广播域隔离的问题,而并没有涉及到网络地址重叠的问题,因此需要一种新的技术来保证在多个租户网络中存在地址重叠的情况下依旧能有效通信的技术。
(3) 虚拟化技术的出现增加了交换机的负担,对于大型的数据中心而言,单台交换机必须支持数十台以上主机的通信连接才足以满足应用需求,而虚拟化技术使得单台主机可以虚拟化出多台虚拟机同时运行,而每台虚拟机都会有其唯一的MAC地址。这样,为了保证集群中所有虚机可以正常通信,交换机必须保存每台虚机的MAC地址,这样就导致了交换机中的MAC表异常庞大,从而影响交换机的转发性能。
基于以上需求,VXLAN技术被提出。
VXLAN技术是网络Overlay技术的一种实现,对于Overlay技术,笔者的理解是:在基于物理网络拓扑的基础上通过一定的技术来构建虚拟的、不同于物理网络拓扑的逻辑网络,而物理网络的拓扑结构对于Overlay终端而言是透明的,终端不会感知到物理网络的存在,而仅仅能感知到逻辑网络结构。对于终端的视角,网络的情况和直接通过物理设备实现逻辑拓扑的效果是相同的。VXLAN技术可以基于三层网络结构来构建二层虚拟网络,通过VLAN技术可以将处于不同网段网络设备整合在同一个逻辑链路层网络中,对于终端用户而言,这些网络设备似乎“真实地”部署在了同一个链路层网络中。
相比VLAN技术,VXLAN技术具有以下的优势:
(1) 24位长度的VNI字段值可以支持更多数量的虚拟网络,解决了VLAN数目上限为4094的局限性的问题。
(2) VXLAN技术通过隧道技术在物理的三层网络中虚拟二层网络,处于VXLAN网络的终端无法察觉到VXLAN的通信过程,这样也就使得逻辑网络拓扑和物理网络拓扑实现了一定程度的解耦,网络拓扑的配置对于物理设备的配置的依赖程度有所降低,配置更灵活更方便。
(3) VLAN技术仅仅解决了二层网络广播域分割的问题,而VXLAN技术还具有多租户支持的特性,通过VXLAN分割,各个租户可以独立组网、通信,地址分配方面和多个租户之间地址冲突的问题也得到了解决。
1.3 总结
通过本章的学习,可以初步了解容器网络相关的基础概念,主要涉及到了容器网络的协议栈、穿越方式以及隔离方式。针对协议栈,到底是采用二层互通,还是采用三层互通,还是结合两种方式的优点整合一个综合的方式取决于业务场景;针对穿越方式,是采用传统的Underlay网络,还是基于SDN的Overlay网络,和客户现场情况,以及硬件设备支持的情况都有比较大的关联;同样,隔离方式采用VLAN还是VXLAN,也和场景强相关。由此可见,容器云网络的设计,需要因地制宜,因材施教,从客户需求以及现场情况发出,才能制定出一个完善的解决方案。
2 基于Docker网络基础和实现原理
Docker网络方案基于OpenStack平台网络解决方案,在不断的摸索中,形成了自己的一套网络模型。
Docker网络在整个Docker技术栈中的位置如图:
图2 Docker生态技术栈
在容器网络项目探索中,随着容器的发展,容器网络的发展也出现了分歧。主要分为两派,一个是Docker原生的CNM(ContainerNetwork
Model),另一个是兼容性更好的CNI(Container Network Interface)。CNI就是后来为Kubernetes等容器平台广泛推崇使用的接口技术,后面的章节会详细讲述。这里,我们简要介绍CNM。
原先Docker的网络相关的代码是直接在Docker中的,网络功能也比较简单,对网络的诟病也是比较多。随着Docker越来越向平台化发展,将功能组件逐渐从Docker中解耦,Docker从1.7把网络相关的代码从Docker的代码中剥离出来新建了一个Libnetwork项目,引入了CNM的网络模型。
图3 CNM(Container Network Model)
CNM模型下的Docker网络模型如上所示。它由Sandbox,Endpoint,Network三种组件组成。注意,该模型只是规定了三种组件各自的作用,他们都有各自的具体实现方式。Sandbox:Sandbox
包含了一个 Container 的网络相关的配置,如网卡 Interface,路由表等。Sandbox
在 Linux 上的典型实现是 Network namespace。在 Linux 系统上的 Docker
环境中,Container,Network namespace,Sandbox 这三者是绑定在一起的。一个Sandbox
可以包含多个 Endpoint,这些 Endpoint 可以来自多个 Network。
Endpoint: Sandbox 加入 Network 的方式是通过 Endpoint 完成的。Endpoint
的典型实现方式是 Vethpair,每个 Endpoint 都是由某个 Network 创建。创建后,它就归属于该Network。同时,Endpoint
还可以加入一个 Sandbox。加入后,相当于该 Sandbox 也加入了此 Network。
Network : Network 的一种典型实现是 Linux bridge。一个 Network
可以创建多个Endpoint。将这些Endpoint 加入到 Sandbox,即实现了多个 Sandbox
的互通。
总结起来:如果要想两个 Container 之间可以直接通信,那么最简单的办法就是由一个Network
创建两个 Endpoint,分别加入这两个 Container 对应的 Sandbox。
注意: 不同 Network 之间默认的隔离性是 docker 通过设置 Iptables 完成的,通过改变Iptables
的设置,可以使得两个 Network 互通。
标准的 Docker 网络支持以下 4 类网络模式:
host 模式:使用--net=host 指定
container 模式:使用--net=container:Name_or_ID 指定
none模式:使用--net=none指定
bridge模式:使用--net=bridge指定,设为默认值
桥接模式是最常见的Docker容器网络类型。在桥接模式下,Docker会为每个容器分配IP地址及创建虚拟以太网网卡对(Veth)。所有的容器都被连接到Docker在主机绑定的桥接设备上。被连接到同一个桥接设备的所有容器,都可以实现互联互通。如果容器要对外界提供服务,则用户需要将容器内的服务端口与宿主机的某一端口绑定。这样所有访问宿主机目标端口的请求都将通过Docker代理转发到容器的服务端,最终到达应用。
除了桥接模式,Docker也支持主机(host)模式,让容器直接使用宿主机的网络设备。宿主机模式使得容器占用宿主机的端口资源,而且要求容器具有更高的权限,因此只有特殊需求的容器,才会使用这种模式,如OpenShift集群中的Router组件。Router主机需要监听计算节点上的端口,以接受外部的请求,因此Router组件的Pod的容器网络为主机模式。
本章节主要介绍了Docker网络的情况,从Docker整个生态栈入手,分析了基于单机和集群两种不同场景的Docker网络,着重分析了在单机模式下Docker网络的情况(host/bridge/none/container)。
3 Kubernetes网络场景分析
在实际的业务场景中,业务组件之间的关系十分复杂,特别是微服务概念的提出,应用部署的粒度更加细小和灵活。为了支持业务应用组件的通信联系,Kubernetes网络的设计主要致力于解决以下场景:
(1)紧密耦合的容器到容器之间的直接通信;
(2)抽象的Pod到Pod之间的通信;
(3)Pod到Service之间的通信;
(4)集群外部与内部组件之间的通信;
3.1 容器到容器的通信
在同一个Pod内的容器(Pod内的容器是不会跨宿主机的)共享同一个网络命名空间,共享同一个Linux协议栈。所以对于网络的各类操作,就和它们在同一台机器上一样,它们甚至可以用localhost地址访问彼此的端口。这么做的结果是简单、安全和高效,也能减少将已经存在的程序从物理机或者虚拟机移植到容器的难度。
如图4中的阴影部分就是Node上运行着的一个Pod实例。容器1和容器2共享了一个网络的命名空间,共享一个命名空间的结果就是它们好像在一台机器上运行似的,它们打开的端口不会有冲突,可以直接用Linux的本地IPC进行通信。它们之间互相访问只需要使用localhost就可以。
图4 容器到容器间通信
3.2 Pod之间的通信
每一个Pod都有一个真实的全局IP地址,同一个Node内的不同Pod之间可以直接采用对方Pod的IP地址通信,而不需要使用其他发现机制,例如DNS、Consul或者etcd。Pod既有可能在同一个Node上运行,也有可能在不用的Node上运行,所以通信也分为两类:同一个Node内的Pod之间的通信和不同Node上的Pod之间的通信。
1)同一个Node内的Pod之间的通信
图5 同一个Node内的Pod关系
如图,可以看出,Pod1和Pod2都是通过Veth连接在同一个Docker0网桥上的,它们的IP地址IP1、IP2都是从Docker0的网段上自动获取的,它们和网桥本身的IP3是同一个网段的。另外,在Pod1、Pod2的Linux协议栈上,默认路由都是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进行通信。之前提到,Kubernetes的网络对Pod的地址是平面的和直达的,所以这些Pod的IP规划也很重要,不能有冲突。综上所述,想要支持不同Node上的Pod之间的通信,就要达到两个条件:
(1)在整个Kubernetes集群中对Pod分配进行规划,不能有冲突;
(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。
具体情况,如下图所示。
图6 跨Node的Pod通信
在图6中,IP1对应的是Pod1,IP2对应的是Pod2。Pod1在访问Pod2时,首先要将数据从源Node的eth0发送出去,找到并到达Node2的eth0。也就是说先要从IP3到IP4,之后才是IP4到IP2的送达。
3.3 Pod 到Service之间的通信
为了支持集群的水平扩展、高可用,Kubernetes抽象出Service的概念。Service是对一组Pod的抽象,它会根据访问策略(LB)来访问这组Pod。
Kubernetes在创建服务时会为服务分配一个虚拟的IP地址,客户端通过访问这个虚拟的IP地址来访问服务,而服务则负责将请求转发到后端的Pod上。这个类似于反向代理,但是,和普通的反向代理有一些不同:首先它的IP地址是虚拟的,想从外面访问需要一些技巧;其次是它的部署和启停是Kubernetes统一自动管理的。
Service在很多情况下只是一个概念,而真正将Service的作用落实的是背后的kube-proxy服务进程。在Kubernetes集群的每个Node上都会运行一个kube-proxy服务进程,这个进程可以看作Service的透明代理兼负载均衡器,其核心功能是将到某个Service的访问请求转发到后端的多个Pod实例上。对每一个TCP类型的Kubernetes
Service,kube-proxy都会在本地Node上建立一个SocketServer来负责接收请求,然后均匀发送到后端某个Pod的端口上,这个过程默认采用RoundRobin负载均衡算法。Kube-proxy和后端Pod的通信方式与标准的Pod到Pod的通信方式完全相同。另外,Kubernetes也提供通过修改Service的service.spec.sessionAffinity参数的值来实现会话保持特性的定向转发,如果设置的值为“ClientIP”,则将来自同一个ClientIP的请求都转发到同一个后端Pod上。此外,Service的ClusterIP与NodePort等概念是kube-proxy通过Iptables和NAT转换实现的,kube-proxy在运行过程中动态创建与Service相关的Iptables规则,这些规则实现了ClusterIP及NodePort的请求流量重定向到kube-proxy进程上对应服务的代理端口的功能。由于Iptables机制针对的是本地的kube-proxy端口,所以如果Pod需要访问Service,则它所在的那个Node上必须运行kube-proxy,并且在每个Kubernetes的Node上都会运行kube-proxy组件。在Kubernetes集群内部,对Service
Cluster IP和Port的访问可以在任意Node上进行,这个因为每个Node上的kube-proxy针对该Service都设置了相同的转发规则。
综上所述,由于kube-proxy的作用,在Service的调用过程中客户端无需关心后端有几个Pod,中间过程的通信、负载均衡及故障恢复都是透明的,如下图所示。
图7 Service的负载均衡转发
访问Service的请求,不论是用Cluster IP+Target Port的方式,还是用节点机IP+Node
Port的方式,都会被节点机的Iptables规则重定向到kube-proxy监听Service服务代理端口。Kube-proxy接收到Service的访问请求后,会如何选择后端Pod?
首先,目前kube-proxy的负载均衡只支持Round Robin算法。该算法按照成员列表逐个选取成员,如果一轮循环完,便从头开始下一轮,如此循环往复。Kube-proxy的负载均衡器在Round
Robin算法的基础上还支持Session保持。如果Service在定义中指定了Session保持,则kube-proxy接收请求时会从本地内存中查找是否存在来自该请求IP的affinityState对象,如果存在该对象,且Session没有超时,则kube-proxy将请求转向该affinityState所指向的后端Pod。如果本地存在没有来自该请求IP的affinityState对象,记录请求的IP和指向的Endpoint。后面的请求就会粘连到这个创建好的affinityState对象上,这就实现了客户端IP会话保持的功能。
接下来我们深入分析kube-proxy的实现细节。kube-proxy进程为每个Service都建立了一个“服务代理对象”,服务代理对象是kube-proxy程序内部的一种数据结构,它包括一个用于监听此服务请求的SocketServer,SocketServer的端口是随机选择的一个本地空闲端口。此外,kube-proxy内部也建立了一个“负载均衡器组件”,用来实现SocketServer上收到的连接到后端多个Pod连接之间的负载均衡和会话保持能力。
kube-proxy通过查询和监听API Server中Service与Endpoint的变化来实现其主要功能,包括为新创建的Service打开一个本地代理对象(代理对象是kube-proxy程序内部的一种数据结构,一个Service端口是一个代理对象,包括一个用于监听的服务请求的SocketServer),接收请求,针对发生变化的Service列表,kube-proxy会逐个处理。下面是具体的处理流程:
(1)如果该Service没有设置集群IP(ClusterIP),则不做任何处理,否则,获取该Service的所有端口定义列表(spec.ports域)
(2)逐个读取服务端口定义列表中的端口信息,根据端口名称、Service名称和Namespace判断本地是否已经存在对应的服务代理对象,如果不存在就新建,如果存在且Service端口被修改过,则先删除Iptables中和该Service相关的的规则,关闭服务代理对象,然后走新建流程,即为该Service端口分配服务代理对象并为该Service创建相关的Iptables规则。
(3)更新负载均衡器组件中对应Service的转发地址表,对于新建的Service,确定转发时的会话保持策略。
(4)对于已经删除的Service则进行清理。
图8 Kube-proxy与APIServer的交互过程
3.4 外部到内部的访问
Pod作为基本的资源对象,除了会被集群内部的Pod访问,也会被外部使用。服务是对一组功能相同Pod的抽象,以它为单位对外提供服务是最合适的粒度。
由于Service对象在Cluster IP Range池中分配到的IP只能在内部访问,所以其他Pod都可以无障碍地访问到它。但如果这个Service作为前端服务,准备为集群外的客户端提供服务,就需要外部能够看到它。
Kubernetes支持两种对外服务的Service的Type定义:NodePort和LoadBalancer。
(1)NodePort
在定义Service时指定spec.type=NodePort,并指定spec.ports.nodePort的值,系统就会在Kubernetes集群中的每个Node上打开一个主机上的真实端口号。这样,能够访问Node的客户端就能通过这个端口号访问到内部的Service了。
(2)LoadBalancer
如果云服务商支持外接负载均衡器,则可以通过spec.type=LoadBalancer定义Service,同时需要指定负载均衡器的IP地址。使用这种类型需要指定Service的NodePort和ClusterIP。
对于这个Service的访问请求将会通过LoadBalancer转发到后端Pod上去,负载分发的实现方式依赖于云服务商提供的LoadBalancer的实现机制。
(3)外部访问内部Service原理
我们从集群外部访问集群内部,最终都是落在具体的Pod上。通过NodePort的方式就是将kube-proxy开放出去,利用Iptables为服务的NodePort设置规则,将对Service的访问转到kube-proxy上,这样kube-proxy就可以使用和内部Pod访问服务一样的方式来访问后端的一组Pod了。这种模式就是利用kube-proxy作为负载均衡器,处理外部到服务进一步到Pod的访问。而更常用的是外部均衡器模式。通常的实现是使用一个外部的负载均衡器,这些均衡器面向集群内的所有节点。当网络流量发送到LoadBalancer地址时,它会识别出这是某个服务的一部分,然后路由到合适的后端Pod。
所以从外面访问内部的Pod资源,就有了很多种不同的组合。
外面没有负载均衡器,直接访问内部的Pod
外面没有负载均衡器,直接通过访问内部的负载均衡器来访问Pod
外面有负载均衡器,通过外部负载均衡器直接访问内部的Pod
外面有负载均衡器,通过访问内部的负载均衡器来访问内部的Pod
第一种情况的场景十分少见,只是在特殊的时候才需要。我们在实际的生产项目中需要逐一访问启动的Pod,给它们发送一个刷新指令。只有这种情况下才使用这种方式。这需要开发额外的程序,读取Service下的Endpoint列表,逐一和这些Pod进行通信。通常要避免这种通信方式,例如可以采取每个Pod从集中的数据源拉命令的方式,而不是采取推命令给它的方式来避免。因为具体到每个Pod的启停本来就是动态的,如果依赖了具体的Pod们就相当于绕开了Kubernetes的Service机制,虽然能够实现,但是不理想。
第二种情况就是NodePort的方式,外部的应用直接访问Service的NodePort,并通过Kube-proxy这个负载均衡器访问内部的Pod。
第三种情况是LoadBalancer模式,因为外部的LoadBalancer是具备Kubernetes知识的负载均衡器,它会去监听Service的创建,从而知晓后端的Pod启停变化,所以它有能力和后端的Pod进行通信。但是这里有个问题需要注意,那就是这个负载均衡器需要有办法直接和Pod进行通信。也就是说要求这个外部的负载均衡器使用和Pod到Pod一样的通信机制。
第四种情况也很少使用,因为需要经历两级的负载均衡设备,而且网络的调用被两次随机负载均衡后,更难跟踪了。在实际生产环境中出了问题排错时,很难跟踪网络数据的流动过程。
(4)外部硬件负载均衡器模式
在很多实际的生产环境中,由于是在私有云环境中部署Kubernetes集群,所以传统的负载均衡器都对Service无感知。实际上我们只需要解决两个问题,就可以将它变成Service可感知的负载均衡器,这也是实际系统中理想的外部访问Kubernetes集群内部的模式。
通过写一个程序来监听Service的变化,将变化按照负载均衡器的通信接口,作为规则写入负载均衡器。
给负载均衡器提供直接访问Pod的通信手段。
如下图,说明了这个过程。
图9 自定义外部负载均衡器访问Service
这里提供了一个Service Agent来实现Service变化的感知。该Agent能够直接从etcd中或者通过接口调用API
Server来监控Service及Endpoint的变化,并将变化写入外部的硬件负载均衡器中。
同时,每台Node上都运行着有路由发现协议的软件,该软件负责将这个Node上所有的地址通过路由发现协议组播给网络内的其他主机,当然也包含硬件负载均衡器。这样硬件负载均衡器就能知道每个Pod实例的IP地址是在哪台Node上了。通过上述两个步骤,就建立起一个基于硬件的外部可感知Service的负载均衡器。
3.5 总结
本章重点介绍了Kubernetes网络的各种场景,包括容器之间、Pod之间、Pod到Service、外部到内部的这4种场景下,不同的通信模式。在设计Kubernetes容器平台的时候,建议按照这些通信模式,根据具体的场景,逐一比对选择合适的解决方案。其中,需要注意的是外部到内部的访问,既可以通过NodePort,也可以通过LoadBalancer的方式亦或是Ingress模式,需要按照具体的场景来分析。
NodePort服务是暴露服务的最原始方式,会在所有节点上打开特定的端口,并且发送到此端口的任何流量都将转发到该服务。这种方法有很多缺点:每个端口只能有一个服务;默认只能使用端口30000~32767;如果节点IP地址发生更改,则会带来问题。由于这些原因,不建议在生产中使用这种方法。如果服务可用性不是特别关注,或者特别关注成本,则这个方案比较合适。
LoadBalancer是服务暴露的标准方式,将会启动一个网络负载均衡器,提供一个将所有流量转发到服务的IP地址。如果直接暴露一个服务,这是默认的方法。指定的端口上所有的流量将被转发到该服务,没有过滤、路由等。这就意味着可以发送几乎任何类型流量,如HTTP、TCP、UDP、Websocket、gRPC或其他。这个方式最大的缺点是,使用LoadBalancer公开的每项服务都将获得自己的IP地址,并且必须为每个服务使用一个LoadBalancer,这将会付出比较大的代价。
Ingress实际上不是一种服务。相反,它位于多个服务之前,充当集群中的“智能路由器”或入口点。默认的Ingress控制器将会启动一个HTTP(s)负载均衡器。这将可以执行基于路径和基于子域名的路由到后端服务。Ingress可能是暴露服务最强大的方式了,但也可能是最复杂的。如果希望在相同的IP地址下暴露多个服务,并且这些服务都使用相同的L7协议,则Ingress是最有用的。
4 Kubernetes网络组件介绍
4.1 Kubernetes网络框架CNI
基于Docker的Kubernetes平台为什么没有选择CNM作为其网络设计框架?毕竟大部分容器平台肯定会支持Docker的网络组件,为什么不使用相同的组件呢?这就要从Kubernetes平台设计初衷说起,Kubernetes是一个支持多容器的运行环境,而Docker只是其中一个容器而已。Docker网络驱动设计中,做了很多和Kubernetes不兼容的假设。
例如,Docker中有“本地”驱动和“全局”驱动概念,“本地”驱动实现单机版,无法实现跨节点协作,“全局”驱动libkv可实现跨节点协作。但是,libkv接口太过于底层,而且架构模式也是Docker内部的量身定制版本,对于Kubernetes的应用会带来性能、可扩展性和安全性方面的问题。
CNI(Container Networking Interface)提供了一种Linux的应用容器的插件化网络解决方案。最初是由rkt
Networking Proposal发展而来。也就是说,CNI本身并不是完全针对Docker的容器,而是提供一种普适的容器网络解决方案。模型涉及两个概念:
容器:拥有独立Linux网络命名空间的独立单元。比如rkt/docker创建出来的容器。
网络(Networking):网络指代了可以相互联系的一组实体。这些实体拥有各自独立唯一的IP。这些实体可以是容器,是物理机,或者是其他网络设备(比如路由器)等。
CNI的接口设计非常简洁,不需要守护进程,只有两个接口ADD/DELETE,通过一个简单的shell脚本就可以完成。相对于CNM的复杂设计,CNI更加适合快速开发和迭代。
4.2 CNI支持的开源组件
4.2.1 Flannel
Flannel之所以可以搭建Kubernetes依赖的底层网络,是因为它可以实现以下两点:
它给每个node上的docker容器分配相互不相冲突的IP地址;
它能给这些IP地址之间建立一个覆盖网络,通过覆盖网络,将数据包原封不动的传递到目标容器内。
Flannel是CoreOS团队针对Kubernetes设计的一个网络规划服务,简单来说,它的功能是让集群中的不同节点主机创建的Docker容器都具有全集群唯一的虚拟IP地址。
在默认的Docker配置中,每个节点上的Docker服务会分别负责所在节点容器的IP分配。
这样导致的一个问题是,不同节点上容器可能获得相同的内外IP地址。并使这些容器之间能够之间通过IP地址相互找到,也就是相互ping通。
Flannel的设计目的就是为集群中的所有节点重新规划IP地址的使用规则,从而使得不同节点上的容器能够获得“同属一个内网”且“不重复的”IP地址,并让属于不同节点上的容器能够直接通过内网IP通信。
Flannel实质上是一种“覆盖网络(Overlay Network)”,也就是将TCP数据包装在另一种网络包里面进行路由转发和通信,默认的节点间数据通信方式是UDP转发。
图10 Flannel跨节点Pod通信图
举个例子,上图是跨节点Pod通信。
可以看到,Flannel首先创建了一个名为flannel0的网桥,而且这个网桥的一端连接Docker0网桥,另一端连接一个叫做flanneld的服务进程。flanneld进程并不简单,它上连etcd,利用etcd来管理可分配的IP地址段资源,同时监控etcd中每个Pod的实际地址,并在内存中建立了一个Pod节点路由表;它下连Docker0和物理网络,使用内存中的Pod节点路由表,将Docker0发给它的数据包包装起来,利用物理网络的连接将数据包投递到目标flanneld上,从而完成Pod到Pod之间的直接地址通信。
4.2.2 OVS
Open vSwitch是一个开源的虚拟交换机软件,有点像Linux中的bridge,但是功能要复杂的多。OpenvSwitch的网桥可以直接建立多种通信通道(隧道)。这些通道的建立可以很容易地通过OVS的配置命令实现。在Kubernetes、Docker场景下,主要是建立L3到L3点隧道。如下图所示。
图11 OVS with GRE原理图
首先,为了避免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网桥的路由规则项,就可以将两个容器的网络连接起来了。
OVS的优势是,作为开源虚拟交换机软件,它相对比较成熟和稳定,而且支持各类网络隧道协议,经过了OpenStack等项目的考验。另一方面,在前面介绍Flannel的时候可知Flannel除了支持建立Overlay网络,保证Pod到Pod的无缝通信,还和Kubernetes、Docker架构体系结合紧密。Flannel能够感知Kubernetes的Service,动态维护自己的路由表,还通过etcd来协助Docker对整个Kubernetes集群中Docker0的子网地址分配。而我们在使用OVS的时候,很多事情就需要手工完成了。无论是OVS还是Flannel,通过Overlay网络提供的Pod到Pod通信都会引入一些额外的通信开销,如果是对网络依赖特别重的应用,则需要评估对业务的影响。
4.2.3 Calico
Calico是一个纯三层的数据中心网络方案(不需要Overlay),并且与OpenStack、Kubernetes、AWS、GCE等IaaS和容器平台都有良好的集成。Calico在每一个计算节点利用Linux
Kernel实现了一个高效的vRouter来负责数据转发,而每个vRouter通过BGP协议负责把自己上运行的workload的路由信息向整个Calico网络内传播——小规模部署可以直接互联,大规模下可通过指定的BGP
route reflector来完成。这样保证最终所有的workload之间的数据流量都是通过IP路由的方式完成互联的。Calico节点组网可以直接利用数据中心的网络结构(无论是L2或者L3),不需要额外的NAT,隧道或者Overlay
Network。此外,Calico基于iptables还提供了丰富而灵活的网络Policy,保证通过各个节点上的ACLs来提供Workload的多租户隔离、安全组以及其他可达性限制等功能。下图是Calico的架构图。
图12 Calico架构图
在满足系统要求的新配置的Kubernetes集群上,用户可以通过应用单个manifest文件快速部署Calico。
如果您对Calico的可选网络策略功能感兴趣,可以向集群应用其他manifest,来启用这些功能。
尽管部署Calico所需的操作看起来相当简单,但它创建的网络环境同时具有简单和复杂的属性。与Flannel不同,Calico不使用Overlay网络。相反,Calico配置第3层网络,该网络使用BGP路由协议在主机之间路由数据包。这意味着在主机之间移动时,不需要将数据包包装在额外的封装层中。BGP路由机制可以本地引导数据包,而无需额外在流量层中打包流量。
除了性能优势之外,在出现网络问题时,用户还可以用更常规的方法进行故障排除。虽然使用VXLAN等技术进行封装也是一个不错的解决方案,但该过程处理数据包的方式同场难以追踪。使用Calico,标准调试工具可以访问与简单环境中相同的信息,从而使更多开发人员和管理员更容易理解行为。
除了网络连接外,Calico还以其先进的网络功能而闻名。网络策略是其最受追捧的功能之一。此外,Calico还可以与服务网格Istio集成,以便在服务网格层和网络基础架构层中解释和实施集群内工作负载的策略。这意味着用户可以配置强大的规则,描述Pod应如何发送和接受流量,提高安全性并控制网络环境。
如果对你的环境而言,支持网络策略是非常重要的一点,而且你对其他性能和功能也有需求,那么Calico会是一个理想的选择。此外,如果您现在或未来有可能希望得到技术支持,那么Calico是提供商业支持的。一般来说,当您希望能够长期控制网络,而不是仅仅配置一次并忘记它时,Calico是一个很好的选择。
图13 Calico功能模块图
Calico主要由Felix、etcd、BGP client以及BGP Route Reflector组成:
Felix,Calico Agent,跑在每台需要运行Workload的节点上,主要负责配置路由及ACLs等信息来确保Endpoint的连通状态;
etcd,分布式键值存储,主要负责网络元数据一致性,确保Calico网络状态的准确性;
BGP Client(BIRD),主要负责把Felix写入Kernel的路由信息分发到当前Calico网络,确保Workload间的通信的有效性;
BGP Route Reflector(BIRD),大规模部署时使用,摒弃所有节点互联的mesh模式,通过一个或者多个BGP
Route Reflector来完成集中式的路由分发。
calico/calico-ipam,主要用作Kubernetes的CNI插件。
|