克服传统的Linux瓶颈:2020年的Seastar

2020-12-13 15:55:11

如上所述,OSv的主要目标是运行现有的Linux软件,因为大多数MIKELANGELO用例需要运行现有的代码。当今的Linux API(包括POSIX系统调用,套接字API等)是由几十年的Unix和Linux遗留下来的,它们中的某些方面本质上效率低下。 OSv可以提高使用这些API的应用程序的性能,但效果不明显。因此,我们在来宾操作系统开发中的第二个目标是提出新的API,与未修改的Linux应用程序相比,它们将为新应用程序提供显着更好的性能-前提是应用程序被重写为使用这些新API,

在研究论文“ OSv-优化虚拟机的操作系统” [[i]]中,使用的基准之一是Memcached,Memcached是一种流行的云应用程序,用于缓存经常请求的对象以降低较慢的数据库服务器上的负载。 Memcached演示了未经修改的网络应用程序如何在OSv上比在Linux上更快地运行-报告了22%的提速。

仅通过用OSv替换来宾中的Linux,而根本不修改应用程序,就可以实现22%的不错的加速。但是我们想了解是否可以做些什么来获得明显更高的性能。在OSv上对memcached进行概要分析时,我们很快发现了两个性能瓶颈:

Posix API固有的低效率,因此OSv无法避免它们并仍然保持与POSIX兼容:例如,在一个基准测试中,我们注意到20%的内存缓存运行时正在锁定和解锁互斥体-几乎总是无人争辩。对于我们发送或接收的每个数据包,我们都会锁定和解锁十几个互斥锁。 OSv相对于Linux的性能优势的一部分是OSv在网络堆栈中使用了“ netchannel”设计来减少锁定(请参阅上一节),但是我们仍然有太多的锁定,而Posix API迫使我们留下许多锁定:例如,Posix API允许许多线程使用相同的套接字,允许许多线程修改文件描述符的列表,轮询相同的文件描述符-因此所有这些关键操作都涉及锁,这是我们无法避免的。套接字API也是同步的,这意味着当send()返回时,允许调用者修改缓冲区,这迫使OSv中的网络代码不为零拷贝。

不可扩展的应用程序设计:在多核计算机中编写应用程序以线性扩展内核数量并不容易,而且许多在一个或两个内核上运行良好的应用程序很难扩展到许多内核。例如,memcached会保留一些全局统计信息(例如,服务的请求数)并在锁定状态下对其进行更新-随着内核数量的增长,这成为主要瓶颈。似乎可以接受的解决方案–无锁原子变量–也无法扩展,因为虽然不涉及互斥锁,但原子操作和高速缓存行反弹(由于不同的CPU读取和写入相同的变量),两者都变得比核心数量增加。因此,编写一个真正可扩展的应用程序(一个可以在(例如)64个内核上运行并且比单个内核上运行的速度快64倍)的应用程序是一个巨大的挑战,并且大多数应用程序的扩展性都没有达到应有的水平。随着每台机器的内核数量不断增加,它将变得越来越引人注目。

在前面提到的OSv论文中,我们尝试了一个实验来量化第一个效果-Posix API的无效性。基准测试所需的memcached子集非常简单:请求是一个单个数据包(UDP),其中包含“ GET”或“ PUT”命令,结果也是一个单个UDP数据包。因此,我们在OSv中实现了一个简单的“数据包过滤器” API:每个传入的以太网数据包都会由一个函数(memcached的哈希表查找)进行处理,该函数会立即创建响应数据包。没有额外的网络堆栈,没有锁或原子操作(我们在单个CPU上运行了),没有文件描述符等。此实现的性能比原始的内存缓存服务器高出4倍。

但是,尽管简单的“数据包过滤器” API对于普通的UDP内存缓存很有用,但对于实现更复杂的应用程序却没有用,例如异步应用程序(无法从一个请求数据包立​​即生成响应),使用TCP或需要使用多个核心。类似于“分组过滤器”的快速API已经非常普遍了(DPDK是一个流行的示例),并且非常适合实现路由器和类似的分组处理软件。但是,如果您尝试编写一种复杂的,高度异步的,在云上经常使用的网络应用程序(例如NoSQL数据库,HTTP服务器,搜索引擎等),它们实际上并没有帮助。

