FlatStore: An Efficient Log-Structured Key-Value Storage Engine for Persistent Memory(清华ASPLOS’20)

(一)研究目的

结合PM设计高效的键值存储。

(二)研究背景

如何设计高效的键值存储来处理写密集型和小型工作负载具有现实需求。新兴的网络和存储技术(InfiniBand和PM)带来了新的机会。研究表明键值存储产生了大量的 small-sized write,写放大问题极大地浪费了硬件带宽。最近基于PM的研究主要采用日志结构来确保故障原子性或减少内存碎片,没有探究使用批处理来分摊持久化开销。

现有键值存储中的小型访问模式与pm的持久性粒度之间不匹配

  • 生产环境中键值存储的工作负载大部分都是小型键值项,且从读密集转向写密集。
  • cacheline的刷新粒度是64B,而PM内部的写粒度是256B,键值存储的写大小与硬件的更新粒度不匹配,极大地浪费了PM带宽。

解决这种不匹配的一个经典解决方案是合并日志结构,通过始终将更新附加到日志尾部来形成连续写入。日志结构还允许批量处理多个小更新。因此,只需要一次刷新数据的开销。然而在基于PM的系统中的应用日志结构更具挑战性:

  • 日志结构的追加写入对顺序写性能好的HDD和SSD友好,而高并发下PM的顺序访问和随机访问性能相似,不能从顺序访问受益。
  • HDD和SSD的刷新粒度大(4KB),批量刷新的数量大。而PM的访问粒度小(64B刷新大小和256B内部块大小)且需要封装额外的元数据,限制了批量刷新的数量。
  • 批处理增加了延迟(系统在处理请求之前需要积累多个请求)。批处理与高速网络的设计原则相冲突。

Optane DCPMM 的实证研究

在这里插入图片描述
图b显示:高并发情况下,PM的顺序访问和随机访问具有相似的带宽。少于20个并发线程时,顺序访问的带宽是随机访问的带宽的两倍或更高。从硬件的角度看,高并发情况下因为每个线程写入不同的地址,顺序访问实际上变成了随机访问。
图c显示:当一个写操作通过clwb被刷新,随后对同一cacheline的写和刷新将被阻塞近800ns(图c中的In-place)。这对采用原地更新方式的系统是一个大问题。

(三)研究概述

将键值存储解耦为一个用于快速索引的易失性索引和一个高效存储的持久日志结构,引入 compact log formatpipelined horizontal batching 从而实现高吞吐量、低延迟和多核可伸缩性。

(四)关键技术

持久键值存储中的更新会引起写放大和缓存刷新。为解决这个问题,FlatStore将键值存储解耦为 a volatile index for fast indexing·、a compacted per-core OpLog to absorb small updates、a persistent allocator for storing large KVs。
三个关键的设计原则:minimal write overhead(最小化写开销), low latency(低延迟), and multi-core scalability(多核可伸缩性)。
在这里插入图片描述
FlatStore的设计组件如上图所示:

  • 客户端通过一个定制的RDMA RPC发送他们的请求到 server cores , server cores 是由键哈希确定的。server cores可以并行地将日志条目保存到本地OpLog中。
  • 易失性索引避免 server cores 在处理get请求时扫描整个OpLog。
  • Compacted OpLog 以批处理的方式吸收频繁的小型更新。为最大化批处理的机会,FlatStore 只存储元数据和小型键值对以压缩每个日志条目。大型的键值对使用分配器单独存储的,因为它们不能从日志记录中获益。
  • Lazy-persist Allocator 存储大型键值条目。
  • Pipelined Horizontal Batching 允许一个core在创建批处理时从其他core窃取日志条目。

Compacted OpLog and Lazy-Persist Allocator

为了处理更新请求(Put/Del),服务器核心只需要写入键值项,在其本地日志末尾追加一个日志条目,然后相应地更新volatile索引。因此,避免了在哈希索引中重复哈希和在树索引中移动/分裂/合并造成的持久性开销。为了处理Get请求,服务器核心首先使用给定的键引用volatile索引来查找确切的日志条目,最后查找键值项,从而定位键值项。通过这种方式,也避免了扫描整个日志以查找特定键的开销。

