Persistent Memory优化实践
Persistent Memory的优缺点
PM给数据中心带来了颠覆式的改变,它将数据持久化的时间从~10us
(nvme ssd)降低到了~100ns
(与rdma的rtt时延~2us
还快),并且由于使用DDR总线,总带宽可以达到100+Gps
,这也是ssd存储介质无法达到的。
Persistent Memory提供近似内存的使用方式,但是内存和持久化这两者之间有一些天然的矛盾:
- 写内存经常会被CPU或者DRAM的控制器优化、重排等等,为了持久化内存,是必要引入一些memory fense、cache line flush(CLFLUSH指令)的机制
- 不像SSD,一般提供block_size的原子写,PM这种byte-addressable的的设备只提供8-bytes的原子写(Memory bus width),如果超过原子写的范围,就需要通过引入额外的logging/COW机制来提供事务性。也就是说保证一致性需要很大开销。
- PM上的write相比read,时延会差很多,并且并发的写时延会更更高,
并且,Challenges and Solutions for Fast Remote Persistent Memory Access提到了NVM DIMM对顺序写场景比较友好,因为NVM的erase block size是256bytes,所以随机写(with flush)会造成cache line 64bytes和256bytes没法对齐。针对顺序写的场景,NVM controller会在内部merge一些64byte的到256bytes的block size。由此可以看到,对PM的write操作有可能在PM的内存造成写放大,论文引入了一个名词“in-DIMM write amplification
”
在这篇文章Closing the Performance Gap Between Volatile and Persistent Key-Value Stores Using Cross Referencing Logs中有测试数据。当然,这个问题在之前的PCM上就存在。
由于cpu中的cache持久化到PM中需要一个个cache line的拷贝,所以单核访问PM的性能很差
时延方面,单线程8byte访问DRAM的时延大概在几十ns,但是写PM的时延要100-300ns,读时延也在100ns以下
相关论文
A Study of Application Performance with Non-Volatile Main Memory
文中讲的NVMM主要是当年的PCM等,但是文章介绍了一些方法来测量NVM的性能指标,主要想回答三个问题:
- How does NVMM performance affect application-level performance?
- Which aspects of NVMM performance have the largest impact on application-level performance?
- How can we minimize the cost of ensuring data is persistent in NVMM-equipped systems?
测量方法一方面是指定工作负载,另一方面是对NVM建立文件系统,在其上运行经典应用,如Mysql、MongoDB,一些后面的论文在测量方面也都是这么做的。
Write-Optimized and High-Performance Hashing Index Scheme for Persistent Memory
有关PM上索引优化的研究方向,之前大多是关于有序表的,而哈希索引这种结构研究不足,使用哈希索引的好处是,CRUD都是常数时间。之前in-memory的哈希索引一般使用链地址法或者开放地址法处理哈希冲突。使用连地址法的问题是每次在链表上append数据都会修改之前链表的next指针,如果这种方法使用在PM上,会导致很多随机的写入,降低PM性能。使用开放地址法(使用f个哈希函数,最多f次查找就能找到对应的目标)的问题是如果f个哈希函数都满了,会导致某个元素的重新insert,导致一些列的random write/re-write,影响pm性能。并且这两种方法都是in-memory的,没有考虑ACID,不能直接应用在PM上。
作者希望提出一种考虑PM特性的哈希索引数据结构,并且这种数据结构可以降低哈希索引的rehash overhead。之所以使用哈希索引,是因为访问数据的时间复杂度是O(1)
,与之相比,之前实现的一些PM上的数据结构如B+tree之类,访问数据的时间复杂度是O(log(N))
,作者提出的数据结构叫level hashing
,其具有如下优点:
- 通过8bytes以内的原子写,降低事务带来的性能开销
- 通过优化写流程降低写开销,sharing-based two-level hash table structure,每个写操作最多访问四个bucket就可以找到需要插入的位置
- 降低rehash的overhead,cost-efficient in-place resizing scheme,每次rehash只更改
1/3
的bucket
level hashing
的结构如图所示。其是一个倒着的二叉树,只有叶节点可以被哈希函数索引到,而根节点是用作存放conflict元素的。
根据Facebook和Baidu发布的键值负载,大多数的键值对的大小小于一个硬件缓存行大小,为了提高硬件缓存的利用效率,层次化哈希表在一个桶(bucket)中放置多个槽位(slot),因而一个桶中可以放多个键值对。此外,对于每个键,有两个哈希函数与其对应(图中hash1和hash2)。也就是说有两个桶可以用来存放这个键所对应的键值对。最后,整个哈希表的结构分为上下两层(图中的TL和BL)。每两个相邻的桶对应了下层的一个桶,即上层的两个桶2n和2n+1对应了下层的n。在实际存储中,上下两层分别为长度为N和N/2的一维数组。
层次化哈希能保证在插入的时候,大多数情况下只需要最多一次数据移动,只有在很少情况下才需要先调整大小再进行插入。在进行插入x时,先查看两个哈希函数(hash1(x)和hash2(x))对应的上层桶是否有空闲的槽位,如果有则将键值对直接放入槽位,插入完成;如果两个上层槽位都满了,则检查在这两个都满了的桶中,看是否有键值对可以放置到其另外一个哈希函数的上层桶中(如键k在hash1(x)或者hash2(x)桶中,则看其能否被移动到hash1(k)或者hash2(k)的桶中,之所以能移动,是因为rehash的策略决定的),如果有,则将其移动过去,那么当前需要插入的键值对就有空间可以存放了;如果没有,则进一步检查hash1(k)和hash2(k)所对应的下层桶是否有空位(即检查hash1(x)/2和hash2(x)/2桶),如果有,则可直接插入;否则,进一步检查两个下层桶中的键值对,是否能被移动到其对应的其他下层桶中,如果可以,则将其移动过去,以给新插入的键值对腾出地方;如果依然没有,则整个插入过程失败。如果插入失败,则需要将整个哈希表变大一倍之后进行插入。从上述的步骤可以看出,一个成功的插入,最多只会迁移一个已经存在的键值对。因而这个方法可以保证哈希表插入的效率。
由于上述的插入方法,给定一个键,其有可能被放置到四个桶内(两个上层桶,两个下层桶),因而在查询的时候需要将这个四个桶全部都遍历一遍。
另外一个很重要的操作是哈希表的调整大小。这里以增大为例,在进行增大操作时,加入原哈希表的上层桶数为N,则分配一个新的2N个桶的数组,将其作为新的上层,原来的上层则作为新的下层,而原来的下层则成为过渡层(图中IL)。此后,需要将过渡层中的所有键值对重新哈希(rehash)到新的上层中。当所有的键值对都被重新哈希到上层之后,可以将过渡层的空间释放掉。哈希表恢复为两层,这个哈希表调整过程结束。在这个调整过程中,被重新哈希的键值约为整个哈希表中键值对总数的1/3,因而这种方法优于传统的调整方法。
closing the gap between DRAM with PM for key-value store
传统的 key-value 存储方案主要分为两种,
- 用作 cache,存放在 DRAM(内存)中,以 memcached 和 redis 为代表;
- 用作持久化存储,以 LSM-tree 方案为代表,大家所熟知的有 Leveldb 和 Rocksdb。
随着当前 Persistent Memory (PM) (其实也就是 NVM 设备,例如 Intel/Micron’s 3D XPoint)技术的发展,PM 设备已经拥有仅次于 DRAM 的 latency 和吞吐,且具有持久化存储和 byteaddressable 的能力。而传统 key-value 存储方案并不完全适用于 PM 设备,或者说并不能发挥PM的全部性能。
论文的作者主要动机就是设计基于 PM 设备的高性能 key-value 存储,从使用者(front)来看其性能尽可能的接近 DRAM 的性能,从后端(back-end)持久化的角度来看,也能很好的用到PM的性能。也就是 closing the gap between DRAM with PM for key-value store。
作者给出了一个名为 Bullet 的 key-value 方案,使用 DRAM 作为 Cache,PM 负责持久化 key-value 和 log 数据,其核心的贡献在于使用多个 log 来衔接 DRAM 和 PM,从而实现了 PM 上的 key-value store 在多线程环境下具有良好的 Scalability,而且为了解决多 log 的 Consistency Apply 的问题(就是 log apply 到 PM 上的 key-value 数据中),创新的使用了一种 cross-referencing logs (CRLs) 技术。
Bullet 分为 front-end 和 back-end:
- front-end 将 kv 写入 DRAM hash table 中,为了保证数据落盘,还要写 log,log 是在 PM 设备上,front-end 有多个 log writers 线程(DRAM 中的 hash table 相当于就是一个 read/write Cache,因为毕竟 DRAM 相比于 PM 还是在性能上有优势的)
- back-end 在 PM 上:有一个 hash table 负责持久化 key-value 数据;多个 log 文件。后端也会有多个 log gleaners 线程,负责将 Op log entry 周期性的applied 到 PM 上的 hash table 中。gleaner在持久化时会获取某个kv键值对的spin-log,将对应的所有CRL都持久化进PM,数据只有持久化进PM之后才会保证不会丢失。
相比传统是只有一个 log,多个 log 会减少竞争,但同时也会带来一致性问题,因为需要将多个 log 按顺序 applied 到 back-end 的 hash-table 中才行,论文给出了一个巧妙的解决方案,它就是 cross-referencing logs (CRLs) ,其大致的思路就是在每个 Op log entry 中会记录其前一个有依赖关系的 log entry 的指针,所谓的有依赖关系,就是操作同一个 key 的 Op log entry 就是有依赖关系,它们 apply 的时候必须按照严格的写入顺序 apply。
Challenges and Solutions for Fast Remote Persistent Memory Access
本论文主要讨论将之前的in-memory应用转化为PM应用需要注意哪些点。特别是PM与rdma这种超快速网络打交道时需要注意什么。作者主要优化的是点是大IO写PM的场景。(另外多说一句,由于网络交互的latency已经几乎无法优化了(pcie+nic switch 至少要1.4us),而现在的rdma RTT大概在2us,因此优化落盘的latency已经是必然要发生的事情。。。)
一般应用访问PM的方式主要有两种(这里指client和server之间的交互),一是client通过rpc访问server,由server负责持久化PM,二是直接通过rdma read/write访问PM,作者惊奇的发现这两种的latency基本上是一致的。。。
这里主要原因是因为rpc的方式是cpu参与了网络流程,因此可以及时的下发刷PM的指令(如clwb),但是rdma read/write的方式cpu没有参与网络交互,因此下刷不及时,并且就算disable DDIO,这样rdma nic可以直接dma到PM,但是,rdma的ack包也会在持久化PM未完成时就返回(rdma spec针对rdma访问pm的特殊优化)
所以现在工业上使用rdma read/write + PM的方案需要在rdma write之后加一个同位置的rdma read,因为read会在读到write持久化的东西之后再返回,因此read的ack返回的之后,可以保证write已经完成了。
这个原因也就造成了,rdma write和rpc的latency基本上差不多
以上可见,想要充分利用NVM+RDMA的性能,需要深入计算机结构底层,查看有哪些优化空间:
- 使用rdma write并且disable DDIO(有关DDIO在usenix的这篇文章有更详细的论述)
- 使用cpu自带的DMA引擎来加速持久化(SPDK I/OAT engine)
memcached with PM
传统的memcached是典型的in-memory应用,通过一致性哈希索引。之前Memcached曾经实现过extstore,将index放在内存里,将不太常用的,比较大的item value放在nvme ssd中,这样做可以降低memcached使用成本,另一方面也给memcached扩容了。另外大value本身就占据较多的内存资源和网络带宽,所以这种大value存放在nvme ssd中一方面节约了内存空间,另一方面也不会降低网络throughput。
memcached随后使用PM来存储value,代替nvme ssd,性能获得了很大的提升。
B+Tree方向
非易失内存往往存在读写不对称的特性,写操作的延迟显著高于读延迟,并且具有有限的写寿命问题。
因此在单线程场景下,B+树的优化方向主要分为两类:
- 优化B+树的持久化开销,例如采用多版本机制的CDDS-Tree(参考链接5),采用无序树节点的NVTree(参考链接6)/wB+Tree(参考链接7);
- 优化B+树的一致性开销,例如采用混合内存架构的NVTree/FPTree(参考链接8),控制缓存行刷出顺序的FAST+FAIR(参考链接10)。
How to Build a Non-Volatile Memory Database Management System
CMU 2017年的文章,主要讲NVM的出现对现在的DBMS有哪些方面的影响
- Access Interface。主要是将Nvm的用法很多和内存类似,可以用PM的编程库使用内存函数(malloc、memcpy等),同时需要注意持久化特性,主要是指令(
CLWB
和SFENSE
),同时,操作系统的文件系统也针对PM做了优化,有诸如direct access storage (DAX)等技术。 - Storage Manager,主要讲NVM对DBMS存储层的影响
- Access Methods,主要是讲使用NVM作为索引层,使用如B+tree、hashmap,应该尽可能进减少对PM的write,比如B+tree:
- 叶子节点不要排序,以减少write和flush,在通过哈希索引等方式降低不排序的叶子节点的scan的开销。
- NVMzhong只存在叶子节点,其它层通过recovery节点重新生成。运行时非叶子节点(inner node)存放在DRAM中
- 另外还可以适当降低B+tree的平衡性,虽然这样会造成额外的的读放大,但是针对NVM系统,写比读昂贵得多。
- WAL,传统的WAL认为持久化存储的顺序访问比随机访问慢得多,但是在NVM上:
- 随机写并没有比顺序写慢很多,(甚至在我的测试中,某些场景随机写会更快)
- NVM的写吞吐比传统SSD多出一个数量级(毕竟内存总线)
- 所以,如果DBMS的存储系统建立在NVM上,可以抛弃WAL,直接对NVM落盘(write-behind logging (WBL)),将插入数据先于修改metadata,这样,在recovery阶段不需要apply redo log,因为修改已经落盘了。
- 当然,我们也可以选择仅将log放入PM中,这种方案更cost-effective,但这种方案只利用到了NVM的wirtelatency远低于传统SSD,而没有利用到NVM其他方面更优的特性(read BW, low latency)
- Data Placement,传统的数据库,很多很多重复的读写操作(frequent row buffer miss)其实是对1%的数据进行操作的(热数据),并且很多热数据是中间结果或者global variable。因此在一个三层存储系统中(DRAM,NVM,SSD),热数据应该放在DRAM中,并向NVM和SSD中逐步搬移合并冷数据,在事务执行阶段,数据持久化进NVM就可以完成,但是会缓慢的搬移到SSD中,因此如果读取数据落到了SSD中,整个IO链路就比较长,冷热数据的搬移、合并,这也是目前学术界的一个关注点。
- replication。不同replica间网络RTT的时延是高于NVM持久化的时延的,在这种情况下,网络时延成为了瓶颈,所以学术界目前也有一些研究是针对RDMA访问NVM的。
- Access Methods,主要是讲使用NVM作为索引层,使用如B+tree、hashmap,应该尽可能进减少对PM的write,比如B+tree:
- Execution Engine,数据库生成query plan和execution在NVM场景下的优化,执行引擎之前就要考虑计算机体系结构的各方面,在加入NVM之后,这个场景更加复杂了。当然,这方面主要的工作是要减少对NVM的write read-write asymmetry
- Plan Executor
- 传统的cache friendly hash-join算法会将数据分片为正好放入CPUcache的大小。在每个分片join完成之后放回内存再拿下一个,但是再NVM上,这种方式会导致额外的写放大。在NVM上,可以通过记录NVM中每个分片的指针等等,记录一张virtual partitions,通过额外的读降低写放大。
- 对NVM中的数据排序时,有一种优化方式叫segment-sort,通过对每个segemnt进行外排,每个segment之间进行选择排序,每个segement会读取多次,但是只会写一次,从而减少数据write操作。
- join算法中,通过segmented Grace hash-join,通过额外的读减少写。
- Query Optimizer 传统的优化器需要考虑顺序IO由于随机IO的特性,但在NVM上主要是要考虑read-write asymmetry,在write比较少的情况下,额外的read不会造成性能降低
- Plan Executor
I/O Alignment Considerations
PM的DAX模式将PM内存直接映射到页表上,而没有通过DRAM memory page cache。因此在PM的编程过程中需要格外注意PM的memeory locality。默认DRAM是4K页,默认PMEM的页是4k,但是可以更改文件系统选项更改到2M页,从而减少页表项,减少一些major pagefault
负载分析
简略的列一点测试过程中发现的现象
- cache locality需要以比DRAM更严肃的态度对待
- 利用到cache locality的小于cacheline的read/write,其性能可以做到线性增长
- 其中小byte write的drain可以提升性能
- 但是单个线程(特别是小byte),能压榨出的性能有限
- randread/randwirte在小byte的情况下,read能做到线性增长,write虽然做不到线性增长,但是也差不多,但是如果write线程中增加一些read,write会拖慢write的性能。
- 尽量减少write
- 大于cacheline的write性能做不到线性增长,4个线程以上会出现性能瓶颈
- drain的开销没有想象中大
参考链接
- Hashing Index for Persistent Memory
- Closing the Performance Gap Between Volatile and Persistent Key-Value Stores Using Cross Referencing Logs
- Closing Performance Gap Between Volatile and Persistent Memory解读
- 卡尔斯巴德中的OSDI’18—SJTU-IPADS的集体见闻(三、四)
- Consistent and Durable Data Structures for Non-Volatile Byte-Addressable Memory
- Yang J , Wei Q , Chen C , et al. NV-Tree: reducing consistency cost for NVM-based single level systems[C]// Usenix Conference on File & Storage Technologies. 2015.
- Persistent B±Trees in Non-Volatile Main Memory
- POLARDB · 敢问路在何方 — 论B+树索引的演进方向(上)
- A Hybrid SCM-DRAM Persistent and Concurrent B-Tree for Storage Class Memory
- Endurable Transient Inconsistency in Byte-Addressable Persistent B±Tree
- Challenges and Solutions for Fast Remote Persistent Memory Access好文 acm
- Reexamining Direct Cache Access to Optimize I/O Intensive Applications for Multi-hundred-gigabit Networks
- The Volatile Benefit of Persistent Memory
- CMU project Non-volatile Memory Databases CMU你懂得
- Using Persistent Memory Devices with the Linux Device Mapper
- Intel Optane Persistent Memory – Memory Mode Decision Guide
- Persistent Multi-Word Compare-and-Swap (PMwCAS) for NVRAM 微软的lock-free访问PM的库
- pcm
- pibench
附录: