CMU15-445 Project 1 Buffer Pool 详解

本系列文章将以倒序的顺序回顾自己在写Spring23的Projects时的主要思路以及遇到的问题,往期文章:

CMU15-445 Project 4 Concurrency Control个人详解 - 嘿喂GO!

CMU15-445 Project 3 Query Execution详解 - 嘿喂GO!

CMU15-445 Project 2 B+Tree 详解

前言

很快啊,一下子CMU15-445系列的文章已经总结到第四篇了。在做完所有的Projects后回过头来看P1,确实能感觉到P1是最朴素的一个Project。但在当时做P1时,给我的感觉并不是最容易的。P1是Spring 2023的所有Projects中最底层的一个,所有后续的Projects的实现都以P1的内存池Buffer Pool为基础,所以请一定确保P1正确实现后再开始后续的Projects。

Project 1的内容也很直白,不像后续的Projects在开始实现前需要了解非常多的前序知识。P1要求我们实现一个支持LRU-K策略的内存管理器缓冲池Buffer Pool Manager(后文简称bpm),并将相关函数封装为Page Guard,实现page的RAII机制。后续Projects的任何操作,包括索引、SQL执行、多线程并发等,在请求数据时都向bpm请求。可见虽然bpm很基础,但也非常非常重要。

在开始Project 1之前,建议观看课程组Lecture1~6的视频。

再次强调,不要公开自己的作业代码,尊重Mr.Pavlo与助教们的劳动成果~本文也仅会展示主要思路,不会展示个人代码(课程组Starter Code除外)。

课程主页

Project 1 主页

BusTub-官方Github

讨论区

Task 1 - LRU-K Replacement Policy

我们知道,在不使用时,数据都是存在磁盘的;而只有当需要对数据进行操作时,数据才会被拿到内存中来进行访问。具体的细节可以了解一下操作系统的虚拟内存与内存管理相关的内容。数据在磁盘中以页为单位进行存储,每一页被bpm拿到内存中后,我们称之为页框frame。需要注意的是DBMS的内存管理并不完全依赖操作系统,用课上的话来说就是“没有什么比DBMS自己更了解数据”,所以DBMS有自己的内存管理系统,在结构上bpm会占用OS的实际物理内存的一部分。

bpm能容纳的frame数量是有限的,当frame满了以后,如果要从磁盘disk中fetch一个新的页面至内存,则需要选择一个frame将其内容替换为新的page,这个选择的过程就是页面置换算法。常见的页面置换算法包括FIFO、NRU、Clock算法等,而我们Task1要实现是LRU-K算法。

LRU-K算法可以认为是LRU的进阶版:对于LRU,当我们需要置换某一个页面时,选择所有页面中上一次访问时间距离当前时刻最久的页面进行置换,LRU-K则是选择所有页面页面中过去第K次访问时间距离当前时刻最久的页面进行置换。换句话说,LRU本质上就是LRU-1算法。具体的LRU-K实现思路如下:

  1. 对于每一个frame,维护一个访问队列,用于保存过去k次访问的时间戳,大于k次访问时需弹出队头,即满足q.size() <= k
  2. 对与每一个frame,如果其队列的size小于k,那么,初始化其k-distance+inf
  3. 若frame的队列size == k,那么,其k-distanceq.front(),即过去第K次访问的时间。
  4. 当需要进行页面置换时,在所有可置换的frame里,查看是否有k-distance+inf的frame。
    1. 若有,从所有+infframe中按照FIFO的策略进行置换。
    2. 否则,从所有frame中,选择过去第k次访问距离当前时刻最久的进行置换。可直接使用队列的队首获得。

具体代码实现中,4.1有一点需要注意一下:无论是Handout还是Starter Code的注释,都说对于+ inf的frame,采用LRU的策略进行置换:

When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).

但实际的Test Case里要求的是使用FIFO的方式进行置换(个人认为使用LRU合理一些),所以,最终,一句话总结就是:优先使用FIFO置换不满k次访问的页面,再从访问次数大于等于k次的frame中对比过去第k次访问的时间进行置换。

我们再稍微看一下涉及到的两个类:

class LRUKNode {
 private:
  /** History of last seen K timestamps of this page. Least recent timestamp stored in front. */
  // Remove maybe_unused if you start using them. Feel free to change the member variables as you want.

  std::list<size_t> history_;
  size_t k_;
  frame_id_t fid_;
  bool is_evictable_{false};
...
}

class LRUKReplacer {
 private:
  // TODO(student): implement me! You can replace these member variables as you like.
  // Remove maybe_unused if you start using them.
  std::unordered_map<frame_id_t, std::shared_ptr<LRUKNode> > node_store_;
  size_t current_timestamp_{0};
  size_t curr_size_{0};
  size_t replacer_size_;
  size_t k_;
}

这里LRUKNodehistory_即为存储时间戳的队列,存储的为current_timestamp_——我们每次access一个frame时,都让current_timestamp_自增,并更新相应的history_,以模拟访问时间的先后关系。根据以上分析,这里在判断置换时,无论是对于+inf还是非+inf的node,我们都是用其history_front()进行判断即可。

此外,curr_size_表示可置换的frame数量,即is_evictable_为true的node数量,注意与replacer_size_区分。RecordAccess访问一个不存在的frame id时,需要创建新的并Insert。