对于MIKELANGELO项目,我们着手设计一个可以满足上述两个要求的新API:新应用程序可以使用该API来实现最佳性能(即“数据包过滤API”实现所达到的相同性能水平),同时允许创建复杂的实际应用程序:Seastar的设计结果是:

Seastar是一个C ++ 14库,可以在OSv和Linux上使用。由于Seastar在大多数情况下都会绕过内核,因此我们并不希望通过在OSv上运行它来提高速度-尽管OSv的其他一些优势(例如映像大小和启动时间)可能仍然有意义。

Seastar专为满足云上常见类型的复杂异步服务器应用程序(例如NoSQL数据库,HTTP服务器等)的需求而设计。此处的“异步”是指请求通常会触发一系列事件(磁盘读取,与其他设备的通信)节点等),只有在稍后的时间才能构成回复。

Seastar为应用程序提供了解决本节顶部提到的两个性能瓶颈所需的机制:在一个核心上实现最佳效率以及核心数量上的可伸缩性。我们将在下面解释Seastar如何做到这一点。

Seastar可以绕过旧版内核API,例如,它可以直接使用DPDK直接访问网卡。 Seastar提供了完整的TCP / IP堆栈(DPDK没有提供)。

我们已经使用Seastar重新实现了内存缓存,并将性能提高了2到4倍,而原始内存缓存的性能却提高了近32核(“数据包过滤器”实现无法做到的)。下图获取更多详细信息。

图:使用TCP和memaslap [[ii]]工作负载生成器的股票memcached(橙色)与Seastar重新实现memcached(蓝色)的性能–内核数量不同红线表示使用多个单独的memcached的非标准memcached部署进程(而不是一个具有多个线程的内存缓存);这样的运行是部分不共享的(单独的进程不共享内存或锁),因此性能要比线程服务器好,但是内核和网络堆栈仍然共享,因此性能不如Seastar。

