CMU15-445:2023 FALL P(1)

目录

简介:

Buffer Pool

功能:

问题与解决方案:

组件图

LRU-k Rplacement Police

功能:

数据结构:

实现功能:

Disk Scheduler

功能:

数据结构:

实现功能:

PAGE

功能:

数据结构:

BufferPoolManager

功能:

功能实现:



简介:

本便文章是对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)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

彼岸丶403

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值