CMU15-445:Project #1 - Buffer Pool

Project #1 - Buffer Pool


本文是对CMU15-445课程第1个项目的一个粗略总结和翻译。仅供个人(M1kanN)复习使用。


1. Overview

  • 本学期要求为 BusTub DBMS实现一个新的面向磁盘的存储管理器(disk-oriented storage manager)。这样的存储管理器假定数据库的主要存储位置是磁盘上。

  • 第一个编程项目是实现一个缓冲池(buffer pool)。缓冲池负责将物理页从主存中来回移动到磁盘。它允许DBMS支持大于系统可用内存量的数据库。缓冲池的操作对系统的其他部分是透明(transparent)的。比如:系统使用其唯一的标识符(page_id_t)向缓冲池要(ask)一个页面,它不知道这个页面是否已经在内存中,或者系统是否必须从磁盘中检索它。

  • 注意:实现必须是线程安全的(thread-safe)。多个线程都将同时访问数据结构,所以必须确保他们的关键部分收到latch的保护。(即:锁)

  • 我们需要在我们的存储管理器中实现以下3个部分:

2. Project Specification

  • 对此次任务实现的每个组件,都提供了包含所需要我们实现的API的存根类(stub class)。我们不应该修改这些类中预先定义的函数签名(function signatures)。如果修改了,用来评分的测试代码可能会被破坏。就评分不了了。
  • 如果一个类已经包含了数据成员,我们不应该删除它们。例如BufferPoolManagerInstance里面的DiskManager, ExtendibleHashTableLRUKReplacer 对象。这些都是实现系统其他部分所需要的功能所需要的。另一方面,我们可能要向这些类来添加数据成员,以便正确实现所需要的功能。也可以给这些类添加辅助函数。
  • 除非有规定,我们可以在项目中使用任何的C++17容器。可以自行选择。注意多线程安全和锁的使用,来保护它们。但是我们不能带入额外的第三方依赖,比如boost。

