Lecture#05 Buffer Pool
review: database workloads
OLTP:收集数据,运算快速,每次只读取/更新一小部分数据。(simple,read)
OLAP:查询复杂,对大量OLTP收集数据进行读取分析,以推断出新数据。(complex,write)
HTAP:既做OLTP又做OLAP的数据库系统。
👇标准配置:前端OLTP数据库,后端大型数据仓库。前端做事务处理,后端完成数据分析。
(有时被称为Data silo 数据孤岛,相互独立的数据存储区)
然后可执行ETL操作
:将业务系统的数据经过抽取、清洗转换后加载到数据仓库;数据仓库完成分析后将分析数据传给前端数据库。

HTAP:在OLTP端也要完成一些数据分析操作。这样就不用等待数据仓库完成OLAP后传给前端。数据仓库做数据汇总用。

前端数据库:MySQL、PostgreSQL、MongoDB等你想要的数据库
后端数据仓库:Hadoop、Spark、Greenplum、Vertica、云端数据库(RedShift、Snowflake)
上节中提到,DBMS 的磁盘管理模块主要解决两个问题:
- 如何使用磁盘文件来表示数据库的数据(元数据、索引、数据表等)
- (本节)如何管理数据在内存与磁盘之间的移动
冯诺依曼结构表明,我们只有将数据放入内存后才能进行读写
本节将讨论第二个问题。管理数据在内存与磁盘之间的移动又分为两个方面:空间控制(Spatial Control)和时间控制(Temporal Control)
内存的访问速度更快,并且磁盘 page 的访问读取在时间和空间上具有局部性的特征,所以一次被访问到的 page,加载到内存之后,有可能被再次访问,这样可以避免频繁从磁盘中加载 page。
-
Spatical Control:空间控制策略决定将 pages 写到磁盘的哪个位置,目标是让常一起使用的 pages 能离得更近,从而提高 I/O 效率。
-
Temporal Control:时间控制策略决定何时将 pages 读入内存/写回磁盘,目标是使读写的次数最小,从而提高 I/O 效率。

本节的提纲如下:
- Buffer Pool Manager 及其优化
- Allocation Policies
- Replacement Policies
- Other Memory Pools
1 Buffer Pool Manager
DBMS 启动时会从 OS 申请一片内存区域,即 Buffer Pool
,并将这块区域划分成大小相同的 pages,为了与 disk 的 pages 区别开,通常称为 frames
,当 DBMS 请求一个 disk page 时,它首先需要被复制到 Buffer Pool 的一个 frame 中,如下图所示:

因为Buffer Pool 中 frame 的顺序与 disk 中 page 的顺序无关。因此还需要一个额外的 indirection 层(page table),告知哪个page在哪个frame中。
DBMS 维护的页表 page table
,负责记录每个 page 在内存中的位置,以及是否被修改(Dirty Flag),是否被引用或引用计数(Pin/Reference Counter)等元信息,如下图所示:

当 page table 中的某 page 被引用时,会记录引用数(pin/reference),表示该 page 正在被使用,空间不够时不应该被移除;当被请求的 page 不在 page table 中时,DBMS 会先申请一个 latch,表示该 entry 被占用,然后从 disk 中读取相关 page 到 buffer pool,释放 latch。以上两种过程如下图所示:

