[TOC]
简介
用了一段时间AEP了,现在写一个材料给大家分享一下,从零开始的AEP入门。
AEP 与 CPU 的 iMC 相连需要 CPU 使用 load/store 指令进行读写,load/store 指令往往就牵扯到 CPU 的 memory-model,再加上使用 AEP 的时候往往是希望利用其持久化的特性的,所以要做到一个正确的设计就首先需要对CPU的memory-model(或者说指令顺序及效果)有个基本的认识。
同时目前 Intel 的 AEP 实现也相对比较复杂,看起来是只是一个新介质,但实际上是一个复合的存储系统,所以要做到一个高性能的设计还需要对 AEP 本身的架构以及内核是如何进行有一个基本的认识。
因此AEP实际上是一个比较综合的话题,我过去几个月中阅读了一些相关的资料,发现很多资料对于小白也不够友好(因为当时我也是小白),同时也结合一些我的心得,打算系统的分享出来。
整体分成下面几块
如何持久化的把一块数据写到内存
在这个部分,先以抽象的方式假设有一段内存地址提供持久化功能,讨论如何写入一条记录,主要目的是引入memory-model相关的话题(cache结构、指令顺序)。为什么先假设有一段内存地址是持久化的,主要是想说明这部分复杂度是由于CPU本身造成的,与AEP本身的硬件无关。
- 结合CPU内存模型简单介绍一下如何写一段数据到内存
真正的持久化内存
通过第一部分,已经了解了一般化的如何持久化的写一条记录,在这一部分,会从最底层介质颗粒开始逐渐向上,介绍AEP的结构以及如何与系统集成的,也介绍了一些必要的特性,比如掉电保护、Interleave。对AEP结构的了解有利于理解AEP一些性能优化的方式。
- 从介质开始简单介绍一下 AEP 100系列的硬件架构,及其特性
如何使用持久化内存
其实AEP用起来还是蛮简单的,在这个部分我们先介绍AEP给我们提供的使用模式,以及如何通过命令行初始化一个AEP创建文件系统,同时也介绍了一下Intel提供的PMDK工具包的功能。
- 简介DAX,简述一下一个fs-dax是怎么写入AEP的
- 简单介绍一下PMDK是啥,各个库大概干啥的
心得&杂记
这个部分就是介绍一下RPMA,介绍一下性能优化的考虑,以及简单介绍一下之前有个AEP的比赛
- 简单介绍一下RDMA配合AEP使用的情况
- 简单介绍一下性能考虑
- 比赛
水平有限且有的地方写的确实不严谨,欢迎提出任何意见
如何持久化的把一块数据写到内存
那么我们现在假设有一段内存(地址)拥有持久化的能力(假设你能获得物理地址或者说不考虑虚拟内存,且写在 Cache 中不算写入内存)。
我们想要存储一段数据,最简单的方案就是直接 copy,如下
const char* data = "hello world";
strcpy(addr, data);
虽说内存有持久化的能力,但是 CPU 写入这些数据不是原子的,要分多次写入(通常64B/cycle), 可能在掉电的时候只有一部分写入了内存,一般我们希望写入内存的数据是失效原子性的(failure atomicity),即如果写入成功数据就是对的。
在讨论解决方案前,对指令执行顺序做一个简单的描述
编译器可能会对代码进行重排
对于未指定 order 的代码,编译器是可能发生重排的
处理器执行中可能会对指令重排
处理器中指令集别是可能会对存储指令进行重排,不过这取决于 cpu 的 memeory model,比如 x86 的平台上使用的是 TSO(Total Store Order),可以理解在单核心上在执行的指令的角度看来读写不会重排,多核心可能读取到旧的数据,所有核心的写请求有顺序,即如果一个核心先写了 A 后写了 B,如果另外一个核心看到了 B,那么 A 一定写完了。
- 这里是Intel对于x86 Memory Order 的更详细介绍,注意第三条对于某些特殊的指令,是不符合我们提到的 TSO 的,所以也就是后面一堆复杂操作的来源。
对于持久化也有几个特点需要了解,虽然指令按照顺序执行了,但是要进到内存还需要考虑下面几点:
数据在 cache 上,没进到 imc 中,或者 压根还没到 cache
这是有可能的,可以看到,实际上 eu 执行后可能在 sto-buffer 中,甚至还没有进入 cache
数据在不同的 cache 上,其写imc的顺序是不确定的
cache 写回是可能随时发生的,比如说 cpu 此时 io 压力较大,很有可能就把某条 cache 写回了
这两张图是casecade处理器的微架构,方便大家理解上面提到的几点
sf 是 snoop filter
llc是 last level cache
adpll 是 all digit phase-locked loops 调节始终频率的
fiwr 是 fully integrated voltage regulator 调节核心电压的
可以看iMC(Memory Controller)与内存相连,iMC 通过 snoop filter 与 last level cache相连
内存模型是在指令的层面上角度看的,对于不影响内存模型语义的指令,CPU是可能会乱序执行的,比如说CPU读取数在不同的地址,同时也没别的写入在这两个地址上,我就算真的reorder了,在指令层面还是等价于顺序执行。
接下来,考虑如何持久化的写入一条 record(或一条log、一段数据),一般我们是通过数据校验的方式来实现我们的需求,这里提供了两种方法。
方案一通过标志位的方式来标志数据写入成功。方案二更加一般化的使用了整体校验的方法。
// 方案一
struct Record {
char data[N];
bool valid;
};
const char* data = "hello world";
strcpy(record->data, data);
fence();
cacheline_writeback();
node->valid = true;
fence();
cacheline_writeback();
// ok to return
// 方案二
struct Record {
char data[N];
uint32_t checksum;
};
const char* data = "hello world";
strcpy(record->data, data);
record->checksum = cal_checksum(record->data);
fence();
cacheline_writeback();
// ok to return
我们先在不涉及CPU结构的情况下进行讨论(假设乱序总是可能发生)
方案一中第一个 fence 是保证 order,再使用 cache line writeback 写回确保数据落在持久化内存中了(实际上不一定到介质中了)。这里简述一下 fence,可以理解有两种作用,1是保证order,即fence返回后,之前的写入都完成了(before is before),2是保证写入已经完成,比如在x86上大多数fence都保证写入已经全局可见,这里还要根据写入的类型来分析,有的指令是写cache有的指令是直接写内存。
方案二中则使用了一次 cache line 写回,fence 是在checksum 和 data 之后发起的,所以其实他们可以以任意顺序写到cache上,fence 返回后保证已经写入cache,随后 cacheline 写回 保证进入内存(同样不一定到介质中)
方案一判断写入是否成功是简单明了的,如果读取到 valid 为 true,说明 data 的内容是合法的。因为先保证数据写好再写的flag,flag写入则一定数据没问题。
方案二判断写入是否成功需要去用读取的数据去计算校验和,如何校验和正确则写入成功。
现在,我们更具体一点,看看可以在x86上使用哪些指令来实现上面的两种写入
memcpy(strcpy): 一般就是普通的 store 指令,或者为了绕开 cache,可以使用 NT(non-temporary) 系列的指令(SSE),大粒度的用AVX-2(同样有NT和普通两种)(AVX-512 最好不要用,很多机器上512执行单元就一个,还不如俩AVX-2)
fence:
sfence
这里补充一下官方的说明The processor ensures that every store prior to SFENCE is globally visible before any store after SFENCE becomes globally visible. The SFENCE instruction is ordered with respect to memory stores, other SFENCE instructions, MFENCE instructions, and any serializing instructions (such as the CPUID instruction). It is not ordered with respect to memory loads or the LFENCE instruction.
cacheline_writeback:
CLFLUSH
CLFUSHOPT
CLWB
,第一个是同步invalid写回,第二个是异步invalid写回,第三个是直接写回,由于第一个性能太差就不考虑了。
那么根据是否需要该条数据仍然在缓冲中(即data跟flag写完后是否需要在cache中),方案一有如下两种方式。(方案二就是方案一的简化所以就不单独讨论了)
// 方案一的实现1 需要在 cache 中
MOV m[pmem_addr], m[addr] // 写入数据
MOV m[pmem_addr+1], m[addr+1]
.
.
// SFENCE // 这里需要一个fence吗?不需要 implicitly
CLWB m[pmem_addr] // cache line 写回
CLWB m[pmem_addr + 64]
SFENCE // 这里是必须的吗?如果没有的话,下面这个 MOV 可能先执行,如果恰巧flag跟data在一个cacheline就不行了
MOV m[pmem_addr + N], D1 // 写入 flag
CLWB m[pmem_addr + N]
SFENCE
// 方案而的实现2 不需要 在 cache 中
MOVNTI m[pmem_addr], m[addr] // nt 写入数据
MOVNTI m[pmem_addr+1], m[addr+1]
.
.
.
SFENCE // 上面 手册截图中提到了 nt 指令是write-combine,非write-back
MOVNTI m[pmem_addr + N], D1 // 写入 flag
SFENCE
对于需要在 cache 中的情况,细心的同学可能注意到了如果 strcpy 使用的是一般的 store 指令,cache-line使用的是 CLFLUSH,实际上是不需要做 fence 的(翻看最上面Intel memory order,CLFLUSH是遵循TSO的),但是需要注意的是因为使用了 CLWB
,其执行模型是非TSO的,这是由于 CLWB 的实现造成的,需要对不同顺序指令的 order 进行分析
CLWB instruction is ordered only by store-fencing operations. For example, software can use an SFENCE, MFENCE, XCHG, or LOCK-prefixed instructions to ensure that previous stores are included in the write-back. CLWB instruction need not be ordered by another CLWB or CLFLUSHOPT instruction. CLWB is implicitly ordered with older stores executed by the logical processor to the same address.
对于不需要在cache中情况,由于使用了 NT 指令,NT指令是特殊的 store 指令,使用的内存模型是 Write-Combine( weak order ),也是非TSO的,所以也需要一个 sfence 来保证不同的 NT store 间的 order
看起来很复杂,但是总结一下实际上写一段数据的模式是差不多的,就三步骤,无论是哪一种都需要 sfence (因为CLFLUSH太慢了)
func write_data(data):
mov 写数据(nt or normal)
sfence
if (not use nt store):
clwb
sfence
那么我们的
方案一是
write_data(str_data)
write_data(flag_data)
方案二是
write_data(str_with_checksum_data)
真正的持久化内存
上面我们使用抽象的持久化内存分析了写入的流程,那么接下来我们看看真正的持久化内存,也就是intel的 AEP,有很多缩写意思差不多,这里列一下,后面可能不加分辨的使用
Apache Pass(AEP): Intel’s codename for Intel® Optane™ DC Persistent Memory
DCPMM:Intel® Optane™ DC Persistent Memory
Optane DIMM:也是指AEP,dual in-line memory module 指接口,因为傲腾还有走NVMe接口的产品
3D Xpoint:指的是AEP存储颗粒使用的技术
NVMEM(non-volatile memory):AEP属于nvmem,nvmem的概念原来就有,挂个电池的那种
PMEM(persistent memory):在一些文章中经常出现,大多数就是指AEP
为什么AEP能做到持久化以及低时延,我认为主要得以与两方面,首先是介质上的优势,3dxpoint 本身拥有很好的读写时延以及较长的耐久性,所以本身就可以作为高性能的磁盘使用,所以最开始Intel先推出了Optane的SSD,由于其读时延非常小,所以让Intel看到了绕开PCIe直接byte-addr的可能性,再配合Intel本身的技术整合,所以pmem就这样产生了
介质
现在有很多 NVM 非易失性内存的技术正在开发中,其中就包括 PCM 、FRAM 等好多种技术[3],据说 intel 已经搞 PCM 搞了40 多年了,目前我们用到的AEP 就是 Intel 的 PCM 介质产品
intel 的这一代AEP产品使用的是 GST(锗锑碲为基的相变化材料) 的相变材料,就如上图所示通过不断的加热来使得材料在结晶和非结晶两个状态间转换。不同状态的材料有不同的特性,比如电阻不同,从而可以表示数据。
图中就是存储颗粒的结构图示,绿色的部分就是相变材料,黄色的部分是一个选择器,通过接收特定的电压对相变材料进行读写。每一个cell都是可以独立进行读写,当写入的时候不像NAND需要block级别进行擦除。
对比内存的优点:一持久化、二可以直接读取(xy索引内存需要行列 读破坏 刷新)
如何组成一个AEP系统
有了存储介质,Intel 在此之上提供了两种方式接入系统,一个是类似传统SSD的方式,使用PCIe与CPU相连,使用NVMe进行通信,叫 Optane SSD,相比传统SSD其随机读写性能以及耐久性有很大优势。
受限于 PCIe 带宽以及使用 NVMe 协议,Optane 并不能完全体现颗粒低时延的优势,所以第二种方式是使用DIMM 物理插槽直接与iMC连接,直接使用 load/store 命令去对数据进行读写(也就是AEP了)。具体长这样:
对图片简单的说明:
- 介质到Controller 是 256B的粒度进行传输的,为什么是256B目前大概率是要做256B的ECC
- 有个 DDR4 的 cache 主要是做 AIT,也就是从映射线上地址到物理地址(LBA映射),虽然AEP是不需要GC的,但是还需要做一些 write-leveling
- PMIC 主要负责两个方面,一是 AEP 是有功率限制的,不同功率下性能略有不同;二是ADR也就是RAS相关的操作,断电保护,一代只有普通的 ADR,保证落到 iMC 就不掉(据说二代有 eADR 可以保证落 cache 不掉,目前未知是否真的可以做到)
- Buffers/DQ buffers,第一个应该是各种文献中出现的xpbuffer,是256B line size的cache,第二个主要是方便 iMC 从总线读数据的,数据OK后需要使用 DDR-T 协议先通知下 iMC,然后通过 DDR4 的方式读走,此时需要把数据放在 DQ buffer 中,因为 DDR4 太快了
整体的跟CPU的连接方式是这样的:
- casecade 架构有一个die上有两个iMC,每个iMC有3个内存channel
- 每个Channel上可以插一条AEP,所以一个socket最多支持6条AEP
可以看出整个AEP还是相当复杂的,是一个介质加控制器的存储器,不像内存一样基本上就是一堆电容。
所以 AEP 能做出来实际上还是得益于 Intel 比较好的技术整合能力,介质跟系统缺一不可(比如镁光也有3dxpoint)
细节
DDR-T
上面我们浏览了整个 AEP 的组成部分以及如何跟 CPU 连接的,其中提到了 AEP 是通过 DIMM 插槽直接连接到iMC 的,也就是使用了跟 DDR4 相同的物理连接(DDR4 pin-compatible, meaning they use the same electrical and mechanical interface as DDR4)。虽然 3D XPoint 的读时延比起 NAND 已经快了非常多了,大概是几百ns的级别,但是我们看一下DDR4的时序,一般 DDR4-2400 的时序是15-15-15(CL-tRCD-tRP),CL 代表从 read 命令到能获取数据的时延、tRCD 代表开启一行的时延,随即读最长时延大概在30ns左右[9],这导致即使是 3D XPoint 也无法满足要求,所以Intel在DDR4的物理接口上实现了一套 DDR-T 的 Protocol,来实现异步的内存获取。大概的意思就是当 iMC 需要读取一个地址的时候,先通知 AEP 要读取的地址,然后 AEP 就进行处理,处理后将结果放到 DQ-buffer 上,再通知 iMC 数据获取完成,iMC 获取数据。
当然目前这个协议是Intel独有的,不过后面有很多类似标准正在制定中。
ADR/Interleaving
左图是是 AEP 的 ADR 示意图,其中也包括了 AEP 内部的一些框图,注意上面好像提到了 DQbuffer,跟 AEP 里面这个 Controller buffer(一般叫做XPBuffer)是不同的(为了不搞混,最好现在就把DQ buffer忘记就可以了),ADR全称 Asynchronous DRAM Refresh,可以直接理解为掉电保护,意味着只要数据到达了iMC,就保证掉电不会丢了,那么如何保证数据进入 iMC 呢,最上面的架构图中可以看到,只要数据从 memory cahce 中出去,或者直接 NT store 写入后,数据就到达 iMC 了,整体的结构可以看下面这幅图。
这个 Custom Power fail protected domain,就是eADR,200系列AEP才能支持
对了,同样的 WPQ (在 iMC中的)与 store buffer (核心里) 不要弄混了(同样为了不搞混,最好把store buffer忘记)。
Intel 为了利用多根AEP,在iMC中实现了 interleave(4KiB),看上上副图右边,这样就增加了吞吐,降低了时延
时延
intel 给出的空闲读时延读时延是 170ns,随机读时延大概在320ns 2,与下面测量的实际读时延相符。
AEP 内部有个小型的 buffer 称之为 xp-buffer cache-line size 是256B,(上面提到了 256B 的原因可能是 AEP 内部为了做 ECC),实际上是以 256B 的粒度存储在介质中的,XP-buffer 大小应该是16KiB[5]
如果一个读请求 hit 了xpbuffer,时延会降至 150ns 左右,否则是 350ns 左右 [1]
由于 ADR 的存在,一个写请求到达 iMC 的时候就可以认为是持久化了,具体说来就是到达 iMC 中的 WPQ,如果WPQ 中有空闲位置,一个写请求到达这里 WPQ 就立即返回了,所以此时写时延基本就等同于内存的写时延。为了测量真正的写时延,文章中使用的办法是让各个内存层次(memory hierarchy)的都满载,这样有的写入需要等待WPQ中有空闲位置的时候才能写入进去(因为这时所有内存层次的出口,相当于多个水池都满了才能获取最下面那个池子的放水速度),而等待的时间大概就等于的写入时延,写时延大概在 1200ns [1](文章中没提写入的 pattern 大小,这个数值只能参考一下)
对于WPQ有空的时候,aep 写就跟内存写时延基本一样了,nt指令写大概90ns,clwb指令大概60ns(均为8B)。
由于 WPQ 的存在, 当 WPQ 满载的时候不可避免的会出现 Tail-Latency,可以看到当 hotspot 变大99.999%的值有提升(时延降低),可能就是 region 太小导致数据关联(后面的写入需要等前面的写入完成,这个等待可能在IMC中,也可能在AEP的controller中)。注意这个是极限情况下情况,可以发现即使是99.99%,也是是小于0.5us的。
吞吐
根据上面的介绍,可以发现 AEP 实际上还是与内存不同,内存是你咋访问时延吞吐都是差不多的,AEP实际上是一个相对复杂的系统,所以顺序的各种性能都高于随机的,并且由于 256B 的介质粒度,小于 256B 的读会造成读放大,写会造成 COW (copy on write),大于256的情况下,顺序和随机其实性能差不太多,单根写2.2GiB 读6GiB,图中NI是 non-interleaving,也就是单根的情况,最右边的凹下去的那块是饥饿造成的最后优化中会谈论一下。
使用方式
AEP使用方式可以大概分成两类,当然也可以两类混用
Far-Memory (Memory Mode)
一类是不需要感知的,称为 far-memory,将 pmem 当做易失性内存使用,dram此时作为pmem的cache
使用后就是内存空间变大了,程序不用改动,
App-Direct
另一类是感知的,称为 app-direct
app-direct 需要使用方适配 AEP,AEP 内存通过 ndctl 映射为 字符设备或者块设备。直接使用映射后的字符设备就是 dev-dax(就类似与用裸盘),块设备则需要搭配支持 dax 的文件系统(比如ext4)通过 mmap 的方式直接访问 AEP,这种方式就称之为 fs-dax。
如果是不支持DAX的文件系统(就是普通的mmap), mamp 系统调用后访问 memory-mapped file,实际上发生的还是 4KB 页大小的 block I/O(实际上还是走的block跟filesystem layer)。
DAX 的机制则是允许直接把用户空间的虚地址直接通过 IOMMU 映射到字符设备或者文件上(需要设备 dax 或者文件系统 dax,dev-dax or fsdax的支持)。当一个 DAX 映射的地址空间触发了缺页中断,此时会调用 dax 的 fault handler function 进行处理。
结构见下图
可以结合内核具体如何处理一个 DAX 文件系统中 mmap 之后的一个缺页中断来了解整个过程(最终虚拟地址获得了PFN被装载进IOMMU),同时了解一下各个mm、fs、driver之间是如何交互的。
具体使用
工具及概念
当前可以使用的工具主要是以下两个
ndctl
这个工具是用来管理内核的 LIBNVDIMM 驱动的,设计上是厂商无关的,因为目前NVDIMM的相关标准已经进入 ACPI(ACPI v6.0 NFIT NVDIMM Firmware Interface Table),厂商会根据标准提供相应的寄存器或者函数地址。
主要功能是以下几个
- 提供容量管理(创建 namespace)
- 枚举设备
- 开启关闭 NVDIMM、Regions、Namespace
- 管理 NVDIMM Labels
ipmctl
这个工具是 intel 提供的 pm 开源管理工具,只支持自家的 intel optane dcpmm,主要功能以下几个
- 发现、查看信息
- 提供平台内存配置(创建 region)
- 查看升级固件
- 数据安全配置
- 性能监控 健康监控 debug相关(主要是硬件相关的debug功能,如dump 固件,dump NFIT,错误注入)
从上面两个工具中可以看出主要的概念有 region、namespace,关系见下图
region 需要使用 ipmctl 进行创建,创建后使用 ndctl 再其之上创建 namespace
region 分为三中模式,一个 socket 下的 DCPMM 可以分成若干个下述三种 region(也就是可以混搭)
- far memory:创建dram做cache的aep区域
- interleaved app-direct:从一个socket下的dcpmm下创建一块用于appdirect的interleave区域
- non-interleaved app-direct:
region 为上层的 libnvdimm(ndctl) 中的提供了 DPA(DIMM Physical Address),在 ndctl 的概念中,region 是指的 ipmctl 中分出来的非 far-memory类型的的 region,使用 ndctl 创建 namespace,可以是下面四种类型之一
- fsdax:默认的类型,创建一个块设备(/dev/pmemX[.Y]),需要搭配支持dax的文件系统
- devdax:字符型的设备(/dev/daxX.Y),也支持使用DAX模式(mmap之后读写),但是不能创建文件系统,一般用于超大块分配,或者虚拟机、或者rdma
- sector:legacymode,提供了atomic的block模式
- raw:就当内存盘使用不支持dax
实际操作
$ ipmctl show -dimm
DimmID | Capacity | LockState | HealthState | FWVersion
===============================================================
0x0001 | 126.422 GiB | Disabled | Healthy | xxxxxxxxxxxxx
0x0011 | 126.422 GiB | Disabled | Healthy |
0x0101 | 126.422 GiB | Disabled | Healthy |
0x0111 | 126.422 GiB | Disabled | Healthy |
0x1001 | 126.422 GiB | Disabled | Healthy |
0x1011 | 126.422 GiB | Disabled | Healthy |
0x1101 | 126.422 GiB | Disabled | Healthy |
0x1111 | 126.422 GiB | Disabled | Healthy |
# 创建 region
$ ipmctl create -goal PersistentMemoryType=AppDirect
$ ipmctl show -region
SocketID | ISetID | PersistentMemoryType | Capacity | FreeCapacity | HealthState
=================================================================================================
0x0000 | 0x541bc3d08abd8888 | AppDirect | 504.000 GiB | 0.000 GiB | Healthy
0x0001 | 0x816fc3d0ccc78888 | AppDirect | 504.000 GiB | 0.000 GiB | Healthy
# 创建 namespace
$ ndctl create-namespace -r region0
# 查看 创建的
$ ndctl list
[
{
"dev":"namespace1.0",
"mode":"fsdax",
"map":"dev",
"size":532708065280,
"uuid":"4416b22a-a683-4dca-92a3-a61d3f197da0",
"sector_size":512,
"align":2097152,
"blockdev":"pmem1"
},
{
"dev":"namespace0.0",
"mode":"fsdax",
"map":"dev",
"size":532708065280,
"uuid":"59fa0dc1-e8ed-4395-b975-a14c8ae7d123",
"sector_size":512,
"align":2097152,
"blockdev":"pmem0"
}
]
# 分区挂载
$ mkfs -t ext4 /dev/pmem0
$ mount -t ext4 -o dax /dev/pmem0 /mnt/aep0
$ lsblk
pmem0 259:1 0 496.1G 0 disk /mnt/aep0
PMDK
PMDK 是 Intel 推出的 AEP 套件,提供了一大堆开源的工具方便用户使用AEP,按照用户使用AEP的目的,可以大概把PMDK提供的库(工具)分成两类
- Volatie Libraries,主要面向那些需要把 AEP 当成内存使用的,比如说提供了类似 tcmalloc 这种形式的分配器库,直接把内存分配在 aep 上
- Persistent Libraries,面向想要使用AEP持久化特性的用户。
对于第二个目的的各种库,主要就是在 fs-dax 模式下提供了很多内置的数据结构,是在比较高层的地方提供的,怎么说呢,上面也看到只要使用 fs-dax 的文件系统,直接 mmap 之后就可以访问 AEP 了,没有什么需要特别的驱动。 PMDK 主要 还是基于 fs-dax 做了一层抽象把常用的操作进行了打包,提供了很多方便使用的抽象
// 这类似与实际的代码 调用 instrict.h 内联汇编
// 数据留在cache中
memcpy(dst, buf, len);
_mm_sfence();
_mm_clwb(dst); // 可能需要多次
// 数据不留在cache中
_mm256_stream_pd(dst,data); // nt 需要多次
_mm_sfence();
//-------------------------
// 如果使用 libpmem,与上面的等价
pmem_memcpy_persistent(dst, buf, len); // 这个是clwb的
pmem_memcpy_nodrain(dst,buf, len); // 这个是ntstore的
pmem_drain();
各个包简介
Persistent lib:
libpmem
像是上面提到的,提供了下面几个接口
libpmem2
这个跟上上面的libpmem差不多,主要是计划更好的支持跨平台以及eADR,所以把整个库的 API 设计的更抽象了,比如首先需要使用函数把操作用的函数拿到(可能是为了更好的跨平台),并且支持了两种 GRANULARITY,一个是
PMEM2_GRANULARITY_CACHE_LINE
andPMEM2_GRANULARITY_BYTE
,用 cache line 级别的时候persist 的行为就是调用 CLWB CLFLUSHOP 之类的刷回去(前面会跟 fence),而如果用 byte 级别的时候,persist 就只需要fence 一下就可以了。libpmemblk
这个库提供了一个定长数组的概念,总长度,以及各个元素的长度是在创建时就确定的, 对于每一个其中的元素(用LBA号索引),其操作是原子的,实现大概是有一个转换表,映射LBA号到文件中的地址,当更新完之后修改LBA号到新的地址上。
libpmemlog
提供了一个 log 的接口,可以进行 append 跟 walk,是用了pool的结构,其中每次append 修改一下header头,所以性能实际上可能不太好
libpmemobj
这个是库是一堆宏,你用这个宏去定义你的struct,就相当于java中给成员搞了了setter,然后提供了transaction的功能,启动trans后,会记录对对象的操作(log),commit的时候apply操作,大概这样子,有一个c++的wrapper,不过个人感觉比较难用,因为有 update 操作性能较差,感兴趣可以看看[7],这个比pmdk 中这个 obj 快50倍。
libpmemset
不知道干啥的
librpma(librpmem)
rdma相关,旧的librpmem 这个已经废弃了,现在pmdk的主要开发人员投入到新的librpma项目上了,后面会单独讲一下rpma
libpmemkv
提供了kv的接口,实现在obj之上的,有多种引擎满足多种kv特性(大部分都是inplace的结构),最下面有文章中测试写入吞吐大概2GiB,主要还是结构是非log的,update拖慢了性能
Volatie Lib:具体有使用过不过应该业界投产的比较多,比如直接集成redis
libvmmalloc
跟jemalloc的差不多只要link进去就直接把所有的内存上的malloc转换到pmem上去了
libvmem(libmemkind)
libvmem已经不推荐使用了整合进memkind了,这个库主要是提供了 malloc free 的语义的函数,在jemalloc上进行了修改,并且对pmem进行了优化,比如numa之类的
libvmemcache
LRU cache
Debug
Valgrind
intel搞了个插件可以配合上去使用,具体参考pm书
pintool
也是intel的工具可以hook指令,hook指令后可以记录每个load store指令访问的位置。
比如想要测试atomic的时候,可以记录每个store执行的写地址,模拟掉电的时候随机把fence之前的store指令的内容抹去。使用方需要单独写逻辑,然后用编译之后的程序加载要运行的程序
pcm-monitor
非常好用,可以监控各个内存channel的流量(包括ddr-t的流量 ddr流量),以及hit-rate
大概这样
|---------------------------------------||---------------------------------------| |-- Socket 0 --||-- Socket 1 --| |---------------------------------------||---------------------------------------| |-- Memory Channel Monitoring --||-- Memory Channel Monitoring --| |---------------------------------------||---------------------------------------| |-- Mem Ch 0: Reads (MB/s): 15.07 --||-- Mem Ch 0: Reads (MB/s): 6.87 --| |-- Writes(MB/s): 13.85 --||-- Writes(MB/s): 3.69 --| |-- DDR-T Reads(MB/s) : 0.00 --||-- DDR-T Reads(MB/s) : 0.03 --| |-- DDR-T Writes(MB/s): 0.00 --||-- DDR-T Writes(MB/s): 0.06 --| |-- Mem Ch 1: Reads (MB/s): 9.16 --||-- Mem Ch 1: Reads (MB/s): 6.83 --| |-- Writes(MB/s): 6.97 --||-- Writes(MB/s): 3.62 --| |-- DDR-T Reads(MB/s) : 0.00 --||-- DDR-T Reads(MB/s) : 0.03 --| |-- DDR-T Writes(MB/s): 0.00 --||-- DDR-T Writes(MB/s): 0.06 --| |-- Mem Ch 2: Reads (MB/s): 12.39 --||-- Mem Ch 2: Reads (MB/s): 6.90 --| |-- Writes(MB/s): 10.00 --||-- Writes(MB/s): 3.69 --| |-- DDR-T Reads(MB/s) : 0.00 --||-- DDR-T Reads(MB/s) : 0.00 --| |-- DDR-T Writes(MB/s): 0.00 --||-- DDR-T Writes(MB/s): 0.00 --| |-- Mem Ch 3: Reads (MB/s): 9.22 --||-- Mem Ch 3: Reads (MB/s): 6.71 --| |-- Writes(MB/s): 7.24 --||-- Writes(MB/s): 3.60 --| |-- DDR-T Reads(MB/s) : 0.00 --||-- DDR-T Reads(MB/s) : 0.03 --| |-- DDR-T Writes(MB/s): 0.00 --||-- DDR-T Writes(MB/s): 0.06 --| |-- Mem Ch 4: Reads (MB/s): 8.63 --||-- Mem Ch 4: Reads (MB/s): 6.66 --| |-- Writes(MB/s): 6.78 --||-- Writes(MB/s): 3.55 --| |-- DDR-T Reads(MB/s) : 0.00 --||-- DDR-T Reads(MB/s) : 0.03 --| |-- DDR-T Writes(MB/s): 0.00 --||-- DDR-T Writes(MB/s): 0.06 --| |-- Mem Ch 5: Reads (MB/s): 11.40 --||-- Mem Ch 5: Reads (MB/s): 6.57 --| |-- Writes(MB/s): 9.98 --||-- Writes(MB/s): 3.48 --| |-- DDR-T Reads(MB/s) : 0.00 --||-- DDR-T Reads(MB/s) : 0.00 --| |-- DDR-T Writes(MB/s): 0.00 --||-- DDR-T Writes(MB/s): 0.00 --| |-- NODE 0 Mem Read (MB/s) : 65.86 --||-- NODE 1 Mem Read (MB/s) : 40.55 --| |-- NODE 0 Mem Write(MB/s) : 54.82 --||-- NODE 1 Mem Write(MB/s) : 21.63 --| |-- NODE 0 DDR-T Read (MB/s): 0.00 --||-- NODE 1 DDR-T Read (MB/s): 0.12 --| |-- NODE 0 DDR-T Write(MB/s): 0.00 --||-- NODE 1 DDR-T Write(MB/s): 0.24 --| |-- NODE 0.0 2LM read hit rate: 1.02 --||-- NODE 1.0 2LM read hit rate: 0.95 --| |-- NODE 0.1 2LM read hit rate: 1.01 --||-- NODE 1.1 2LM read hit rate: 0.95 --| |-- NODE 0 Memory (MB/s): 120.69 --||-- NODE 1 Memory (MB/s): 62.54 --| |---------------------------------------||---------------------------------------| |---------------------------------------||---------------------------------------| |-- System Read Throughput(MB/s): 106.54 --| |-- System Write Throughput(MB/s): 76.69 --| |-- System Memory Throughput(MB/s): 183.22 --| |---------------------------------------||---------------------------------------|
杂记&心得
With RDMA
https://downloads.openfabrics.org/WorkGroups/ofiwg/remote persistent memory/RPMEM 2.0 Public.pdf
RDMA
直接内存访问(DMA)允许数据不经过CPU进行传输,数据传输的工作 off-loaded 给了硬件DMA引擎,绕开内核,使得CPU可以处理更加重要的工作,同时DMA技术也使得小包传输的时延大大降低。
RDMA 主要用的接口是 verb,verb接口主要的操作分成两个类型
on side operation
WRITE
跟READ
直接操作另一台机器的内存,此时不需要另一台机器的CPU介入,配合无损网络可以做到不用ACKtwo side operation
SEND
跟RECV
类似于 socket,需要另一方CPU的介入。
为什么AEP结合RDMA是个好主意
对于分发写的协议,跨机器做Replication需要拷贝数据到别的机器,传统方式数据就从NIC 进来到了CPU,然后CPU再到内核,最后到介质,即使是使用了DPDK跟SPDK,实际上还是要另一台机器的CPU介入,而AEP的到来给了整个Replication不介入接收方CPU的希望(实际上这不是个新主意 微软FaRM)。
怎么做
DDIO 是啥
当DDIO打开的时候,对于DMA而言相当于在 L3 上给开了一块Buffer,DMA直接写到了L3上,对于绝大多数应用来说,这是好的,因为接下来 CPU 马上就要使用这些数据,但是对于现在的AEP来讲,ADR并不能覆盖L3,所以直接使用 RDMA 写 AEP的内存地址是不能持久化的。
但是,DDIO是可以关闭的(这个选项叫做 Non-Allocating Write 在BIOS),这个关闭的范围是 PCIe-root complex 级别的,就是 CPU 中的 PCIe控制器关联的所有设备都会收到影响。
那么 rpma的方案就有以下两种
- 关闭 DDIO,这个可能会导致机器其他应用的性能降低,但是可以直接使用RDMA持久化one-side写
- 开启 DDIO,若干次one-side 写之后使用send通知接收者把cache刷回内存
http://janekmi.github.io/2020/06/21/ddio.html
别慌,还是有解决方法的,这个人(PMDK核心开发)发现可以直接修改 CPU 的 CSR 从而强制某一个port 的写操作全部转换为non-allocating写,也就相当于是在这个设备上关闭了DDIO
具体操作需要先找到网卡的pci地址,然后手动修改CSR,对于专用的服务器,即使有性能下降也是可以接受的,下面摘自文章中
# as root
# 1. identify an address of the PCIe Root Port
$ export PCIe_Root_Port=0000:17:00.0
# 2. read the current value of the PERFCTRLSTS_0 register
$ setpci -s $PCIe_Root_Port 180.b
91
# 3. turn off the Use_Allocating_Flow_Wr bit
$ setpci -s $PCIe_Root_Port 180.b=11
# 4. verify the result
$ setpci -s $PCIe_Root_Port 180.b
11
这些东西在librpma中都有
优化
我认为需要注意的点
numa
numa是很重要的,写入不同socket的时候,AEP下降非常明显,比如numa内随机写,四根8GiB左右,跨socket顶多1GiB左右(3根aep测下来700mib)。
这个图片[5]也可以看到,跨numa 内存应用性能下降有限,而pmem应用直接砍没了
所以,对于要使用AEP的应用,可能需要考虑分socket部署
线程相关
最好是一个 thread 操作一块专用的 AEP 区域,可以减小cache-line频繁invalid,并且一个thread 处理一个区域可以保证 xpbuffer 里面的数据不会被频繁写回。这大概可以提供 5X 以上的性能提升。对每个AEP的访问的thread数目也需要限制一下,太多了会导致iMC中的竞争。
注意 interleave
interleave 粒度是4KiB,如果每个线程都写4KiB 粒度,就相当于按着一个AEP写,其他thread就饿死了,也就造成了上面吞吐的图中的下凹
避免同一个地址频繁写(cacheline write)
刚才提到了一个地址小于写会导致COW,但是当吞吐不大的时候cow不是什么问题,因为cache在xpbuffer中了,不会被频繁写回。但是对于一个cacheline的clwb是非常慢的[6],(todo NT store + fence 可能没这个问题,可以后面测试一下)
对于大粒度的写 用nt store 别用 clwb
clwb先写了cache再把cache写回iMC,本来指令就多一倍,并且因为clwb cache-line还在cache中,一会被evict的时候(虽然clwb保留,但是可能mem压力大就给evic了),估计会向AEP发什么啥的,影响写的AEP的操作。大粒度的写就可以上avx(nt)指令了
最好还是顺序
因为AEP还是一个存储系统,有介质有cache,所以顺序还是对性能有理的,比如说xpbuffer 中evict的数量会少,并且有的文章提出AEP中还有一个xp-prefetcher,这个存疑,因为顺序读没看出比随机读好特别多。
上面列举了很多AEP的使用的考量,同时下面是我认为比较有价值的地方,设计时可以着重考虑
- 256 byte的随机写 顺序写吞吐差不多,虽说64Byte的随机写性能较差,但是256Byte的随机写,吞吐基本能跑满2GiB 一根
- byte-addressable 这个是可以配合 RDMA 直接把kernel以及接收方的cpu使用给去掉
个人感觉,对于绝大多数设计不是特别针对AEP的算法,如果适配到AEP上,最好不要在线 update AEP 中的数据结构(比如说搞一个在AEP中的树之类的),直接使用 log 就可以了(log 写 AEP,数据结构还是在内存中),构建的时候顺序读 log,带宽肯定不是瓶颈,如果CPU处理起来实在跟不上,需要做一些snapshot。当然前提是内存放得下。
竞赛
阿里之前搞了个AEP竞赛三人一组分初赛复赛两轮,就不说初赛了,复赛要求简述如下
详细
初赛评测逻辑 评测程序会调用参赛选手的接口,启动16个线程进行写入和读取数据,最终统计读写完指定数目所用的时间,按照使用时间的从低到高排名。 评测分为2个阶段: 1)程序正确性验证,验证数据读写的正确性(复赛会增加持久化能力的验证),这部分的耗时不计入运行时间的统计 2)初赛性能评测 引擎使用的内存和持久化内存限制在 4G Dram和 74G Aep。 每个线程分别写入约48M个Key大小为16Bytes,Value大小为80Bytes的KV对象,接着以95:5的读写比例访问调用48M次。其中95%的读访问具有热点的特征,大部分的读访问集中在少量的Key上面。 复赛评测逻辑 复赛要求实现一个可持久化的高性能数据库,引擎使用的内存和持久化内存限制在8G Dram和 64G Aep,复赛要求数据具有持久化和可恢复(Crash-Recover)的能力,确保在接口调用后,即使掉电的时候依然能保证数据的完整性和正确恢复。 复赛评测分为三个阶段 1)正确性评测 验证数据读写的正确性,提供运行日志。这部分的耗时不计入运行时间的统计。 本阶段会开启16个线程并发写入一定量Key大小为16Bytes,Value大小范围为80-1024Bytes的KV对象,并验证读取和更新后的正确性。 2)持久化评测 评测程序会使用工具记录写操作与持久化操作,并在随机时刻模拟掉电情形。选手需保证已写入的数据在恢复后不受影响。 本环节不提供日志,提供评测程序中给出的部分日志。因此评测程序会在比赛开始前放出。 为评测的公平性考虑,评测程序的随机KV生成部分代码不给出。 在模拟的持久化内存设备上运行评测程序的结果未知,因此本地运行结果仅供参考,一切以评测机结果为准。 本环节时长90s160s,如果超时,希望选手优化自己的回复部分实现。 3)性能评测 本阶段首先会开启16个线程并发调用24M次Set操作,写入Key大小为16Bytes,Value大小范围为80-1024Bytes的KV对象,并选择性读取验证;接着会进行10次读写混合测试,取最慢一次的结果作为成绩,每次会开启16个线程以75%:25%的读写比例调用24M次。其中75%的读访问具有热点的特征,大部分的读访问集中在少量的Key上面。最后的分数为纯写入操作的耗时与最慢一次读写混合操作耗时的和。 本阶段提供运行日志,并会覆盖正确性评测的日志。 数据安排如下 本阶段保证任意时刻数据的value部分长度和不超过50G。 纯写入的24M次操作中 大约55%的操作Value长度在80-128Bytes之间; 大约25%的操作Value长度在129-256Bytes之间; 大约15%的操作Value长度在257-512Bytes之间; 大约5%的操作Value长度在513-1024Bytes之间; 总体数据写入量大约在75G左右。 读写混合的24M操作中,所有Set操作的Value长度均不超过128Bytes。
- 提供一个kv服务,要求kv操作有原子性,只有set get,没有remove
- 64G存储,写入量75G(需要GC)
- key size 定长16Byte,value长度从128~1024不等
- 测试是先先写100GiB左右,然后运行10次读写混合测试
前十名的成绩基本都在40s以内,第一名大概25多s的写入时间折算下来大概1500w 写入qps(这是持久化的写,就是每次Set之后都可以保证断电后Set的结果还在)
我最后获得了第25名,耗时大概120s,其中问题最大就是GC跟写入没有做thread-local, GC跟写入的时候一定要充分考虑thread-local,每个线程至对自己负责的区域做写入或者回收,基本上log结构配合thread-local不需要太多优化就可以到45s左右,剩下的就是如何去优化index,或是使用更优化的指令。
QA
参考资料
[1] System measurement of Intel AEP Optane DIMM https://arxiv.org/pdf/2009.14469.pdf
[2] Intel 64 and IA-32 ArchitecturesOptimization Reference ManualOrder Number: 248966-042bSeptember 2019
[3] Yu, S., & Chen, P.-Y. (2016). Emerging Memory Technologies: Recent Trends and Prospects.
[4] https://www.youtube.com/watch?v=BShO6h8Lc1s Intel Optane DC Persistent Memory Architecture Overview
[5] yang 2020 An Empirical Guide to the Behavior and Use of Scalable Persistent Memory
[6]FlatStore: An Efficient Log-Structured Key-Value Storage Engine for Persistent Memory
[7]ArchTM: Architecture-Aware, High Performance Transaction for Persistent Memory
[8]Understanding the Idiosyncrasies of Real Persistent Memory
[9]NVDIMM-C: A Byte-Addressable Non-Volatile Memory Module for Compatibility with Standard DDR Memory Interfaces
内核态的dirver如何工作
https://www.kernel.org/doc/html/latest/driver-api/nvdimm/nvdimm.html
order相关
http://materials.dagstuhl.de/files/15/15021/15021.MichaelSwift1.Slides.pdf
使用手册
overview
https://www.suse.com/media/presentation/SPO1422_Persistent_Memory_in_Operation.pdf
nt implementation
https://stackoverflow.com/questions/37070/what-is-the-meaning-of-non-temporal-memory-accesses-in-x86
nt order
ddr-t细节|Samsung的compatible protocol
osu 的分析aep特性的文章