<CMU 15-445> project 1: Buffer Pool 实现思路

72 阅读3分钟

Task #1 - LRU-K Replacement

LRU-K 替换算法考虑的是最近 k 次的页面访问,除了最近一次访问外,还考虑了访问次数。主要的作用是用于保护热点数据,将频繁访问的数据尽可能留在缓存中。

替换策略: a. 当所有可替换的页面访问次数超过 k 次时,替换策略跟 LRU 一致,取最近一次的访问时间; b. 当存在可替换的页面的访问次数小于 k 次时,将优先替换这个页面,如果有多个这样的页面,替换策略变为 FIFO,取第一次的访问时间。

Task #2 - Disk Scheduler

主要是 optional 和 promise 的使用。
optional 可以很方便传递一个空的 DiskRequest,值为 nullopt 的 optional 便表示为空。当 DiskScheduler 退出时,传递一个空 DiskRequest,让后台处理线程也顺利退出。
promise 用于获取 DiskRequest 的执行结果,后台处理线程处理完 DiskRequest 就会设置 promise 的值,外部就可以获取到结果,获取结果时将会阻塞线程。

// promise 用法
auto promise = disk_scheduler_->CreatePromise();
auto future = promise.get_future();
disk_scheduler->Schedule({/*is_write=*/true, data, /*page_id=*/0, std::move(promise)});
future.get();

Task #3 - Buffer Pool Manager

实现细节

脏标记 设置页面为脏是通过 UnpinPage() 的 is_dirty 参数。
如果页面本身已经为脏,将其设置为非脏,将会出现逻辑错误,同时也将无法通过线上测试。因此,在 UnpinPage() 中,需要做一次检查,防止将脏页面设置为非脏。

加锁时机
BPM 对象 和 Page 对象都有自己的 latch 锁,用于保护对象的内部数据。
1、在 BPM 中,调用 FetchPage()、UnpinPage() 等函数,都会对 Page 对象的数据进行修改,例如 FetchPage() 会涉及到 Page 对象内部数据 pin_count_ 和 page_id_ 的更改,但这些都属于对象的元数据,是非页面数据,同时只会在 BPM 内部进行修改,因此只需要使用 BPM 对象 latch 锁进行保护即可。
2、对于 Page 对象 的 latch 锁,只用于保护页面数据,当外部从 BPM 中获取到一个Page对象,然后对其页面数据进行读写操作,或者 BPM 内部进行页面同步到磁盘操作,都需要用 Page 对象的 latch 锁进行保护。

性能优化

性能瓶颈
瓶颈 1:磁盘 IO 是阻塞操作,若页面切换采用同步的方式,在进行页面切换操作时持有互斥锁就会阻塞其他页面访问。
优化:将页面切换由同步操作转化为异步操作,可以有效提高并发性。当页面需要进行切换时,释放持久的锁,等切换结束时,再获取锁;当其他线程访问正在切换的页面,需要阻塞等待切换完毕,可以使用条件变量完成这样的线程同步操作。

// 在 BPM 的函数中使用如下代码片段
// 先释放锁,然后进行页面切换操作,再获取锁
lk.unlock();
... // 切换页面
lk.lock();

// 获取页面需要等待页面切换完成
lk.lock();
cond.wait(); //等待页面切换

瓶颈 2:磁盘 IO 是串行访问的,效率较低。 优化:可以改用并行磁盘 IO,目前未实现。

瓶颈 3:替换算法还可以进一步优化,可以考虑优先替换非热点页面。