本文来源:微信公众号:腾讯云数据库
原文地址:https://mp.weixin.qq.com/s?__biz=Mzg4NjA4NTAzNQ==&mid=2247488667&idx=1&sn=c1d3f497b2f31699a41ae6b4e8080feb
作者 张鹏义,腾讯云数据库高级工程师,曾参与华为Taurus分布式数据研发及腾讯CynosDB for PG研发工作,现从事腾讯云redis数据库研发工作。
存储体系结构
回顾一下计算机的存储体系结构。正如下图所示,计算机存储设备根据访问速度及容量等形成了一个金字塔形的层次结构。在这个层次结构中,从上到下,设备的访问时延越来越大,容量也越来越大,而每字节的造价也越来越便宜。在这个层次结构中,每一层都被看作是其下一层次的缓存。
同时在这个层次结构中存在一个明显的分界线,如图中绿色的虚线。在分界线以上都是易失性的设备,掉电数据丢失,CPU通过load/store指令访问存储设备,即数据直接以cache line(64 byte)的粒度从寄存器中copy到存储设备中,或者从存储设备中copy到寄存器。而分界线以下都是可持久化的存储设备,通过向I/O控制器发送命令,以block的粒度将数据换入换出到DRAM中,然后才能直接被CPU访问。
随着近互联网的发展,数据密集型应用已然成为主流,其主要特点表现为数据容量大,对数据分析的实时性及数据存储的高效性提出了更高的要求,以spark、redis为代表的基于内存的数据分析与存储系统广受欢迎。
但是DRAM的容量受限于晶体管-电容式(1T1C)单元的密度,以及难以扩展的动态刷新(refresh)操作,DRAM的容量没办法做得很大,服务器上主流的DRAM一般为16G或32G,容量再大功耗相应也会增加。而且DRAM的价格比较高,数据不能持久化。
对于SSD等一些块设备来说,确实能够满足大容量持久化的要求,但是访问延迟却是DRAM的1000多倍,同时块设备存在随机访问能力比较差的问题。虽然近些年来出现了以rocksdb/leveldb为代表的LSM-Tree结构的存储系统从一定程度上解决了块设备低效的写问题,但同时又引入了一个比较严重的写放大问题。
SSD与DRAM的访问延迟差异导致在分界线处形成了一条巨大的鸿沟,持久化内存的出现正好给填补上了。
持久化内存在存储体系层次结构中的位置如下图所示,它位于DRAM与SSD之间,同时注意到它是横跨在分界线上的, 它模糊了传统层次结构中内存与外存的边界 ,它既具有DRAM一样的数据访问方式,也具有SSD一样的持久化特性。所以可以把它当成内存或者持久化的存储使用。
Intel optane DC persistent memory是Intel推出的基于3D Xpoint技术的持久化内存产品,其代号为Apace Pass (AEP)。AEP的物理封装为DIMM,直接插在内存插槽上,单条AEP的容量目前已经达到了512G,如上图所示,如果一半的插槽插上AEP,那么单个CPU内存容量很轻松的就可以扩展到3TB以上。IMC(integrated memory controller)与AEP之间以cache line大小的粒度传输数据,但是AEP的ECC(E rror correction code ) size为256字节,即AEP内部的controller与介质之间是以256字节(4个cache line)为单位进行读写。同时AEP的读写不对称,读性能明显优于写性能,所以应该避免频率的写入小对象。
AEP工作模式
AEP有两种工作模式:Memory Mode和App Direct Mode,如果细分就还有Storage Over App Direct。
根据AEP在存储层次结构中所处的位置,自然想到的是将DRAM作为AEP的缓存,AEP中的热数据缓存在DRAM中,这也符合计算机存储系统一惯的设计思想。在这种模式下AEP和DRAM共同组成了一块对上层透明且容量更大的易失性内存,这时系统的总容量等于AEP的容量,应用无需做任何额外的修改即可使用。DRAM到AEP的缓存算法由IMC(集成在CPU里的memory controller)硬件管理。
当前的缓存策略采用 direct-mapped-cache 算法实现, 数据以cache line的粒度换入换出,但是这种算法会把AEP中多块内存映射到DRAM中的同一个位置,以致于在某些场景下出现冲突不命中的情况可能比较严重。所以这么一个通用的算法并不能很好地将热数据从AEP中分离出来。
Memory Mode配置方法:
ipmctl create -goal memorymode=100 (ipmctl是Intel开发的一个管理持久化内存的工具)
reboot后 free -g 就可以看到系统内存容量等于AEP的总容量。
应用可以不经过DRAM而直接访问AEP,用由应用自己管理,这种使用方式叫做AppDirect Mode。那么在这种模式下应用怎么访问AEP?SNIA(Storage Networking Industry Association)制定了一套编程模型,如下图所示,其中NVDIMM指非易失性内存模块,即AEP设备。
传统的文件系统都有一层page cache, 访问数据时先将数据copy到page cache中,然后再copy到应用buffer 中。对于低延迟及可按字节寻址的AEP来说,没这个必要。Pmem-Aware File System指的是支持直接访问设备(DAX)的文件系统,DAX特性从IO路径上移除page cache, 同时允许mmap()直接建立到持久化内存的映射关系。目前ext4和xfs都已支持DAX特性。
AppDirect Mode配置方法:
ipmctl create -goal PersistentMemoryType=appdirect
reboot (reboot后就可以在/dev下看到pmem0,pmem1设备)
ndctl create-namespace -m fsdax -r 0
mkfs -t ext4 /dev/pmem0
mount -o dax /dev/pmem0 /mnt/pmem0 (-o dax开启DAX)
配置完成后就可以在/mnt/pmem0下创建、删除、读写文件了,但是一般不会直接这么使用。通常在/mtn/pmem0/上通过mmap建立memory-map file, 一旦映射建立后,用户虚拟地址空间通过MMU就直接映射到AEP的物理空间中,应用无需陷入内核态即可高效地以字节寻址的方式访问AEP。再借助于libmemkind库,就可像使用DRAM一样来使用AEP。Libmemkind将create/open file, mmap进行了封闭,并提供类malloc/free的接口在AEP上分配内存。所以应用需要显示通过类malloc/free接口决定哪些数据直接写到AEP,对已经代码要做一定的修改。
要注意的是mmap必须是以shared的方式建立映射,如果是以private的方式映射,更新数据时并不会写到介质上,而是写到进程私有的空间中(page cache中),然而这里AppDirect模式下是没有page cache的。这里引出了一个问题:如果进程在AEP中分配了一块内存,然后fork一个子进程,那么子进程也是能够看到父进程对这块内存的更新,因为两个父子进程的更新都会立即反映到同一块物理内存上。所以对于分配在AEP上的内存就没办法利用fork的copy on write机制来获取一致性的内存状态。(Redis正是利用fork的copy on write机制获取一对致性内存状态做备份操作)。
AppDirect下即可以将AEP当作易失性的内存使用也可以当作持久化的内存使用。当作易失性内存使用时,仅仅是我们不关注重启后AEP上的数据内容而已,并不是指掉电后AEP上的内容真的丢失了。如果当持久化的内存使用,则应用需要处理持久化及数据一致性等问题,下节详细讲。
如上图中的第三、四条路径,分别对外提供block接口和标准的文件接口,应用无需额外的适配工作,这和使用SSD并没有什么区别,仅仅是延迟更小。但是这种使用方式软件栈上的开销所占的比例相当大,并不能发挥出很好发挥出AEP的极致性能,基本不用考虑这种用法,除非是已有代码确实不能改动但又想获得低延迟访问的情况。
当一条store指令完成后,数据并没有立即写到DIMM介质上,很有可能还存在于cpu cache中或者Memory Controller的Write Pending Queue中,掉电后还是会造成数据丢失,也就说这里是一个异步的持久化过程。所以应用需要显示flush cpu cache和WPQ后才能说明数据确实已经持久化,当然在Intel平台上ADR模块掉电后会触发硬件中断到Memory Controller并flush WPQ中的数据到DIMM介质上。即Intel平台上关注cpu cache就好。clflush、clflushopt、clwb指令都是用于flush cpu cache, 其中clflushopt、clwb是为了提升持久化内存的flush效率新引入的,不一定所有平台都支持。其中clflush是串行flush, 而clflushopt可以并行flush, clwb与clflush类似,只是并不一定会立即失效cache line,提升后续读性能。
由于CPU乱序执行和cpu cahe 并行flush问题可能会导致两个数据对象实际在介质上持久化的顺序与应用写入的顺序相反,如果说这两个数据数据对象具有因果关系,那就出大问题了。
如下面这段代码,如果list->length修改后的值所在的cache line先被flush到介质,而在list->tail->next修改后的值还未flush到介质之前机器掉电,重启后根据list->length再去遍历list就可能会造成崩溃。
typedef struct List {
int length;
struct List* head;
struct List* tail;
};
typedef struct Node {
void * data;
struct Node* next;
}Node;
void appendList(List* list, Node* node){
list->tail->next = node;
list->tail = node;
list->length++;
}
正确的做法是在flush(&(list->length))之前加上fence指令确保持久化的先后顺序。
void appendList(List* list, Node* node){
list->tail->next = node;
list->tail = node;
__mm_clwb(&(list->tail->next));
__mm_clwb(&(list->tail));
__mm_sfence(); //
list->length++;
__mm_clwb(&(list->length));
}
在X86平台上,仅不大于8字节的对象能够保证是原子写,大于8字节的数据对象可能写到一半出现机器掉电,重启后的数据是否完整无法得知。所以应用需要通过flag/redo log/undo log等方式判断数据是否完整,以及不完整时该怎么去处理,当然这势必会引入比较重的开销。
再比如将Redis的索引结构(hash table)及数据都写入到持久化内存中,那么当用户写入一条数据时,内部可能发生hash table扩容,hash entry搬迁等多个动作,要维护数据的一致性问题,那就也需要引入类似mini transaction的机制。所以把AEP当作持久化内存与易失性内存来使用时性能肯定是一定的差异的。
一旦考虑把AEP当作持久化的内存来使用时,所写下的每一行代码都考虑怎么处理数据一致性的问题,这并不是一件容易的事情。
为了简化持久化内存在AppDirect下的使用,Intel开发了 PMDK (Persistent Memory Development Kit)。我们可以直接在PMDK的基础上去开发自己的应用,但是这些通用的库也并不一定适合所有场景。
下面这几个库用的比较多一些:
1. libpmem: 用来将数据持久化到介质上,以及提供一些优化的内存操作(memcpy, memset等)函数。
2. libpmemlog: 基于这个库可以用来写顺序追加的log等。
3. ibpmemobj: 这个库实现了一套事务机制,用来保证数据的一致性问题。
最后用一张图总结一下AEP的使用方式及在数据领域潜在的价值。
本文来源:微信公众号:腾讯云数据库
原文地址:https://mp.weixin.qq.com/s?__biz=Mzg4NjA4NTAzNQ==&mid=2247488667&idx=1&sn=c1d3f497b2f31699a41ae6b4e8080feb
作者 张鹏义,腾讯云数据库高级工程师,曾参与华为Taurus分布式数据研发及腾讯CynosDB for PG研发工作,现从事腾讯云Redis数据库研发工作。