Task 1只关注替换,即只关注非空frame里哪些可置换。至于在进行置换时是否还有frame为空,LRUKReplacer不负责,这一点由Task 2的Buffer Pool调度。总体而言,Task 1只需要搞明白LRU-K算法的原理即可,实现起来并不复杂。

Task 2

Task1主要实现了页面置换的算法,但具体地从磁盘fetch页面、将dirty page写回磁盘等操作则需要bpm实现。这里区分一下frame id与page id:在磁盘中的页面我们称之为page,其page id是递增式的(理论上只要磁盘有多大,page id就能递增到多大);而frame id则有一定的范围,最大不能超过bpm的frame size限制。因此,我在实现时使用了两个map来对frame idpage id进行双向映射。此外,我还增加了一个成员evictable_number_用于表示可置换页面的数量。

Task2要求我们实现bpm诸如fetch、unpin、newPage、delete、flush等功能,概括起来就是打通内存与磁盘。这里磁盘的相关接口课程组已经为我们写好,我们只需要专注于bpm即可。在Handout里提到,bpm不关心page的内容或结构,但作为开发者,我们需要了解。(可以看一下Lecture视频)另外,建议熟悉一下bpm.h以及page.h文件,代码量不多,逻辑清晰,熟悉起来很快。

  • FetchPage:FetchPage要求我们根据给定的page id,在内存中拿到对应的page。如果page在frame中,则直接拿;否则,需要从disk拿。

    1. 首先,根据map判断是否在内存中,如果不在,且没有frame空闲、没有可置换的页面,返回null。
    2. 若page在内存中,更新该page的access history,更新pin count,并根据pin count前后的值更新evictable_number_,返回page。
    3. 若不在内存中,我们需要将对应的page fetch至内存中:
      1. 先确定取得的page放到哪个frame:若有空闲frame,则选择一个frame id用于存放;否则,通过LRU-K选择需要被置换的frame,根据is_dirty_判断是否需要将该frame的page在置换前写回disk。
      2. bookkeeping:更新map以及evictable_number_
      3. 清空frame的page数据(ResetMemory),调用disk相关接口将page读取至frame。
      4. 调用LRU-K相关函数更新frame的属性,返回。
  • NewPageNewPageFetchPage很像,不同之处在于逻辑上前者是创建一个新的page并fetch,后者则是fetch一个已有的page。主要思路为调用AllocatePage构建一个新的page id,之后与Fetch的步骤三相同。

  • UnpinPage:这里先写一下我对一下pin与lock的理解:一个frame被pin时,表示不可置换,但能够访问;而一个frame被lock时,不仅不可置换,同一时刻只能一个对象访问。

    1. 首先判断需要unpin的page是否在内存中,若不在,直接返回false。
    2. 根据map拿到page,并查看对应的pin_count_是否为0。若否,返回false。
    3. pin_count_减一,并根据前后状态更新is_evictable_evictable_number_
    4. 更新page的is_dirty_。这一点注意要结合实际情况,不能简单地set为传入的参数,举个例子:一个事务修改了page1,之后事务Unpin page1,set此时page1的is_dirty_为true。之后另一个事务读取page1,读取结束后Unpin,对应的dirty形参为false。思考一下,此时,能否简单粗暴地将is_dirty_直接set为false?

余下的函数实现起来都不复杂,思路都很清晰,注意及时bookkeeping即可(FlushPage不需要)。所有的函数在实现时我都使用了unique_lock来保证线程安全。

Task3 - Read/Write Page Guards

从22Fall的讨论区以及网上的相关讨论可以看到,在Project 2的B+树中,由于没有及时Unpin相关的page,导致出现了各种问题。这一问题也使得22Fall的Project 2难度远远大于其他的Project。为此,课程组今年在Task3中增加了一个PageGuard的类,用于实现RAII。简单说就是实现三个PageGuard的相关接口(构造、析构、Drop)等,并在bpm中进行封装,以便在外界通过bpm获取page时能够及时Unpin,提高bpm的存取效率以及系统性能。

  • BasicPageGuardBasicPageGuard有三个成员:bpm、page以及一个is_dirty_标识位,我们需要实现移动构造函数、移动赋值运算符、Drop函数以及析构函数。
    1. BasicPageGuard(BasicPageGuard &&that):思路很明确,参数初始化列表进行赋值就行,注意初始化后需要将that的成员赋空。
    2. operator=(BasicPageGuard &&that):与移动构造相似,不同之处在于赋值前this的成员需要先赋空(实际上即是Drop的内容)。
    3. Drop():Unpin page并赋空。
    4. 析构:直接调用Drop()

Read/Write Guard的成员只有一个BasicPageGuard,需要实现的函数均可调用BasicPageGuard的实现,不同在于析构时解锁——这里的上锁我是在bpm的封装里实现的。最后注意一下空指针的处理。

后记

总体而言,Project 1的复杂度不及后面的Projects,坑也没有后面的Projects那么多。但Project 1是所有Projects中唯一给了我较大顿挫感的一个Project。现在回过头来分析一下,归根结底还是当时对整个代码没有一个全局的了解,每写一部分代码都想着赶紧结束当前的Task,想着赶进度,跟心态有很大关系。但,无论如何,写完P1后,收获还是有的。

最后,本篇文章花了将近三个小时,如果能够帮助到你,那么就是值得的~
在这里插入图片描述

显示Ungraded是网站bug~
  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值