想要使用Seastar的应用程序将需要重写,以使用其新的(且完全不同)的API。这需要大量的投资,但也带来了丰厚的回报:Seastar库的创建者ScyllaDB用了近两年的时间,用C ++和Seastar重新实现了流行的Cassandra分布式数据库,结果是“ Scylla”(例如Seastar和OSv作为开放源代码[[iii]]发布,其吞吐量比Cassandra高得多:三星的独立基准测试[[iv]]显示,在24核集群上,Sylla的吞吐量比Cassandra高出10到37倍。实际上,Scylla分布式数据库的性能如此之好,以至于它已成为ScyllaDB的主要产品,这使得该公司高度依赖Seastar的开发。因此,ScylllaDB一直在对Seastar进行更多的投资,超出了MIKELANGELO项目的资助范围,并且计划在项目结束后继续开发Seastar。

在“ Cloud Bursting”用例中,我们使用Scylla(基于Seastar的Cassandra重新实现)。但是我们的目标是使Seastar成为通用API,这将对通常在云上运行的多种异步服务器应用程序有用。因此,我们正在努力提供一个丰富,平衡且记录良好的API,并编写了有关编写Seastar应用程序的教程(其草案已包含在D4.4和D4.5中)。在撰写本文时,除了ScyllaDB以外,我们还知道至少还有两家公司的产品基于Seastar,并且还有更多公司正在考虑这样做。

设计为使用Seastar的应用程序如何比使用更传统的API(例如线程,共享内存和套接字)的应用程序快得多?简短的答案是,现代计算机体系结构具有多个容易陷入的性能陷阱,Seastar通过使用以下体系结构来确保您不会陷入这种情况:

现代的多核计算机具有共享内存,但是不正确地使用它会大大降低应用程序的性能:锁非常慢,处理器提供的“无锁”原子操作和内存屏障也是如此。与一个内核在其缓存中找到对象相比,从不同内核读取和写入同一内​​存对象会大大减慢处理速度(这种现象称为“缓存行反弹”)。所有这些缓慢的操作已经损害了单核性能,但是随着核数的增加而逐渐变慢,因此也损害了将应用程序扩展到许多核的能力。

此外,随着内核数量的增加,多核计算机不可避免地会变成多路插座,并且我们开始看到NUMA(非统一内存访问)问题。也就是说,某些内核距离内存的某些部分更近-访问内存的“远”部分可能会明显变慢。

因此,Seastar应用程序使用无共享设计:每个核心负责数据的一部分(“碎片”),并且不直接访问其他核心的数据-如果两个核心希望进行通信,则它们通过消息传递来实现Seastar提供的API(在内部,此消息传递使用CPU提供的共享内存功能)。

当Seastar应用程序在N个内核上启动时,可用内存被划分为N个部分,并且每个内核都分配了一个不同的部分(当然,在此内存划分中要考虑NUMA)。当内核上的代码分配内存(使用malloc(),C ++的new等)时,它会从该内核的内存部分中获取内存,并且只有该内核才能使用它。

十多年来,众所周知,高性能服务器应用程序不能为每个连接使用线程,因为这些应用程序会带来更高的内存消耗(线程堆栈很大)和大量的上下文切换开销。相反,应用程序应仅使用几个线程(理想情况下,每个内核仅使用一个线程),每个线程处理许多连接。这样的线程通常运行“事件循环”,该事件循环等待其分配的连接上的新事件(例如,传入请求,磁盘操作完成等)并对其进行处理。但是,编写此类“事件驱动”应用程序传统上非常困难,因为程序员需要仔细跟踪正在进行的连接的复杂状态,以了解每个新事件的含义以及下一步需要做什么。

Seastar应用程序每个内核也只运行一个线程。 Seastar为异步编程实现了Futures andcontinuation API,与传统的事件循环编程相比,它可以更轻松地(每个内核只有一个线程)编写非常复杂的应用程序。未来由异步函数返回,并且最终将被实现(变得准备就绪),此时可以运行一段非阻塞代码。延续是C ++ lambda,匿名函数,可以从封装代码中捕获状态,从而可以轻松地跟踪复杂的延续级联在做什么。我们将在下面更详细地说明Seastar的未来和延续。

未来/继续编程模型并不是新的,并且之前已在各种应用程序框架(例如Node.js)中使用,但是在Seastar之前,它仅由C ++ 14标准(std :: future)部分实现。此外,Seastar的期货实现比std :: future效率更高,因为Seastar的实现使用较少的内存分配,并且没有锁或原子操作:由于Seastar的分片设计,期货及其延续属于一个特定的核心。

继续无法阻止OS调用,否则整个核心将等待而无所事事。因此,Seastar使用内核的AIO(“异步I / O”)机制,而不是传统的Unix阻塞磁盘IO API。使用AIO,继续操作仅会启动磁盘操作,并返回一个future,当操作最终完成时,future将变为可用。

异步I / O对于使用磁盘而不只是网络的应用程序的性能非常重要:一种流行的替代方法(例如,在Apache Cassandra中使用)是使用线程池来处理连接,因此当一个线程阻塞时,进行磁盘访问时,将运行另一个线程,并且内核不会保持空闲状态。但是,如上所述,使用多个线程会带来较大的性能开销,尤其是在可能需要进行大量并发磁盘访问的应用程序(如Cassandra)中。使用现代的SSD磁盘硬件,并发非顺序磁盘I / O不再是瓶颈(就像旋转磁盘及其缓慢的寻道时间一样),因此编程框架不应使其成为一个瓶颈。

通过提供自己的网络堆栈,Seastar可以选择绕过内核(Linux或OSv)的网络堆栈及其所有固有的低效率(如锁):

Seastar使用DPDK(在Linux或OSv上)或virtio(仅在OSv上支持)直接访问基础网卡。最重要的是,它提供了功能齐全的TCP / IP网络堆栈,它本身是用Seastar编写的(未来和延续),因此不使用任何锁,而是在内核之间分配连接。将连接分配给核心后,只有该核心可以使用它。如果应用程序明确决定将连接移动到另一个核心,则该连接将被移动。重要的是,Seastar的网络堆栈支持多队列网卡和RSS(接收侧控制),因此不同的内核可以彼此独立地发送和接收数据包,而无需锁定,也不会像单个内核那样接收所有数据包而产生瓶颈。当硬件的队列数限制在可用核心数以下时,Seastar还将使用软件RSS-即某些核心接收数据包并将其转发到其他核心。

上面我们解释了Seastar体系结构的几个关键部分,包括分片设计,未来和延续,异步磁盘I / O和用户空间TCP / IP堆栈。但是Seastar包含许多其他组件,我们现在将对其进行简要调查。抛光和记录所有这些组件的工作正在进行中,但是CPU调度程序将在接下来的几个月中受到特别的关注和改进,以继续减少ScyllaDB和其他Seastar应用程序的延迟。

反应堆:Seastar的“反应堆”是Seastar的主要事件循环;反应器线程在专​​用于该应用程序的每个内核上运行,轮询新事件(网络活动,完成的磁盘I / O,到期的计时器以及内核之间的通信)以解决相关的将来,并在就绪队列上继续运行(与已解决的期货相关的延续)。

Seastar反应堆不是抢先的,这意味着每次延续都将完成或直到其自愿结束。这意味着每个延续中的应用程序代码无需担心对数据的并发访问-在该内核上没有任何东西可以与正在运行的延续并行运行,并且其他内核上的延续也无法访问该内核的内存(由于共享,没有设计,更多内容请见下文)。

闲置时,反应堆可以继续轮询或进入睡眠状态。前一种方法可以改善延迟,但是后者可以降低功耗。用户可以选择这些选项之一,或者让Seastar根据负载自动在它们之间进行选择。

在任何情况下,Seastar应用程序都是要垄断给定的CPU内核,而不是与其他应用程序共享时间。这很自然地出现在云中,其中VM始终运行单个应用程序。

内存分配:Seastar在不同的内核(反应器线程)之间分配给它的内存总量。 Seastar知道NUMA配置,并为每个核心分配了一块它可以最有效地访问的内存。 C库的malloc()(以及相关的标准C和C ++分配器函数)被Seastar自己的内存分配器覆盖,该内存分配器从当前分片的内存区域分配内存。在一个分片上分配的内存只能由同一分片使用。 Seastar具有允许一个分片使用由另一个分片分配的内存的有限功能,但是通常不使用这些分片,并且分片应通过如下所述的显式消息传递进行通信。

信息在分片之间传递:Seastar应用程序是分片的,或者没有共享内容,并且分片通常不会从彼此的内存中读取数据,因为正如我们已经解释的那样,这样做需要缓慢的锁定,内存屏障和高速缓存行反弹。相反,当两个分片要通信时,它们通过显式消息传递来实现此目的,该消息传递是在共享内存内部实现的。消息传递API允许在远程分片上运行一段代码(一个lambda)。该代码由运行在远程分片上的Reactor运行-如上所述,没有并行执行连续操作的风险。

日志结构分配器(LSA):上述Seastar的malloc()/ free()与传统的分配器大致相同,不同之处在于它为每个核心使用不同的内存池。 malloc()/ free()API的最大缺点可能是导致碎片化–即使很长一段时间,分配和释放小对象也可能阻止分配较大的对象,即使我们可能仍有大量未使用的内存。因为分配对象的用户可以保存指向它们的指针,所以不允许内存分配器来回移动它们以修复碎片。

垃圾回收是解决此问题的常用方法,因为垃圾回收器的一部分工作是压缩(即移动)内存中的对象,以便释放大量连续的内存区域。但是,GC还具有一个附加的反功能:它需要搜索垃圾(已释放)对象的位置,而该搜索正是GC大部分缺点的来源(例如,暂停以及需要大量备用内存)。

Seastar的LSA(日志结构化分配器)旨在做到两全其美:与malloc()/ free()一样,显式分配和释放内存(现代C ++样式,通过对象构造/销毁自动实现),因此我们总是确切知道哪个内存可用。但是,此外,LSA内存可能会四处移动(或自动移出),并且LSA API允许跟踪对象移动的位置或暂时阻止移动。

I / O调度程序:“ Cloud Bursting”用例中出现的关键要求之一是确保在群集增长期间性能不会显着下降。当Cassandra群集增长时,新节点需要从旧节点复制现有数据,因此现在,旧节点使用其磁盘将数据流传输到新节点,并服务于普通请求。控制之间可用磁盘带宽的划分至关重要

......