Concurrent Updates to Pages with Fixed-Size Rows Using Lock-Free Algorithms
0x00 基本内容
这篇Paper讨论的是SQL Server中一个具体的优化。SQL Server中使用PageFreeSpace (PFS) 的结构来管理空闲的存储空间,每个PFS Page管理64MB大小的chunk,一般情况下更新FPS的信息要加上写类型的锁,这里可以优化为Lock-Free的方式。Paper中的例子都是以FPS的更新为例子说明这里的算法,但是可以推广到其他的使用Fixed-Size Rows的Page更新中。SQL Sever中PFS的基本结构如下,SQL Sever使用8KB大小的Page,每个PFS Page前面的几个bit是标志位,后面为记录chunk使用前面的部分,所有这里可以标记8088个page的使用情况。可以看到对应page的标记是固定的,且更新其中的一个不会干涉到另外的一个,这个为这个算法提供了一定的基础。除此之外,SQL Sever使用Index Allocation Map (IAM) Page来记录4GB大小的extent,这个extent为一个allocatation的单元,使用Global Allocation Map (GAM) Page记录依据allocated的extent的情况。
SQL Server是使用ARIES风格的WAL,会周期性地scan buffer pool将dirty page写入到磁盘上面,以及其它的一些操作,来完成checkpoint。SQL Sevrer使用DirtyPageManager管理一个数据库中的dirty page,将它们保存到两个list中,一个list保存是准备更新,后面会变长dirty的,另外一个是记录依据变长dirty的。这里的两个list估计和获取到锁之后,开始写日志到写日志完成的时间过程中使用的。Dirty Page Context(DPC)用来记录一个buffer中 checkpoint ID,也记录buffer中的为dirty状态的第一个 LSN,即用于确定一个checkpoint中最老的page。目前PFS更新的时候,会使用独占类型的latch来保存更新的互斥。在更新一个PFS Page的时候,如果这个信息已经在DPC中存在了,则不用管,否则可能需要记录为最小的LSN。
Void UpdatePage()
{
Latch the data page exclusively.
{
Compute the necessary PFS update;
If the transaction is in rollback, update the PFS page;
Generate log record for the data page;
Update the data page;
If the transaction is not in rollback, update PFS page;
}
Release the latch on the data page;
}
//
Void UpdatePFSPage()
{
Obtain PFS page corresponding to the data page;
Update latch the PFS page;
{
Prepare the PFS page to be dirtied;
Generate log record for the PFS update;
Update the byte on PFS page;
Set LSN on PFS page;
Mark PFS buffer as dirty;
}
Release the latch on the PFS page;
}
0x01 基本算法
这里的优化是将更新PFS Page的时候获取的独占类型的latch改成share latch。主要的改动是获取page的latch的时候变成了share类型的latch,另外更新对应位置的信息和LSN的时候使用interlock的方式。这样需要其它方面的一些改动才能保证正确性。一个是Buffer Pool的管理,为了支持concurrent updates scheme的方式,和之前的方式不同,这里BufferManagement会区分不同的page类型,可以使用这种方式更新的page会有一个BUF_CAN_HAVE_SHARED_UPDATES来标记。另外DirtyPageContext(DPC)一些操作不是线程安全的,这里改用interlocked compare and exchange的操作另外实现。
Void UpdatePFSPageUsingSHLatch()
{
Obtain PFS page corresponding to the data page;
Share latch the PFS Page;
{
Prepare the PFS buffer to be dirtied;
Generate log record for the PFS update;
Update byte on PFS page using interlocked operations;
Set LSNon PFS page header using interlocked operations;
Mark PFS buffer as dirty;
}
Release the latch on the PFS page;
}
另外的一个是日志和LSN相关的。在ARIES风格的WAL中,一个log record会记录和这个log record相关的前一个log record的LSN,称之为previous LSN。Recovery操作的时候,如果这个previous有问题,则会造成恢复失败。在concurrent updates的模式下面,可能存在多个log record的previous LSN是同一个的情况,在原始的ARIES风格的WAL中这种情况会被assert掉。concurrent updates的模式下面其实这种情况是可以的,也可以保证正确性,所以这里要放松这种模式下面的previous检查。在concurrent updates的模式下面,更新Page的LSN信息的时候,需要使用原子的方式,SQL Server中的LSN不是一个8byte的字段,不能直接使用原子指令。这里获取Page LSN的时候使用的原子指令实现的,但是更新的时候需要获取一个latch。
struct LSN
{
ULONG m_fSeqNo;
ULONG m_blockOffset;
USHORT m_slotId;
}
//
Void SetPFSPageLsn(LSN targetLSN)
{
Get the current LSN value on the page;
If the current LSN > targetLSN
{
return;
}
Else
{
Obtain the lock on m_slotId using interlocked operations;
{
Get the current LSN value on the page;
If Current LSN on page < targetLSN
{
Update the LSN value on the page;
}
}
Release the lock on the m_slotId;
}
}
//
LSN GetPFSPageLsn()
{
do
{
read1 = Atomic read of first 8 bytes of LSN;
read_s1 =Atomic read of last 2 bytes of LSN;
Memory barrier to prevent compiler reordering;
read2= Atomic read of first 8 bytes of LSN;
read_s2 = Atomic read of last 2 bytes of LSN;
} while (read1 != read2 || read_s1 != read_s2 ||
SLOT_MASK_set_in_read_s1 ||
SLOT_MASK_set_in_read_s2);
Double read succeeded, return the value;
}
另外一个需要改动的地方是checkpoint相关的,concurrent updates的模式下面,可能线程t1产生了一个更小的LSN的日志,t2产生了一个LSN更大的日志,t2却赶在t1之前更新了DPC的信息。这个时候checkpoint操作去查看最小的dirty page LSN的时候,可能获取到一个比实际的更大的值,从而导致错误。这里使用一个PENDING_COUNT的方式解决这个问题。checkpoint操作只有在这个PENDING_COUNT为0的时候才能继续后面的操作,否则需要等待这个PENDING_COUNT变成0。更新Page操作的时候,t1准备生成log record的时候,如果dirty bit为0,则编码目前还没有其它的更新操作。如果t1的操作对DPC中记录的LSN值有影响,则t1递增PENDING_COUNT的值。这个时候如果t2准备产生log record,dirty bit发现此时的值为1,而t2对PENDING_COUNT不用操作。
Void PrepareToDirty()
{
If the page does not already have DPC, create one;
Increment PENDING_COUNT in DPC in interlocked way;
Other prepare to dirty aspects;
}
Void Dirty()
{
Perform other Dirty tracking;
Set the Dirty bit;
Decrement PENDING_COUNT on DPC if needed;
}
0x02 评估
这里的具体信息可以参看[1].
Segment-Based Recovery: Write-ahead logging revisited
0x10 基本思路
WAL是现在的数据库系统基本都会采用的系统,一般的关系型数据库使用Btree作为索引结构,以page为管理单位。这篇Paper提出来了segment-based recovery的思路,提出以segment为管理单位,并支持segment和page的管理方式同时存在。Segment在这里就是一段存储的空间,即a set of bytes that may span page boundaries,recovery操作的时候以segment为单位,而不是page。一个segment可以是有多个page组成。这里segment的一个优势是对于大对象来说,可以保存在一个segment,和page为单位管理的系统那样使用多个page来保存,另外就是能更好利用DMA 和 zero-copy IO。在ARIES风格的WAL中,每个page会有一个LSN,记录应用到这个page上面的Log的位置,而这里的Segment-Based Recovery的一个特点就是不需要每个page一个LSN。而对于Segment-Based Recovery,其redo记录的限制与blind writes,即写入一个segment数据的时候不用care这个segment的内容,和一般基于page的需要理解这个page的结构的方式不同。一般的数据库,比如MySQL,使用redo log为 physiological redo,这个physiological是physical和logical的合成次,即即有physical也有logical的log。对于page-oriented的recovery机制,paper中总结了这样的一些问题:
-
Multi-page Objects,需要使用多个pages的大对象的处理。这个在page-oriented的方式中处理比较麻烦。另外由于page中需要保存一些元数据,比如LSN,所以及时这些page在磁盘上面是连续保存的,Multi-page Objects也会被切分为多段来保存。这样对于利用DMA 和 zero-copy I/O有些劣势。
-
另外一个就是系统不同的部分的耦合。比如下面的一般的page-oriented的操作逻辑,
pin page get latch newLSN = log.write(redo) update pages page LSN = newLSN release latch unpin page
这里认为这种方式类似于一个write through caching,而更想要的是一种write back caching。而在Segment-Based Recovery中,数据在被从cache中ecivt中驱逐的时候写入磁盘,更像是一个write back cache。
-
Log Reordering,对于page-orientd的系统,更新操作的顺序是有LSN决定的,recovery的时候也必须按照这个顺序操作。Segment-Based Recovery在recovery操作的时候,不需要知道page的LSN (just need to make sure it is assigned before you commit, so that it is ordered before later transactions;)这样在支持不同优先级的事务的时候,可以将高优先级的安排到写入的前面。
-
Distributed recovery,page-orientd的方式不同组件耦合更多,分布式操作的时候麻烦更多。 Segment-Based Recovery分割了几个部分的[3],
• App <-> Buffer manager: this is the write-back caching described above: only need to interact on evic- tion, not on each update • Buffer manager <-> log manager: no holding a latch across the log manager call; log manager call can now by asynchronous and batched together • Segments can be moved via zero-copy I/O directly, with no meta data (e.g. page LSN) and no CPU involvement. Simplifies archiving and reading large objects (e.g. photos).
当然使用page的方式也有很多的优点。
0x11 基本设计
Segment-Based Recovery实际上也是一个WAL的系统,包含一般的四个基本组件:1. log file。按顺序记录每个操作,每个log entry包含一个LSN,事务的id,更新哪一个segment,这个segment是否包含一个LSN,已经这个操作如何redo的一些信息;2. application cache,没啥;3. buffer manager,内存中一个page cache。这里的coherent表示这里的更新反应了log order,读取立即能看到前面的更新。但是 Segment-Based Recovery允许先写入日志,在后面才更新buffer manager,这样可能导致一些incoherent的现象。4. page file,将数据保存在磁盘上面。Semgent的写入操作和buffer manager的交互如下:
和ARIES风格的WAL类似,这里也使用three-pass的恢复操作:1. 分析,估计要从log那里开始恢复操作。这里的一个特点是开始的点是估计的,而不像ARIES一样是确定的;2. Redo,重复操作;3. Undo,撤销不能redo的操作;另外还可以支持segment-based和page-based同时存在:
// Hybrid redo:
foreach(redo entry) {
if(entry->clears_contents())
segment->corrupt = false;
if(entry->is_lsn_free()) {
entry->redo(segment);
} else if(segment->LSN < entry->LSN) {
segment->LSN = entry->LSN
error = entry->redo(segment);
if(error) segment->corrupt = true;
}
}
// 支持segment-based和page-based同时存在,并支持切换
log(transaction id, segment id, new page type);
clear_contents(segment);
initialize_page_header(segment, new page type);
这里的redo是physical的,即会记录下具体改变的数据,而不是一个逻辑的操作(physical: write pre- or post-images (or both), logical: write the logical operation (“insert”)),undo是logical的。这个的redo操作和ARIES风格不同,这里通过分析LSN等来保证redo只会apply一次,而Segment-Based Recovery保证的是至少apply一次,这样对于写入操作也有一定的限制。Segment-Based Recovery要求是blind write。Paper中举例了一个segment-based的index设计的例子,
// Insert value into B-Tree node
make in-memory preimage of page
insert value into M’th of N slots
log (transaction id, page id, binary diff of page)
// Update N segments
min_log = log->head
Spawn N parallel tasks; for each update:
log (transaction id, offset, preimage, postimage)
Spawn N parallel tasks; for each update:
pin and latch segment, s
update s
unlatch s
s->lsn_stable = min(s->lsn_stable, min_log);
Wait for the 2N parallel tasks to complete max_log = log->head
Spawn parallel tasks; for each segment, s:
s->lsn_volatile = max(s->lsn_volatile, max_log); unpin s;
Segment-based indexes的更新操作比如是blind write(write content that do not depend on the previous contents)的,知道在哪个位置写入,而不管这个node的其它内容。在写入日志的时候,可以记录下page的binary diff,也可以直接记录这个page的preimage和postimage。Blind writes可能会明显增加比physiological方式的log开心。这里使用这两种方式同时存在来优化这个问题。
恢复操作要处理的两个问题是torn (incoherent) data(the object was partially writ- ten to disk), 和 inconsistent data(An object is consistent if it is coherent at an LSN that was generated when there were no in-progress modifications to the object)。Paper中的Coherency和Consistency用了很学术的定义。在segment-based recovery中,recovery操作第一个确定一个可以丢弃的log点,这个点是估计的。第二步是按照log顺序重放log。redo操作的时候,这里的segment每个可以并发进行。redo之前的segment可能是torn状态的,segment写入磁盘的时候会有一个checksum。如果checksum不对,会让redo操作完成在检查。如果不能的话只能向前会滚。这个逻辑和ARIES风格的WAL有不少的不同,这里写入的segment可能是存在问题的,可能可以由redo来fix。
0x12 评估
这里的具体信息可以参看[2].
参考
- Concurrent Updates to Pages with Fixed-Size Rows Using Lock-Free Algorithms, VLDB ‘20.
- Segment-Based Recovery: Write-ahead logging revisited, VLDB ‘09.
- https://people.eecs.berkeley.edu/~brewer/cs262/Lec-Segments.pdf