一、前言
k8s对Pods之间如何进行组网通信提出了要求,k8s对集群的网络有以下要求:
所有的Pods之间可以在不使用NAT网络地址转换的情况下相互通信
所有的Nodes之间可以在不使用NAT网络地址转换的情况下相互通信
每个Pod自己看到的自己的ip和其他Pod看到的一致
k8s网络模型设计基础原则:每个Pod都拥有一个独立的 IP地址,而且 假定所有 Pod 都在一个可以直接连通的、扁平的网络空间中 。 所以不管它们是否运行在同 一 个 Node (宿主机)中,都要求它们可以直接通过对方的 IP 进行访问。设计这个原则的原因 是,用户不需要额外考虑如何建立 Pod 之间的连接,也不需要考虑将容器端口映射到主机端口等问题。
由于 Kubemetes 的网络模型假设 Pod 之间访问时使用的是对方 Pod 的实际地址,所以一个
Pod 内部的应用程序看到的自己的 IP 地址和端口与集群内其他 Pod 看到的一样。它们都是 Pod 实际分配的IP地址。将IP地址和端口在Pod内部和外部都保持一致, 我们可以不使用 NAT 来进行转换,地址空间也自然是平的。
鉴于上面这些要求,我们需要解决四个不同的网络问题:
Docker容器和Docker容器之间的网络
Pod与Pod之间的网络
Pod与Service之间的网络
Internet与Service之间的网络
下面我们一一进行讨论每种网络问题,以及如何解决。
在k8s中每个Pod中管理着一组Docker容器,这些Docker容器共享同一个网络命名空间。
Pod中的每个Docker容器拥有与Pod相同的IP和port地址空间,并且由于他们在同一个网络命名空间,他们之间可以通过localhost相互访问。
什么机制让同一个Pod内的多个docker容器能够相互通信那?其实是使用Docker的一种网络模型:–net=container
container模式指定新创建的Docker容器和已经存在的一个容器共享一个网络命名空间,而不是和宿主机共享。新创建的Docker容器不会创建自己的网卡,配置自己的 IP,而是和一个指定的容器共享 IP、端口范围等
docker run -d --net=container:6c5da4316866(已有容器id) e66c955efe8d(镜像id)
每个Pod容器有一个pause容器,其有独立的网络命名空间,在Pod内启动Docker容器时候使用 –net=container就可以让当前Docker容器加入到Pod容器拥有的网络命名空间(pause容器的)
pause跟容器
k8s中,每个Pod拥有一个ip地址,不同的Pod之间可以直接使用该ip与彼此进行通讯
在同一个Node上,从Pod的视角看,它存在于自己的网络命名空间中,并且需要与该Node上的其他网络命名空间上的Pod进行通信。
那么是如何做到的?这多亏了使用linux虚拟以太网设备或者说是由两个虚拟接口组成的veth(virtual-ethernet)对使不同的网络命名空间链接起来,这些虚拟接口分布在多个网络命名空间上(这里是指多个Pod上)。
为了让多个Pod的网络命名空间链接起来,我们可以让veth对的一端链接到root网络命名空间(宿主机的),另一端链接到Pod的网络命名空间。
每对Veth就像一根接插电缆,连接两侧并允许流量在它们之间流动;这种veth对可以推广到同一个Node上任意多的Pod上,如上图这里展示使用veth对链接每个Pod到虚拟机的root网络命名空间。
下面我们看如何使用网桥设备来让通过veth对链接到root命名空间的多个Pod进行通信。
linux以太网桥(Linux Ethernet bridge)是一个虚拟的2层网络设备,目的是把多个以太网段链接起来,网桥维护了一个转发表,通过检查转发表通过它传输的数据包的目的地并决定是否将数据包传递到连接到网桥的其他网段,网桥代码通过查看网络中每个以太网设备特有的MAC地址来决定是传输数据还是丢弃数据。
网桥实现了ARP协议用来根据给定的ip地址找到对应机器的数据链路层的mac地址,一开始转发表为空,当一个数据帧被网桥接受后,网桥会广播该帧到所有的链接设备(除了发送方设备),并且把响应这个广播的设备记录到转发表;随后发往相同ip地址的流量会直接从转发表查找正确的mac地址,然后转发包到对应的设备。
如上图显示了两个Pod通过veth对链接到root网络命名空间,并且通过网桥进行通信
鉴于每个Pod有自己独立的网络命名空间,我们使用虚拟以太网设备把多个Pod的命名空间链接到了root命名空间,并且使用网桥让多个Pod之间进行通信,下面我们看如何在两个pod之间进行通信:
通过网桥这里把veth0和veth1组成为一个以太网,他们直接是可以直接通信的,另外这里通过veth对让pod1的eth0和veth0、pod2的eth0和veth1关联起来,从而让pod1和pod2相互通信。
Pod 1通过自己默认的以太网设备eth0发送一个数据包,eth0把数据传递给veth0,数据包到达网桥后,网桥通过转发表把数据传递给veth1,然后虚拟设备veth1直接把包传递给Pod2网络命名空间中的虚拟设备eth0.
k8s网络模型需要每个pod必须通过ip地址可以进行访问,每个pod的ip地址总是对网络中的其他pod可见,并且每个pod看待自己的ip与别的pod看待的是一样的(虽然他没规定如何实现),下面我们看不同Node间Pod如何交互
k8s中每个集群中的每个Node都会被分配了一个CIDR块(无类别域间路由选择,把网络前缀都相同的连续地址组成的地址组称为CIDR地址块)用来给该Node上的Pod分配IP地址。(保证pod的ip不会冲突)
另外还需要把pod的ip与所在的nodeip关联起来()
如上图Node1(vm1)上的Pod1与Node2(vm2)上Pod4之间进行交互。
首先pod1通过自己的以太网设备eth0把数据包发送到关联到root命名空间的veth0上,然后数据包被Node1上的网桥设备cbr0接受到,网桥查找转发表发现找不到pod4的Mac地址,则会把包转发到默认路由(root命名空间的eth0设备),然后数据包经过eth0就离开了Node1,被发送到网络。
数据包到达Node2后,首先会被root命名空间的eth0设备,然后通过网桥cbr0把数据路由到虚拟设备veth1,最终数据表会被流转到与veth1配对的另外一端(pod4的eth0)
每个Node都知道如何把数据包转发到其内部运行的Pod,当一个数据包到达Node后,其内部数据流就和Node内Pod之间的流转类似了。
对于如何来配置网络,k8s在网络这块自身并没有实现网络规划的具体逻辑,而是制定了一套CNI(Container Network Interface)接口规范,开放给社区来实现。
例如AWS,亚马逊为k8s维护了一个容器网络插件,使用CNI插件来让亚马逊VPC(虚拟私有云)环境中的Node与Node直接进行交互.
CoreOS的Flannel是k8s中实现CNI规范较为出名的一种实现。
UDP帧格式
vlan
3,1,12 vid
vxlan协议格式 VXLAN 全称是 Virtual eXtensible Local Area Network,虚拟可扩展的局域网。它是一种 overlay 技术(覆盖网络通过将一个分组封装在另一个分组内来将网络服务与底层基础设施分离。在将封装的数据包转发到端点后,将其解封装。),通过三层的网络来搭建虚拟的二层网络,其帧格式:
从这个报文中可以看到三个部分:
1.最外层的 UDP 协议报文用来在底层网络上传输,也就是 vtep 之间互相通信的基础
2.中间是 VXLAN 头部,vtep 接受到报文之后,去除前面的 UDP 协议部分,根据这部分来处理 vxlan 的逻辑,主要是根据 VNI 发送到最终的虚拟机
3.最里面是原始的报文,也就是虚拟机看到的报文内容
VTEP(VXLAN Tunnel Endpoints):vxlan 网络的边缘设备,用来进行 vxlan 报文的处理(封包和解包)。vtep 可以是网络设备(比如交换机),也可以是一台机器(比如虚拟化集群中的宿主机)
VNI(VXLAN Network Identifier):VNI 是每个 vxlan 的标识,是个 24 位整数,一共有 2^24 = 16,777,216(一千多万),一般每个 VNI 对应一个租户,也就是说使用 vxlan 搭建的公有云可以理论上可以支撑千万级别的租户
Tunnel:隧道是一个逻辑上的概念,在 vxlan 模型中并没有具体的物理实体想对应。隧道可以看做是一种虚拟通道,vxlan 通信双方(图中的虚拟机)认为自己是在直接通信,并不知道底层网络的存在。从整体来说,每个 vxlan 网络像是为通信的虚拟机搭建了一个单独的通信通道,也就是隧道
Flannel是CoreOS团队针对Kubernetes设计的一个网络规划实现,简单来说,它的功能有以下几点:
使集群中的不同Node主机创建的Docker容器都具有全集群唯一的虚拟IP地址。
建立一个覆盖网络(overlay network),通过这个覆盖网络,将数据包原封不动的传递到目标容器。覆盖网络是建立在另一个网络之上并由其基础设施支持的虚拟网络。覆盖网络通过将一个分组封装在另一个分组内来将网络服务与底层基础设施分离。在将封装的数据包转发到端点后,将其解封装。
创建一个新的虚拟网卡flannel0接收docker网桥的数据,通过维护路由表,对接收到的数据进行封包和转发(vxlan)。
路由信息一般存放到etcd:多个node上的Flanneld依赖一个etcd cluster来做集中配置服务,etcd保证了所有node上flanned所看到的配置是一致的。同时每个node上的flanned监听etcd上的数据变化,实时感知集群中node的变化
Flannel首先会在Node上创建一个名为flannel0的网桥(vxlan类型的设备),并且在每个Node上运行一个名为flanneld的代理.每个node上的flannel代理会从etcd上为当前node申请一个CIDR地址块用来给该node上的pod分配地址。
Flannel致力于给k8s集群中的nodes提供一个3层网络,他并不控制node中的容器是如何进行组网的,仅仅关心流量如何在node之间流转。
如上图ip为10.1.15.2的pod1与另外一个Node上的10.1.20.3的pod2进行通信。
首先pod1通过veth对把数据包发送到docker0虚拟网桥,网桥通过查找转发表发现10.1.20.3不在自己管理的网段,就会把数据包 转发给默认路由(这里为flannel0网桥)
flannel.0网桥是一个vxlan设备,flannel.0收到数据包后,由于自己不是目的地10.1.20.3,也要尝试将数据包重新发送出去。数据包沿着网络协议栈向下流动,在二层时需要封二层以太包,填写目的mac地址,这时一般应该发出arp:”who is 10.1.20.3″。但vxlan设备的特殊性就在于它并没有真正在二层发出这个arp包,而是由linux kernel引发一个”L3 MISS”事件并将arp请求发到用户空间的flanned程序。
flanned程序收到”L3 MISS”内核事件以及arp请求(who is 10.1.20.3)后,并不会向外网发送arp request,而是尝试从etcd查找该地址匹配的子网的vtep信息,也就是会找到node2上的flanel.0的mac地址信息,flanned将查询到的信息放入node1 host的arp cache表中,flanneel0完成这项工作后,linux kernel就可以在arp table中找到 10.1.20.3对应的mac地址并封装二层以太包了:
由于是Vlanx设备,flannel0还会对上面的包进行二次封装,封装新的以太网mac帧:
node上2的eth0接收到上述vxlan包,kernel将识别出这是一个vxlan包,于是拆包后将packet转给node上2的flannel.0。flannel.0再将这个数据包转到docker0,继而由docker0传输到Pod2的某个容器里。
如上图,总的来说就是建立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有关的钩子函数,数据包经过每个钩子时,都会检查上面是否注册有函数,如果有的话,就会调用相应的函数处理该数据包
NF_IP_PRE_ROUTING: 接收的数据包刚进来,还没有经过路由选择,即还不知道数据包是要发给本机还是其它机器。
NF_IP_LOCAL_IN: 已经经过路由选择,并且该数据包的目的IP是本机,进入本地数据包处理流程。
NF_IP_FORWARD: 已经经过路由选择,但该数据包的目的IP不是本机,而是其它机器,进入forward流程。
NF_IP_LOCAL_OUT: 本地程序要发出去的数据包刚到IP层,还没进行路由选择。
NF_IP_POST_ROUTING: 本地程序发出去的数据包,或者转发(forward)的数据包已经经过了路由选择,即将交由下层发送出去。
netfilter是工作在那一层?
iptables是运行在用户态的用户程序,其基于表来管理规则,用于定义使用netfilter框架操作和转换数据包的规则。根据rule的作用分成了好几个表,比如用来过滤数据包的rule就会放到filter表中,用于处理地址转换的rule就会放到nat表中,其中rule就是应用在netfilter钩子上的函数,用来修改数据包的内容或过滤数据包。目前iptables支持的表有下面这些:
Filter:从名字就可以看出,这个表里面的rule主要用来过滤数据,用来控制让哪些数据可以通过,哪些数据不能通过,它是最常用的表。
NAT(*):里面的rule都是用来处理网络地址转换的,控制要不要进行地址转换,以及怎样修改源地址或目的地址,从而影响数据包的路由,达到连通的目的,这是家用路由器必备的功能。
Mangle:里面的rule主要用来修改IP数据包头,比如修改TTL值,同时也用于给数据包添加一些标记,从而便于后续其它模块对数据包进行处理(这里的添加标记是指往内核skb结构中添加标记,而不是往真正的IP数据包上加东西)。
Raw:在netfilter里面有一个叫做connection tracking的功能,主要用来追踪所有的连接,而raw表里的rule的功能是给数据包打标记,从而控制哪些数据包不被connection tracking所追踪。
Security:里面的rule跟SELinux有关,主要是在数据包上设置一些SELinux的标记,便于跟SELinux相关的模块来处理该数据包。
在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通信。
如上图当从一个Pod发送数据包到Service时候,数据包先从Pod1所在的虚拟设备eth0离开pod1,并通过veth对的另外一端veth0传递给网桥cbr0,网桥找不到service对应ip的mac地址,所以把包转发给默认路由,也就是root命名空间的eth0
在root命名空间的设备eth0接受到数据包前,数据包会经过iptables进行过滤,iptables接受数据包后会使用kube-proxy在Node上安装的规则来响应Service或Pod的事件,将数据包的目的地址从Service的IP重写为Service后端特定的Pod IP(本例子中是pod4).
现在数据包的目的ip就不再是service的ip地址了,而是pod4的ip地址。
iptables利用Linux内核的conntrack来记住所做的Pod选择,以便将来的流量路由到同一个Pod(禁止任何扩展事件)。 从本质上讲,iptables直接在Node上进行了集群内负载均衡,然后流量使用我们已经检查过的Pod-to-Pod路由流到Pod。
收到此数据包的Pod将会回发包到源Pod,回包的源IP识别为自己的IP(比如这里为Pod4的ip),将目标IP设置为最初发送数据包的Pod(这里为pod1的ip)。
数据包进入目标Pod(这里为Pod1)所在节点后,数据包流经iptables,它使用conntrack记住它之前做出的选择,并将数据包的源IP重写为Service的IP。 从这里开始,数据包通过网桥流向与Pod1的命名空间配对的虚拟以太网设备,并流向我们之前看到的Pod1的以太网设备。
到目前为止,我们已经了解了如何在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来做
如上图中,数据包源自Pod1的网络命名空间,并通过veth对连接到root命名空间。
一旦数据包到达root命名空间,数据包就会从网桥cbr0流传到到默认设备eth0,因为数据包上的目的IP与连接到网桥的任何网段都不匹配,在到达root命名空间的以太网设备eth0之前,iptables会修改数据包。
在这种情况下,数据包的源IP地址是Pod1的ip地址,如果我们将源保持为Pod1,则Internet网关将拒绝它,因为网关NAT仅了解连接到vm的IP地址。解决方案是让iptables执行源NAT - 更改数据包源 - 以便数据包看起来来自VM而不是Pod。
有了正确的源IP,数据包现在可以离开VM,并到达Internet网关。 Internet网关将执行另一个NAT,将源IP从VM内部IP重写为Internet IP。最后,数据包将到达公共互联网。在回来数据包的路上,数据包遵循相同的路径,任何源IP都会与发送时候做相同的修改操作,以便系统的每一层都接收它理解的IP地址:Node,以及Pod命名空间中中的Pod IP。
让Internet流量进入k8s集群,这特定于配置的网络,可以在网络堆栈的不同层来实现:
(1) NodePort
(2)Service LoadBalancer
(3)Ingress控制器。
让外网访问k8s内部的服务的第一个方法是创建一个NodePort类型的Service,
对于NodePort类型的Service,k8s集群中的每个Node都会打开一个端口(所有Node上的端口相同),并将该端口上收到的流量重定向到具体的Service。
对于NodePort类型的Service,我们可以通过任何Node的ip和端口号来访问NodePort服务。
创建NodePort类型的服务:
如下图,服务暴露在两个节点的端口30123上,到达任何一个端口的链接会被重定向到一个随机选择的Pod。
如何做到的?
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类型.本质是所有NodeIp:nodePort的负载均衡器
创建k8s service时,可以选择指定LoadBalancer。 LoadBalancer的实现由云控制器提供,该控制器知道如何为您的service创建负载均衡器。 创建service后,它将公布负载均衡器的IP地址。 作为最终用户,可以开始将流量定向到负载均衡器以开始与提供的service进行通信。
创建一个负载均衡服务:
借助AWS,负载均衡器可以识别其目标组中的Node,并将平衡群集中所有节点的流量。 一旦流量到达Node,之前在整个群集中为Service安装的iptables规则将确保流量到达感兴趣的Service的Pod上。
下面看下LoadBalancer到Service的一个数据包的流转过程:
我们来看看LoadBalancer在实践中是如何运作的。部署Service后,正在使用的云提供商将会创建一个新的负载均衡器(1)。
由于负载均衡器不能识别容器,因此一旦流量到达负载均衡器,它就会把数据包发送到在构成群集的某个Node中(2)。每个Node上的iptables规则将来自负载均衡器的传入流量重定向到正确的Pod(3)
这些iptables规则是在service创建时候创建。从Pod到请求方的响应将返回Pod的IP,但请求方需要具有负载均衡器的IP地址。这就需要iptables和conntrack用于在返回路径上正确地重写IP。
上图显示了承载Pod的三个Node前面的网络负载平衡器。首先流量被传到的Service的负载均衡器(1)。一旦负载均衡器收到数据包(2),它就会随机选择一个VM。这里我们故意选择了没有Pod运行的node:node 2。在这里,node上运行的iptables规则将使用kube-proxy安装在集群中的内部负载平衡规则将数据包定向到正确的Node 中的Pod。 iptables执行正确的NAT并将数据包转发到正确的Pod(4)。
需要注意的是每个服务需要创建自己独有的负载均衡器,下面要讲解的一种方式所有服务只需要一个公开服务。
5.2.3 第七层流量入口:Ingress Controller
这是一个与上面提到的两种方式完全不同的机制,通过一个公开的ip地址来公开多个服务,第7层网络流量入口是在网络堆栈的HTTP / HTTPS协议范围内运行,并建立在service之上。
如上图,不像负载均衡器每个服务需要一个公开ip,ingress所有服务只需要一个公网ip,当客户端向Ingress发送http请求时候,ingress会根据请求的主机名和路径决定请求转发到那个服务。
创建Ingress资源:
如上定义了一个单一规则的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来访问。
第7层负载均衡器的一个好处是它们具有HTTP感知能力,因此它们了解URL和路径。 这允许您按URL路径细分服务流量。 它们通常还在HTTP请求的X-Forwarded-For标头中提供原始客户端的IP地址。