最后一个优化是绕过缓冲池(buffer pool bypass)
对于顺序扫描运算符,它要扫描每一页,但是这些页仅仅一次扫描之后就立刻用不到了,如果都加载到缓冲池的话,会严重影响执行效率,并且污染缓冲池。
在 Informix 这个系统中叫做轻量扫描(Light Scans)。并且 Oracle,SQLServer,PostgresSQL 中也有这种优化机制。
下一个我们要讲的是,如果缓冲池满了,在缓冲池中替换页的不同策略,在此之前,我们先来看下操作系统中的机制:
我们要执行的大多数磁盘操作都是通过操作系统提供的 api,我们讲过mmap()或者read()和write(),对于这些 api 除非你明确指定其中的参数告诉操作系统,否则操作系统会维护自己的文件系统缓存,它通常被称为页面缓存(Page Cache)。简单来说:当你请求从磁盘读取一个页面,如果它还没有加载,就从磁盘中获取它加载到页面缓存中,然后返回一个指向你的页面的指针,之后你必须从操作系统页面缓存复制到用户空间。所以你会有一个多余的复制,一个存储在操作系统页面缓存中,一个存储在你实现的缓冲池中,所以大多数 DBMS 所做的是使用 O_DIRECT 标志来绕过操作系统的页面缓存。
我们可以采用不同的策略来确定当缓冲池填满时,我们需要腾出一个帧,以便插入一个新页。我们如何决定从缓冲池中删除哪些页呢?我们要考虑不同的方面:
- 正确性(Correctness):我们不想有任何数据被破坏的问题,例如,扔掉我们没有正确地写出的脏页。
- 准确性(Correctness):即查询结果是查询想要查的数据
- 速度(Speed):缓冲池过期需要保证查询速度,也需要考虑能快速决定哪些帧被替换,如果你花了所有的时间思考去掉哪个页最合适,那么你花在这上面的时间可能比你从智能算法中得到的好处还要
- 元数据大小(MetaData):我们需要担心的是为了进行页删除,我们存储了多少元数据,不能太多
这是最简单的一种算法,LRU(Least Recently Used,最近最少使用策略),它在很多不同的系统领域被使用。简单的实现方式是为每个页面维护一个时间戳,记录它最后一次被查询访问的时间。当 DBMS 需要删除一个页时,这很简单,我们只需要找到时间戳最早的页面,也就是最近访问最少的页面。
这是一种通用的依赖于你最近使用过的页,你最近访问过的页,很快就会再次被使用这个假设的算法。
另一个策略是时钟策略(Clock Policy),和 LRU 有点不同,这也是经常被用到的一种策略。它的实现方式通常是在每页上分配一个标记位,这个引用位可以是 0 或者 1。如果这个页面被访问到了,就会标记为 1。我们有一个时针会不断的扫描每一页,如果时钟扫描的这一页标记位为 1,那么就会更新为 0,如果本来就是 0 了,就会把这个页面过期掉。过这个算法给你带来的好处是不用维护一个完整的时间戳(每页占用4个字节或者8个字节),这个算法每页只需要一个比特标记位,所以空间的消耗会小很多。
LRU 以及时钟策略都有一些问题,它们很容易受到连续洪流(Sequential Flooding)的影响。就像前面提到的,你要做的主要假设是你最近使用过的页,你最近访问过的页,很快就会再次被使用。这对于倾斜的访问模式很好,但是如果查询执行顺序扫描,需要读取每个页,会造成只读一次然后就再也不会看的页污染缓冲池。
你可以使用一些更好的策略,例如LRU-K,它不是跟踪某件东西是否被访问,也不是跟踪某件东西最近被访问的时间戳,你要看最后 K 次引用的历史。假设 K 现在是 2,即你记录了最近当某东西被访问时的两个时间戳,然后你就可以计算出以后访问的间隔。如果这个间隔时间比较长,那么就不能经常使用,我们可以把它扔掉。如果间隔时间短得多,证明它经常被访问*,我们可能会想保留它。
同时,你还可以做更高级的优化,例如:根据算法我们算出这页平均访问间隔大概是10分钟左右,你可以期望10分钟后,我需要这个页,这样你可以做一些更智能的预取。
但是,这个算法也会带来更多的元数据开销,这是我们需要权衡的。
另一种选择是使用一些本地化的策略,即以某个查询或者事务的基础决定缓冲池的过期策略,例如 Postgres 就在每个查询或事务的级别维护一个独立的唤醒缓冲池,
另一个更好的策略可能是根据不同的访问模式提供优先级提示(Priority Hints),例如,DBMS 知道访问索引和顺序扫描的访问模式的区别,因此,您可以向缓冲池提供一些提示说明哪些页面是重要的,哪些页面是我们可能不关心的。
举个例子,假设我们正在插入一些连续的 id,因为这些值是单调递增的,所以它们总是会被插入到树的右边;如果你想要执行一次扫描,你的访问路径可能会不同,但它们有一个共同点就是你总是从这个根页开始。你可以向缓冲池管理器提供的一个提示是,缓冲池保留这个根页,这两种类型的查询都需要它。
如果你的缓冲池中有一个不脏的页面,当它需要从缓冲池中剔除的时候,你可以直接删除它,覆盖它,我们不需要保留它。因为没有任何变化,它备份在磁盘上,如果我们需要我们总是可以从磁盘恢复它。但是如果是脏页的话,你需要将脏页的更新写回磁盘以保证更新的持久化。
在快速缓冲页驱除和持久写脏页之间有一种权衡。
为了减少持久化脏页的时候带来的写磁盘的 I/O 阻塞,一般会有一个后台写过程(Background writing process)。DBMS可以定期遍历页表并将脏页写入磁盘。
除了元组和索引缓冲池之外 DBMS 还需要内存来管理其他东西,包括:
- 排序与连接缓冲
- 查询缓存
- 维护缓冲
- 日志缓冲
- 词典缓冲
这些元素有些可能有底层持久化到磁盘的元素,有些只存在于内存中,我们在后面的课程可能也会涉及到。