现代存储系统的线程每核缓冲区管理

2020-12-13 03:12:55

正如我之前所观察到的,软件不是基于类别理论运行的,而是在具有宽通道,多通道GB / s存储单元和NVMe SSD访问时间约为10-100微秒的超标量CPU上运行。十年前在不同的硬件平台上编写的某些软件感到缓慢的原因是因为它无法利用现代硬件的进步。

存储系统中的新瓶颈是CPU。 SSD设备比旋转磁盘快100-1000倍,并且今天比十年前便宜10倍[1],从每TB 2500美元降至200美元。从1Gbps到100Gbps,网络在公共云中的吞吐量提高了100倍。

实际上,尽管计算机的确速度更快,但单核速度仍大致相同。原因是CPU频率与功耗有立方关系,因此我们遇到了麻烦。指令级并行性,预取,推测执行,分支预测,数据高速缓存和指令高速缓存的深层次结构等,使程序在与它们交互时感觉更快,但是在数据中心,实质性的改进来自于内核的提高计数。尽管每个时钟的指令数量比十年前增加了3倍,但内核数却增加了20倍。

这就是说,随时可用的多核系统的兴起需要采用不同的方法来构建基础结构。案例[9]:为了在AWS的i3en.metal上充分利用96个vCPU,您需要找到一种方法来利用3.1 GHz的持续CPU时钟速度,60 TB的NVMe实例存储总容量, 768 GiB内存和NVMe设备能够以4 KB的块大小提供多达200万个随机IOPS。这种野兽需要一种利用这些硬件优势的新型存储引擎和线程模型。

Redpanda是一个Kafka-API兼容系统,用于关键任务工作负载[3]-解决了所有这些问题。它使用具有结构化消息传递(SMP)的每核心线程体系结构在这些固定线程之间进行通信。无论您使用的是线程池,具有单个生产者单消费者SPSC [7]队列网络的固定线程,还是任何其他高级安全内存回收(SMR)技术,线程都是任何应用程序的基础决策。您的ring-0,是应用程序的真正内核。它告诉您您对阻塞的敏感程度-Redpanda的敏感度小于500微秒-否则,Seastar的[4]反应堆将打印堆栈跟踪,警告您发生阻塞,因为它有效地增加了网络轮询器的延迟。

一旦确定了线程模型,下一步就是您的内存模型,对于存储引擎,最终是缓冲区管理。在本文中,我们将介绍每个线程线程环境中缓冲区管理的风险,并介绍iobuf,这是我们在Seastar领域中针对0拷贝内存管理的解决方案。

如前所述,Redpanda在每个核心体系结构中使用单个固定线程来完成所有工作。网络轮询,向内核提交异步IO,获取事件,触发计时器,安排计算任务等。从结构上讲,这意味着没有任何事情可以阻塞500毫秒以上,否则您将在堆栈的其他部分引入延迟。这是一个非常严格的编程范例,但是这个自以为是的想法迫使一个真正的异步系统,无论您是否喜欢程序员。

TpC(每核线程)体系结构的挑战是核之间的所有通信都是明确的。与通过互斥锁进行的直接多线程实现相比,这使程序员能够精疲力竭地实现偏向于核心局部性(d缓存,i缓存)的算法。必须与基于未来的实现方式的异步性共同设计这一必要性。

对于如图1所示的Kafka-API实现,我们显式地交换内存使用量,以通过实现关键组件来减少延迟并增加吞吐量。由于每个请求都必须知道该分区是否存在,并且该特定机器实际上是该分区的领导者,因此在每个内核上都实现了元数据缓存。分区路由器维护一个映射,该映射的逻辑核心实际上拥有该计算机上的基础Kafka分区。诸如访问控制列表(ACL)之类的其他内容将推迟到请求到达目标核心之前,因为它们可能会占用大量内存。对于每个核心上要实现的内容与目标核心上要延迟的内容,我们没有严格的规则,并且通常取决于内存(较小的数据结构适合广播),计算(决定花费多少时间) )和访问频率(很可能在每个核心上实现操作)。

