Unimog:CloudFlare的XDP/eBPF边缘负载均衡

2020-09-10 04:29:19

随着Cloudflare边缘网络规模的增长,我们有时会达到架构部分的极限。大约两年前,我们意识到我们现有的在数据中心内分散负载的解决方案不再能满足我们的需求。我们启动了一个项目,部署内部称为Unimog的第4层负载均衡器,以提高我们边缘网络的可靠性和运营效率。Unimog现在已经投入生产一年多了。

这篇文章解释了Unimog解决的问题以及它是如何工作的。Unimog构建在其他第4层负载均衡器中使用的技术之上,但是它的实现有很多细节是根据我们边缘网络的需要量身定做的。

CloudFlare运营任播网络,这意味着我们在全球200多个城市的数据中心提供相同的IP地址。例如,我们自己的cloudflare.com网站使用Cloudflare服务,其中一个IP地址是104.17.175.85。我们的所有数据中心都将接受到该地址的连接并响应HTTP请求。借助Internet路由的魔力,当您访问cloudflare.com并且您的浏览器连接到104.17.175.85时,您的连接通常会连接到最近的(因此也是最快的)数据中心。

这些数据中心内部有许多服务器。每个中心的服务器数量差别很大(最大的数据中心的服务器数量是最小的数据中心的100倍)。服务器运行实现我们产品的应用程序服务(我们的缓存、DNS、WAF、DDoS缓解、Spectrum、WARP等)。在单个数据中心内,任何服务器都可以在我们的任何任播IP地址上为我们的任何服务处理连接。这种一致性使事情变得简单,并避免了瓶颈。

但是,如果数据中心内的任何服务器都可以处理任何连接,那么当连接从浏览器或其他客户端到达时,是由什么来控制连接到哪台服务器的呢?这是Unimog的工作。

我们需要这种控制的主要原因有两个。首先,我们定期将服务器移入和移出运行,并且服务器应该只在运行时接收连接。例如,我们有时会将服务器从运行中移除,以便对其执行维护。有时服务器会自动退出运行,因为健康检查表明它们不能正常运行。

第二个原因与服务器上的负载管理有关(我们所说的负载是指每个服务器需要执行的计算工作量)。如果服务器上的负载超过其硬件资源的容量,那么对用户的服务质量将受到影响。当服务器接近饱和时,用户体验到的性能会下降,如果服务器过载过多,用户可能会看到错误。我们还希望防止服务器负载不足,这会降低我们从硬件投资中获得的价值。因此,Unimog可确保负载跨数据中心的服务器分布。这种一般概念称为负载平衡(平衡,因为工作必须在某个地方完成,因此要想降低一台服务器上的负载,就必须增加另一台服务器上的负载)。

请注意,在这篇文章中,我们将讨论Cloudflare如何平衡边缘数据中心自己服务器上的负载。但是负载均衡是分布式计算系统中很多地方都存在的要求。CloudFlare还有一款第7层负载平衡产品,让我们的客户可以跨其服务器平衡负载。而Cloudflare在其他地方内部使用负载平衡。

部署Unimog大大提高了我们平衡边缘数据中心服务器负载的能力。下面是一个数据中心的图表,显示了Unimog带来的差异。每行显示单个服务器的处理器利用率(线条的颜色表示服务器型号)。服务器上的负载在白天随此数据中心附近用户的活动而变化。白线标记我们启用Unimog时的点。您可以看到,在那之后,服务器上的负载变得更加均匀。当我们将Unimog部署到其他数据中心时,我们看到了类似的结果。

有多种负载平衡技术。Unimog属于称为第4层负载均衡器(L4LB)的类别。L4LB通过检查OSI网络模型的第4层信息来定向网络上的数据包,这将它们与更常见的第7层负载均衡器区分开来。

L4LB的优势在于它们的效率。它们在不处理数据包的有效负载的情况下引导数据包,因此避免了与更高级别协议相关的开销。对于任何负载均衡器来说,与投入有用工作的资源相比,负载均衡器消耗的资源要低,这一点很重要。在Cloudflare,我们已经密切关注我们的服务的高效实现,这为我们放在这些服务前面的负载均衡器设置了一个很高的门槛。