OpLog

在这里插入图片描述
上图显示日志条目只包含描述每个操作的最小信息,而不是记录每次内存更新。

  • Op:记录操作类型(put/delete)
  • Emd:键值项是否放置在日志项的最后
  • Version:保证日志清理的正确性
  • key:8-byte
  • Ptr:指向存储在OpLog之外的实际记录(分配器管理的大的KV)。只保留了40位。
  • Size、Value(<256B):值及其大小

Ptr-based的日志条目大小被限制在16字节(128 bits),可同时刷新16个日志条目,而开销相当于持久化单个条目。
图3底部:两个相邻批可能在OpLog中共享相同的cacheline,导致后一批的持久化会延迟。因此在每一批的末尾添加填充,使它们 cacheline 对齐。

Lazy-persist Allocator

将NVM空间切分成4 MB chunks,4 MB chunks被切分成不同类别的data blocks,同一chunk中的data blocks大小相同。切割大小会持久化记录在准备分配的每一个 NVM chunk 的头部,一个bitmap也放在每一个chunk的头部以追踪未使用的data blocks。chunk中已分配的block的偏移量可以通过有效日志条目中的Ptr直接计算出来,即使在系统崩溃前无法持久化,也可以恢复位图。

put请求的处理步骤:

  • 从 lazy-persist allocator 中分配一个data block,将记录以(v_len,value)的格式复制到block中并持久化。(对于小KV记录跳过此步骤)
  • 用填充的每个字段初始化一个日志条目:如果记录被放置在OpLog之外,Ptr指向步骤1中的block,如果这个记录已经存在,则Version增加1。然后,将日志条目附加到日志中并持久化它。最后,更新尾部指针,使其指向日志的尾部,并持久化它。
  • 更新volatile索引中的相应条目,使其指向此日志条目。
Horizontal Batching

在这里插入图片描述
垂直批处理 vertical batching
OpLog旨在更好地支持批处理。通过这种方式,服务器核心可以接收来自网络的多个客户机请求,并一起处理它们。假设有N个Put请求到达,FlatStore首先分配数据块来存储和持久化每个请求的KV条目。然后合并它们的日志条目,并将它们一起刷新到OpLog。最后,相应地更新内存中的索引。通过批处理,PM写入的次数从原来的3N减少到N + 2。

上述批处理方案只对每个服务器核心接收到的请求进行批处理,因此称为垂直批处理。垂直批处理减少了持久开销,但它带来了更高延迟。(图4a和b进行比较)

Pipelined Horizontal Batching(Pipelined HB)
水平批处理允许服务器核心在构建批处理时从其他核心窃取哟持久化的日志条目。
在这里插入图片描述
如图5所示,为了从其他内核窃取日志条目,引入了

  • 1)一个全局锁来同步内核(Global Lock)
  • 2)用于内核间通信的per-core request pool (Req pool)

put操作解耦成三个阶段:

  • l-persist:空间分配和持久化键值对
  • g-persist:持久化日志项
  • volatile:易失性索引的更新