1.1 locks 🆚 latches
locks
- 保护 数据库逻辑内容(例如元组、表、数据库)不受其他事务的影响。
- 在事务持续期间保持。
- 需要能够回滚更改。
latches
- 保护 DBMS内部数据结构 的关键部分不受其他线程的影响。
- 操作期间保持。
- 不需要能够回滚更改。
lock是数据库中更高级的逻辑原语,latch是底层保护原语
1.2 page table 🆚 page directory
page directory:数据库文件中 page id 到 page 位置的映射
- 是告诉执行引擎数据在哪个page的哪个slot(即offset)的关键索引,所有改变都应被记录在磁盘上(持久化),以保证DBMS重启时能找到
page table:是page id 到 buffer pool 中 frame 的映射
- 是个在内存中的数据结构(hash表),因此无需存储在磁盘上(无需保证持久化,丢失了重新建立一个就好,但要线程安全)
因此 page table 可用 hashtable 或 hashmap 实现
2 Allocation Policies
Allocation Policies 指 DBMS 如何为不同的查询分配缓冲池内存:
全局策略
(Global Policies):同时考虑所有查询和事务来分配内存局部策略
(Local Policies):为单个查询/事务分配内存frames时不考虑其它查询的情况。可以让特定的事务表现更好,当然全局上可能会有糟糕的表现。
大多数DBMS都会尽量兼顾全局和局部信息来进行分配。
3 Buffer Pool 优化
简单的缓存池做到上面的内容就够了,但是对于实际的生产其表现远远不够,因此需要对其进行特定的优化。下面展示一些Buffer Pool的优化方法:Multiple Buffer Pools、Pre-fetching、Scan Sharing、Buffer Pool Bypass。
3.1 Multiple Buffer Pools(复数缓存池)
实际上没有规定一个数据库只能有一个Buffer Pool,我们可以分配多块内存区域以支持多个Buffer Pool,每个区域可以有自己的页表,并且自己一套page id和frame的映射关系,这样可以更好地运用局部策略,并且根据不同的类型来分配page的置换策略(例如index或者table data),这样也能减少访问缓存池的不同线程之间对于页表锁的争夺。
为了减少并发控制的开销(减少那些试图访问Buffer池的不同线程间争抢Latch的情况)并改善数据的局部性,DBMS 可以在不同维度上维护多个 Buffer Pools:
- 多个 Buffer Pools 实例,相同的 page hash 到相同的实例上
- 每个 Database 分配一个 Buffer Pool
- 每种 Page 类型一个 Buffer Pool
实现 multiple buffer pools 的方法:
- 在 record id 中添加一个 objectId
- 使用 hash 将 page id 映射到不同 buffer pool


3.2 Pre-fetching(预取)
这是一种大胆的猜测(实际上有迹可循)。对于顺序的扫描或者索引的扫描,我们可能会读一大段连续的page,那么我们可以一次性把许多连续的 page 预存在缓存池中,这样就不用每次只将一个 page 加载进来而是一次性加载,这样可以减轻很多负担。
希望最小化数据库系统的停顿,则DBMS 可以通过查询计划来预取 pages,如:
- 顺序扫描 Sequential Scans
- 按索引扫描 Index Scans:需要额外记录一些元数据(index-page按照二叉搜索树的方式去组织)

