k8s对Pods之间如何进行组网通信提出了要求,k8s对集群的网络有以下要求:
k8s网络模型设计基础原则:每个Pod都拥有一个独立的 IP地址,而且 假定所有 Pod 都在一个可以直接连通的、扁平的网络空间中 。 所以不管它们是否运行在同 一 个 Node (宿主机)中,都要求它们可以直接通过对方的 IP 进行访问。设计这个原则的原因 是,用户不需要额外考虑如何建立 Pod 之间的连接,也不需要考虑将容器端口映射到主机端口等问题。
由于 Kubemetes 的网络模型假设 Pod 之间访问时使用的是对方 Pod 的实际地址,所以一个 Pod 内部的应用程序看到的自己的 IP 地址和端口与集群内其他 Pod 看到的一样。它们都是 Pod 实际分配的IP地址 (从dockerO上分配的)。将IP地址和端口在Pod内部和外部都保持一致, 我们可以不使用 NAT 来进行转换,地址空间也自然是平的。
鉴于上面这些要求,我们需要解决四个不同的网络问题::
下面我们一一进行讨论每种网络问题,以及如何解决。
image.png
container模式指定新创建的Docker容器和已经存在的一个容器共享一个网络命名空间,而不是和宿主机共享。新创建的Docker容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等
每个Pod容器有有一个pause容器其有独立的网络命名空间,在Pod内启动Docker容器时候使用 –net=container就可以让当前Docker容器加入到Pod容器拥有的网络命名空间(pause容器)
image.png
image.png
那么是如何做到的?这多亏了使用linux虚拟以太网设备或者说是由两个虚拟接口组成的veth对使不同的网络命名空间链接起来,这些虚拟接口分布在多个网络命名空间上(这里是指多个Pod上)。
为了让多个Pod的网络命名空间链接起来,我们可以让veth对的一端链接到root网络命名空间(宿主机的),另一端链接到Pod的网络命名空间。
每对Veth就像一根接插电缆,连接两侧并允许流量在它们之间流动;这种veth对可以推广到同一个Node上任意多的Pod上,如上图这里展示使用veth对链接每个Pod到虚拟机的root网络命名空间。
下面我们看如何使用网桥设备来让通过veth对链接到root命名空间的多个Pod进行通信。
linux以太网桥(Linux Ethernet bridge)是一个虚拟的2层网络设备,目的是把多个以太网段链接起来,网桥维护了一个转发表,通过检查转发表通过它传输的数据包的目的地并决定是否将数据包传递到连接到网桥的其他网段,网桥代码通过查看网络中每个以太网设备特有的MAC地址来决定是传输数据还是丢弃数据。
image.png
网桥实现了ARP协议用来根据给定的ip地址找到对应机器的数据链路层的mac地址,一开始转发表为空,当一个数据帧被网桥接受后,网桥会广播该帧到所有的链接设备(除了发送方设备),并且把响应这个广播的设备记录到转发表;随后发往相同ip地址的流量会直接从转发表查找正确的mac地址,然后转发包到对应的设备。
image.png
如上图显示了两个Pod通过veth对链接到root网络命名空间,并且通过网桥进行通信
鉴于每个Pod有自己独立的网络命名空间,我们使用虚拟以太网设备把多个Pod的命名空间链接到了root命名空间,并且使用网桥让多个Pod之间进行通信,下面我们看如何在两个pod之间进行通信:
image.png
k8s网络模型需要每个pod必须通过ip地址可以进行访问,每个pod的ip地址总是对网络中的其他pod可见,并且每个pod看待自己的ip与别的pod看待的是一样的(虽然他没规定如何实现),下面我们看不同Node间Pod如何交互
k8s中每个集群中的每个Node都会被分配了一个CIDR块(无类别域间路由选择,把网络前缀都相同的连续地址组成的地址组称为CIDR地址块)用来给该Node上的Pod分配IP地址。(保证pod的ip不会冲突) 另外还需要把pod的ip与所在的nodeip关联起来()
image
每个Node都知道如何把数据包转发到其内部运行的Pod,当一个数据包到达Node后,其内部数据流就和Node内Pod之间的流转类似了。
对于如何来配置网络,k8s在网络这块自身并没有实现网络规划的具体逻辑,而是制定了一套CNI(Container Network Interface)接口规范,开放给社区来实现。
例如AWS,亚马逊为k8s维护了一个容器网络插件,使用CNI插件来让亚马逊VPC(虚拟私有云)环境中的Node与Node直接进行交互.
CoreOS的Flannel是k8s中实现CNI规范较为出名的一种实现。
image.png
image.png
image.png
image.png
从这个报文中可以看到三个部分: 1.最外层的 UDP 协议报文用来在底层网络上传输,也就是 vtep 之间互相通信的基础 2.中间是 VXLAN 头部,vtep 接受到报文之后,去除前面的 UDP 协议部分,根据这部分来处理 vxlan 的逻辑,主要是根据 VNI 发送到最终的虚拟机 3.最里面是原始的报文,也就是虚拟机看到的报文内容
image.png
Flannel是CoreOS团队针对Kubernetes设计的一个网络规划实现,简单来说,它的功能有以下几点:
image.png
image.png
image.png
如上图,总的来说就是建立VXLAN 隧道,通过UDP把IP封装一层直接送到对应的节点,实现了一个大的 VLAN。
上面展示了Pod之间如何通过他们自己的ip地址进行通信,但是pod的ip地址是不持久的,当集群中pod的规模缩减或者pod故障或者node故障重启后,新的pod的ip就可能与之前的不一样的。所以k8s中衍生出来Service来解决这个问题。
k8s中 Service管理了一系列的Pods,每个Service有一个虚拟的ip,要访问service管理的Pod上的服务只需要访问你这个虚拟ip就可以了,这个虚拟ip是固定的,当service下的pod规模改变、故障重启、node重启时候,对使用service的用户来说是无感知的,因为他们使用的service的ip没有变。
当数据包到达Service虚拟ip后,数据包会被通过k8s给该servcie自动创建的负载均衡器路由到背后的pod容器。下面我们看看具体是如何做到的
为了实现负载均衡,k8s依赖linux内建的网络框架-netfilter。Netfilter是Linux提供的内核态框架,允许使用者自定义处理接口实现各种与网络相关的操作。 Netfilter为包过滤,网络地址转换和端口转换提供各种功能和操作,以及提供禁止数据包到达计算机网络内敏感位置的功能。在linux内核协议栈中,有5个跟netfilter有关的钩子函数,数据包经过每个钩子时,都会检查上面是否注册有函数,如果有的话,就会调用相应的函数处理该数据包
|
| Incoming
↓
+-------------------+
| NF_IP_PRE_ROUTING |
+-------------------+
|
|
↓
+------------------+
| | +----------------+
| routing decision |-------->| NF_IP_LOCAL_IN |
| | +----------------+
+------------------+ |
| |
| ↓
| +-----------------+
| | local processes |
| +-----------------+
| |
| |
↓ ↓
+---------------+ +-----------------+
| NF_IP_FORWARD | | NF_IP_LOCAL_OUT |
+---------------+ +-----------------+
| |
| |
↓ |
+------------------+ |
| | |
| routing decision |<----------------+
| |
+------------------+
|
|
↓
+--------------------+
| NF_IP_POST_ROUTING |
+--------------------+
|
| Outgoing
↓
netfilter是工作在那一层?
iptables是运行在用户态的用户程序,其基于表来管理规则,用于定义使用netfilter框架操作和转换数据包的规则。根据rule的作用分成了好几个表,比如用来过滤数据包的rule就会放到filter表中,用于处理地址转换的rule就会放到nat表中,其中rule就是应用在netfilter钩子上的函数,用来修改数据包的内容或过滤数据包。目前iptables支持的表有下面这些:
在k8s中,iptables规则由kube-proxy控制器配置,该控制器监视K8s API服务器的更改。当对Service或Pod的虚拟IP地址进行修改时,iptables规则也会更新以便让service能够正确的把数据包路由到后端Pod。
iptables规则监视发往Service虚拟IP的流量,并且在匹配时,从可用Pod集合中选择随机Pod IP地址,iptables规则将数据包的目标IP地址从Service的虚拟IP更改为选定的Pod的ip。总的来说iptables已在机器上完成负载平衡,并将指向Servcie的虚拟IP的流量转移到实际的pod的IP。
在从service到pod的路径上,IP地址来自目标Pod。 在这种情况下,iptables再次重写IP头以将Pod IP替换为Service的IP,以便Pod认为它一直与Service的虚拟IP通信。
k8s的最新版本(1.11)包括了用于集群内负载平衡的第二个选项:IPVS。 IPVS(IP Virtual Server)也构建在netfilter之上,并实现传输层负载平衡(属于Linux内核的一部分)。 IPVS包含在LVS(Linux虚拟服务器)中,它在主机上运行,并在真实服务器集群前充当负载均衡器。 IPVS可以将对基于TCP和UDP的服务的请求定向到真实服务器,并使真实服务器的服务在单个IP地址上显示为虚拟服务。这使得IPVS非常适合Kubernetes服务。
声明Kubernetes服务时,您可以指定是否要使用iptables或IPVS完成群集内负载平衡。 IPVS专门用于负载平衡,并使用更高效的数据结构(哈希表),与iptables相比,允许几乎无限的规模。在创建IPVS负载时,会发生以下事情:在Node上创建虚拟IPVS接口,将Service的IP地址绑定到虚拟IPVS接口,并为每个Service额IP地址创建IPVS服务器。将来,期望IPVS成为集群内负载平衡的默认方法。
image
image
到目前为止,我们已经了解了如何在Kubernetes集群中路由流量。下面我们希望将服务暴露给外部使用(互联网)。 这需要强调两个相关的问题:(1)从k8s的service访问Internet,以及(2)从Internet访问k8s的service.
从Node到公共Internet的路由流量是特定于网络的,实际上取决于网络配置。为了使本节更具体,下面使用AWS VPC讨论具体细节。
在AWS中,k8s集群在VPC内运行,其中每个Node都分配了一个可从k8s集群内访问的私有IP地址。要使群集外部的流量可访问,需要将Internet网关连接到VPC。 Internet网关有两个目的:在VPC路由表中提供可以路由到Internet的流量的目标,以及为已分配公共IP地址的任何实例执行网络地址转换(NAT)。 NAT转换负责将群集专用的节点内部IP地址更改为公共Internet中可用的外部IP地址。
通过Internet网关,Node可以将流量路由到Internet。不幸的是,有一个小问题。 Pod具有自己的IP地址,该IP地址与承载Pod的Node的IP地址不同,并且Internet网关上的NAT转换仅适用于Node的 IP地址,因为它不知道Node上正在运行那些Pod - Internet网关不是容器感知的。让我们再次看看k8s如何使用iptables解决这个问题。
本质都是使用NAT来做
image
image.png
让Internet流量进入k8s集群,这特定于配置的网络,可以在网络堆栈的不同层来实现:
让外网访问k8s内部的服务的第一个方法是创建一个NodePort类型的Service, 对于NodePort类型的Service,k8s集群中的每个Node都会打开一个端口(所有Node上的端口相同),并将该端口上收到的流量重定向到具体的Service。
对于NodePort类型的Service,我们可以通过任何Node的ip和端口号来访问NodePort服务。
创建NodePort类型的服务:
image.png
如下图,服务暴露在两个节点的端口30123上,到达任何一个端口的链接会被重定向到一个随机选择的Pod。
image.png
如何做到的?
NodePort是靠kube-proxy服务通过iptables的nat转换功能实现的,kube-proxy会在运行过程中动态创建与Service相关的iptables规则,这些规则实现了NodePort的请求流量重定向到kube-proxy进程上对应的Service的代理端口上。
kube-proxy接受到Service的请求访问后,会从service对应的后端Pod中选择一个进行访问(RR)
但 NodePort 还没有完全解决外部访问 Service 的所有问题,比如负载均衡问题,假如我们 的集群中有 10 个 Node,则此时最好有一个负载均衡器,外部的请求只需访问此负载均衡器的 IP 地址,由负载均衡器负 责转发流量到后面某个 Node 的 NodePort 上
该方式是NodePort方式的扩展,这使得Service可以通过一个专用的负载均衡器来访问,这个是由具体云服务提供商来提供的,负载均衡器将流量重定向到所有节点的端口上,如果云提供商不支持负载均衡,则退化为NodePort类型
创建k8s service时,可以选择指定LoadBalancer。 LoadBalancer的实现由云控制器提供,该控制器知道如何为您的service创建负载均衡器。 创建service后,它将公布负载均衡器的IP地址。 作为最终用户,可以开始将流量定向到负载均衡器以开始与提供的service进行通信。
创建一个负载均衡服务:
image.png
借助AWS,负载均衡器可以识别其目标组中的Node,并将平衡群集中所有节点的流量。 一旦流量到达Node,之前在整个群集中为Service安装的iptables规则将确保流量到达感兴趣的Service的Pod上。
下面看下LoadBalancer到Service的一个数据包的流转过程:
image
上图显示了承载Pod的三个Node前面的网络负载平衡器。首先流量被传到的Service的负载均衡器(1)。一旦负载均衡器收到数据包(2),它就会随机选择一个VM。这里我们故意选择了没有Pod运行的node:node 2。在这里,node上运行的iptables规则将使用kube-proxy安装在集群中的内部负载平衡规则将数据包定向到正确的Node 中的Pod。 iptables执行正确的NAT并将数据包转发到正确的Pod(4)。
需要注意的是每个服务需要创建自己独有的负载均衡器,下面要讲解的一种方式所有服务只需要一个公开服务。
这是一个与上面提到的两种方式完全不同的机制,通过一个公开的ip地址来公开多个服务,第7层网络流量入口是在网络堆栈的HTTP / HTTPS协议范围内运行,并建立在service之上。
image.png
如上图,不像负载均衡器每个服务需要一个公开ip,ingress所有服务只需要一个公网ip,当客户端向Ingress发送http请求时候,ingress会根据请求的主机名和路径决定请求转发到那个服务。
创建Ingress资源:
image.png
如上定义了一个单一规则的Ingress,确保Ingress控制器接受的所有请求主机kubia.example.com的http请求被发送到端口80上的kubia-nodeport服务上。
工作原理: 如下图,客户端首先对kubia.example.com执行DNS查找,DNS服务器返回Ingress控制器的IP,客户端拿到IP后,向Ingress控制器发送Http请求,并在Host投中指定kubia.example.com。控制器接受到请求后从Host头部就知道该访问那个服务,通过与该Service关联的endpoint对象查询Pod ip,并将请求转发到某一个Pod。
这里Ingress并没把请求转发给Service,而是自己选择一个一个Pod来访问。
image.png
第7层负载均衡器的一个好处是它们具有HTTP感知能力,因此它们了解URL和路径。 这允许您按URL路径细分服务流量。 它们通常还在HTTP请求的X-Forwarded-For标头中提供原始客户端的IP地址。
本文参考了大量资料,并结合作者的理解,如有不对,欢迎讨论^^