目录
录
简介:
本便文章是对CMU15-445 2023 FALL 中实验:BUFFER POOL的一个总结。
实验内容:Project #1 - Buffer Pool | CMU 15-445/645 :: Intro to Database Systems (Fall 2023)
Buffer Pool
功能:
将物理页从主内存来回移动到磁盘。它允许DBMS支持大于系统可用内存量的数据库。缓冲池的操作对系统中的其他部分是透明的。
问题与解决方案:
问题1:如何完成数据在物理页与内存页之间的移动
class DiskManager {
public:
explicit DiskManager(const std::string &db_file);
DiskManager() = default;
virtual ~DiskManager() = default;
void ShutDown();
virtual void WritePage(page_id_t page_id, const char *page_data);
virtual void ReadPage(page_id_t page_id, char *page_data);
void WriteLog(char *log_data, int size);
auto ReadLog(char *log_data, int size, int offset) -> bool;
auto GetNumFlushes() const -> int;
auto GetFlushState() const -> bool;
auto GetNumWrites() const -> int;
inline void SetFlushLogFuture(std::future<void> *f) { flush_log_f_ = f; }
inline auto HasFlushLogFuture() -> bool { return flush_log_f_ != nullptr; }
protected:
auto GetFileSize(const std::string &file_name) -> int;
// stream to write log file
std::fstream log_io_;
std::string log_name_;
// stream to write db file
std::fstream db_io_;
std::string file_name_;
int num_flushes_{0};
int num_writes_{0};
bool flush_log_{false};
std::future<void> *flush_log_f_{nullptr};
// With multiple buffer pool instances, need to protect file access
std::mutex db_io_latch_;
};
分析:磁盘管理器中主要有连个IO对象,其中db_io负责数据的读写操作,log_io负责进行日志的记录操作。实际的实验中我们并没有直接使用DiskManager,我们是通过DiskScheduler进行的读写调度的,在DiskScheduler中我们通过一个后台的工作线程来调用DiskManager中的WritePage和ReadPage函数,完成具体的数据读写操作。
问题2:当缓冲池满后要调入新页时,采用什么策略去淘汰旧页
分析:通过页面调度策略要尽量减少磁盘I/O次数,页面调度策略有很多:LRU、Clock、LRU-K等等。这里我们选用的是LRU-K调度策略:LRU-K调度策略从两个方向来考虑一个页面是否需要被替换掉,分别是时间方向和历史使用次数方向,比起LRU只考虑时间方向是替换方式,LRU-K是更优的。
组件图
分析:组件图画的并不是特别的标准,主要是方便自身的一个理解,其中的箭头表示一种依赖关系。
LRU-k Rplacement Police
功能:
记录buffer pool中页面(frame)的使用情况
数据结构:
class LRUKReplacer {
private:
std::unordered_map<frame_id_t, std::list<size_t>> history_queue_;
std::unordered_map<frame_id_t, bool> behavior_queue_;
std::unordered_map<frame_id_t, std::list<frame_id_t>::iterator> ref_first_queue_;
std::list<frame_id_t> first_queue_; // 历史队列
MinHeap secend_queue_; // 缓存队列
size_t *exist_; // 表示frame是不是有与之对应的page
size_t evictable_size_{0}; // 可以驱逐的frame的数量
size_t replacer_size_; // 替换器中frame的总数(buffer pool page’s num)
size_t k_;
std::mutex latch_;
};
分析:LRU-K将缓存在buffer pool中的页分为两类,访问次数小于K(first_queue队列:采用FIFO策略进行淘汰),访问次数大于等于k(second_queue队列:采用LRU-K策略进行淘汰),在进行页面淘汰的时候,优先在历史队列中进行选择。
实现功能:
- Evict(frame_id_t* frame_id):与Replacer跟踪的所有其他可驱逐帧相比,驱逐向后k距离最大的帧。将帧id存储在输出参数中并返回True。如果没有可驱逐的帧返回False。
- RecordAccess(frame_id_t frame_id):记录在当前时间戳访问给定的帧id。这个方法应该在页面被固定在BufferPoolManager之后调用。
- Remove(frame_id_t frame_id):清除与帧相关的所有访问历史。只有在BufferPoolManager中删除页面时才应该调用此方法。
- SetEvictable(frame_id_t frame_id, bool set_evictable):这个方法控制一个帧是否可被驱逐。它还控制LRUKReplacer的大小。在实现BufferPoolManager时,您将知道何时调用该函数。具体来说,当一个页面的引脚数达到0时,将其对应的帧标记为可驱逐的,并增加替换器的大小。
- Size():这个方法返回当前在LRUKReplacer中的可驱逐帧的数量。
分析:
在实现Evict(frame_id_t* frame_id),RecordAccess(frame_id_t frame_id),Remove(frame_id_t frame_id)这三个函数时,我们要分情况进行讨论:
情况1:访问次数<k
情况2:访问次数>=k
对于RecordAccess的情况还有访问次数为0,也就是第一次被读入bufferpool的情况。
对于SetEvictable(frame_id_t frame_id, bool set_evictable)要分两种情况进行处理:
情况1:evictable->Noevictable
情况2:Noevictable->evictable
自己在做的时候一开始就没有进行分类处理,直接进行了相应的behavor_queue_的设置,导致没有更新对应的evicatable_size_。
Disk Scheduler
功能:
负责调度对DiskManager的读写操作
数据结构:
class DiskScheduler {
private:
DiskManager *disk_manager_ __attribute__((__unused__));
std::optional<std::thread> background_thread_;
};
分析:一个后台工作线程,一个磁盘管理器对象,后台工作线程调用磁盘管理器中的读写页函数,处理任务队列中的读写任务,这个后台工作线程的使用其实和实现线程池有点像。
实现功能:
- Schedule(DiskRequest r):调度DiskManager执行的请求。DiskRequest结构指定请求是否为读/写,数据应该写入/从哪里写入,以及操作的页ID。DiskRequest还包括一个std::promise,一旦请求被处理,它的值应该被设置为true。
- StartWorkerThread():处理调度请求的后台工作线程的启动方法。工作线程在DiskScheduler构造函数中创建并调用此方法。该方法负责获取排队的请求并将其分配给DiskManager。记住要在DiskRequest的回调上设置值,以向请求发出请求已经完成的信号。在调用DiskScheduler的析构函数之前,这个函数不应该返回。
PAGE
功能:
页是数据库系统中的基本存储单位。Page为保存在主存中的实际数据页提供了一个包装器。同时Page中还保存用于缓冲池管理器的一些元数据。
数据结构:
class Page {
public:
/** Constructor. Zeros out the page data. */
Page() {
data_ = new char[BUSTUB_PAGE_SIZE];
ResetMemory();
}
~Page() { delete[] data_; }
inline auto GetData() -> char * { return data_; }
inline auto GetPageId() -> page_id_t { return page_id_; }
inline auto GetPinCount() -> int { return pin_count_; }
inline auto IsDirty() -> bool { return is_dirty_; }
inline void WLatch() { rwlatch_.WLock(); }
inline void WUnlatch() { rwlatch_.WUnlock(); }
inline void RLatch() { rwlatch_.RLock(); }
inline void RUnlatch() { rwlatch_.RUnlock();
inline auto GetLSN() -> lsn_t { return *reinterpret_cast<lsn_t *>(GetData() + OFFSET_LSN); }
inline void SetLSN(lsn_t lsn) { memcpy(GetData() + OFFSET_LSN, &lsn, sizeof(lsn_t)); }
protected:
static_assert(sizeof(page_id_t) == 4);
static_assert(sizeof(lsn_t) == 4);
static constexpr size_t SIZE_PAGE_HEADER = 8;
static constexpr size_t OFFSET_PAGE_START = 0;
static constexpr size_t OFFSET_LSN = 4;
private:
/** Zeroes out the data that is held within the page. */
inline void ResetMemory() { memset(data_, OFFSET_PAGE_START, BUSTUB_PAGE_SIZE); }
/** The actual data that is stored within a page. */
// Usually this should be stored as `char data_[BUSTUB_PAGE_SIZE]{};`. But to enable ASAN to detect page overflow,
// we store it as a ptr.
char *data_;
/** The ID of this page. */
page_id_t page_id_ = INVALID_PAGE_ID;
/** The pin count of this page. */
int pin_count_ = 0;
/** True if the page is dirty, i.e. it is different from its corresponding page on disk. */
bool is_dirty_ = false;
/** Page latch. */
ReaderWriterLatch rwlatch_;
};
BufferPoolManager
功能:
通过对对Page、LRU_K、DiskScheduler的集成实现更高级的一些功能。(总结不出来了)
数据结构:
class BufferPoolManager {
private:
/** Number of pages in the buffer pool. */
const size_t pool_size_;
/** The next page id to be allocated */
std::atomic<page_id_t> next_page_id_ = 0;
/** Array of buffer pool pages. */
Page *pages_;
/** Pointer to the disk sheduler. */
std::unique_ptr<DiskScheduler> disk_scheduler_;
/** Pointer to the log manager. Please ignore this for P1. */
LogManager *log_manager_ __attribute__((__unused__));
/** Page table for keeping track of buffer pool pages. */
std::unordered_map<page_id_t, frame_id_t> page_table_;
int64_t *frames_flag_;
/** Replacer to find unpinned pages for replacement. */
std::unique_ptr<LRUKReplacer> replacer_;
/** List of free frames that don't have any pages on them. */
std::list<frame_id_t> free_list_;
/** This latch protects shared data structures. We recommend updating this comment to describe what it protects. */
std::mutex latch_;
};
分析:对于缓冲池管理器,其核心的元数据就是page_id和frame_id之间的映射关系,并且frame_id也是Pgaes数组的下标,最初没看懂frame_id和Pages数组标的关系,多建了一层映射关系。
功能实现:
- NewPage (page_id_t * page_id):在缓冲池中创建一个新页面。将page_id设置为新页面的id,如果所有帧当前都在使用并且不可驱逐(换句话说,固定),则将其设置为nullptr。您应该从空闲列表或替换器中选择替换框架(总是先从空闲列表中查找),然后调用AllocatePage()方法来获取新的页面id。如果替换框架有脏页,您应该首先将其写回磁盘。您还需要为新页面重置内存和元数据。记住通过调用replacer来“固定”框架。SetEvictable(frame_id, false),以便替换器在缓冲池管理器“Unpin”之前不会将帧驱逐。此外,要记住在替换器中记录帧的访问历史,以便使lru-k算法工作。
- FetchPage (page_id_t page_id):从缓冲池中获取请求的页面。如果page_id需要从磁盘中获取,但所有帧当前都在使用中并且不可驱逐(换句话说,固定),则返回nullptr。首先在缓冲池中搜索page_id。如果没有找到,从空闲列表或替换器中选择一个替换帧(总是先从空闲列表中找到),通过使用disk_scheduler_>Schedule()调度读DiskRequest从磁盘读取该页,然后替换该帧中的旧页。与NewPage()类似,如果旧页面是脏的,则需要将其写回磁盘并更新新页面的元数据。此外,请记住禁用驱逐并记录帧的访问历史,就像对NewPage()所做的那样。
- UnpinPage(page_id_t page_id, bool is_dirty):从缓冲池中解除目标页的锁定。如果page_id不在缓冲池中或其引脚数已经为0,则返回false。减少页面的引脚计数。如果引脚数达到0,则该帧应被替换器移除。另外,在页面上设置脏标志,以指示页面是否被修改。
- FlushPage (page_id_t page_id):将目标页刷新到磁盘。使用DiskManager::WritePage()方法将页刷新到磁盘,而不考虑脏标志。刷新后取消设置页的脏标志。
- DeletePage (page_id_t page_id):从缓冲池中删除页面。如果page_id不在缓冲池中,则不执行任何操作并返回true。如果该页已固定且无法删除,则立即返回false。从页表中删除该页后,停止跟踪替换器中的帧,并将该帧重新添加到空闲列表中。另外,重置页面的内存和元数据。最后,您应该调用DeallocatePage()来模拟释放磁盘上的页面。
- FlushAllPages ():Flush all the pages in the buffer pool to disk
分析:
其中NewPage (page_id_t * page_id),FetchPage (page_id_t page_id)这两个返回值为*Pgae的函数,要对page的引用次数(pin_count_)进行增加,我在做的时候一开始根本没有想到这个问题,现在想一想页确实很有道理,返回的是page的指针,表示接下来肯能会对page内的数据进行操作。
其实在实现这个功能模块时犯了一个致命的错误(希望对大家有帮助):
于DeletePage (page_id_t page_id)、FetchPage (page_id_t page_id)、NewPage (page_id_t * page_id)、FlushAllPages (),这些涉及到页面刷新的函数,我直接调用FlushAPages (),出现了一个问题,我在FlushPages ()函数中是要加锁的,我不得不先在原函数中解锁,然后在调用FlushPages (),最后在原函数中把锁加上,这样的话在多线程的情况下会出现破坏数据的一致性。我最后弄了两个私有的读写函数,这两个函数是不需要加锁的,直接在其他函数中进行调用。
参考链接:CMU 15445 spring 2023 - project 1 Buffer Pool实验笔记 - 1v7w - 博客园 (cnblogs.com)