从index-page0开始,假设我想查找value范围在100到250的所有结果:我会跳到page1,再到叶子结点。
这也是为什么DBMS要自己管理内存的原因,DBMS知道这个操作要做什么,因此可以做对应的优化。
3.3 Scan Sharing(扫描共享)
Scan Sharing 技术主要用在多个查询存在数据共用的情况。当两个查询 A, B 先后发生,B 发现自己有一部分数据与 A 共用,于是先共用 A 的 cursor,等 A 扫完后,再扫描自己还需要的其它数据。
⚠️这与结果缓存(result caching)不同,结果缓存指——我计算出某些结果并将之缓存起来,再遇上相同的查询时直接使用,而不需要重新执行查询。通常情况下,result caching中,查询必须一样;但scan sharing中,查询不一定要一样。
Oracle支持一种基本的 Scan Sharing,他们称之为 Cursor Sharing(游标共享)
3.4 Buffer Pool Bypass(跳过Buffer Pool)
当遇到大数据量的 Sequential Scan 时,如果将所需 pages 顺序存入 Buffer Pool,将造成后者的污染,因为这些 pages 通常只使用一次,而它们的进入将导致一些可能在未来更需要的 pages 被移除。因此一些 DBMS 做了相应的优化,在这种查询出现时,为它单独分配一块局部内存,将其对 Buffer Pool 的影响隔离;当查询完成时,所有这些page就会被丢弃。
3.5 Buffer Pool 🆚 OS Page Cache
大部分 disk operations 都是通过操作系统调用完成,通常操作系统会维护自身的数据缓存,这会导致一份数据分别在操作系统和 DMBS 中被缓存两次。是否要对OS提供的缓存进行利用就是一种取舍。大多数 DBMS 都会使用 (O_DIRECT) 来告诉 OS 不要缓存这些数据,除了 PostgresSQL。
4 Buffer Replacement Policies
当 Buffer Pool 空间不足时,读入新的 pages 必然需要 DBMS 从已经在 Buffer Pool 中的 pages 选择一些移除,这个选择就由 Buffer Replacement Policies 负责完成。需从以下四因素选择合适的置换算法:
- 正确性(Correctness):操作过程中要保证脏数据同步到 disk
- 准确性(Accuracy):尽量选择不常用的 pages 移除
- 速度(Speed):决策要迅速,每次移除 pages 都需要申请 latch,使用太久将使得并发度下降
- 元数据负荷(Meta-data overhead):决策所使用的元信息占用的量不能太大
4.1 LRU 最近最少使用算法
维护每个 page 上一次被访问的时间戳,每次移除时间戳最早的 page。
我们可以维护一个单独的数据结构,比如queue,它根据page的时间戳进行排序;若某时刻有人对某个page进行读或写,则将此page从queue中取出并放入队列尾部(因为队列是先进先出的)
4.2 Clock 算法
Clock 是 LRU 的近似策略(二者性能接近),它不需追踪每个 page 上次被访问的时间戳,而是为每个 page 保存一个标志位 reference bit,它告诉你自从上次检查完该page后,此page是否被访问了。
- 每当 page 被访问时,reference bit 设置为 1
- 每当需要移除 page 时,从上次访问的位置开始,按顺序轮询每个 page 的 reference bit,若该 bit 为 1,则重置为 0;若该 bit 为 0,则移除该 page
4.3 LRU 与 Clock 的问题
二者都容易被 sequential flooding (顺序溢出)现象影响,从而导致最近被访问的 page 实际上却是最不可能需要的 page。
顺序读取所有page,则buffer pool会被只读了一次之后再也不会读取的page污染。即在某些特定的workload下,我们想移出的page是那些最近被使用的,而不是最近最少被使用的。
我们可以有多种办法来解决这个问题:
- LRU-K:记录页面最后k次访问的时间戳并且计算时间戳的间隔
- 局部策略:对不同的访问采用不同的内存池
- 优先级提示:这也是需要一些先验知识。如果例如我们访问的是index,而index-page按照B+树或者有组织的形式存储,我们就可以按照这个信息来帮助页面置换信息的判断。
4.3.1 LRU-K
LRU-K 保存每个 page 的最后 K 次访问时间戳,利用这些时间戳间隔来估计它们下次被访问的时间,时间间隔最长的page是最近最少被使用的。通常 K 取 2 就能获得很好的效果。
4.3.2 Localization
使用多个buffer池,让每个查询本地化。DBMS 针对每个查询做出移除 pages 的限制,使得这种影响被控制在较小的范围内,类似 API 的 rate limit。
4.3.3 Priority Hints
有时候 DBMS 知道每个 page 在查询执行过程中的上下文信息,因此它可以根据这些信息判断一个 page 是否重要。
4.4 Dirty Pages
移除一个 dirty page 的成本要高于移除一般 page,因为前者需要写 disk,后者可以直接 drop,因此 DBMS 在移除 page 的时候也需要考虑到这部分的影响。除了直接在 Replacement Policies 中考虑,有的 DBMS 使用 Background Writing (后台写)的方式来处理。它们定期扫描 page table,发现 dirty page 就写入 disk,在 Replacement 发生时就无需考虑脏数据带来的问题。
⚠️在将 dirty page 写入 disk之前,要确保该 dirty page 对应的修改操作写入日志。这对于数据的一致性和rollback非常的重要(这也就是所谓的 WAL
,Write Ahead Logging)。
5 Other Memory Pools
除了存储 tuples 和 indexes,DBMS 执行查询等操作时还需要 Memory Pools 来存储其它数据,如:
- Sorting + Join Buffers
- Query Caches
- Maintenance Buffers
- Log Buffers
- Dictionary Caches
总结
DBMS 管理数据在内存和磁盘间的移动,需考虑 page 的访问在时间和空间上的局部性特征,以提高 I/O 效率。(一次被访问到的 page 加载到内存之后,有可能被再次访问,这样可以避免频繁从磁盘中加载 page)
- 空间控制策略:将 pages 写到磁盘的哪个位置,使常一起使用的 pages 能离得更近。
- 时间控制策略:何时将 pages 读入内存/写回磁盘,使读写的次数最小。
DBMS 启动时向 OS 申请一块共享内存区域 —— Buffer Pool
,用于对磁盘上的 page 进行缓存,尽量减少磁盘 IO,提升 DBMS 性能。该区域被划分为多个与 page 大小相同的小块,称为 frame
,用于缓存 page。此外,还维护一个哈希表 page table
,存储 page id 到 frame 的映射,以及一些 page 的元数据信息,如 page 是否为脏页,page 的引用计数 pin count 等。若请求的 page 不在 page table 中,DBMS 会先申请一个 latch,锁住该 entry,然后从 disk 中读取相关 page 到 Buffer Pool,释放 latch。
page table 的访问,一般要保证并发安全,因为在多线程环境下,对于同一个内存中 frame 的读写不能同时进行。
DBMS 如何为不同的查询分配缓冲池内存,涉及Allocation Policies:
- 全局策略:同时考虑所有查询和事务来分配内存。
- 局部策略:为单个查询/事务分配内存frames时不考虑其它查询的情况。可以让特定的事务表现更好,当然全局上可能会有糟糕的表现。
- 大多数 DBMS 都兼顾全局和局部信息来进行分配。
当 Buffer Pool 空间不足时,读入新的 pages 必然需要 DBMS 从已经在 Buffer Pool 中的 pages 选择一些移除,这个选择就由 Buffer Replacement Policies 负责完成。具体算法有:LRU、Clock。二者都存在 sequential flooding (顺序溢出)问题,可采用 LRU-K、Localization (局部策略)、Priority Hints (优先级提示) 解决。
- 顺序溢出:大量的顺序扫描 (sequential scan) 会使 buffer pool 被只读了一次之后再也不会读取的 page 污染。从而导致最近被访问的 page 实际上却是最不可能需要的 page。
- 局部策略:使用多个 buffer 池,让每个查询本地化,DBMS 针对每个查询做出移除去 page 的限制。
- 优先级提示:记录每个 page 在查询执行过程中的上下文信息,根据这些信息判断该 page 是否重要。
移除一个 dirty page 的成本要高于移除一般 page,有的 DBMS 使用 Background Writing (后台写) 的方式来处理。它们定期扫描 page table,发现 dirty page 就写入 disk,在 Replacement 发生时就无需考虑脏数据带来的问题。
- ❗️ 在将 dirty page 写入 disk 之前,要确保该 dirty page 对应的修改操作写入日志——
WAL
实际生产中有很多对 Buffer Pool 的优化方法:
- Multiple Buffer Pools (复数缓存池):在不同维度上维护多个 Buffer Pool,相同的 page hash 到相同的实例上。🌰 每个 database / 每种 page 类型,分配一个 Buffer Pool 等。
- Pre-fetching (预取):对于顺序扫描或者索引扫描,可能会读一大段连续的 page,因此可一次性把许多连续的 page 预存在缓存池中。
- Scan Sharing (扫描共享):多个查询存在数据共用时,可共用 cursor。
- 🆚 result caching (结果缓存):计算出某些结果并将之缓存起来,再遇上相同的查询时直接使用,而不需要重新执行查询。结果缓存中,查询必须一样;扫描共享中,查询不一定要一样。
- Buffer Pool Bypass (跳过 Buffer Pool):大量数据的顺序扫描会导致 Buffer Pool 的污染,因此这种查询出现时,可为它单独分配一块局部内存,隔离它对 Buffer Pool 的影响。查询完成后,就丢弃这些 page。
page table 🆚 page directory:
- page directory:数据库文件中 page id 到 page 位置的映射。是告诉执行引擎数据在哪个 page 的那个 slot(即 offset)的关键索引,所有改变都需持久化到磁盘,以保证DBMS重启时能找到。
- page table:是 page id 到 buffer pool 中 frame 的映射。是个在内存中的 hash表结构,无需存储在磁盘上(无需保证持久化,丢失了重新建立一个就好,但要线程安全)。
lock 🆚 latch:
lock:保护 数据库逻辑内容 (例如元组、表、数据库)不受其他事务的影响。在事务持续期间保持,能够回滚更改。
latches:保护 DBMS内部数据结构 的关键部分不受其他线程的影响。操作期间保持,不能够回滚更改。
本文作者:Joey-Wang
本文链接:https://www.cnblogs.com/joey-wang/p/16915995.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步