从容器诞生开始,存储和网络这两个话题就一直为大家津津乐道。我们今天这个环境下讲网络这个问题,其实是因为容器对网络的需求,和传统物理、虚拟环境对网络环境需求是有差别的,主要面临以下两个问题:
- 过去IaaS层在网络方便做了很多工作,已经形成了成熟的环境,如果和容器环境适配,两边都需要做很多改造
- 容器时代提倡微服务,导致容器的粒度之小,离散程度之大,原有IaaS层网络解决方案很难承载如此复杂的需求
我们来看下一些主流的容器网络接入方案:
Host network
最简单的网络模型就是让容器共享Host的network namespace,使用宿主机的网络协议栈。这样,不需要额外的配置,容器就可以共享宿主的各种网络资源。共享物理网卡
这种接入方式主要使用SR-IOV技术,每个容器被分配一个VF,直接通过PCIe网卡与外界通信。SR-IOV 技术是一种基于硬件的虚拟化解决方案,可提高性能和可伸缩性。SR-IOV 标准允许在虚拟机之间高效共享 PCIe设备,并且它是在硬件中实现的,可以获得能够与本机性能媲美的 I/O 性能。启用了 SR-IOV 并且具有适当的硬件和 OS 支持的 PCIe 设备(例如以太网端口)可以显示为多个单独的物理设备,每个都具有自己的 PCIe 配置空间。
SR-IOV主要用于虚拟化中,当然也可以用于容器:
SR-IOV配置(需要网卡支持,线上主流万兆卡Intel 540可以支持128个VF(Virtual Function)):
modprobe ixgbevf lspci -Dvmm|grep -B 1 -A 4 Ethernet echo 2 > /sys/bus/pci/devices/0000:82:00.0/sriov_numvfs # check ifconfig -a. You should see a number of new interfaces created, starting with “eth”, e.g. eth4
Intel也给Docker实现了一个[SR-IOV network plugin](https://github.com/clearcontainers/sriov "SR-IOV network plugin"),同样也有相应的[CNI(后面会提到)Plugin](https://github.com/Intel-Corp/sriov-cni "CNI(后面会提到)Plugin")。SR-IOV的接入方式可以达到物理网卡的性能,但是需要硬件支持,而且VF的数量是有限的。
共享容器网络
多个容器共享同一个netns,只需要第一个容器配置网络。比如Kubernetes Pod就是所有容器共享同一个pause容器的网络。VSwitch/Bridge
容器拥有独立的network namespace,通过veth-pair连接到VSwitch或者Bridge上。容器网络技术日新月异,有些相当复杂,而且提供某种功能的方式也多种多样。这样就给容器runtime与调度系统的实现与各种接入方式的适配带来的很大的困难。为了解决这些问题,Docker与CoreOS相继发布了容器网络标准,便是CNM与CNI,为Linux容器网络提供了统一的接口。
先来看下CNM:
Libnetwork是CNM的原生实现。它为Docker daemon和网络驱动程序之间提供了接口。网络控制器负责将驱动和一个网络进行对接。每个驱动程序负责管理它所拥有的网络以及为该网络提供的各种服务,例如IPAM等等。由多个驱动支撑的多个网络可以同时并存。网络驱动可以按提供方被划分为原生驱动(libnetwork内置的或Docker支持的)或者远程驱动 (第三方插件)。原生驱动包括 none、bridge、overlay 以及 MACvlan。驱动也可以被按照适用范围被划分为本地(单主机)的和全局的 (多主机)。
- Sandbox:一个Sandbox对应一个容器的网络栈,能够对该容器的interface、route、dns等参数进行管理。一个Sandbox中可以有多个Endpoint,这些Endpoint可以属于不同的Network。Sandbox的实现可以为linux network namespace、FreeBSD Jail或其他类似的机制。
- Endpoint: Sandbox通过Endpoint接入Network,一个Endpoint只能属于一个Network。Endpoint的实现可以是veth pair、Open vSwitch internal port或者其他类似的设备。
- Network:一个Network由一组Endpoint组成,这些Endpoint彼此间可以直接进行通信,不同的Network间Endpoint的通信彼此隔离。Network的实现可以是linux bridge、Open vSwitch等。
我们可以以Docker操作为例,简单看下CNM创建容器的工作流:
- Create Network(docker network create)
- IpamDriver.RequestPool:创建subnetpool用于分配IP
- IpamDriver.RequestAddress:为gateway获取IP
- NetworkDriver.CreateNetwork:创建network和subnet
- Create Container(docker run / docker network connect)
- IpamDriver.RequestAddress:为容器获取IP
- NetworkDriver.CreateEndpoint:创建port
- NetworkDriver.Join:为容器和port绑定
- NetworkDriver.ProgramExternalConnectivity
- NetworkDriver.EndpointOperInfo
CNM与libnetwork相对比较复杂,这里不做详细介绍,相关内容可参考libnetwork的[Design Doc](https://github.com/docker/libn ... gn.md "Design Doc")。
再看下CNI:
其基本思想为:Container Runtime在创建容器时,先创建好network namespace,然后调用CNI插件为这个netns配置网络,其后再启动容器内的进程。
CNI插件包括两部分:
- CNI Plugin负责给容器配置网络,它包括两个基本的接口
- 配置网络:AddNetwork(net NetworkConfig,rt RuntimeConf)(types.Result,error)
- 清理网络:DelNetwork(net NetworkConfig, rt RuntimeConf) error
- IPAM Plugin负责给容器分配IP地址,实现包括host-local和dhcp等
再来看下CNI创建容器的工作流:
- 容器runtime分配一个网络命名空间以及一个容器ID
- 连同一些CNI配置参数传给网络驱动
- 网络驱动会将该容器连接到网络并将分配的IP地址以JSON的格式返回给容器runtime
所有CNI插件均支持通过环境变量和标准输入传入参数:
$ echo '{"cniVersion": "0.3.1","name": "mynet","type": "macvlan","bridge": "cni0","isGateway": true,"ipMasq": true,"ipam": {"type": "host-local","subnet": "10.244.1.0/24","routes": [{ "dst": "0.0.0.0/0" }]}}' | sudo CNI_COMMAND=ADD CNI_NETNS=/var/run/netns/a CNI_PATH=./bin CNI_IFNAME=eth0 CNI_CONTAINERID=a CNI_VERSION=0.3.1 ./bin/bridge $ echo '{"cniVersion": "0.3.1","type":"IGNORED", "name": "a","ipam": {"type": "host-local", "subnet":"10.1.2.3/24"}}' | sudo CNI_COMMAND=ADD CNI_NETNS=/var/run/netns/a CNI_PATH=./bin CNI_IFNAME=a CNI_CONTAINERID=a CNI_VERSION=0.3.1 ./bin/host-local
直观的来看,CNI 的规范比较小巧。它规定了一个容器runtime和网络插件之间的简单的契约(详细内容请参考[SPEC](https://github.com/containerne ... EC.md "SPEC"))。
在斟酌某项技术的时候,我们需要考虑采纳的成本;是否会增加对供应商的依赖。社区的采纳程度和支持程度也是比较重要的考量。插件开发者还需要考虑哪种方式相对比较容易实现。
目前,主流容器调度框架(Mesos、Kubernetes)与网络解决方案(Calico、Flannel等)都对CNI有良好的支持,另外其简单的接口使插件的开放成本很低;并且不依赖docker runtime,使容器解决方案有了更多选择(例如Mesos Containerizer)。所以,我们选择了CNI。
在对比业内支持CNI接口的主流网络解决方案之前,需要对多主机条件下,容器的组网方案做下分类,来方便大家理解不同解决方案之间的区别:
Flat
- L2 Flat
- 各个host中容器在虚拟与物理网络形成的VLAN(大二层)中
- 容器可以在任意host间迁移不用改变其IP
- 不同租户IP不可Overlap
- L3 Flat
- 各个host中容器在虚拟与物理网络中可路由,且为32位路由
- 因为是32位路由,所以容器在任意host间迁移不用改变IP
- 不同租户IP不可Overlap,容器与物理网络IP也不可Overlap
L3 Hierarchy
- 各个host中容器在虚拟与物理网络中可路由
- 路由在不同层次上(VM/Host/Leaf/Spine)以聚合路由的形式存在
- 相同CIDR的容器需在物理上被组织在一起
- 容器在host间迁移需要改变IP
- 不同租户IP不可Overlap,容器与物理网络IP也不可Overlap
Overlay
- L2 over L3
- 容器可跨L3 Underlay进行L2通信
- 容器可在任意host间迁移而不改变其IP
- L3 over L3
- 容器可跨L3 Underlay进行L3通信
- 容器可在任意host间迁移可能需要改变其IP
- 不同租户IP可Overlap,容器IP也可与Underlay网络Overlap
下面我们来看下社区比较认可的几种容器网络解决方案。
Flannel
通过给每台宿主机分配一个子网的方式为容器提供虚拟网络,它基于Linux TUN/TAP,使用UDP封装IP包来创建overlay网络,并借助etcd维护网络的分配情况。控制平面上host本地的flanneld负责从远端的ETCD集群同步本地和其它host上的subnet信息,并为POD分配IP地址。数据平面flannel通过Backend(比如UDP封装)来实现L3 Overlay,既可以选择一般的TUN设备又可以选择VxLAN设备。
从上图可以看出,Flannel是典型的L3 over L3组网方案。
支持的Backend:
- udp:使用udp封装,默认使用8285端口
- vxlan:vxlan封装,需要配置VNI,Port(默认8472)和GBP
- host-gw:直接路由的方式
- 公有云vpc:aws-vpc、gce、ali-vpc
如上图,可以方便的与Docker集成:
source /run/flannel/subnet.env docker daemon --bip=${FLANNEL_SUBNET} --mtu=${FLANNEL_MTU} &
也提供CNI接口:
CNI插件会将flannel网络配置转换为bridge插件配置,并调用bridge插件给容器netns配置网络。比如下面的flannel配置:
{ "name": "mynet", "type": "flannel", "delegate": { "bridge": "mynet0", "mtu": 1400 } }
会被插件转换为:
{ "name": "mynet", "type": "bridge", "mtu": 1472, "ipMasq": false, "isGateway": true, "ipam": { "type": "host-local", "subnet": "10.1.17.0/24" } }
Flannel便是通过CNI与Mesos、Kubernetes集成。由于历史原因,我们线上Kubernetes集群是通过前一种方式直接与Docker集成的。
优点:
- 配置安装简单,使用方便
- 与公有云集成方便,VPC方式无Overhead
缺点:
- Vxlan模式对平滑重启支持不好(重启需要数秒来刷新ARP表,且不可变更配置,例如VNI、iface)
- 功能相对简单,不支持Network Policy
- Overlay存在一定Overhead
Weave
不同于其它的multi-host方案,其支持去中心化的控制平面,各个host上的wRouter间通过建立Full Mesh的TCP链接,并通过Gossip来同步控制信息。这种方式省去了集中式的K/V Store,能够在一定程度上减低部署的复杂性。不过,考虑到docker libnetwork是集中式的K/V Store作为控制平面,因此Weave为了集成docker,它也提供了对集中式控制平面的支持,能够作为docker remote driver与libkv通信。数据平面上,Weave通过UDP封装实现L2 Overlay,封装支持两种模式:
- 运行在user space的sleeve mode:通过pcap设备在Linux bridge上截获数据包并由wRouter完成UDP封装,支持对L2 traffic进行加密,还支持Partial Connection,但是性能损失明显。
- 运行在kernal space的 fastpath mode:即通过OVS的odp封装VxLAN并完成转发,wRouter不直接参与转发,而是通过下发odp 流表的方式控制转发,这种方式可以明显地提升吞吐量,但是不支持加密等高级功能。
- 所有容器都连接到Weave网桥
- weave网桥通过veth pair连到内核的OpenVSwitch模块
- 跨主机容器通过openvswitch vxlan通信
- policy controller通过配置iptables规则为容器设置网络策略
关于Service的发布,Weave做的也比较完整。首先,wRouter集成了DNS功能,能够动态地进行服务发现和负载均衡,另外,与libnetwork 的overlay driver类似,weave要求每个容器有两个网卡,一个就连在lb/ovs上处理L2 流量,另一个则连在docker0上处理Service流量,docker0后面仍然是iptables作NAT。同事,提供了一个容器监控和故障排查工具Weave Scope,可以方便的生成整个集群的拓扑并智能分组。
优点:去中心化、故障自愈、加密通讯、支持组播
缺点:
- sleeve mode性能损耗巨大
- full mesh模型限制了集群规模
- Partial Connection在fastpath未实现
Contiv
思科开源的容器网络方案,是一个用于跨虚拟机、裸机、公有云或私有云的异构容器部署的开源容器网络架构,并与主流容器编排系统集成。Contiv最主要的优势是直接提供了多租户网络,并支持L2(VLAN),L3(BGP),Overlay(VXLAN)以及思科自家的ACI。主要由两部分组件组成:
- Netmaster
- 对外提供REST API
- 学习并分发路由到Netplugin
- 管理集群资源(IP、VLAN、VXLAN ID等)
- Netplugin
- 运行在集群中每个节点上
- 实现了CNI与CNM接口
- 通过REST方式与Netmaster通讯,监听事件,并做相应网络配置
- 数据平面使用OVS实现(插件架构,可替换,例如VPP、BPF)
优点:
- 支持多种组网方式,集成现有SDN方案
- 多租户网络混部在同一台主机上
- 插件化的设计,可替换的数据平面
- 即时生效的容器网络Policy/ACL/QoS规则
- 基于OVS的高性能Load Balancer的支持
- 清晰的设计,方便二次开发
缺点:
- 功能丰富带来的副作用便是配置和使用比较复杂
- 因为发展时间较短,社区和文档都不怎么成熟,也存在一些bug,稳定性有待商榷
- CNI plugin对Mesos支持不友好(跟Cisco官方沟通后,对Mesos支持仍在POC阶段)
OpenContrail
是Juniper推出的开源网络虚拟化平台,其商业版本为Contrail。OpenContrail主要由控制器和vRouter组成:
- 控制器提供虚拟网络的配置、控制和分析功能
- vRouter提供分布式路由,负责虚拟路由器、虚拟网络的建立以及数据转发
vRouter支持三种模式:
- Kernel vRouter:类似于ovs内核模块
- DPDK vRouter:类似于ovs-dpdk
- Netronome Agilio Solution (商业产品):支持DPDK, SR-IOV and Express Virtio (XVIO)
从上图可以看出,OpenContrail的架构是相当复杂的。相应的,其提供的feature基本覆盖了前所有提到的方案;甚至包括基于Cassandra后端的流量采样与分析平台;由于篇幅所限,这里不做详细介绍,相关内容可参考[Arch Doc](http://www.opencontrail.org/op ... tion/ "Arch Doc")。
计算机科学的先驱David Wheeler曾经说过——“Any problem in computer science can be solved with another level of indirection.”, SDN即是Network的indirection。容器网络解决方案多不胜数,我们在这里就不一一介绍了。但是我们可以看到,每种方案其实都是对network做了不同程度的indirection,但是不要忘了后面还有一句——“except of course for the problem of too many indirections”,所以大家在不同的方向上做出了取舍与权衡。正如前面所提到的,在斟酌某项技术的时候,我们需要考虑采纳成本、对供应商的依赖、社区的支持程度等等。我们正在如此权衡之后,并没选择上面的方案,而是选择了Calico,下面我会详细说明。
Calico
[Calico](https://www.projectcalico.org/ "Calico") 是一个纯三层的数据中心网络方案(不需要Overlay),并且与OpenStack、Mesos、Kubernetes、AWS、GCE等IaaS和容器平台都有良好的集成。Calico是一个专门做数据中心网络的开源项目。当业界都痴迷于Overlay的时候,Calico实现multi-host容器网络的思路确可以说是返璞归真——pure L3,pure L3是指容器间的组网都是通过IP来完成的。这是因为,Calico认为L3更为健壮,且对于网络人员更为熟悉,而L2网络由于控制平面太弱会导致太多问题,排错起来也更加困难。那么,如果能够利用好L3去设计数据中心的话就完全没有必要用L2。
不过对于应用来说,L2无疑是更好的网络,尤其是容器网络对二层的需求则更是强烈。业界普遍给出的答案是L2 over L3,而Calico认为Overlay技术带来的开销(CPU、吞吐量)太大,如果能用L3去模拟L2是最好的,这样既能保证性能、又能让应用满意、还能给网络人员省事,看上去是件一举多得的事。用L3去模拟L2的关键就在于打破传统的Hierarchy L3概念,IP不再以前缀收敛,大家干脆都把32位的主机路由发布到网络上,那么Flat L3的网络对于应用来说即和L2一模一样。
这个思路不简单,刨了L2存在必要性的老底儿,实际上如果不用考虑可扩展性、隔离性,IP地址和MAC地址标识endpoint的功能上确实是完全冗余的,即使考虑可扩展性,一个用L3技术形成的大二层和一个用L2技术形成的大二层并没有本质上的差距。而且,L3有成熟的、完善的、被普遍认可接受的控制平面,以及丰富的管理工具,运维起来要容易的多。
于是,Calico给出了下面的设计:
Calico在每一个计算节点利用Linux Kernel实现了一个高效的vRouter来负责数据转发,而每个vRouter通过BGP协议负责把自己上运行的workload的路由信息向整个Calico网络内传播—,路由条目全是/32的v4或者/128的v6。小规模部署可以直接互联,大规模下可通过指定的BGP route reflector来完成。 这样保证最终所有的workload之间的数据流量都是通过IP路由的方式完成互联的。Calico节点组网可以直接利用数据中心的网络结构(无论是L2或者L3),不需要额外的NAT,隧道或者Overlay Network。
此外,Calico基于iptables还提供了丰富而灵活的网络Policy,保证通过各个节点上的ACLs来提供Workload的多租户隔离、安全组以及其他可达性限制等功能。另外,对CNM与CNI的支持使其可以方便适配绝大多数场景。
这里有必要说明下,CNI和CNM并非是完全不可调和的两个模型。二者可以进行转化的,calico项目最初只支持CNI,后面才加入CNM的支持。从模型中来看,CNI中的container应与CNM的sandbox概念一致,CNI中的network与CNM中的network一致。在CNI中,CNM中的endpoint被隐含在了ADD/DELETE的操作中。CNI接口更加简洁,把更多的工作托管给了容器的管理者和网络的管理者。从这个角度来说,CNI的ADD/DELETE接口其实只是实现了docker network connect和docker network disconnect两个命令。类似的,[kubernetes/contrib](https://github.com/kubernetes/ ... ocker "kubernetes/contrib")项目提供了一种从CNI向CNM转化的过程,这里就不做详细介绍了。
我们详细看下Calico中的各个组件:
- Felix:Calico Agent,跑在每台需要运行Workload的节点上,主要负责通过iptables来配置ACLs
- etcd:分布式键值存储,主要负责网络元数据一致性,确保Calico网络状态的准确性
- confd:根据etcd上状态信息,与本地模板,生成并更新BIRD配置
- BGP Client(BIRD):主要负责容器的路由信息分发到当前Calico网络,确保Workload间的通信的有效性
- BGP Route Reflector(BIRD):大规模部署时使用,摒弃所有节点互联的 mesh 模式,通过一个或者多个BGP Route Reflector来完成集中式的路由分发
这个架构,技术成熟、高性能、易维护,看起来是生产级别的容器网络环境最好的选择。但是,也有不如意的地方:
- 没有了外面的封装,就谈不上VRF,多租户的话地址没法Overlap
- L2和L3的概念模糊了,那么network级别的安全就难以实现,port级别的安全实现难是因为需要的规则都是1:1的,数量太多
上面并不是什么严重的问题。但是有一点,Calico控制平面的上述设计中,物理网络最好是L2 Fabric,这样vRouter间都是直接可达的,路由不需要把物理设备当做下一跳。如果是L3 Fabric,控制平面的问题就来了:物理设备如果要存32位的路由,路由表将变得巨大无比。
因此,为了解决以上问题,Calico不得不采取了妥协,为了支持L3 Fabric,Calico推出了IPinIP的选项,用作cross-subnet下容器间通讯,但是有一定性能损耗。另一种方案是回退到L3 Hierarchy(calico目前支持),如果对容器迁移保持IP不变的需求不大,可以选择这种,这也是我们最后选择的方案。不过,对于使用L2 Fabric、没有多租户需求的企业来说,用calico上生产环境应该最理想选择。
Calico最理想的部署方式是基于L2 Fabrics的,官方也有相应说明([Calico over an Ethernet interconnect fabric](http://docs.projectcalico.org/ ... abric "Calico over an Ethernet interconnect fabric")),这里就不做详细说明了。由于我们数据中心规模比较大,显然L2 Fabrics不是理想的选择。事实也是如此,我们多个数据中心组成的网络是以BGP为控制层面组成的L3 Fabrics。每个数据中心为一个AS。这里不讨论跨数据中心的问题,大家只需知道我们跨数据中心在L3上是互通的便可以。
官方给出了3种基于L3 Fabrics的组网建议(详细介绍可点击进去查看):
- [AS per Rack model](http://docs.projectcalico.org/ ... model "AS per Rack model")
- [AS per Compute Server model](http://docs.projectcalico.org/ ... model "AS per Compute Server model")
- [Downward Default model](http://docs.projectcalico.org/ ... model "Downward Default model")
AS per Rack这种模型在每个ToR下加入了RR(Route Reflector),关于RR,前面有提到过,这里有必要解释一下:
首先说下BGP的水平分割原则(这里只谈IBGP),从IBGP学习到的路由绝不对再传播给其它的IBGP邻居(可以传给EBGP邻居)。IBGP水平分割主要是为了防止在AS内部产生路由环路。所以,为了AS内路由正确收敛,需要所有节点组成full mesh结构(也就是两两之间建立邻居)。但是,这种架构在大型网络中显然行不通。
另一种解决方案便是RR,一种C/S模型。在同AS内,其中一台设备作为RR,其他设备作为Client。Client与RR之间建立IBGP连接。路由反射器和它的客户机组成一个Cluster。这里不详细讨论RR的工作原理。显然,路由反射破坏了水平分割的原则,所以便要许多额外的配置来防止环路,这会给实际部署过程带来很多负担。另外,我们同一数据中心内运行的是IBGP,也就是都在同AS内。这使的配置改造更为复杂,而且,显然不符合AS per Rack。
另外两种模型同样要求每个ToR在不同的AS,也面临上述问题。另外,如果不在ToR上做路由聚合,AS per Compute Server会造成Linux服务器上有全网的路由,庞大的路由表也会成为性能的瓶颈。
经过与网络部门的认真研讨,我们最终选择了下面的部署模型:
- 每个Server选择不同的AS号,且与数据中心AS不同,与ToR建立EBGP邻居
- Server使用原有网关(默认路由),ToR不向Server宣告任何路由条目
- Server本地聚合路由到28位,并宣告给ToR
- 与网络部门制定一定算法(方便自动化),用Server的IP地址计算出AS号
可以看到,上述方案,几乎不用对数据中心现有网络架构做出改变;而且,也大大减少了Server与ToR上路由条目数。
实际部署Calico的Agent时,我们还遇到了一些问题:官方默认只提供基于Docker的部署方式,也就是之前提到的几个组件都部署在一个Docker容器中。calicoctl也是基于Docker容器做部署操作。但是,实际环境中,我们线上的Mesos集群有部分是只是用Mesos Containerizer的,也就是并没有Docker环境;而且,我们认为,Calico Agent加入对Docker daemon的依赖,考虑到稳定性,显然不是一个良好的选择。
所以,我们对Calico组件进行拆解,重新拼装,并去掉我们用不到IPv6模块;最终通过systemd在每台server上运行bird、confd、felix这三个模块,实现了去Docker化的部署(实际上对confd的模板也做了些改造,去掉了IPinIP的支持,并且适配的非Docker环境)。整个过程基于Ansible,实现了一键部署。
关于性能问题,虽然Calico在众多基于软件的容器网络解决方案里是性能最好的,但是也不是完全没有性能损耗。前面说过,Calico实现namespace内外通讯是通过linux内核的veth pair来实现的(就是之前提到的接入方式中的VSwitch/Bridge)。确实增加了一层overhead,但是好在veth pair的实现比较简单,性能损耗几乎可以忽略(无policy条件下)。我们实际测试中,对于echo server(小报文测试),请求延迟会增加0.02到0.04ms;对于bandwidth(大报文测试,万兆环境),很难看到差别。
到了这一步,你可能觉得我们大功告成,实则不然。其实我们还有很多事情可以做,也需要去做。Calico支持丰富的安全策略,但是在server上是用kernel的netfilter实现的(iptables),大量policy下的性能仍有待考察。
业内也有一些其他的实现思路,例如Google大力支持cilium方案,其policy的实现就用了BPF(Berkeley Packet Filter,最早在kernel 3.15中加入,4.8版本以后标为stable)。BPF的基本思路是把用作包处理的代码通过llvm编译成ir,然后插入到正在运行的kernel网络栈中,通过jit方式执行,大幅度提高了处理能力。这里不做详细介绍,相关信息可以看这里。cilium还加入了对XDP的支持(一种类似DPDK的技术,但是目前集成在kernel中),来加速网络栈的处理。这些都不失为未来改造calico的一个方向。
另外一个问题是,calico目前并没有对traffic shaping的支持。试想,当大量容器运行与同一物理机,其进出流量都共享物理网卡,如果不对容器流量进行限制,单个容器过分使用网络资源就会影响其他容器提供服务(磁盘IO也有同样问题,但是不在本文讨论范围)。Calico社区目前也在讨论这个问题,详情可见这个issue,目前还在API制定的阶段。
上述问题,底层实现目前可行选择是linux自带的tc(OVS同样用到了tc)。上层切入点却有两个选择,一是拓展Calico的API,在felix上实现应用tc规则的逻辑;另一种是在CNI plugin上hook,通过相应的调度系统,传参数进来,由plugin来应用tc规则。由于,calico社区已经在讨论这个问题,而且第二种方案实现起来成本也比较低。现阶段,我们只考虑方案二,也做了相应的POC。
实践过程中,由于veth pair两端的流量是相反的(ingress与egress),理论上可以在两端的egress上应用tc规则(tc只支持egress方向的shaping),来实现两个方向上的QoS。但是,我们还是遇到了一些问题,namespace内的虚拟网卡上的tc规则虽然可配置,但是并不生效。而且,这些规则对容器内应用是可见、可修改的,这样做也并不安全。所以,我们用到了ifb内核模块,把物理机上虚拟网卡的ingress流量变成egress,最终在在ifb网卡上应用tc规则,来实现容器egress方向的QoS。关于ifb,功能比较简单,大家可以看下kernel代码中相关的介绍:
/* drivers/net/ifb.c: The purpose of this driver is to provide a device that allows for sharing of resources: 1) qdiscs/policies that are per device as opposed to system wide. ifb allows for a device which can be redirected to thus providing an impression of sharing. 2) Allows for queueing incoming traffic for shaping instead of dropping. The original concept is based on what is known as the IMQ driver initially written by Martin Devera, later rewritten by Patrick McHardy and then maintained by Andre Correa. You need the tc action mirror or redirect to feed this device packets. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Authors: Jamal Hadi Salim (2005) */
迫于篇幅限制,这次就先到这里吧,后续我会把一些详细实现拿出来给大家分享,欢迎共同交流。
看了上面这么多,有人可能会问,我们做了这么多工作,究竟是为了什么。回过头来看今天,我们理所当然的用着方便的跨数据中心环网、接入负载均衡、共享存储、对象存储,甚至到上层的云平台等等。正是因为有了这些,业务们拿出编译好的代码,快的不到几分钟就可以被线上的用户所使用。
这就是基础设施,这些是这座大楼的地基。正因为如此,基础设施的改造才异常困难。我们需要用合理的抽象来解决顽疾,更需要对上层建筑的兼容。正如我们在网络方面所做的工作,让容器和现有物理设备在三层上的互通,实现了网络上对业务的透明;同时又提供了良好的性能,消除了业务迁移到云平台上的顾虑;在提高资源利用率的混部环境下,同时提供可配置的策略,为服务质量提供了有效的保障。