naive HB 的步骤如图5c:(黑色是leader,白色是followers)

  • 空间分配并持久化 KV 对 ❶/➀ (l-persist
  • 每个 CPU core 将需要持久化的日志项对应的地址放在本地的请求池中 ❷/➁
  • 试图请求全局锁 ❸/➂
  • 拿到锁的 core 成为 leader,其他的 core 成为 followers,followers 等待提交了的请求完成 ➃
  • leader core 从其他核那里抓取日志项 ❹(这里通过让 leader core 扫描所有其他 cores 来抓取所有存在的日志项,而不是使用其他复杂的策略,来减少延迟)
  • 将收集到的日志信息进行合并,并批量添加到OpLog中 ❺ ❻
  • 完成后释放全局锁,并通知其他核心持久化操作已经完成 ❼
  • 最后 leader 和 followers 都更新内存索引结构并向客户端发送响应消息 ❽/➄ (volatile

naive HB 不是最优的,因为三个阶段的处理是严格有序的,而且大多数CPU周期都花在等待日志持久化完成上。于是提出 Pipelined HB 来交叉执行每个阶段(图4d):

  • 一旦服务器核心无法获得全局锁,它就轮询下一个请求的到来,并执行第二次水平批处理的逻辑
  • followers 只异步地等待来自前一个leader的完成消息
  • leader一旦从其他核心收集到日志条目,就会释放全局锁,因此日志持久化开销从全局锁中移出,相邻的HBs可以并行处理

Pipelined HB 也存在问题。Pipelined HB 可能会对客户端请求进行重新排序:当服务器核心处理一组请求时,后一个Get操作可能无法看到前一个Put操作同一key带来的影响。这样的例子发生在 PUT 仍然在被 leader 处理时,server core 切换处理下一个请求。为了解决这个问题,每个 server core 维护一个独占的冲突队列来跟踪正在服务的请求。如果键冲突,以后的任何请求都将被延迟。

Pipelined HB 与 work stealing 类似但工作方式不同:work stealing 通过让空闲线程窃取繁忙线程的任务来重新平衡负载;Pipelined HB 依赖 work stealing,但以相反的方式让一个 core 获得所有的请求从而减小 Batching 的延迟。

Pipelined HB with Grouping:在 multi-socket 平台上,由于大量的 cores 都需要请求全局锁会导致严峻的同步开销,为了解决这个问题,cores 通常会被分区成不同的 groups 来执行 Pipelined HB。因此,一个合适的分组大小和 batch 大小能够较好地平衡全局同步开销。分组太小,虽然锁开销更小,但是也就意味着 batch size 变小。基于我们的实验结果,通过将一个 socket 对应的所有核安排在一个组就能提供较优的性能。

Log Cleaning

为了避免OpLog的长度任意增长,需要一种方法来压缩它并删除过时的日志条目。
在FlatStore中,每个 server core 维护一个内存表,以便在处理普通的Put和Delete请求时跟踪OpLog中每个 4MB chunk 的使用情况。是否将一个block插入回收列表取决于该块中有效KV的比例和空闲chunk的总数。每一个 HB group 启用一个后台线程来清理日志,因此日志的回收操作是在不同的组并行进行的,cleaner 线程是周期执行的,周期地扫描回收列表。

为了回收 NVM chunk,cleaner 线程首先扫描该 chunk 来判断每个日志项的活性,通过比较日志项的版本号和内存索引中存储的最新的版本号,所有仍然有效的日志项将被拷贝到新分配的 NVM chunk 中。对 tombstones (对于Del)的活性进行识别则更为复杂,只有在回收了与此KV项相关的所有日志项之后,才能安全地回收它。之后,cleaner 更新内存中索引中的对应项,以使用原子 CAS 指向它们的新位置。最后,通过将旧块放回分配器来释放它。当新的NVM chunk 被填满时,它将该块链接到相应的 OpLog。还需要跟踪新的 NVM 块的地址,以防止在系统故障时丢失它。我们把这样的地址记录在日志区域上上(PM中的预保护区域),可以在恢复期间重新读取。

Recovery

Recovery after a normal shutdown
正常关闭之前,FlatStore将volatile索引拷贝到NVM的预定义位置,并刷新每个NVM chunk的bitmap。最后,FlatStore写入一个shutdown标志来指示正常关闭。重启后,FlatStore首先检查并重置该标志的状态。如果该标志指示正常关闭,FlatStore就将volatile索引加载到DRAM。

Recovery after a system failure.
如果shutdown标志无效(不是正常关机),server cores需要从头到尾扫描OpLogs来重建内存中的索引和位图。每个扫描到的日志条目的Key被用来在volatile索引中定位一个slot,如果没有相应的slot则在索引中执行插入操作。如果有slot则进一步比较版本号来判断是否更新该索引项的指针。使用Ptr字段,分配元数据(即位图)也被设置。因此FlatStore只需要顺序扫描OpLog就可以恢复所有易失性数据结构。实验表明恢复 10 亿个 KV 只需要花费 40s,这样的恢复时间是容许的,因为:

  • 许多生产环境中的负载有更多不同大小的 KV 分布(小值在数量上占主导地位,大值在空间上占主导地位),因此在大多数场景下需要恢复的索引数较少。
  • 为了缩短恢复时间,FlatStore 也支持使用 Checkpoint 方式在 CPU 不是特别繁忙的时候周期地将索引数据写入 PM

实现

FlatStore-H: FlatStore with Hash Table

在DRAM中部署CCEH作为volatile索引。CCEH被单独的keyhash划分为多个范围,每个核心拥有一个CCEH实例。因此多核可以无锁开销地修改哈希表。因为索引的持久性已经被 OpLog 来保证,所以直接将 CCEH 放在 DRAM 中且移除了他的刷回操作。客户端直接放请求发送到具体的负责该 Key 的核,每个核将只负责更新他自己的索引,但仍然需要使用 pipelined HB 持久化日志项。CCEH中的每个bucket包含多个index slots,包括:(1)一组Keys和相应的Versions,以区分不同的键值项;(2)一组指针指向在 OpLog 中对应的日志项。
分区的设计可能会导致在倾斜负载下的负载不均衡,然而,水平批处理能够在很大程度上减轻这种不平衡:它将最耗时的持久化日志操作的负载分散在各个核之间。

FlatStore-M: FlatStore with Masstree

现有的键值存储使用树结构来支持顺序和范围查询,因此FlatStore使用Masstree(multi-core scalability
and high efficiency)。为了支持范围查询,在启动时创建一个全局的Masstree实例,并由所有服务器核心共享。Keys,Versions 和指针都被存储在叶子节点中,和 FlatStore-H 类似,客户端发送的请求也会先经过 HASH 找到一个具体的 core,同时也就减少了 Masstree 更新时的冲突。

RDMA-based FlatRPC

RDMA能够直接访问远程内存,而不需要远程cpu的参与。分为两种:(1)Two-sided,如 RDMA send/recv (2)One-sided,如RDMA read/write。RDMA请求通过 Queue Pairs(QPs)发送。

RDMA-based FlatRPC 让客户端直接写 core 的 message buffer,但 reply 被委托给 agent core 来发送。当一个客户端连接到每个 server node,只会在它和 agent core 之间常见一个 QP,agent core 随机选择离 NIC 近的 socket。每个 core 也会预分配一个 message buffer 给客户端来存储收到的消息,但是共享一个 QP。

如下图所示,为了发送一个消息:

  • 客户端首先将消息内容跟直接写到一个具体的核对应的 message buffer 中
  • 每个服务器核心都会轮询消息缓冲区以提取新消息并处理它
  • 然后按照响应的规则准备 response 并发送:
    • 如果当前 core 恰好是 agent core,直接使用 MMIO 发送 write 原语
    • 不是 agent core,通过共享内存将原语委托给代理核心
  • NIC 收到原语,将获取对应的消息内容并发送到客户端
    在这里插入图片描述

(五)实验

环境:

  • 节点数目:1 server,12 clients
    • 服务器:
      • 内存:
        • Optane DCPMM 1TB in total,每个 256GB
        • 128 GB DRAM
        • CPU:two 2.6GHz Intel Xeon Gold 6240M CPUs (36 cores in total)
        • OS:Ubuntu 18.04
    • 客户端:
      • 内存:128GB DRAM
      • CPU: two 2.2GHz Intel Xeon E5-2650 v4 CPUs (24 cores in total)
      • OS:CentOS 7.4
  • 网络:Mellanox MSB7790-ES2F switch,MCX555AECAT ConnectX-5 EDR HCAs,100Gbps IB
  • 参数:默认 batch_size 8

选择了四种最先进的持久索引方案进行比较。
在这里插入图片描述

(六)总结

在处理当今流行的工作负载时,现有的KV存储会产生大量的小型写操作,这与pm中的持久性粒度不匹配。为了解决这个问题,FlatStore将KV存储解耦为volatile索引和日志结构存储,它使用per-core OpLog管理索引和小型kv,以便更好地支持批处理。Pipelined horizontal batching 提供高吞吐量和低延迟的请求处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值