L4LB的缺点是它们只能控制哪些连接连接到哪些服务器。它们不能修改通过连接的数据,这会阻止它们参与TLS、HTTP等更高级别的协议(相比之下,第7层负载均衡器充当代理,因此它们可以修改连接上的数据并参与这些更高级别的协议)。

L4LB并不新鲜。它们主要用于那些有扩展需求的公司,仅靠L7LB很难满足这些需求。谷歌发表了关于磁悬浮的文章,Facebook发布了开源的Katran,Github也开源了他们的GLB。

Unimog是Cloudflare为满足我们边缘网络的需求而构建的L4LB。它与其他L4LB具有相同的功能,并且特别受到GLB的影响。但是,现有的L4LB并没有很好地满足一些需求,因此我们需要构建自己的L4LB:

Unimog设计为在提供应用程序服务的相同通用服务器上运行,而不需要单独的一层专门用于负载平衡的服务器。

它执行动态负载平衡:服务器负载的度量用于调整到每台服务器的连接数量,以便准确地平衡负载。

虚拟IP地址按范围进行管理(Cloudflare代表我们的客户为数十万个IPv4地址提供服务,因此单独配置这些地址是不切实际的)。

Unimog与我们现有的DDoS缓解系统紧密集成,实现依赖于Linux内核中相同的XDP技术。

本文的其余部分将更详细地描述这些特性以及随之而来的设计和实现选择。

为了让Unimog平衡负载,仅向每台服务器发送相同(或大致相同)数量的连接是不够的,因为我们服务器的性能各不相同。我们定期更新服务器硬件,现在已经是第10代了。一旦我们部署了一台服务器,只要它经济高效,我们就会让它保持服务状态,而且服务器的生命周期可以是几年。由于随着时间的推移进行扩展和升级,单个数据中心包含多种服务器型号的情况并不少见。我们各代服务器的处理器性能都有显著提高。因此,在单个数据中心内,我们需要向不同的服务器发送不同数量的连接,以利用相同百分比的容量。

基于对服务器容量的静态估计,为每台服务器提供固定的连接份额也是不够的。并非所有连接都消耗相同数量的CPU。在我们的服务器上运行并消耗CPU的其他活动不是由来自客户端的连接直接驱动的。因此,为了准确地平衡服务器间的负载,Unimog执行动态负载平衡:它定期测量我们每台服务器上的负载,并使用一个控制循环来增加或减少连接到每台服务器的数量,以便它们的负载收敛到适当的值。

TCP数据包和连接之间的关系是Unimog操作的核心,因此我们将简要描述该关系。

(Unimog支持UDP和TCP,但为清楚起见,本文的大部分内容将集中在TCP支持上。我们将解释UDP支持在接近尾声时有何不同。)。

该分组所属的TCP连接由跨越IPv4/IPv6(即,第3层)和TCP(即,第4层)报头的四个带标签的报头字段标识:源和目的地址,以及源和目的端口。这四个字段统称为4元组。当我们说Unimog将连接发送到服务器时,我们的意思是所有带有标识该连接的4元组的数据包都被发送到该服务器。

TCP连接是通过客户端和处理该连接的服务器之间的三次握手建立的。一旦建立了连接,该连接的所有传入数据包都必须发送到同一服务器,这一点至关重要。如果属于该连接的TCP数据包被发送到不同的服务器,则它将发出信号,表明它不知道使用TCP RST(重置)数据包到客户端的连接。收到此通知后,客户端将终止连接,这可能会导致用户看到错误。因此,错误定向的数据包比丢弃的数据包糟糕得多。像往常一样,我们认为网络不可靠,偶尔丢弃数据包也没问题。但是,即使是单个定向错误的数据包也可能导致连接中断。

CloudFlare代表我们的客户处理各种连接。这些连接中的许多都携带HTTP,并且通常是短暂的。但是有些HTTP连接用于WebSocket,并且可以保持数小时或数天的建立状态。我们的Spectrum产品支持任意TCP连接。TCP连接可以由于多种原因而终止或停止,理想情况下,所有使用长期连接的应用程序都能够透明地重新连接,并且应用程序将被设计为支持此类重新连接。但并不是所有的应用程序和协议都符合这一理想,因此我们努力保持长期连接。Unimog可以维持多天的连接。

