当前位置: 首页 > 科技观察

CMU15445学习BufferPool

时间:2023-03-13 22:12:55 科技观察

什么是bufferpool磁盘管理?BufferPool本质上是一块共享内存区域。其目的是将页面缓存在磁盘上,最大限度地减少磁盘IO,提高数据库系统的性能。前面讲存储模块时提到,内存访问速度较快,磁盘页的访问具有时间和空间局部性的特点。因此,访问过一次的页面加载到内存后,有可能再次访问,避免了频繁从磁盘加载页面。缓冲池将内存区域划分为称为帧的小块,每个帧缓存磁盘的页面内容。另外还需要维护一个页表,它是一个哈希表,里面保存了一个pageid和bufferpool中frame的映射关系,还需要保存page的一些metadata信息,比如是否页面是脏页,页面Referencecounting的数量多。对于页表访问,一般需要保证并发安全,因为在多线程环境下,不能同时对同一内存中的帧进行读写。PostgreSQL中缓冲池的说明和代码可以参考:https://github.com/postgres/postgres/blob/master/src/backend/storage/buffer/README缓冲池优化缓冲池的基本概念是之前简单解释过。在实际使用中,会对缓冲池进行各种优化,下面依次介绍常见的。MultipleBufferPool缓冲池不是由一块内存划分的。不同的数据库系统有不同的用法。大多数系统已经实现了可以开启多个缓冲池,避免多个线程在同一个缓冲池上竞争锁,可以大大提高数据读写的效率。具体实现可以多样化,例如:创建多个缓冲池实例,每个数据库分配一个相同类型的缓冲池磁盘页,分配一个缓冲池。一般来说,元组到缓冲池的映射有两种实现方式:objectid和hashing。ObjectID一般可以是tuple的一个隐藏列(比如PostgreSQL中叫做ctid),主要记录了tuple的磁盘存储位置。根据这个objectid,我们就可以知道我们需要访问哪个bufferpool,然后拿到里面的page进行数据读写。另一种散列方式是对元组的一些信息进行散列运算,然后定位到某个缓冲池上。Pre-Fetchingprefetch一般称为预读。例如,在执行查询时,如果某个页面不在缓冲池中,我们就需要加载该页面。这时候我们可以使用一些预读策略,将页面提前记录在缓冲池中。这样下次查询的时候就不需要读盘了,可以提高整体的响应性能。拿一个简单的顺序扫描来说明。例如下图中,加载所有页面顺序扫描表格。如果没有prefetch,加载一个页面后,上层执行引擎处理完,再加载另一个页面。这样每次加载的时候都会等待页面加载完成,效率较低。如果是索引扫描,叶子节点指向的页面可能不连续,但是作为内部数据库系统,我们可以识别出来,将需要的页面加载到缓冲池中(这也是前面提到的数据库系统)前一篇文章一般情况下内存会自己管理,不依赖mmap)。比如下图中,叶子节点的页数分别为3和5。在这种情况下,加载时会跳过中间不需要的页面。ScanSharingscansharing的思路一般是,如果一个query要扫描一个表,而此时另一个query已经在扫描这个表,那么可以将两个query的操作结合起来,共享同一个表的内容页。大多数数据库都实现了这种优化,例如SqlServer、Oracle、PostgreSQL。下面是一个简单的例子,假设有一个查询需要全表扫描,表内容分布在0-5页,那么它会依次将所有页读入缓冲池。这个时候BufferPool就满了。根据淘汰策略,在加载page3时,需要淘汰一个页面。这里我们假设page0被淘汰。page0被淘汰后,再加载page3,此时如果有另外的查询,也需要对这张表进行全表扫描。没有任何优化,它也是从头开始读写这张表的所有页。首先它还会加载page0,但实际上page0只是从BufferPool中替换出来的。因此,一种优化策略是将两个查询的扫描指针合并,共享同一个BufferPool的内容。在上一个完成后,第二个查询将继续扫描之前未扫描的页面。BufferPoolBypass在一些特殊情况下,我们可能不需要BufferPool,比如顺序扫描磁盘页,如果我们需要加载的磁盘页分布是连续的,我们可以直接加载磁盘数据,因为是顺序IO,性能是仍然很好,并且节省了换入和换出缓冲池的开销。PostgreSQLdemo下面以postgres为例,讲解数据库缓冲池的具体行为。postgres的默认缓冲池大小为128MB。查询时可以加上explain(analyze,buffers)前缀打印sql的执行时间和详细计划,以及bufferpool的使用情况。系统第一次启动时,缓冲池中没有数据,查询需要从磁盘中获取所有表数据。可以看下面的例子:read55140表示从磁盘中获取的页数。当我们再次运行这个查询时,由于之前的查询中已经缓存了部分数据到bufferpool中,相应的执行计划会发生一些变化:sharedhit是指bufferpool中的数据被命中,高于bufferpool中的数据。之前的查询从磁盘中获取的页面较少。缓冲池淘汰策略由于缓冲池是一块容量有限的内存区域,其大小通常远小于磁盘上存储的数据容量,当缓冲池满时,如果有新的数据要加载,则适当淘汰和替换策略,以确保删除旧数据并插入新数据。LRUlru代表LeastRecentlyUsed,最近最少使用原则。这是一种应用广泛的缓存淘汰算法,思想比较简单直观。可以为每个页面分配访问时间戳,并且当访问页面时,时间戳被更新。当您需要淘汰旧页面时,只需选择最长时间未访问的页面即可。时钟时钟淘汰算法与LRU的思想类似,只是实现方式不同。该算法把页面区域看成一个环,每个页面都有一个标记,称为引用位,初始为0。如果访问该页,引用位会被设置为1。一般会设置一个定时任务,并且然后我们可以按顺序扫描每一页。如果该位值为1,表示该页已被访问,则将该位复位为0。如果该位为0,则表示该页未被访问,直接清空该页。LRU-K前面提到的LRU算法虽然思路简单,但是还是存在一些问题。如果将频繁访问的热点页面替换为短时间内仅访问一次的页面,则缓存命中率会降低。这种情况通常称为缓存污染。因此,我们可以提高页面访问次数的上限,只有达到k次才更换其他页面,这样就不难理解,传统的LRU算法可以看成是LRU-1。Localiztion的淘汰策略也可以缓解LRU可能带来的缓存污染问题。它实际上类似于上面提到的多缓冲池。当多个查询进行时,它可以从全局缓冲池中获取页面数据,但是当淘汰数据时,它可以自己维护一个缓冲池,并且淘汰这个缓冲池中的数据不会影响全局缓冲池。例如,PostgreSQL在顺序表扫描期间维护本地环形缓冲区缓存。脏页最后,我们来看一个简单的概念脏页。Dirtypage,即脏页,是指缓存在缓冲池中的数据已经被修改,但是还没有来得及写入磁盘。每个页面都有一个相应的标志来表示它是否是脏页。如果一个页面不是脏页,系统在更换页面时可以直接将其从缓冲池中移除;否则,需要持久化页面中的数据。一般我们可以启动一个后台进程来定时处理脏页。将脏页数据写入磁盘后,我们可以将脏页从缓冲池中移除,或者直接重置该页的脏页标志位。本节介绍内存缓冲区的管理、淘汰策略,以及它如何与磁盘页面交互。下一节开始讲索引。