剩下的一个问题是,内存管理在TpC架构中究竟如何工作?使用完全异步执行模型中的SPSC队列网络,数据实际上如何从L-core-0到L-core-66安全地传输,因此事物可以在任何时间点挂起?

要了解iobuf,我们需要了解我们的TpC框架Seastar的实际内存限制。在程序引导过程中,Seastar会分配计算机的全部内存,并将其平均分配给所有内核。它咨询硬件以了解每个特定内核属于什么内存,从而减少了到主内存的内核间流量。

如图2所示,在core-0上分配的内存必须在core-0上重新分配。但是,无法保证连接到Redpanda的Java或Go客户端实际上将与拥有数据的确切内核进行通信。

iobuf的核心是带有延迟删除的引用计数的碎片缓冲区链,它允许Redpanda在碎片进入时简单地共享远程核心已解析消息的视图,而不会产生复制开销。

零散的缓冲区抽象并不是新事物。 linux内核具有sk_buff [5],而freebsd内核具有mbuf [6],大致相似。 iobuf的另一个扩展是,它可以利用Seastar的SPSC队列网络在TCP模型中工作,并具有适当的删除功能,此外还可以根据共享的存储工作量随意共享子视图。

删除C ++模板,分配器,池,指针缓存等后,人们可能会认为iobuf等效于:

结构{无效*数据; size_t ref_count; size_t容量; size_t大小;片段*下一个; //列出片段* prev; }结构{片段*头; };

iobuf的起源植根于我们的核心产品宗旨之一,即为任务关键型系统构建Kafka®替代产品-为大多数工作量的用户提供10倍的低尾部延迟。除了每个核心线程的体系结构之外,如果不针对延迟进行重新设计,内存管理将是我们的第二个瓶颈。在长期运行的存储系统上,内存碎片是一个实际的问题,最终会遇到一个问题,即使用适当的解决方案(iobuf),停顿或OOM。

像它的前辈skbuff和mbuff一样,iobuf允许我们优化和训练具有可预测内存大小的内存分配器。这是我们的iobuf分配表逻辑:

struct {static constexpr size_t max_chunk_size = 128 * 1024;静态constexpr size_t default_chunk_size = 512; //>>> x = 512 //>>>而x< int((1024 * 128)):// ... print(x)// ... x = int((((x * 3)+1)/ 2)// ... x = int(min( 1024 * 128,x))//打印(1024 * 128)静态constexpr std :: array< uint32_t,15> alloc_table = //根据{{512,768,1152,1728,2592,3888,5832,8748,13122,19683,29525,44288,66432,99648,131072}}以上的python脚本计算得出;静态size_t next_allocation_size(size_t data_size); };

可预测性,内存池,固定大小,大小上限,碎片遍历等都是减少延迟的已知技术。请求连续且大小可变的内存可能会导致分配器压缩所有区域,并为可能是短暂请求的请求重排很多字节,不仅在请求路径上注入了延迟,而且还为整个系统注入了延迟,因为我们拥有一个线程执行所有操作。

硬件是平台。当我们要求网络层在连续内存中给我们准确的11225个字节时,我们只是在要求分配器线​​性化一个具有正确大小的空缓冲区,并要求网络层将字节复制为字节,因为片段是从硬件进入目标缓冲区的。尝试压缩硬件的每一盎司性能时,最终没有免费的午餐,而且通常需要从零开始重新架构。

如果到目前为止,我鼓励您注册我们的Community Slack(在这里!)并直接向我们提问,或者通过@vectorizedio在Twitter上与我们互动,或者亲自通过@emaxerrno与我们互动。 特别感谢我们的Sarah,Noah,Ben,David,Michal以及我们的外部审阅者Mark Papadakis和Travis Downs审阅了本文的早期草稿。