LAB 第一个TASK的实现目标,就是去记录LOG。但是不是每次写LOG都会直接去落盘,也是会先CACHE在一个BUFFER里,当遇到下面3种情况之一的时候,才会刷到磁盘上。
(1) When log_buffer is full. (2) When LOG_TIMEOUT is triggered. (3) When buffer pool is going to evict a dirty page from LRU replacer.
记录LOG,在DB端会由LOG MANAGER 去暴露方法,供其他组件调用。
如:
- BUFFER POOL MANAGER, 当发现有一个脏页要被VICTIM的时候,要回写磁盘,如果ENABLE LOGGING打开的话。需要把内存里的LOG也强制刷入磁盘。
Before your buffer pool manager evicts a dirty page from LRU replacer and write this page back to db file, it needs to flush logs up to pageLSN. You need to compare persistent_lsn_ (a member variable maintains by Log Manager) with your pageLSN.
- TXN MANAGER 在做COMMIT,ABORT的时候 需要阻塞,一直到他所做的操作的LOG落盘后,TXN才能返回。
Within TransactionManager, whenever you call Commit or Abort method, you need to make sure your log records are permanently stored on disk file before release the locks.
- TABLE PAGE, 当对TABLE PAGE 进行增删改的时候,我们需要往BUFFER里写LOG。
所以大致需要改这么些文件
image.pngThe files you need to modify for this task are the LogManager class (logging/log_manager.cpp and logging/log_manager.h) plus the TablePage class (table/table_page.cpp and include/logging/table_page.h) plus the TransactionManager class (concurrency/transaction_manager.cpp and include/concurrency/transaction_manager.h) plus your original implementation of BufferPoolManager class.
第一步 完成LOG MANAGER
首先是要求实现一个后台线程,去等待超时,然后交换2个BUFFER,把一个BUFFER缓存的LOG后台写进磁盘。
Inside RunFlushThread function in Log manager, you need to start a separate background thread which is responsible for flushing the logs into disk file. The thread is triggered every LOG_TIMEOUT seconds or when the log buffer is full. Since your Log Manager need to perform asynchronous I/O operations, you will maintain two log buffers, one for flushing (We will call it as flush_buffer) one for concurrently appending log records (We will call it as log_buffer)
3个函数也有对应的注释,
大致思想是RunFlushThread
后台开一个线程,无线循环直到ENABLE LOGGING是FALSE。用CONDITION VARIABLE 来控制WAIT,设置超时时间。也可以被唤醒。然后醒来后,判断LOG BUFFER里有东西,就交换到FLUSH BUFFER,随后把BUFFER里的东西刷到磁盘。
disk_manager_->WriteLog(flush_buffer_, flushBufferSize_);
StopFlushThread
主要是去设置ENABLE LOGGING 为FALSE,然后等待后台线程退出。
AppendLogRecord
的思路是当要写的东西还能往BUFFER里放的情况下,根据不同的操作类型写入不同的LOG。如果放不下了,就要唤醒后台线程,然后换一个空的BUFFER,随后放LOG。
注释里已经给出了序列化的例子,模仿这个把别的给实现了即可。
* if (log_record.log_record_type_ == LogRecordType::INSERT) {
* memcpy(log_buffer_ + pos, &log_record.insert_rid_, sizeof(RID));
* pos += sizeof(RID);
* // we have provided serialize function for tuple class
* log_record.insert_tuple_.SerializeTo(log_buffer_ + pos);
* }
image.png
第二步 实现GROUP COMMIT
- Your Log Manager will integrate the group commit feature. Motivation behind group commit is to amortize the costs of each fsync() over multiple commits from multiple parallel transactions. If there are say 10 transactions in parallel trying to commit, we can force all of them to disk at once with a single
fsync()
call, rather than do one fsync() for each. This can greatly reduce the need forfsync()
calls, and consequently greatly improve the commits-per-second throughput. WithinTransactionManager
, whenever you callCommit
orAbort
method, you need to make sure your log records are permanently stored on disk file before release the locks. But instead of **forcing **flush, you need to wait forLOG_TIMEOUT
or other operations to implicitly trigger the flush operations.
大概的意思是可以不用每一次COMMIT 立刻去FSYNC。可以等到TIMEOUT的时候,一起FSYNC。这要可以在多TRANSACTION 并行的情况下,减少FSYNC的调用,提高吞吐率。
但是再BUFFER POOL MANAGER刷脏页的时候,要强制刷回磁盘,不要等TIMEOUT。
However unlike group commit, buffer pool can force log manager to flush log buffer, but still needs to wait for logs to be permanently stored before continue.
大致思想是实现一个函数,根据是否FORCE刷回磁盘来做不同的情况。
image.png
第三步 修改BUFFER POOL MANAGER
这里主要就是PAGE IS DIRTY的时候,如果ENABLE LOGGING了,就比较下当前PAGE的LSN 是否比已经刷进磁盘的LSN要大(logManager persistentLsn),如果大的话,需要刷LOG进磁盘,更新pageLSN。
image.png
第四步 修改对应的TXN MANAGER
主要是在BEGIN,COMMIT,ABORT的时候,插LOG,随后设置自己的PREV LSN。
image.png
COMMIT 和 ABORT ,需要FLUSH FALSE,来阻塞当前线程,直到LOG MANAGER 把LOG刷到磁盘,唤醒之后,TXN才能释放。
image.png
但是这边只COVER了TXN 的启动和结束。中间插LOG的位置 是在TABLE PAGE里。
对每一个操作加上对应的LOG即可。
比如INSERT LOG
image.png
基于上述实现,我们可以完成第一个BASIC LOGGING的测试。
image.png在CHECK 输出的时候,因为LOG的开始 都会先是BEGIN, NEW PAGE,然后才是INSERT
所以前2条LOG的长度是固定的,一个是20,一个是24,最后一个和你INSERT的TUPLE的长度有关。
开始实现RECOREY
因为没有CHECKPOINT,所以整个ARIES 就没必要做ANALYSE阶段。大致思想是首先REDO 从头开始读LOG,发现PAGE LSN 大于等于LOG LSN 就跳过。不然需要做REDO,REDO就是要看那个操作做了什么,重做一下。
在读LOG的时候需要调用disk_manager_->ReadLog(log_buffer_ + bufferOffset, LOG_BUFFER_SIZE - bufferOffset, offset_)
这里我一开始没有自己定一个一个BUFFER OFFSET的变量,到后面测试的时候发现,如果LOG 超过了LOG_BUFFER_SIZE,是最后会有一段LOG被切开了。所以我们要维护好这个小段。造成代码在这里需要自己思考和笔划一下。
把LOG读进log_buffer_后,就需要DESERIALIZE 一个又一个LOG。
直到DESERIALIZE 失败,什么时候失败呢?
就是要解析的LOG的位置,超过了log_buffer_+LOG_BUFFER_SIZE
image.png image.png
在实现NEW PAGE的REDO的时候,会发现需要PAGE_ID 和 PREV_PAGE_ID 2个信息。不然我不知道怎么根据PREV_PAGE_ID 去拿PAGE_ID,所以这块我修改了LOG RECORD。
image.png image.png
NEW PAGE的REDO 如下
image.png
最后就是INSERT 和DELETE的REDO
image.png
把最后一段的前半部分的LOG,给存好。
memmove(log_buffer_, log_buffer_ + bufferOffset, LOG_BUFFER_SIZE - bufferOffset);
bufferOffset = LOG_BUFFER_SIZE - bufferOffset;//rest partial log
实现UNDO
在REDO的环节中,我们已经存储好了ATT,和一个TXNID 映射到LSN的MAPPING 关系。
UNDO 在ARIES的算法里,是每次取最大的LSN,然后去UNDO,每次UNDO要打一个CLR。因为每次取最大,应该用堆来比较好做,或者MAP(红黑树的有序MAP)来做比较好。
这个PROJECT因为不需要考虑UNDO的时候CRASH的情况,所以我们可以不用取最大的算法。只要把ATT里的要UNDO 的TX逐个做完即可。
所以就是遍历每个ACTIVE TXN, 找到对应的LSN。然后利用LOG里的PREV_LSN 不断向上追溯,逐个UNDO。一直到PREV_LSN是INVALID LSN的时候结束这个ACTIVE TXN的UNDO。
image.png image.png
这个实现完,应该可以通过第2个测试。
但是连着跑2个测试,会出问题,在下面的ASSERT报错。
image.png
发现问题是因为DISK_MANAGER里用了个STATIC变量造成的。而这个BUFFER_USED的唯一作用就是查看 2个BUFFER是否SWAP。
解决方法
把这个变量变成成员变量。
image.png
最后我写了9个测试,
压力测试的问题
在做压力测试的时候,发现会抛ASSERT的问题,发现TUPLE SIZE可能会《0
基于APPLY DELETE也是用!=0这个判断。我就把》0 改成了!=0
image.png
为了加速测试我把CONFIG.CPP的TIMEOUT时间从1s 改到100ms,同时把睡眠时间从2S改到了200MS
这样跑完9个测试大概需要11S
image.png
测试了1000次通过。
image.png
代码提交到GIT PROJECT 4
https://github.com/yixuaz/CMU-15445/tree/master/project4
网友评论