上一节描述了Unimog的功能是引导到服务器的连接。我们现在将解释这是如何实现的。

首先,让我们考虑一下我们的一个数据中心在没有Unimog或任何其他负载均衡器的情况下会是什么样子。以下是概念性视图:

数据包从Internet到达,并通过路由器,路由器将其转发到服务器(实际上,路由器和服务器之间通常有额外的网络基础设施,但它在这里不起重要作用,因此我们将忽略它)。

但是,这样简单的安排可能吗?路由器是否可以在没有某种负载均衡器的情况下在服务器上传播流量?路由器有一种称为ECMP(等价多路径)路由的功能。其最初目的是允许流量跨两个位置之间的多条路径传播,但通常会重新调整用途,以跨数据中心内的多台服务器传播流量。事实上,在我们部署Unimog之前,Cloudflare仅依靠ECMP来跨服务器分散负载。ECMP使用散列方案来确保给定连接上的数据包使用相同的路径(Unimog也使用散列方案,因此我们将在下面更详细地讨论这是如何工作的)。但ECMP很容易受到活动服务器集变化的影响,例如服务器何时进入和退出服务。这些更改会导致重新散列事件,从而中断与ECMP组中所有服务器的连接。此外,路由器对ECMP组的大小有限制,这意味着单个ECMP组不能覆盖我们较大的边缘数据中心中的所有服务器。最后,ECMP不允许我们通过调整到每台服务器的连接份额来进行动态负载平衡。这些缺点意味着ECMP本身并不是一种有效的方法。

理想情况下,要克服ECMP的缺点,我们可以使用适当的逻辑对路由器进行编程,以便以我们想要的方式将连接定向到服务器。但是,尽管可编程网络数据平面是近年来的研究热点,但商用路由器本质上仍然是固定功能设备。

我们可以绕过路由器的限制,方法是让路由器将数据包发送到一些负载均衡服务器,然后对这些负载均衡器进行编程,使其按我们希望的方式转发数据包。如果负载均衡器都以一致的方式作用于数据包,那么哪个负载均衡器从路由器获得哪些数据包都无关紧要(因此我们可以使用ECMP在负载均衡器之间传播数据包)。这意味着这样的安排:

相反,Unimog使每台服务器都成为负载均衡器。路由器可以将任何数据包发送到任何服务器,初始服务器会将数据包转发到该连接的正确服务器:

首先,在我们的边缘网络中,我们避免为服务器提供专门的角色。我们在边缘网络中的服务器上运行相同的软件堆栈,提供我们的所有产品功能,无论是DDoS攻击防御、网站性能功能、Cloudflare Workers、WARP等。这种一致性是边缘网络高效运行的关键:我们不必管理每个数据中心内有多少负载均衡器,因为我们的所有服务器都充当负载均衡器。

第二个原因与阻止攻击有关。CloudFlare的边缘网络是持续攻击的目标。其中一些攻击是体积庞大的数据包泛滥,它们试图压倒我们的数据中心处理来自互联网的网络流量的能力,从而影响我们为合法流量提供服务的能力。要成功缓解此类攻击,重要的是尽早过滤掉攻击数据包,将它们消耗的资源降至最低。这意味着我们的攻击缓解系统需要在Unimog完成转发之前发生。这个缓解系统叫做14Drop,我们以前已经写过了。L4drop和Unimog紧密结合在一起。因为l4drop在我们所有的服务器上运行,而且l4drop先于Unimog,所以Unimog也在我们的所有服务器上运行是很自然的。

Unimog使用名为XDP的Linux内核工具实现数据包转发。XDP允许将程序附加到网络接口,在内核的主网络堆栈处理每个到达的数据包之前,都会运行该程序。XDP程序返回一个动作代码,告诉内核如何处理数据包:

通过:将包传递到内核的网络堆栈以进行正常处理。

TX:将数据包从网络接口传回。XDP程序可以在传输之前修改分组数据。此操作是Unimog转发的基础。

