前文Kubernetes中的ClusterIP、NodePort、LoadBalancer、Ingress服务访问方式比较中总结了服务接入访问的主要方式,以及它们之间隐含关系。有了这些概念基础后,K8S应用开发和服务部署就容易很多了,但Under the hood服务访问究竟是如何实现的呢?这篇内容就Kubernetes的网络模型和典型的容器网络实现,特别是阿里云自己的容器网络插件(Terway)的实现做了一个较详细的总结。
Pod之间Container-to-Container networking
Linux networking namespace为进程通讯提供了一个逻辑网络栈,包括network devices、routes、firewall rules。Network namespace(NS)管理实际是为其中的所有进程提供了一个独立的逻辑网络Stack。
缺省情况下,Linux将每个进程挂载在Root NS下,这些进程通过eth0通往外面的世界。
在Pod世界里所有其中的容器共享一个NS,这些容器都有相同的IP和Port空间,通过localhost访问也是互通的。Shared storage也是可以访问的,通过SharedVolume挂载到容器中。如下一个NS per pod图例:
同Node中Pod-to-Pod networking
先看同一个Node下Pod之间的networking如何实现?答案是通过Virtual Ethernet Device (or veth pair)
的两块Virtual interfaces,每块veth挂载在一个NS上,来实现跨NS的连接。比如,一块挂在Root NS(host)上,另一块挂在Pod NS上,好比一根网线把两个在不同网络空间的traffic连接起来了,如图:
有了veth pair
这条网线,Pods网络可以连通到Root NS了,但在Root NS上如何实现对来自不同Pod的packet通讯呢?答案是通过Linux Ethernet Bridge
,一个虚拟的Layer2网络设备来实现不同network segments之间的Ethernet packet switching。不得不提这个old-school协议:ARP,实现了MAC地址到IP地址的发现协议。Bridge广播ethframe到所有连接的设备(除发送者外),收到ARP回复后将packet forward到对应veth设备上。如图:
跨Node之间Pod-to-Pod networking
进入这部分之前,先提及K8S在其(Pod)networking设计上的3个fundamental requirements,任何networking部分的实现都必须遵循这三个需求。
- 在不使用NAT下,所有Pods都能和其它任何Pods通讯
- 在不使用NAT下,所有Nodes都能和所有Pods通讯
- Pod所看到自己的IP和其它Pods看到它的IP一定是相同的
简要来看,K8S网络模型要求Pod IP在整个网络中都能通达。具体实现方案有三方面:
- Layer2(Switching)Solution
- Layer3(Routing)Solution,如,Calico, Terway
- Overlay Solution,如Flannel
这部分下文介绍,目前且认为Pod IP的网络通达性是确保的。
在Pod获得IP之前,kubelet为每个Node分配一个CIDR地址段(Classless inter-domain routing),每个Pod在其中获取唯一IP,CIDR 地址块的大小对应于每个Node的最大 Pod 数量(默认110个)。在Pod IP和跨Node网络层部署成功后,从源Pod1到目的Pod4的通讯如图:
Pod-to-Service networking
K8S Service管理服务的Pods状态,在Pod有变化下管理对应IP的变化,并管理对外提供服务的Virtual IP到Pod IPs路由访问,实现外部对服务Virtual IP的访问路由到Pod IP,以此屏蔽外部对服务后端的实现形态。所以在服务创建时,会对应生成一个Virtual IP(也即是Cluster IP),任何对该Virtual IP的访问将打散路由到服务所属的Pods上。
K8S的服务是如何实现对Virtual IP的访问负载均衡呢?答案是netfilter和iptables。netfilters是Linux built-in networking framework,为Linux提供网络包过滤、NAT和Port translation等丰富的自定义handler实现。iptables是运行在Linux user-space的规则管理系统,为netfilter框架提供丰富的包转发规则管理。
在K8S实现中kube-proxy(node deamon)通过watch apiserver来获得服务配置的变化,比如,服务的Virtual IP变化、Pod IP变化(ie, pod up/down)。iptables规则随之变化并将请求路由到服务对应的Pod上,Pod IP选取是随机的,这样看iptables起到了Pod负载均衡作用。在访问请求Return path上,iptables会做一次SNAT以替换IP header的Pod IP为服务Virtual IP,这样使得Client看起来请求仅在服务Virtual IP上进行通讯。
从K8S v1.11中IPVS(IP Virtual Server)被引入成为第二种集群内负载均衡方式。IPVS同样也是构建基于netfilter之上,在创建服务定义时可指定使用iptables或IPVS。IPVS是特定适合于服务负载均衡的解决方案,提供了非常丰富的均衡算法应用场景。
使用DNS
每个服务会设置一个DNS域名,kubelets
为每个容器进行配置--cluster-dns=<dns-service-ip>
,用以解析服务所对应DNS域名到对应的Cluster IP或Pod IP。1.12后CoreDNS成为缺省DNS方式。服务支持3种类型DNS records(A record、CNAME、SRV records)。其中常用的是A Records,比如,在cluster.local
的DNS name下,A record格式如pod-ip-address.my-namespace.pod.cluster.local
,其中Pod hostname和subdomain字段可以设置为标准的FQDN格式,比如,custom-host.custom-subdomain.my-namespace.svc.cluster.local
CNI
网络模型的实现上K8S在Pod资源管控(kubelet)和一套标准Container Networking Interface(CNI)定义和实现共同完成的。CNI在其中充当了"胶水"作用:各种容器网络实现能在一致的操作接口下由kubelet统一管控调度。另外,多个容器网络也能共存于一个集群内,为不同Pod的网络需求提供服务,都是在kubelet的统一管控下完成。
Overlay networking: Flannel
Flannel是CoreOS为K8S networking开发的解决方案,也是阿里云ACK产品支持的容器网络解决方案。Flannel的设计原理很简洁,在host网络之上创建另一个扁平网络(所谓的overlay),在其上地址空间中给每个pod容器设置一个IP地址,并用此实现路由和通讯。
主机内容器网络在docker bridge docker0
上完成通讯,不再赘述。主机间通讯使用内核路由表和IP-over-UDP封装进行实现。容器IP包流经docker bridge会转发到flannel0
网卡(TUN)设备上,进而流入到flanneld
进程中。flanneld
会对packet目标IP地址所属的网段信息查询其对应的下一跳
主机IP,容器子网CIDR和所属主机IP的映射(key-value)保存在etcd中,flanneld
查询得到packet目标IP所属的主机IP地址后,会将IP packet封装到一个UDP payload中并设置UDP packet目标地址为所得到的目标主机IP,最后在host网络中发送出UDP packet。到达目标主机后,UDP packet会流经flanneld
并在这里解封出IP packet,再发送至flannel0
、docker0
最后到达目标容器IP地址上。下图示意流程:
值得一提是,容器CIDR和下一跳主机IP的映射条目容量没有特殊限制。在阿里云ACK产品上该条目容量需要在VPC/vSwitch控制面中进行分发,考虑到整体性能因素,在数量上做了一定数量限制(缺省48个)。但在自建主机网络部署中,该数量限制就不会明显了,因为主机下一跳主机网络在一个大二层平面上。
Flannel新版本backend不建议采用UDP封装方式,因为traffic存在3次用户空间与内核空间的数据拷贝,(如下图)性能上存在比较大的损耗。新版本推荐用VxLan和云服务商版本的backends进行优化。
L3 networking: Calico
Calico是L3 Routing上非常流行容器网络架构方案。主要组件是Felix,BIRD和BGP Route Reflector。Felix和BIRD均是运行在Node上的deamon程序。
Felix完成网卡的管理和配置,包括Routes programming和ACLs。实现路由信息对Linux kernel FIB的操作和ACLs的管理操作。由于Felix功能完整性和运行独立性非常好,其功能作为Off-the-shelf被集成到阿里云Terway网络插件中,实现其网络策略功能。
BIRD(BGP client)完成内核路由FIB条目向集群网络侧分发,使其路由条目对所有网络节点中可见,并实现BGP路由协议功能。每一个BGP client会连接到网络中其它BGP client,这对规模较大的部署会是明显的瓶颈(due to the N^2 increase nature)。鉴于该限制引入了BGP Route Reflector组件,实现BGP clients路由分发的中心化汇聚。在集群网站中Reflector组件可以部署多个,完全能于部署规模大小来决定。Reflector组件仅仅执行路由信令和条目的分发,其中不涉及任何数据面流量。如下简要的Calico架构:
L3 networking:Terway
Terway是阿里云自研CNI插件,提供了阿里云VPC互通和方便对接阿里云产品的基础设施,没有overlay网络带来的性能损耗,同时提供了简单易用的backend功能。
Terway功能上可分为三部分:1. CNI插件,一个独立的binary运行程序;2. Backend Server(也称为daemon),程序以独立daemonSet方式运行在每个Node上;3. Network Policy,完全集成了Calico Felix实现。
CNI插件binary是通过daemonSet部署中initContainer安装到所有节点上,实现了ADD
、DEL
、CHECK
三个接口供kubelet调用。这里以一个Pod在创建过程中的网络setup步骤来说明:
- 当一个Pod被调度到节点上时,kubelet监听到Pod创建在自己节点上,通过runtime(docker...)创建sandbox容器来打通所需namespace。
- kubelet调用插件binary的
cmdAdd
接口,插件程序对接口参数设置检查后,向backendServer发起AllocIP
调用。 backendServer程序的
networkService
根据Pod的网络类型进行相应的Pod IP申请,支持三种网络类型ENIMultiIP
、VPCENI
、VPCIP
:ENIMultiIP
是eni网卡带多IP类型,由networkService
中的ResourceManager
在自己的IP地址池中进行IP地址分配VPCENI
是为Pod创建和挂载一个eni,由networkService
中的allocateENI
向阿里云Openapi发起对所属ecs实例的eni创建、挂载,并获得对应eni IP地址VPCIP
是为Pod在VPC网络平面上分配一个IP地址,这是在插件程序中通过调用ipam
接口从vpc管控面获取的IP地址
- 在backendServer返回
AllocIP
调用(IP地址)结果后,插件调用不同网络类型的driver完成net namespace link setup。
综上图示:
为什么需要支持上述三种网络类型?根本上是由阿里云vpc网络基础设施所决定,同时覆盖阿里云主流应用对vpc网络资源的使用场景需求。另一方面是对标Amazon AWS的容器网络解决方案,在基于VPC和ENI的网络设施上能支持同等功能。
ENI多IP、VPC ENI和VPC IP的主要区别在于前两者下的Pod网段和VPC网段是相同的,而VPC IP的网段和节点的宿主机网段不同。这样使得在ENI网络环境下的IP路由完全在VPC的L2网络平面上进行,而VPC IP网络需要在VPC路由表中进行配置Pod网段的下一跳主机,和Flannel的路由模式类似。可以看出,ENI网络能带来更灵活的路由选择和更好的路由性能。如下两个截图反映其不同路由特点:
VPC ENI网络:
VPC IP网络:
ENI多IP(1个主IP/多个辅助IP)网络下有2种路由模式:veth策略路由
和ipvlan
。两者本质区别在于使用不同的路由模式,前者使用veth pair
的策略路由,后者使用ipvlan
网络路由。策略路由需要在节点上配置策略路由条目来保证辅助IP的流量经过它所属的弹性网卡。ipvlan
实现了一个网卡虚拟出多个子网卡和不同的IP地址,eni将其辅助IP绑定到这些虚拟出来的子网卡上形成一个与vpc平面打通的L3网络。这种模式使ENI多IP的网络结构比较简单,性能相对veth策略路由
网络也更好。两种网络模式切换通过配置即可完成(缺省是vethpair
):
值得一提的是Terway还实现了ENI多IP地址资源池的管理和分配机制。networkService
中的eniIPFactory
为每个eni网卡创建一个goroutine,该eni网卡上的eniIP的分配释放都在这个goroutine中完成。在创建一个eniIP时扫描已经存在的eni网卡,如该eni还存在空闲的eniIP,该goroutine会通过ipResultChan
返回给eniIPFactory
一个分配的IP。如果所有的eni网卡的eniIP都分配完毕,会先创建一个新的eni网卡和对应的goroutine,首次创建eni网卡时无需做IP分配,直接返回eni网卡主IP即可。eniIP释放是逆向的,在eni网卡的最后一个eniIP释放时,整个eni网卡资源会释放掉。
另外,有一个startCheckIdleTicker
goroutine会定期扫描地址池的MaxPoolSize
和MinPoolSize
水位,在低于和高出水位阀值时会对地址池eniIP资源进行进行创建和释放,使得地址池IP资源处于一个可控水位范围中。为了保证资源状态一致性,有一个startGarbageCollectionLoop
goroutine会定期扫描IP地址是否在用或过期状态,如检测到会进行资源GC操作。最后,Pod资源状态数据都持久化在本地的一个boltDB
文件中/var/lib/cni/terway/pod.db
,即使Pod已经在apiServer中删除,GetPod
会从本地boltDB
中读取副本数据。在Pod已经删除但副本还存在DB的情况下,GC goroutine检测到会执行清理。截图简述:
总结下,从这些backend功能可以看到Terway为阿里云vpc网络资源设施连通性做了很好的封装,让基于阿里云Kubernetes的应用开发和部署带来更加简便和高效的优点。