Task #1 - Extendible Hash Table

  • 在这个项目的第一部分,我们将建立一个通用的哈希表,使用无序的桶(unordered buckets)来存储唯一的键值对(key/value pairs)。
    我们的哈希表必须支持

    • 插入/ 删除 键值对的能力。
    • 我们无需指定表的最大size,表应该可以根据需要逐步增加大小,但是我们不需要缩小表。也就是说,我们不需要实现对缩小或者压缩哈希表的功能。
    • 我们还需要支持检查一个键是否存在于哈希表中,并返回相应的值。
  • 在正式开始编程之前,建议先着手一些分割和目录增长的小例子。

  • 我们必须在项目源代码的指定文件中实现哈希表。而且只允许修改哈希表头文件(src/include/container/hash/extendible_hash_table.h)和对应的实现文件(src/container/hash/extendible_hash_table.cpp)。我们不需要修改任何其他文件,在视线中,我们不能在内部使用另一个内置哈希表。而且我们必须在ExtendibleHashTable类中实现以下函数:

    • Find(K, V): 给定键K,检查它是否存在于哈希表中,如果存在,则在V中存储指向其对应值的指针并返回true。如果键不存在,返回false。

    • Insert(K, V): 将键值对插入哈希表。如果K已经存在,则用新的值V覆盖其值,并返回true。如果键值对不能被插入到桶中(因为桶已经满了,而且键没有更新现有的对),在重试之前做以下步骤:

      1. 如果桶的本地深度(local depth)等于全局深度(global depth)。递增全局深度。并将目录(directory)的大小增加一倍。
      2. 增加桶的局部深度。
      3. 分割桶。并重新分配目录指针和桶中的K/V对。

      有限实现是在插入后,如果桶满了就分割桶,但是在这个项目桶,请检测桶是否溢出,并在插入之前执行分割!

    • Remove(K):给定K, 从哈希表删除其对应的键值对,并返回true。如果不存在,返回false。

    • GetGlobalDepth(): 返回整个哈希表的当前全局深度。

    • GetLocalDepth(dir_index):返回给定目录索引所指向的桶的当前本地深度。

    • GetNumBuckets(): 返回在哈希表中分配的桶的总数。

  • 我们可以利用给定的IndexOf(K)的私有函数来计算一个给定键所切分的目录索引。此外,还提供了一个Bucket嵌套类,表示可扩展哈希表中的桶。通过遵循代码文档,首先完成Bucket类的TODOs可以让我们更容易实现``ExtendibleHashTable`的API。但也可以自由的表写我们自己的内部类和辅助函数。

  • 我们需要使用std::mutex确保哈希表中的所有操作都是线程安全的。如何保护数据机构由我们自己决定。

Task #2 - LRU-K Replacement Policy

LRU-K算法参考:LRU-K和2Q缓存算法介绍

  • 这个组件负责跟踪缓冲池中的页面使用情况。我们将在src/include/buffer/lru_k_replacer.h 中实现一个名为LRUKReplacer的新类。并在src/buffer/lru_k_replacer.cpp 中实现相应的实现文件。
    注意:LRUKReplacer是一个独立的类,它与其他的Replacer类没有关系。我们应该只实现LRU-K替换策略,而不需要实现LRU或者时钟(clock)替换策略,即使有相应的文件。
  • LRU-K算法驱逐一个帧,该帧的后向k-距离(Backward k-distance)是替换器中所有帧的最大值。后向k-距离的计算方法是当前时间戳和第k 次访问的时间戳之间的时间差。一个少于k次历史访问的帧被赋予 +inf来作为它的后向k-距离。
    当多个帧有+inf的后向k-距离的时候,替换器会驱逐具有最早时间戳的帧。
  • LRUReplacer的最大尺寸与缓冲池的尺寸相同,因为它包含BufferPoolManager中所有帧的占位符(记录每个page的访问次数)。然而在任何给定的时刻,并不是复制器中的所有帧都被认为是可驱逐的。LRUReplacer的大小由可驱逐的数量表示。LRUReplacer被初始化为里面没有帧,然后只有当一个帧标记为可驱逐后,替换器(replacer)的大小才会增加。
  • 我们需要实现课堂上讨论的LRU-K策略。我们将实现以下方法:
    • Evict(frame_id_t*)
      驱逐Replacer正在跟踪的所有其他可驱逐帧相比,具有最大后向k-距离的帧。在输出参数中存储该帧的ID,并返回true。如果没有可驱逐帧,返回false。
    • RecordAccess(frame_id_t)
      记录给定的帧ID在当前时间戳被访问,这个方法应该在一个页面被pin在BufferPoolManager后被调用。
    • Remove(frame_id_t)
      清除与一个帧相关的所有访问历史。只有在BufferPoolManager中删除一个页面的时候,才应该调用这个方法。
    • SetEvictable(frame_id_t, bool set_evictable)
      该方法控制一个帧是否可以被驱逐。它还控制LRUReplacer的大小。
      当我们实现BufferPoolManager的时候,我们就会知道啥时候调用这个函数。
      具体来说,当一个页面的pin count达到0的时候,其对应的帧就会标记为可驱逐(Evictable)。Replacer的大小也会增加。
    • Size()
      这个方法返回当前在LRUReplacer中可驱逐帧的数量。
  • 实现的方法取决于我们。我们被允许使用内置的STL容器。而且可以假定不会耗尽内存,但必须保证操作是线程安全的(thread-safe)。

Task #3 - Buffer Pool Manager Instance

  • 最后我们需要实现缓冲池管理系统(BufferPoolManagerInstance)。BufferPoolManagerInstance是用来从DiskManager中获取数据库的页的,以及在内存中存储它们。BufferPoolManagerInstance 也可以将脏页(dirty page)写到磁盘上,当它被明确指示的时候,或者当它需要驱逐一个页面来腾出空间给新的页面的时候。(计组和OS的知识)。

  • 为了确保我们的实现能和系统的其他部分正常工作,本项目将提供一些已经填写好的函数。我们也不需要实现向磁盘实际读写数据的代码(这些在我们的实现中被称为: DiskManager)。本项目将提供这一功能。

  • 系统中的所有内存页都由Page对象表示。BufferPoolManagerInstance不需要理解这些页面的内容。但是作为系统开发者,我们必须了解Page对象只是缓冲池中内容的容器,因此并不特定于某个独立的页面。也就是说,每个Page对象都包含一个内存块,DiskManager将把它作为一个位置来复制从磁盘读取的物理页的内容。

  • BufferPoolManagerInstance将重复使用同一个Page对象来存储数据,因为它从磁盘来回移动。这意味着在系统的整个生命周期中,同一个Page对象可能包含不同的物理页。Page对象的标识符(page_id)可以跟踪它所包含的物理页。如果一个Page对象不包含一个物理页,那么page_id被设为INVALID_PAGE_ID

  • 每个页面对象也维护一个计数器,用于记录 Pinned 该页面的线程数量。我们的BufferPoolManagerInstance不允许释放一个被 Pinned 住的页面!每个页面对象也会记录它是否是脏的,我们的工作就是记录一个页面在被接触 Pinned 住之前,是否被修改过。 我们的BufferPoolManagerInstance必须将一个脏页的内容写回到磁盘上。然后该对象才能被重新使用。

  • 我们的BufferPoolManagerInstance实现将使用我们在本作业中前几步中创建的ExtendibleHashTableLRUReplacer类。它将使用ExtendibleHashTable表来映射page_idframe_id。它还将使用LRUKReplacer来跟踪Page对象被访问的时间,这样当它必须释放一个frame以便为从磁盘复制一个新的物理页腾出空间的时候,它就可以决定驱逐哪个。

  • 我们需要实现定义在头文件(src/include/buffer/buffer_pool_manager_instance.h)的源文件(src/buffer/buffer_pool_manager_instance.cpp)

    • FetchPgImp(page_id)
    • UnpinPgImp(page_id, is_dirty)
    • FlushPgImp(page_id)
    • NewPgImp(page_id)
    • DeletePgImp(page_id)
    • FlushAllPagesImpl()
  • 函数的解释:

    • For FetchPgImp, 如果空闲列表没有可用的页面,并且所有的其他页面目前都被Pinned住了,我们应该返回nullptrFlushPgImp应该刷新一个页面,不管它的 pin status 如何。

    • For UnpinPgImp, is_dirty参数记录了一个页面在Pin住的时候是否被修改。

    • 当我们向在NewPgImp()创建一个新的页面的时候,AllocatePage私有方法为BufferPoolManager提供了一个唯一的新页面ID。另一个方面,DeallocatePage()方法是一个模仿释放磁盘上的页面的no-op,我们应该在DeletePgImp()实现中调用,

      原文:
      On the other hand, the DeallocatePage() method is a no-op that imitates freeing a page on the disk and you should call this in your DeletePgImp() implementation.

    • 最后请参考函数文档,来了解如何实现这些函数,以及BufferPoolManagerInstance如何与LRUKReplacer互动的细节。不要碰那些non-impl 版本,本项目需要这些来评分。

Disk Manager

  • 磁盘管理器类(src/include/storage/disk/disk_manager.h), 从磁盘读取和写入页面数据。当我们的缓冲池管理器将在需要向缓冲池获取页面,或者向磁盘刷新页面的时候,使用DiskManager::ReadPage()DiskManager::WritePage()

Detailed Documentation

更多细节可以直接参考头文件。

3. Instructions

Testing

跟项目0相同

$ mkdir build
$ cd build
$ make extendible_hash_table_test -j$(nproc)
$ ./test/extendible_hash_table_test

记得开启Debug模式,顺便检测内存泄露!

Formatting

跟项目0相同

$ make format
$ make check-lint
$ make check-clang-tidy-p1

4. Implemention Note

  • 知识点方面:参考这两篇博客。都讲得很详细了

  • 自己编写的时候:

    • 注意注释的格式,一个tab
    • 注意在extendible hash中的insert,while循环的时候会一直改变index的数值,所以dir_[index]也会变!不能开始让一个值等于这个指针,因为指针会变
posted @ 2023-01-12 18:08  M1kanN  阅读(308)  评论(0编辑  收藏  举报