XDP程序在内核中运行,即使在高数据包速率下,这也是一种有效的方法。XDP程序表示为eBPF字节码,并在内核虚拟机中运行。在加载XDP程序时,内核将其eBPF代码编译成机器码。内核还会验证该程序,以检查它是否不会损害安全性或稳定性。EBPF不仅在XDP上下文中使用:许多最近的Linux内核创新使用eBPF,因为它提供了一种方便而有效的方式来扩展内核的行为。

XDP比数据包级处理的替代方法方便得多,特别是在我们的环境中,所涉及的服务器还有许多其他任务。自从Unimog最初部署以来,我们一直在改进它。我们的Unimog XDP代码新版本的部署模型与用户空间服务的部署模型基本相同,如果需要,我们可以每周部署新版本。此外,用于优化Linux网络堆栈性能的成熟技术为XDP提供了良好的性能。

内核旁路网络(如DPDK),其中用户空间中的程序直接管理网络接口(或网络接口的某一部分),而不涉及内核。当服务器可以专用于网络功能时,这种方法效果最好(因为需要专用处理器或网络接口硬件资源,并且与正常内核网络堆栈的集成不方便;请参阅我们关于这一点的旧帖子)。但我们避免让服务器扮演专门的角色。(GitHub的开源GLB使用DPDK,这是导致GLB不适合我们的主要因素之一。)。

内核模块,将代码添加到内核以执行必要的网络功能。Linux IPVS(IP虚拟服务器)子系统就属于这一类。但是与XDP相比,开发、测试和部署内核模块很麻烦。

下图显示了我们使用XDP的概述。L4drop和Unimog都是由XDP程序实现的。L4drop匹配攻击数据包,并使用丢弃操作将其丢弃。Unimog使用tx操作重新发送数据包,从而转发数据包。未丢弃或转发的数据包将通过正常的Linux网络堆栈。为了支持我们对XPD的详细使用,我们开发了xdpd守护进程,它为我们的XDP程序执行必要的监督和支持功能。

我们有一系列必须为每个数据包运行的XDP程序,而不是单个XDP程序(l4drop、Unimog和其他我们在这里没有介绍的)。Xdpd的职责之一是准备这些程序,并进行适当的系统调用以加载它们并组装整个链。

我们的XDP计划有两个来源。有些是以传统方式开发的:工程师编写C代码,我们的构建系统(用clang)将其编译成eBPF ELF文件,然后我们的发布系统将这些文件部署到我们的服务器上。我们的Unimog XDP代码是这样工作的。相反,14drop XDP代码是由xdpd基于其从攻击检测系统接收的信息动态生成的。

可以使用称为映射的数据结构向XDP程序提供数据。Xdpd根据从控制平面接收的信息填充我们的程序所需的地图。

程序(例如,我们的Unimog XDP程序)可能依赖于在程序运行时固定的配置值,但在编译其C代码时没有已知的通用值。可以通过映射将这些值提供给程序,但效率会很低(从映射中检索值需要调用助手函数)。因此,xdpd将修复eBPF程序,以便在加载之前插入这些常量。

CloudFlare仔细监控我们所有软件系统的行为,这包括我们的XDP程序:它们发出指标(通过另一种地图使用),xdpd将这些指标暴露给我们的指标和警报系统(普罗米修斯)。

当我们部署新版本的xdpd时,它以一种不会中断unimog或l4drop操作的方式进行了优雅的升级。

虽然XDP程序是用C语言编写的,但xdpd本身是用GO编写的。它的大部分代码都是特定于Cloudflare的。但在开发xdpd的过程中,我们与cilium合作开发了https://github.com/cilium/ebpf,一个开源围棋库,它提供了xdpd操作和加载ebpf程序及相关对象所需的操作。我们还与Linux eBPF社区合作,分享我们的经验,并以使xdpd特性过时的方式扩展核心eBPF技术。

在评估Unimog的性能时,我们主要关注的是效率:即用于负载平衡的资源相对于用于客户可见服务的资源。我们的测量显示,与未使用负载平衡的场景相比,Unimog的成本不到处理器利用率的1%。其他旨在与专用于负载平衡的服务器一起使用的L4LB可能会更强调以下各项的最大吞吐量。

.