ArchTM: Architecture-Aware, High Performance Transaction for Persistent Memory
ArchTM is a variant of copy-on-write (CoW) system to reduce write traffic to PM.
Introduction
现有的保证数据crash一致性的方法多是基于logging的(问题是双重写入)和基于CoW的(问题是metadata更新会带来很多的微小写入,导致CoW更新的开销太大)。
一些优化PM事务的思路:
- 减少data copying;
- 减少持久化开销;
但是这些思路并未考虑PM的微体系结构,而且这对PM的表现还是有非常大(3x-58x)的影响。
所以综合PM的微体系结构,提出了两个设计要点;
- 避免256bytes(Optane PM write block)以下的写入;
- 鼓励合并写,特别是顺序写;
所以ArchTM也遵循这些设计要点,而且是CoW的变体,目的是减少重复写。为了减少微小写入,ArchTM把metadata的内存管理和数据存储放在DRAM上,虽然会有很高的性能,但是还是会带来crash一致性的问题。这个问题的本质是metadata是事务状态和数据对象(文件)之间唯一的关联。ArchTM引入了一个注释机制来解决这个问题。这个注释机制的基本设计是:把数据对象元数据和TxID放到数据对象中,并且把TxID放到事务元数据中。这里的TxID是一个持久的,并不是动态分配的,而且可以起到连接数据对象和事务状态的作用。所以通过TxID,数据对象ID和数据大小,就可以很容易的定义数据对象,并且检测一致性。
为了鼓励顺序合并写,ArchTM尽可能保证连续的内存分配,因为根据观察,连续分配的数据对象很可能一起更新,但是目前的内存分配策略是管理一系列free list,每个list保存一定的连续内存空间,这样的分配可以避免大量的内存碎片,因此这是一个内存碎片和连续空间之间的tradeoff。所以ArchTM只使用一个free list,同时实现了一个尽可能避免碎片的机制。对于内存分配,使用单一的free list和一个recycle list来进行收集和合并空闲内存快。对于避免碎片,原文说的是:
For defragmentation, ArchTM aggregates data objects in highly fragmented memory regions to create large and contiguous memory blocks.
也不知道是怎么"aggregates data objects"的。。。
总结一下这篇文章的贡献:
- 解释PM的微体系结构与PM的事务表现有着非常大的关系;
- 提出了两个设计要点以及两个tradeoff,并设计了一个CoW的变体:ArchTM;
- 比较了一下,很牛。。。
Background
PM事务
主要两种范式:logging和CoW。
PM事务中的内存管理
目前比较优秀的PM内存分配器都是维护thread-local的free lists和全局的free lists。对于分配请求,现在local free lists上尝试,不行再转发到global free lists上,对于回收请求,空闲的内存块先挂到local free lists上,避免全局free lists出现竞态条件。在PM上的内存碎片的影响要比易失内存上严重,因为他的影响一直存在,此外PM分配器还需要保证metadata保持一致性。
PM架构
PM与CPU之间的数据交换粒度是64B(cacheline size),但是Optane的内部事务块大小是256B的,因此一个cacheline的更新会导致256B的写入(写放大),NVDIMM中存在一个16KB的buffer,来整合顺序写入。如果CPU的连续写入是连续的256B块,那么多个写入可以整合成一个事务。
Performance Characterization
Transaction Performance Study
四种事务系统:
- PMDK - undo logging
- Romulus - redo logging
- DudeTM - redo logging
- Oracle - CoW
一个事务中的写操作包括持久化数据,日志(如果用了)以及更新metadata。而且根据实验发现,metadata更新是small writes的主要开销。总的来说,事务系统分为四种类型的metadata:
- 事务运行时的metadata:比如事务状态(TxID、事务状态比如commit,prepare等等)
- 内存分配的metadata:关于内存消费
- Log metadata:log index
- metadata for persistent objects:保存持久性对象的指针。
因此容易得出,CoW的系统更容易带来小的写入,因为异地更新,CoW的每次更新都会复制一份数据,remapping pointer并且回收旧数据,因此会带来大量的小写入。
Performance Study of PM Writes
我们知道,每个write都会包括一个或者多个cacheline的刷回。而且基于PM和DRAM之间的读写性能差异,我们希望尽可能减少PM的写,这对提高PM事务的性能非常重要。
一些PM的设备特性:
- 顺序写优于随机写(因为Optane的粒度大,会导致write amplification),顺序写对应CoW,随机写对应Logging;
- 在写大小等于PM内部的写粒度的倍数时,写带宽会出现一个峰值,是因为PM内部存在一个write combining buffer,这个buffer就是合并64B的写入组成一个256B的写入。就是说,如果我们想要利用write combining buffer,需要尽可能的顺序写。
总结
总结一下,这一节就说了两个事:
- 小的写入会对PM事务的表现产生影响,而小的写入的主要来源是metadata的更新
- PM的顺序写要优于随机写,而且为了利用PM内部的硬件,需要尽可能的用顺序写,避免随机写。
Design Principles and Major Techniques
总的来说,有两大设计原则和5个主要技术。下面分别介绍:
- 避免小写入
-
Logless:使用CoW
-
最小化PM上的metadata修改,并保证crash consistency
在DRAM上保存metadata,防止metadata在PM上的频繁修改;同时使用一个注释机制关联PM事务状态和持久化数据对象,使得ArchTM可以检测PM上的数据一致性并且可以自动恢复。
- Scalable persistent object referencing:ArchTM在DRAM上使用一个scalable object lookup table快速定位PM数据对象的最新副本。
- 鼓励顺序合并写
-
连续的分配请求会分配连续的内存块。
-
减少内存碎片。实现了一个GC线程整理内存碎片。
logless
使用CoW来减少PM上的写流量,CoW在commit改变的时候,连续写入连续的内存地址增加了combining buffer的使用概率。但是如果在PM上直接使用CoW会导致额外的metadata更新、remapping以及额外的配置管理。所以把metadata存储在DRAM上。
minimize metadata modification on PM
ArchTM使用一个scalable lookup table查询持久数据的最新修改。但是如果把metadata存在DRAM上,会在把旧的pointer改成新的pointer的时候会出现崩溃一致性的问题,所以,ArchTM使用一个注释机制保证数据一致性。
注释机制:就是在事务的metadata(事务的状态等)中加入一个事务ID。当一个事务状态改为start的时候,这个事务ID立即持久化。同时ArchTM对每个数据对象也进行注释,最重要的是添加TxID以及数据大小,这两种信息可以用来检测数据一致性,而且ArchTM使用TxID来检查这个持久化数据最新的copy、回收那些旧数据以及放弃那些未生效的修改。
scalable object referencing
直接看数据结构部分吧。。。文字比较不好理解。。。
Contiguous Memory Allocations
ArchTM自定义了内存分配以及PM上的事务负载。ArchTM中对大内存的分配使用常规的分配方式(JEMalloc等),对于小内存的分配,使用单一free list和全局的回收进程来优化。
- single free list
多个free list会导致连续的内存分配请求分配到不同位置,丧失了连续性,为了增加并发性,ArchTM为每个线程分配了free list不同的使用区域,为了减少竞争条件。
- recycle and merge memory blocks globally
现在的回收方式是每个thread直接回收空闲的内存块,之后同步到global的free list上,这样对空闲内存块的一致性有损害,会导致空闲内存块的地址不连续,所以ArchTM实现了一个helper线程,整理每个thread中的空闲内存块,排序之后再同步到global的free list上。虽然涉及大量的排序等工作,但是这个操作并不在内存回收的关键路径上,因此不会影响内存回收的效率。
Reduce Memory Fragmentation
single free list的局限在于大量的内存碎片,ArchTM通过一个用户态的线上回收机制来减少内存碎片。
Implementation
Data Structures
Persistent Data Structures on PM
-
root object:
-
Tx State variables:
记录全部正在进行的事务的状态。TxID是事务开始的硬件时间戳,CommitID是事务结束的硬件时间戳。事务的状态分为BEGIN, COMMITTED, END or ABORT.
- checkpoint field:
stores a persistent checkpoint of the object lookup table to speedup recovery.
- checkpoint-diff field:
stores the list of memory blocks preallocated to each thread. 通过一个数组实现,数组元素由以下三个部分组成:使用这个segment的正在进行的事务的TxID,segment的起始地址以及segment大小。
- user data area
每个object都有一个header,header中包括:objID, size和TxID。
Transient Data Structures on DRAM
- object lookup table & write-set(Tx-private):
ArchTM维护一个object lookup table和一个write set(Tx-private)。
一个object lookup table entry包括:object的old和new指针,修改new指向的object的事务对应的TxState(writer)以及一个write lock。
一个write-set(Tx-private)记录了一个正在进行的事务修改的全部objID(objID是object lookup table的index)。事务提交之前,write-set中记录的objIDs对应的object都必须要持久化。
- two allocators
第一个allocator在创建object时为object分配object lookup table中的一个entry。allocator维护一个free id的list(global)。分配的时候先看free id list(thread-local)中是否存在id,如果存在就重复使用free id(thread-local),如果不存在再从global free id list中新分配ids。
第二个allocator分配持久objects。对于那些常规的分配,重用了JEMalloc中metadata的结构;对于那些小的写入的分配(small writes),ArchTM维护了一个global free list(memory block)和一个global recycle list,为了防止争用,每个thread分别使用global free list的一部分。而global recycle list管理全部threads的空闲memory blocks,而且allocator中维护了每个thread的allocation和deallocation list,每个thread的空闲的memory blocks被整合进global recycle list中。
详细的memory allocation在后文详述。
Background Threads
后台线程对应用层透明。
- GC manager (for CoW)
GC manager 回收PM objects。
- fragmentation manager
检验内存区域的使用以及内存块的聚合。(5.4)
Transaction Operations
ArchTM实现了五种关键的操作:begin, read, write, commit & postcommit。
begin的伪代码:
function APT_TX_BEGIN
volatile TxID = GLOBALTIMESTAMP()
TxState.ATOMIC_STORE(TxID, BEGIN)
Fence()
end function
语义比较清晰,说一下一些需要注意的点:
- TxState存储在PM的Metadata部分;
- TxID是全局的时间戳,通过硬件获取。
read的伪代码:
function APT_TX_READ(TxState, ObjID)
obj = objLookUpTable[objID]
if obj.new == NULL then return obj.old
end if
if obj.writer -> TxID == TxState.TxID then return obj.new
end if
if obj.writer -> State == COMMITTED and obj.writer -> CommitID <= TxState.TxID
then return obj.new
end if
return obj.old
end function
read的3个if判断:
- 如果没被修改;
- 如果被当前事务修改;
- 如果在当前事务开始之前,有其他事务提交;
write的伪代码:
function APT_TX_WRITE(TxState, objID)
obj <- objLookUpTable[objID]
if obj.new != NULL and obj.writer -> TxID == TxState.TxID then
return obj.new
end if
if LOCK(obj.writer) then
obj.writer = &TxState
obj.new = ALLOC(obj.old)
else ABORT_AND_RETRY()
end if
obj.new = DUPLICATE(obj.old)
obj.new.header.TxID = TxState.TxID
# append the object to write-set
write_set.insert(objID)
return obj.new
end function
如果已经分配了一个new copy,而且最近的修改是当前事务修改的,那么直接返回obj.new;否则分配一个新的copy,需要先拿到write lock,并且在最后把objID放到write_set中,在commit之前需要保证全部的objID对应的obj都是已经持久化的。
commit的伪代码:
function APT_TX_ON_COMMIT(TxState)
if EMPTY(write_set) then return
end if # read-only Tx
for each obj in write_set do FLUSH(obj.new)
end for Fence() # persist all modified objects
volatile CommitID = GLOBALTIMESTAMP()
TxState.ATOMIC_STORE(COMMITID, CommitID)
Fence()
APT_TX_POST_COMMIT(TxState)
end function
语义比较清晰,不再赘述。
postcommit的伪代码:
function APT_TX_POST_COMMIT(TxState)
for each tx in Ongoing_TXs do
if tx.TxID < TxState.CommitID then WAIT_FOR(tx)
end if
end for
while obj <- write_set.pop() do
FREE(obj.old)
obj.old = obj.new
obj.new = NULL
obj.writer = NULL
UNLOCK(obj.writer)
end while
TxState.ATOMIC_STORE(END, INF)
Fence()
end function
post-commit的作用是:clean up a commited transaction
首先,遍历所有正在进行的事务,如果开始时间(TxID)小于当前事务的提交时间(CommitID),那么需要等待之前的事务完成,因为不确定当前事务的old copy是否被其他事务需要。
随后,回收obj.old(到thread-local deallocation list),之后把当前的new变成old,并且unlock writer。
最后,事务状态转换成END,CommitID变成INF。
Memory Management for Transactions
ArchTM使用客制的PM allocator,对内存的分配分为:locality-aware data path和regular data path,regular data path基本是对JEMalloc的复用,本节重点介绍locality-aware的内存分配,主要包括以下三个操作:
- Allocation
首先在thread-local中的allocation free list中查找合适大小的空闲memory block;
如果thread-local找不到,就在global free list中每次fetch一个segment到thread-local;
如果还找不到,就在global recycle list中补充空闲memory block到global free list。
需要注意的是,每次的fetch操作或者补充操作都存储在CHKT-diff中,每个CHKT-diff数组元素包括:TxID,segment的起始地址(fetch from address),segment大小。
- Deallocation(GC)
回收没人使用的object。GC manager线程负责的是周期性的从thread-local deallocation lists回收到global recycle list。global recycle list是排好序的。排序的开销不大,因为释放的内存块被添加到global recycle list时,内存块基本是有序的。
- Defragmentation
defragmentation监控线程在global recycle list中每个内存区域的碎片比例(除以4KB),如果低于 f f f(本文中是50%),就认为未充分使用。ArchTM会自动聚合objects到未充分使用的内存区域,并且把他们通过一个mock write Tx迁移到一块新的内存区域,迁移完成后,原始的数据区域被deallocation。
Recovery Management
recovery分成两个步骤:
一是检测未提交的事务。把所有状态不是COMMITTED, END的TxID记录下来到一个buffer中。同时必须回收这些TxID对应的object的copy。
二是rebuild object lookup table。ArchTM检测PM上的user data area,找到持久的object并在合适的位置(objID->object lookup table index)放入他们的信息(pointers等)。通过比较TxID的大小(时间)来确定最新的object,回收旧的object。
关于crash consistency:
- 所有未提交的修改被放弃(recovery时保证);
- 所有提交的修改被持久化(事务提交时保证);
- 只有最新的object被保存(在设计上保证:更新同一object的一个事务的TxID不得早于另一个事务的CommitID,也就是说,在事务未提交之前,数据对应用是不可见的)。
基于以上三点,认为crash consistency得到保证。
Reduction of Recovery Time
increment checkpoint技术加快recovery的速度。就是把PM最新的修改放到checkpoint中,出现crash时直接从checkpoint恢复。increment checkpoint的具体设计是说在执行完一次checkpoint之后,先暂停所有的事务,并把DRAM上object lookup table中对应的页面设置为write-protection,之后继续事务,那么这个时候那些被写的页面就会触发page fault,此时checkpoint记录下这些page fault对应的页面,在下次执行checkpoint的时候,只copy这些被修改过的页面。(注意,page fault触发一次之后我们就把write-protection解除)
仅仅使用checkpoint加速是不够的,因为如果crash,那么两个checkpoint之间的修改就完全丢失了,所以使用checkpoint-diff这个区域来记录内存segment的获取,以及修改记录。
总结
- 一些频繁修改的data可以放到DRAM上,与PM分开存储;
- PM的存储架构特性对于事务的表现影响很大;
实现难点:
实现一个memory allocator并且需要区分小的写入(小于64B的)与大的写入。