1 概述
穷则软件优化,达的硬件堆积,本文主要介绍存储领域在硬件方向的一些探索。硬件方向的探索又分为大体两块,对现有硬件的性能压榨,以及新硬件设备的创新。对现有硬件的压榨主要是 SPDK RDMA,通过by pass驱动层来最大限度利用硬件的性能。新硬件的探索则包含了傲腾持久化内存和KV-SSD两块。其中持久化内存目前有些云厂商已经投入了使用,KV-SSD 则目前主要还是在学术理论阶段,业界未看到大规模的使用。
以下几篇论文分别介绍了在 SPDK KV-SSD方向的一些研究。
SpanDB: A Fast, Cost-Effective LSM-tree Based KV Store on Hybrid Storage介绍了在SPDK的探索。
Towards Building a High-Performance, Scale-In Key-Value Storage System 是三星发布的介绍了他们最新的产品KV-SSD的论文,介绍了KVSSD 这种新型存储在kv的应用。
PinK: High-speed In-storage Key-value Store with Bounded Tails则针对现在一些KVSSD实现的缺点做了优化,提出了一种针对KVSSD 优化的变种LSM 实现。
下文将非常对这三篇论文进行展开做详细的介绍。
2 SpanDB: A Fast, Cost-Effective LSM-tree Based KV Store on Hybrid Storage
本文主要介绍了如何基于 SPDK 构建LSM-tree Based KV Store。传统的kv实现需要通过调用文件系统层最后才会达到磁盘 ,对于 Nvme SSD 这种高性能设备,传统的io链路无法完全发挥硬件的性能,通过SPDK ByPass驱动层直接操作裸盘则可以将硬件性能发挥到极致。
2.1 简介
使用SPDK对WAL进行并发写入,可以大大提升写入性能,支持异步请求处理,减少线程切换的开销。测试表明,对于小value写入,绕过ext4文件系统直接通过SPDK进行写入可以降低6.8-12.4倍的延时。由于WAL 处于关键路径上,这会对写入带来严重的性能开销导致性能瓶颈。其次,现在的kv架构都是假设磁盘设备速度较慢,因此设计上通常都嵌入了较高的软件开销,如果基于轮训的机制则会比较浪费cpu周期。本文基于rocksdb做了如下的优化:
- 冷热数据分离,将热数据写在 fast speed disk(SD),将冷数据下沉到 cheaper capacity disk(CD).通过冷热分离降低总体的存储成本。
- 对于SD 设备,使用SPDK 绕过linux io协议栈直接操作落盘,压榨设备性能。
- 使用异步polling-based io,减少线程间的同步,同时协调前后台线程的io优先级处理
- 根据负载和磁盘带宽进行自适应数据分区,以充分利用CD的资源,减少对SD的写放大。
2.2 背景
2.2.1 LSM-tree based KV store
- 数据分层,(LSM 的 具体 实现这里不做过多介绍,默认已经有了 LSM 的相关知识。)
- 前台线程的写入首先写入WAL和memtable,前台的读取 操作通过可以通过memtable和blockcache来进行加速,但是如果读操作的数据落在了CD盘,那么会带来较大的读 长尾
- 后台 flush和compaction线程会定期根据策略将Ln层的文件compact到Ln+1 层,compact过慢会导致io stall, compact过快又可能抢占 在线IO。
- 通过配置后台写线程数量来控制后台 io
2.2.2 Group WAL Write
多线程排队写入,有队列头部的线程直接获取当前的写入任务进行提交,可以做写入io合并,提升写入性能。如果写请求已经被提交,队列中部的请求发现已经完成写请求后直接返回,无需在进行io操作,通过group write,将大量的小io转化成顺序大io。
2.2.3 高性能SSDs 接口
随着io设备性能的提升,linux io协议栈的开销变得不可忽视,通过SPDK将 驱动移动用户态,减少了系统调用并且支持zore copy,通过poll的方式而不是中断的方式进行io操作,减少了内核态的切换和io路径锁争用。同时这个章节对 Optane P4800x和P4610 两种设备进行了压测对比,简单列下压测结果。
- 对于大型的顺序读,通过ext4和spdk差别不大
- 通过ext4的4k顺序写无法完全发挥磁盘的带宽,同时延时大概是spdk的4.5倍
- 单线程的的spdk写入也无法发挥硬件性能,多线程多请求并发提交能更充分发挥硬件性能
图3的N 表示 P4610 0表示Optane 3-N 表示3个线程写入,CR=2 表示每个线程同时提交2个请求
可以看出使用多线程的SPDK 可以大大发挥磁盘性能
2.2.4 SPDK的限制
一旦进程绑定了SPDK,该磁盘就不能被其他进程访问,无论是通过io栈还是spdk。此外绑定cpu核心可以降低io的性能开销。加上polling-based io机制,导致后台的flush和compact线程不适合使用spdk进行读写,因为如果不绑核,会导致 io变慢,如果绑核了,则难以释放cpu空闲资源。
2.3 SpanDB概览
- 使用CD和SD进行冷热数据分离
- SD则进一步分为WAL area和 LSM-tree top level area
- 根据实时的带宽统计进行level的动态放置
- 抽象了topFS层减少了对rocksdb本身代码的改动
2.3.1 核心优化点
- SD盘使用SPDK对WAL进行并发写入
- SD 同时还用于热 数据存储,充分利用磁盘带宽资源
- 通过自适应的负载监测,可以充分利用跨设备的io资源,而不是仅仅 将CD作为额外的数据存储层?
- 虽然主要指针对写优化,但是通过将写操作倾斜到SD,可以降低CD的读长尾。
- 通过平衡协调前后台的io需求,利用polling-io减少了cpu开销
2.4 设计与实现
2.4.1 异步请求处理
对于rocksdb和leveldb,前台的client写入通常都是同步的,用户同步提交读写请求然后等待io操作完成后返回,这种操作 通常会受限于io的延时导致吞吐的不足,为了提高吞吐,用户通常会设置超过cpu核心数的线程来进行并发的操作。但是对于使用了SPDK 的高速nvme,线程间的同步唤醒往往开销比io请求本身还大,超线程机制会带来额外的开销和降低了cpu的使用率。(好比redis 内存处理足够快,不需要多线程进行处理,多线程主要带来额外的开销争用)
对于一个n-core的机器,spandb配置Nc和线程数用于处理用户请求同时 进行了绑核,剩下的n-Nc个核心则用于处理内部的io请求,分为loggers和workers。loggers用于处理wal的写入 ,workers用于处理后台的flush和compaction以及memtable的读取和更新操作。
2.4.2 请求处理流程
后台线程使用Qflush和Qcompact队列进行flush和compact操作,继承于rocksdb的实现,只是改为了直接操作SPDK
使用Qread队列来处理读请求,写请求则被拆分为Qprolog Qlog和Qepilog三个队列
- 读请求:
- client线程先判断数据是否存在memtable,如果存在则直接读取返回.
- 否则提交请求任务到Qread并等待请求处理完成,通过check检查请求状态。
- 写请求:
- 请求被拆分成三个阶段,首先将请求dump到Qprolog交给worker处理,处理完成后会生成wal然后提交给Qlog
- 当Qlog通过spdk写入完成后,将请求提交给Qepilog队列
- epilog队列完成包括memtable更新 等流程然后修改写入状态为完成,check检查状态完成以后完成一次写操作
- 任务调度
基准压测表明,少数的几个核通过批量提交请求就可以充分利用磁盘的io,因此spandb默认使用了一个logger线程进行wal的写入,该线程数量可以动态在1-3之间调整,根据队列的长度和处理耗时动态调整线程数。对于前后台线程,优先保证前台线程的调度,同时会 监控后台队列的长度,以便在高负载写入的时候即使进行flush和compact。
2.4.3 WAL的SPDK实现
-
数据布局
将磁盘的WAL 数据分区分割成逻辑页面,逻辑页面的数量可以通过配置进行配置,每一个逻辑页都具有唯一的id(LPN),并将其中的一个逻辑页作为metadata page 。meta page的每一条记录表示一个memtable,包含memtable的id ,memtable对应的log数据所在的log 逻辑页。memtable被flush完成以后,对应的逻辑页即可被回收。
2.4.4 SST的SPDK实现
为了减少对rocksdb本身的改动,抽象了一个TopFS 来管理spdk的相关操作。sst的数据布局与wal类似,也是通过metadata page管理,metadata page为hashtable,key为文件名,内容为文件对应的逻辑页起止id。
2.4.5 SPDK优化
- 保证优先写WAL 通过限制flush 和compact的byte size控制磁盘带宽,同时限background的线程数
- spdk cache 复用spdk_dma_malloc()减少内存拷贝作为数据cache
- 动态数据放置 数据的冷热分离不是简单的根据磁盘level进行选择,而是根据磁盘带宽选择当前的level,通过当前的指针决定数据应该放在哪个设备。
3 Towards Building a High-Performance, Scale-In Key-Value Storage System
3.1 背景
传统是kv存储实现,通常是存储引擎暴露kv接口,用户通过kv接口写入数据,key和value通过一定的数据结构被存储到文件里面,从用户调用接口到数据最终落到磁盘的整个流程可以用下图的左边部分来描述。
kv接口将数据写入文件系统,文件系统通过块设备驱动将数据写入磁盘,为了屏蔽磁盘的内部物理结构,文件系统到磁盘其实是通过LBA进行逻辑地址映射的,磁盘内部通过FTL将LBA转化为PBA,同时FTL还承担了磁盘的COW GC功能。可以看到从写入到落盘中间经历了非常多的环节。那么是否可以通过减少这些环节来进行写入流程的优化呢。比如上面提到的SPDK 通过bypaas文件系统少掉了一层文件系统的开销,那么是否还能更近一步呢。这篇论文正是基于这样想法,直接将kv的操作下层到设备层,由设备直接暴露了kv interface,直接bypass了设备层以及LBA到PBA的映射。通过设备内部的FTL ,直接实现了key到NAND location的映射。
3.2 KV-SSD设计
- io流程
设备暴露了put get delete iterate接口,kv请求通过pipeline的流程处理kv请求,固件驱动从磁盘io队列获取请求,然后传递给request handle,request handle再讲请求传递给index manager,index manager先将变长的key散列成定长的key缓存到local hashtable,最后在合并到全局的hashtable完成到磁盘位置offset的映射。同时index manager还会根据key的前4B进行分桶,通过将相同前缀的数据放在一起来实现迭代操作。local hashtable主要是为了提高写入并发减少全局hashtable的锁竞争。
- 垃圾回收
垃圾回收时,垃圾回收器扫描flash中kv数据,然后和 全局hashtable做对比来判断数据的有效性,丢失已经被删除和更新的数据
3.3 性能测试
性能测试部门主要测试了kvssd的cpu开销,在同样的ssd下,kvssd只需要一个线程就能达到与普通ssd8线程一样的io吞吐,同时,随着单机磁盘数量的提升,kvssd拥有更好的线性拓展能力。对于普通的ssd,单机的吞吐受限于cpu无法随着磁盘数量程线性增长,而kvssd由于极低的cpu开销可以提供更高的单机吞吐能力,对于昂贵的机柜资源,如果能在单机插更多的磁盘提供更高的吞吐显然是更经济的。
其实kvssd之所以需要更低的cpu开销,除了bypass了很多层,更重要的部分是他的固件模块其实已经有点类似于一个小型的cpu了,通过把cpu的工作offload到固件本身的做法在业界由来已久,比如将网络包的解析的bypass 内核offload到网卡直接进行。
4 PinK: High-speed In-storage Key-value Store with Bounded Tails
4.1 背景
虽然使用KV-SSD可以带来延迟的降低和吞吐的上升,但是这个降低只是针对平均值,目前现在大多数的KV-SSD的实现都存在一个长尾的问题。最常见的KV-SSD的实现包括hash-based KV-SSD 和LSM-tree Based KV-SSD,但是这两种实现都存在长尾的问题。
4.1.1 Hash-based KV-SSD缺点
磁盘控制器DRAM有限
对key进行压缩编码会导致hash冲突
-
性能随数据量增长而下降
hash-based的实现是把索引信息存放在磁盘控制器的DRAM 模块,把实际的数据存放在Flash模块,当数据量小的时候由于索引都直接在flash命中因此访问速度很快,但是随着数据量的上升,DRAM无法放下这么多的数据,有些所以数据需要把移动到FLASH,这就导致了读放大。
对于1块4T 的SSD,假设key 为32B value为1KB,那么就需要 (242/210) * (32B+4B)= 144G的DRAM,其中4B 为指针的大小,目前的SSD控制器显然无法提供这么大的DRAM,即便对key进行16bit的编码,也需要24G的DRAM,SSD同样无法提供这么大的DRAM空间,而且编码以后由于hash冲突也会导致性能下降。
4.1.2 LSM-based KV-SSD缺点
- 即便使用了bloom-filter 也无法避免极端情况的读长尾
- lsm本身的写放大问题,compact会加剧恶化FTL的垃圾回收
- compact时期重建bloom filter和kv的比较排序会给控制器的芯片带来额外的开销
此外,控制器内部的DRAM和FLASH 的发展速度也不一样,平均每年DRAM增长1.13倍但是FLASH则增长了1.43倍,随着DRAM的增长,FLASH的需求将越来难以满足使用,因此设计一个减少FLASH使用的数据结果是势在必行的
4.2 设计实现
4.2.1 总体架构
总体架构的实现如下图,由于pink是LSM 的改进版本,此处会将pink的数据结构和rocksdb进行比较。pink的存储结构可以分为4部分,分别是 SkipList、 levelList 、meta segment 和data segment。
- skiplist类似rocksdb中的memtable,存储用户最新写入的数据
- levelList存储level分布的信息,和rocksdb不一样的是,rocksdb每一层的level存储的是完整的kv信息,而pink中存储的则是kv的索引信息,其中key是startkey,value则是这一startkey对应的meta segment。levelList和skiplist一样存放在控制器的FLASH当中。
- meta segment存放在DRAM中,包含一个个的kv对,其中key是用户写入的key,value则是一个4字节的指针,指向实际value所在的DRAM位置。此处采用了类似wisckey的key value存储分离的实现。
- data segment存储实际的value数据。
上面提到skiplist和levellist都存在FLASH 当中,那么FLASH 是否有足够的容量容纳这些levellist呢?对于一块4T 的磁盘,假设key value大小分别为32B和1KB,page的大小为16KB,那么对于一个16KB的meta segment,可以容纳的
- 写入流程
写入流程很简单,用户写入的数据会直接写入到skiplist中,熟悉LSM的人可能发现了,传统的LSM 写入为了保证数据的可靠性,都会先写入WAL 然后在写入内存memtable,但是此处并没有WAL而是直接写入skiplist会不会有问题呢。这个主要得益于磁盘控制器的自带的电容器,通过电容器可以保证断电数据不丢失,因此可以省掉一次的WAL写入
- 读取流程
相比写入,读取流程则显得比较繁琐,以下图的查询key(39)为例。
- 首先从skiplist中查询39是否存在,由于skiplist不存在39因此进入下一层L1进行查询
- 对L1进行二分查找找到39的下界startkey 2,然后读取2的value对应的meta segment。
- 由于2的meta segment中不包含39,说明key不在L1,因此继续到L2进行查询
- 同样二分查找找到上界startkey 33,然后读取33的meta segment
- 从33的meta segment所在page 2中发现了key(39),读取key(39)的value的指针
- 最终定位到39的value所在的page位置信息完成读取流程
4.2.2 使用level Pinning进行读优化
从上面的读流程可以看到,对于极端的情况,一次的查询可能涉及非常多次的FLASH读取,比如上面的流程就需要读取metaPage0 metaPage2以及最终value所在的datapage12。对于level更多的场景需要读取FLASH 的次数将更多。
level Pinning的思路很简单,就是把meta segment尽可能的放在FLASH,这样就可以减少FLASH的读取了。那么问题来了,FLASH能放的下这么多的meta segment么。
同样直接对数据进行分析计算,由于LSM 的指数分层存储机制,每个Ln+1 层的数据都是Ln层的T倍,对于4T的磁盘,实际数据表明总共需要的存储层级为5级,对与L1到L4的层级分别需要的meta segment为0.91MB 50.86MB 2.83GB 161.3GB.由于一块4T的SSD 通常有4G的FLASH,那么显然是可以把L1到L3的meta segment放到FLASH里面的。
把metasegment放到了FLASH里面还带来了一个额外的好处,就是compaction的开销也随之降低的。因为L1到L3的meta 都在FLASH,可以大幅减少compact时meta更新带来的开销。
4.2.3 优化查找路径
优化查找路径使用了级联的方法,简单说就是每一层的kv额外存储指向下一层的指针,其实就是类似skiplist的实现。通过级联的方法,当查询一个key的时候可以缩小每次二分查找的边界。查询Ln层的时候,先找到这个key的上下边界,如果Ln不满足,则根据上下边界直接定位到Ln+1 的上下边界,而无需对整个Ln+1进行二分查找。
级联需要额外的8字节指针开销,由于最后一层不需要存储级联指针,因此总共增加的级联指针开销为43.9M, FLASH 依然是够用的。
不过这里有一个问题论文里没说清楚,当Ln+1由于compaction更新以后,如何去更新Ln的级联指针,考虑到由于levellist都在FLASH中,这里去更新上层的级联指针的开销是可以接受的。
4.2.4 compact加速
简单概括就是硬件加速,把计算逻辑offload到FPGA来实现硬件加速,达则堆积硬件。
4.2.5 GC优化
gc优化包含meta segment的GC以及data segmetn的GC。
- meta segment的GC
通过metapage的start从levelList中查询是否还有指针指向page,如果没有则直接回收,否则将数据迁移到空闲的page,然后修改levelList中的指针。由于上层的meta可以通过DRAM直接进行复制更新,因此开销是很低的
- data segment的更新
遍历需要回收的page逐条读取kv信息,根据key查询判断该数据是否还有效,如果失效了直接忽略,否则则需要对该value进行rewrite。对于rewrite,最简单的实现是参考wisckey,直接更新meta 中的value指针,如果meta segment是保存在DRAM中的那没有任何问题,但是如果meta page是在FLASH中的,由于越底层的数据都是旧数据,因此一个data segment的数据在meta 中往往很离散,这时更新meta的指针会带来meta segment的写放大,为了避免这个问题,pink对于存在FLASH的meta segment使用了延时更新,compact的时候直接把kv写入到L0进行覆盖,对于该kv由于读取是从上往下的,因此读取流程不会存在任何问题,metasegment的数据则可以等待meta指针失效的时候进行删除。
4.2.6 可靠性和扩展性
- 可靠性: 除了高配的企业级SSD,有些SSD病没有足够的电容来保证FLASH数据的可靠性,为了避免断电数据的丢失,pink将L0的数据写入到了磁盘的Log area,同时定期将flash的其他数据定期刷到FLASH来保证数据可靠性。
- 拓展性: 对于之前提到的FLASH和DRAM 不同的增长系数,随着FLASH 的增加FLASH将变得不够用,pink可以通过减少pinning level的级数来解决这个问题,虽然对最差查询性能带来了一定影响,但是依然比hash-based和传统LSM 提供了更好的性能。
5 总结
新硬件探索,硬件加速是今后存储发展的一个重要方向,同时随着新硬件的出现,现有的数据结构可能并不适合新硬件的的特性。从HDD 到SDD再到NVMe,硬盘性能不断升级,业界也针对SSD做了大量的存储优化。英特尔最新推出的PMEM则对存储又是一次大的革新,对于PMEM现在的文件系统其实已经不太合适了,因此也有针对该方面的优化Rethinking File Mapping for Persistent Memory。至于KVSSD,在该几篇论文后目前则又有了一些新的进展,NVMe 2.0 规范已经将KVSSD的指令集规范为NVMe-KV 指令集,因此KV-NVMe应该也不远了。
Reference:
https://github.com/OpenMPDK/KVSSD
Rethinking File Mapping for Persistent Memory
SpanDB: A Fast, Cost-Effective LSM-tree Based KV Store on Hybrid Storage
Towards Building a High-Performance, Scale-In Key-Value Storage System
PinK: High-speed In-storage Key-value Store with Bounded Tails
https://unix.stackexchange.com/questions/106861/what-is-the-relationship-of-inodes-lba-logical-volumes-blocks-and-sectors