**导读:**前文 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 路由信息在汇聚层上再进行分发(propagation)。在集群网站中 Reflector 组件可以部署多个,完全能于部署规模大小来决定。Reflector 组件仅仅执行路由信令和条目的分发,其中不涉及任何数据面流量。路由汇聚层分发:
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 地址)结果后,插件调用不同网络类型下的NetnsDriver``Setup
接口实现来完成从容器网卡通往主机网卡的链路设置,其中:ENIMultiIP
和VPCIP
均是采用vethDriver
的链路模式,步骤包括:- Create veth pair
- Add IP addr for container interface
- Add routes
- Host side namespace config
- Add host routes and rules
VPCENI
稍有不同是为每个 Pod 在 VPC 平面上绑定一个 eni,其中包括两次NetnsDriver
接口调用:vethDriver
rawNicDriver
(主要实现 VPC 平面网络路由设置,包括缺省路由和网关等配置)
综上图示:
为什么需要支持上述三种网络类型?根本上是由阿里云 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 的应用开发和部署带来更加简便和高效的优点。