背景介绍
众所周知,计算机中CPU对于硬件磁盘的访问速度远远慢于内存中的速度,为减少对持久性储存的访问次数,在操作系统中引入缓冲区/缓冲池的概念:它将物理文件页读入缓冲区种,当CPU再次访问时只需要读取该缓冲液即可,无需持久性存储进行IO操作,大大提高了访问效率;其缺点是当数据库崩溃时,缓冲区的内容会丢失。同样的对于绝大多数数据库而言,缓冲区/缓冲池管理是其很重要的组件之一。
将未缓存的页面从磁盘中加载进来的过程称为换入(page in),相反的,将修改的缓冲页刷写回磁盘称为换出(page out)。由于缓冲区通常是在内存中申请,所以其容量大小远远小于实际物理文件大小,故缓冲区终究会被填满。为了后续新页的读入,必须将其中的某页换出缓存。当然我们不能随意的将缓存中的某页给置换出去,因为如果该缓存页上包含热点数据,访问十分频繁,这会导致大部分时间浪费在换入换出,并没有进行实际的工作,因此需要针对不同的情况设计不同的页面置换算法)
来淘汰某缓存页。
1 缓冲页的功能
缓存页的功能如下:
- 在内存中保留被缓存的页内容;
- 把对磁盘页的修改缓存起来,在未刷盘前修改的是缓存版本;
- 当被请求的页不在内存中时且内存空间可用时,数据页会被换人缓冲区中;
- 如果缓冲区填满放不下新读取的数据页时,则会通过页面置换算法将其中的某块换出,被换出的缓冲页会被刷至磁盘。
2 缓冲区的管理机制
当存储引擎访问页时,首先检查页内容是否已存在于缓存中,如果存在则直接返回缓存页;如果未被缓存,则将页缓存的逻辑地址或页号转变成物理地址,然后将其内容载入内存中,最后将缓存版本返回给存储引擎。一旦返回,这个缓存页就被称为被引用的(referenced),存储引擎必须在使用完之后将其归还给缓冲区或者解除引用。
如果某个缓存页被修改,则将该页标记未脏页。表示其内容与物理页不同步,必须将其刷写至磁盘以保证持久性。
3 页面置换算法
当缓存容量使用完之后,为了加载新页,必须换出旧页。但是,除非我们换出最近不太可能再次访问的数据页,否则这些页面会反复地被载入–花费很长时间进行IO操作
为了实现高效的页缓存,需要一种有效的页面置换算法–它能够预测未来缓冲池中缓冲页的访问次序,仅换出那些在最长时间被不会被使用的缓存页。其中最典型的有先进先出FIFO、最近最少使用LRU和LFU页面置换算法。
3.1 FIFO页面置换算法
FIFO通过插入顺序维护一个页ID的队列,将新页添加至队列尾。当缓冲区满时,从队列头取出元素,也就是最早进入的页。由于它不考虑后续页的访问顺序,仅考虑页的换入时间,该策略对于大多数真实系统是不切实际的。页面置换算法)例如,根节点和最上层节点所对应的页最先被换入,根据此算法这些页最先被换出缓存,但是很明显这些页会很快被再次换入[在磁盘中,大多数系统采用B树及其变体作为存储结构]。
3.2 LRU页面置换算法
针对FIFO算法的缺点,研究者提出LRU策略:根据插入顺序维护一个换出候选队列,但是在重复访问该页时,LRU会将其放入队列尾部,就像首次换入时一样。然而,在高并发场景下,每次访问需要更新引用和重新链接节点可能会付出较大代价。
因此,为进一步改善效率,学者提出CLOCK SWEEP算法:该算法将页的引用和与之关联的访问位保存在环形缓冲区中。一些算法变体使用的计数器而不是比特位来描述频率。每当访问某个页面时都将其访问位置为1,访问结束后将其置为0;在postgres数据库中,共享缓冲区的页面置换算法即采用的是CLOCK SWEEP,实现原理入下图所示:
注释:
1 深蓝色的缓冲页其含义是被其他进程引用,即pin;
2 浅蓝色的缓冲页其含义是此时未被引用,即unpin;
3 缓冲页中的数字代表其最近被使用的次数;
淘汰流程:
a 首先查看缓冲环中的第一个缓冲页,发现该缓冲也被pin住[被引用的缓冲区其使用次数不发生变化],于是跳过,时钟指针顺时针旋转至下一个缓冲页;
b 发现第二个缓冲页未被pin,且该缓冲页最近的使用次数为2,于是将其使用次数减1,继续顺时针探测;
c 探测到第三个缓冲页,发先其unpin,其最近的使用次数为0,因此该缓冲页即为淘汰页。
根据上述规则,只要缓冲环存在未引用的缓冲页,最终一定能够找到换出页。
优点:时钟指针和缓冲页内容都可以采用CAS原子操作来实现,无须额外的加锁操作,而且该算法实现简单。)
LRU并非对于所有的数据库来说都是最佳的置换策略。有时,考虑使用频率而非最近使用时间作为预测因子更贴合实际。最后对于负载很重的数据库系统,LRU参考性不是很大,因为他只是表示项目被访问的顺序。
3.3 LFU页面置换算法
为改善这一情况,可以通过追踪引用事件而不是页换入事件。其中一种实现是跟踪具有最小使用频率的页。
TinyLFU是一种基于频率的页置换策略,它通过使用频率直方图来维护一个缓存访问历史记录,其元素可能位于以下三个队列的某一个:
- 入场队列(admission):维护新加入的元素,用LRU策略实现;
- 考察队列(probation) : 其中的元素最有可能被换出;
- 保护队列(protected) : 其中的元素将在队列中保留更长的时间。
TinyLFU不是选择要换出哪些元素,而是选择保留哪些元素。访问频率相对较高的元素会被晋升到考察队列中。在后续访问中,元素可以从考察队列移至保护队列。如果保护队列已满,则必须将其中的某元素放入考察队列中。经常被访问的元素有较高的机会被保留,而不经常被访问的元素更倾向被换出。
图1 入场队列/考察队列/保护队列以及频率过滤器与换出之间的逻辑关系