【数据库设计与实现】第7章:缓存与检查点

缓存与检查点

设计原则

数据缓冲区与检查点是相辅相成的,所以放在同一个章节介绍。由于CPU与持久化设备之间存在巨大的速度差距,所以在内存中引入缓冲区缩小这个差距。从读的角度来看,将热点数据或预判用户可能读取的数据提前加载到内存中,从而将持久化设备的读时延和带宽提升至内存的时延和带宽。从写的角度来看,直接修改缓冲区中的数据而不是磁盘中的数据,可以带来两方面的优势。其一,将持久化设备的写时延和带宽提升至内存的时延和带宽。其二,将多次数据写合并为一次对持久化设备的写。

数据缓冲区将写缓存在内存中,并通过顺序写的REDO日志确保事务的数据不丢失,即事务的持久性。然而,随着写事务的不断进行,可能带来多方面问题,如日志文件持续增长,系统重启需要恢复的时间不断增加,脏块或脏页持续增多导致内存空间不足等等。这就需要通过检查点机制定期将脏块或脏页写入到持久化设备中,从而降低REDO日志长度。

在设计数据缓冲区和检查点时,有如下几点需要考虑:

  • 缓冲区的效率:在多样性负载(不同数据大小、不同负载类型)、并发和LRU算法方面的效率;
  • 缓冲区的弹性:缓冲区大小的可调节性,即随着负载的变化的调节能力;
  • 检查点的平衡性:体现为脏块或脏页持久化与系统恢复时长之间的平衡能力,写脏块或脏页过于频繁会与正常负载争抢资源,而过于稀少则导致系统恢复时间过长;

Oracle设计原理

缓冲区与granule

表7.2-1 缓冲区类型和控制参数

缓冲区类型

控制参数

说明

db_cache

db_cache_size

默认缓冲区大小,systemsysauxtemporary表空间都使用默认block   size,默认block size表空间对应的缓冲区大小由db_cache_size设置

db_2k_cache_size

除默认的block   size外,还可以创建block size2k的表空间,该类表空间对应的缓冲区大小由db_2k_cache_size设置

db_4k_cache_size

除默认的block   size外,还可以创建block size4k的表空间,该类表空间对应的缓冲区大小由db_4k_cache_size设置

db_8k_cache_size

除默认的block   size外,还可以创建block size8k的表空间,该类表空间对应的缓冲区大小由db_8k_cache_size设置

db_16k_cache_size

除默认的block   size外,还可以创建block size16k的表空间,该类表空间对应的缓冲区大小由db_16k_cache_size设置

db_32k_cache_size

除默认的block   size外,还可以创建block size32k的表空间,该类表空间对应的缓冲区大小由db_32k_cache_size设置

db_keep_cache

db_keep_cache_size

keep缓冲区,用于存放热点数据,提高命中率,该缓冲区的block size为默认block   size,缓冲区的大小由db_keep_cache_size设置

db_recycle_cache

db_recycle_cache_size

recycle缓冲区,用于存放一次性数据,降低对热点数据的影响,该缓冲区的block size为默认block   size,缓冲区的大小由db_recycle_cache_size设置

缓冲区用于在内存中缓存block,从而提高数据库的访问效率。如表7.2-1所示,Oracle设计了三大类缓冲区:db_cachedb_keep_cachedb_recycle_cachedb_cache用于缓存正常的数据,db_keep_cache用于缓存热点数据,而db_recycle_cache用于缓存一次性数据。Oracle设计三类缓冲区的目的是让应用有机会根据业务的特点将不同的数据缓存在不同的缓冲区中(创建表时通过storage子句指定),从而降低不同类型数据之间的相互影响,提高内存命中率。当然这三类缓冲区在各自的内存空间不足时都会采用LRU算法进行block淘汰。

db_cache是最常用的普通缓冲区。由于业务诉求的多样性,block size可以是2k4k8k16k32k,所以普通缓冲区又可以进一步细分为5种不同block size的缓冲区,大小分别由db_Nk_cache_size设置(N可以是2481632)。当然,这五种缓冲区中有一个是默认缓冲区(即block size为默认block size的那个缓冲区),该缓冲区的大小不是由db_Nk_cache_size设置的,而是由db_cache_size设置的。不过考虑到系统复杂度,db_keep_cachedb_recycle_cache没有设计过多的block size,仅支持默认的block size

除了手工配置上述缓冲区大小之外,Oracle还支持AAMAutomatic Memory Management)功能,可以根据负载情况自动动态调节各类缓冲区的大小。达成自动调节的基础是granule机制,其原理是将内存按照固定大小划分为一个个granule内存块,自动调节就是以granule为粒度将一个内存块从某个缓冲区调节给另外一个缓冲区(share pool和缓冲区之间的内存也可以自动调节)。

图7.2-1 db cache缓冲区与share pool共享池

【数据库设计与实现】第7章:缓存与检查点 

如图7.2-1所示,db cacheshare pool都是由granule内存块构成的。图中上半部分和下半部分相比,AMMgranule3db cache缓冲区调节给share pool共享池。可见每个缓冲区或者共享池等内存使用者都是一个由granule组成的链表,调节内存实际上就是将一个或多个granule内存块从一个链表移到另一个链表中。系统启动时会创建并维护一个granule Entry的数组,数组成员中的关键元素如下:

  • Granuum:数组的下标,每个granule内存块都对应于数组中的一个元素;
  • Baseaddr:指针地址,指向和本数组元素相对应的granule内存块的地址;
  • Granutype:本granule内存块归属于哪个内存使用者,0 free2 share pool3 large pool4 java pool5 buffer pool
  • Granustategranule内存块的状态,allocated表示已经申请,invalid表示尚未申请;
  • Granuprev:将Granutype相同的数组元素链接在一起,指向前一个数组元素;
  • Granunext:将Granutype相同的数组元素链接在一起,指向下一个数组元素;

通过x$ksmge可以查看granule entry的使用情况。Oracle在每个granule内存块中还会维护一个granule header,通过granule header以及granule entry就可以将所有内存管理起来,并根据负载情况,在各内存使用者之间动态调配。

图7.2-2 granule内存结构(db cache)

【数据库设计与实现】第7章:缓存与检查点 

7.2-2给出了db cache缓冲区中单个granule内存块中的组成,总体包括granule headerbuffer headerbuffer block三个部分:

  • granule headergranule内存块的通用部分,用于granule内存块自身的管理,与db cache等内存块使用者无关;
  • buffer blockdata block,用于存放实际数据,buffer block的大小与具体是哪种db cache有关,例如对于db_8k_cache缓冲区,buffer block的大小即为8K,对于db_2k_cache缓冲区,buffer block的大小即为2K
  • buffer header:与buffer block一一对应,用于对db cache进行管理,例如LRU、脏块控制等等;

可见,granule内存块是Oracle管理缓冲区的基石,Oracle是以granule为单位在各个内存使用者之间进行调配的。当SGA的大小小于128Mgranule的颗粒度为4M,当SGA的大小大于128Mgranule的颗粒度为16M

工作集

图7.2-3 db cache、granule、DBW、工作集之间的关系

【数据库设计与实现】第7章:缓存与检查点 

在上节我们知道db cache是由大量的granule内存块组成的。如果db cache只是一条双向链表,高并发下将产生大量的竞争,严重影响性能。为此,Oracle将单个db cache进一步分解成多个工作集,每个工作集进行独立的管理,从而降低冲突概率,提高性能。图7.2-3给出了db cachegranuleDBW、工作集之间的关系。图中假设将db cache划分为4个工作集,每个工作集占用每个granule内存块的1/4空间(buffer block)。一个工作集只会被一个DBW进程处理,但DBW采用异步IO方式,所以一个DBW进程可以处理多个工作集。实际上,工作集的数量一般等于cpu_count,而DBW的数量一般等于ceil(cpu_count/8),且所有类型的db cache的工作集数量是相等的。

表7.2-2 工作集头部部分关键信息

类型

含义

INST_ID

NUMBER

归属的实例id

SET_ID

NUMBER

本工作集(work   set)的id

DBWR_NUM

NUMBER

负责本工作集的DBW

BLK_SIZE

NUMBER

本工作集的block   size

CKPT_LATCH

RAW(8)

指向本工作集的check   point queue latch

CKPT_LATCH1

RAW(8)

指向本工作集的check   point queue latch

SET_LATCH

RAW(8)

指向本工作集的cache   buffer LRU chain latch

CNUM_SET

NUMBER

本工作集中的block数量

NXT_REPL

RAW(8)

本工作集的replacement   list(主链),指向链头

PRV_REPL

RAW(8)

本工作集的replacement   list(主链),指向链尾

NXT_REPL_AX

RAW(8)

本工作集的replacement   list(辅链),指向链头

PRV_REPL_AX

RAW(8)

本工作集的replacement   list(辅链),指向链尾

CNUM_REPL

NUMBER

Replacement list主链和辅链中的总block

ANUM_REPL

NUMBER

Replacement list辅链中的block

COLD_HD

RAW(8)

replacement list(主链)分为冷区和热区,本指针指向链中的某个buffer header,在buffer   header前面为热区,后面为冷区

HBMAX

NUMBER

replacement list主链可容纳的最大block

HBUFS

NUMBER

replacement list主链中当前的热block

NXT_WRITE

RAW(8)

本工作集的write   list(主链),指向链头

PRV_WRITE

RAW(8)

本工作集的write   list(主链),指向链尾

NXT_WRITE_AX

RAW(8)

本工作集的write   list(辅链),指向链头

PRV_WRITE_AX

RAW(8)

本工作集的write   list(辅链),指向链尾

CNUM_WRITE

NUMBER

write list主链和辅链中的总block

ANUM_WRITE

NUMBER

write list辅链中的block

NXT_XOBJ

RAW(8)

本工作集的XOBJ   list(主链),指向链头,用于drop/truncate等场景

PRV_XOBJ

RAW(8)

本工作集的XOBJ   list(主链),指向链尾,用于drop/truncate等场景

NXT_XOBJ_AX

RAW(8)

本工作集的XOBJ   list(辅链),指向链头,用于drop/truncate等场景

PRV_XOBJ_AX

RAW(8)

本工作集的XOBJ   list(辅链),指向链尾,用于drop/truncate等场景

NXT_XRNG

RAW(8)

本工作集的XRNG   list(主链),指向链头,用于tablespace offline等场景

PRV_XRNG

RAW(8)

本工作集的XRNG   list(主链),指向链尾,用于tablespace offline等场景

NXT_XRNG_AX

RAW(8)

本工作集的XRNG   list(辅链),指向链头,用于tablespace offline等场景

PRV_XRNG_AX

RAW(8)

本工作集的XRNG   list(辅链),指向链尾,用于tablespace offline等场景

表7.2-3 buffer header部分关键信息

类型

含义

INST_ID

NUMBER

buffer   header归属的实例id

HLADDR

RAW(8)

管理本buffer   headerhash chain latch的地址

BLK_SIZE

NUMBER

本工作集的block   size

NXT_HASH

RAW(8)

hash链,指向双向链表中的下一个buffer header

PRV_HASH

RAW(8)

hash链,指向双向链表中的上一个buffer header

NXT_REPL

RAW(8)

replacement list链、Write链、XOBJ链、XRNG链,指向双向链表中的下一个buffer header

PRV_REPL

RAW(8)

replacement list链、Write链、XOBJ链、XRNG链,指向双向链表中的上一个buffer header

FLAG

NUMBER

BIT标志位:

0x0000001buffer   dirty

0x0000002notify   dbwr after change

0x0000004modification   started, no new writes

0x0000008block   logged

0x0000010temporary   data(no redo for changes)

0x0000020being   written, can’t modify

0x0000040waiting   for write to finish

0x0000080multiple   waiters when gc lock acquired

0x0000100recovery   reading, do not reuse, being read

0x0000200unlink   from lock element(make non-current)

0x0000400write   block & stop using for lock down grade

0x0000800write   block for cross instance call

0x0001000reading   from disk into KCBBHCR buffer

0x0002000has been   gotten in current mode

0x0004000stale(unused   CR buf made from current)

0x0008000deferred   ping

0x0010000direct   access to buffer contents

0x0020000hash   chain dump used in debug print routine

0x0040000ignore   redo for instance recovery

0x0080000sequential   scan only flag

0x0100000indicates   that buffer was prefetched

0x0200000buffer   hash been written once

0x0400000buffer is   logically flushed

0x0800000resilvered   already (do not redirty)

0x1000000buffer is   nocache

0x2000000redo   generated since block read

0x10000000skipped   write for checkpoint

0x20000000buffer is   directly from a foreign DB

0x40000000flush   after writing

LOBID

NUMBER

如果缓冲区属于SecureFiles对象,该字段是SecureFiles对象的唯一标识符

LRU_FLAG

NUMBER

blockreplacement list中的位置,bit标志位:

1LRU dump flag used in debug print routine

2moved to tail of LRU(for extended stats)

4on auxiliary list

8hot buffer(not in cold portion of LUR)

TS

NUMBER

block归属的表空间

FILE

NUMBER

block归属的绝对文件号

DBARFIL

NUMBER

block归属的相对文件号

DBABLK

NUMBER

block对应的块号

CLASS

NUMBER

block的类型:

1data block2sort block3save undo block4segment header5 save undo header6free list7extent map81st level bmb92nd level bmb103rd level bmb11bitmap block12bitmap index block13file header block14unused15system   undo header16system   undo block17undo   header18undo   block

STATE

NUMBER

block的状态:

0FREE, no valid block image

1XCURa current   mode block, exclusive to this instance

2SCUR, a current mode block, shared with   other instances

3CR, a consistent read(stale) block image

4READ, buffer is reserved for a block being   read from disk

5MREC, a block in media recovery mode

6IREC, a block in instance(crash) recovery   mode

7WRITE, writing to disk

8PL, past image block involved in cache   fusion block transfer

MODE_HELD

NUMBER

block当前被pin的模式:

NULL:空

SHR:共享

EXL:排它

LE_ADDR

RAW(8)

指向本block归属的PCM锁地址

OBJ

NUMBER

block归属的对象

BA

RAW(8)

指向block的内存地址

CR_SCN_BAS

NUMBER

Consistent ReadSCN

CR_SCN_WRP

NUMBER

CR_XID_USN

NUMBER

Consistent ReadXID

CR_XID_SLT

NUMBER)

CR_XID_SQN

NUMBER

CR_UBA_FIL

NUMBER

Consistent ReadUBA

CR_UBA_BLK

NUMBER

CR_UBA_SEQ

NUMBER

CR_UBA_REC

NUMBER

LRBA_SEQ

NUMBER

block为脏块,该脏块第一次修改对应的REDO地址

LRBA_BNO

NUMBER

LRBA_BOF

NUMBER

HRBA_SEQ

NUMBER

block为脏块,该脏块第新一次修改对应的REDO地址

HRBA_BNO

NUMBER

HRBA_BOF

NUMBER

US_NXT

RAW(8)

已经拥有本block的使用者双向链表,即已经pin住本block的使用者,指向链表的下一个pin对象

US_PRV

RAW(8)

已经拥有本block的使用者双向链表,即已经pin住本block的使用者,指向链表的上一个pin对象

WA_NXT

RAW(8)

阻塞等待本block的使用者双向链表,因为pin不相容而等待的的使用者,指向链表的下一个pin对象

WA_PRV

RAW(8)

阻塞等待本block的使用者双向链表,因为pin不相容而等待的的使用者,指向链表的上一个pin对象

CKPTQ_NXT

RAW(8)

checkpoint queue双向链表,指向链中下一个buffer   header

CKPTQ_PRV

RAW(8)

checkpoint queue双向链表,指向链中上一个buffer   header

FILEQ_NXT

RAW(8)

file queue双向链表,指向链中下一个buffer   header

FILEQ_PRV

RAW(8)

file queue双向链表,指向链中上一个buffer   header

OQ_NXT

RAW(8)

对象双向链表,指向链中下一个buffer header

OQ_PRV

RAW(8)

对象双向链表,指向链中上一个buffer header

AQ_NXT

RAW(8)

辅助对象双向链表,指向链中下一个buffer header

AQ_PRV

RAW(8)

辅助对象双向链表,指向链中上一个buffer header

OBJ_FLAG

NUMBER

对象标志:

object_ckpt_list:本block同时在object ckpt

TCH

NUMBER

touch count

TIME

NUMBER

touch time

每个工作集都有一个工作集头部和若干buffer header以及与buffer header一一对应的buffer block组成,其中工作集头部和buffer header的详细信息分别如表7.2-27.2-3所示。Oracle正是通过工作集头部和buffer header中的相关结构将工作集中的所有buffer block管理起来的,其中关键的链表有:

  • replacement链表:用于将本工作集中的所有buffer block按照冷热情况管理起来,当buffer block不足时可以及时将不常用的block从缓冲区剔除。replacment链表由主链和辅链组成,涉及的相关元素有工作集头部中的NXT_REPLPRV_REPLNXT_REPL_AXPRV_REPL_AXCNUM_REPLANUM_REPLCNUM_REPLANUM_REPLCOLD_HDbuffer header中的NXT_REPLPRV_REPLLRU_FLAG
  • checkpoint queue链表:用于将本工作集中的所有脏块按照LRBA顺序组成双向链表,用于checkpoint决策脏块的写入策略,涉及的相关元素有工作集头部中的CKPT_LATCHCKPT_LATCH1(链表头尾的指针待进一步核实)和buffer header中的CKPTQ_NXTCKPTQ_PRV
  • File queue链表:用于将本工作集中的所有脏块按照归属的数据文件组成双向链表,用于按文件决策脏块的写入策略,涉及的相关元素有buffer header中的FILEQ_NXTFILEQ_PRV(工作集头部中的相关部分待进一步核实);
  • Write链表:用于将本工作集中的部分冷脏块管理起来,当buffer block不足时可以及时将这些脏块刷入磁盘。Write链表由主链和辅链组成,涉及的相关元素有工作集头部中的NXT_WRITEPRV_WRITENXT_WRITE_AXPRV_WRITE_AXCNUM_WRITEANUM_WRITEbuffer header中的NXT_REPLPRV_REPL
  • 其它写链表:按照对象的视角将相关的脏块组织起来,尽快将某个对象的脏块写入磁盘,主要有XOBJ链表和XRNG链表,涉及工作集头部中的NXT_XOBJPRV_XOBJNXT_XOBJ_AXPRV_XOBJ_AXNXT_XRNGPRV_XRNGNXT_XRNG_AXPRV_XRNG_AXbuffer header中的OQ_NXTOQ_PRVAQ_NXTAQ_PRVOBJ_FLAG

通过上述链表,可以将所有buffer header管理起来,而buffer headerbuffer block是一一对应的,所以本质上就是将所有buffer block管理起来了。

Hash Chain

除了通过工作集管理buffer block,我们还需要查询和定位某个特定的block。查询某个block时首先要确定该block是否已经缓存在缓冲区中,如果已经在缓冲区中,需要快速定位到该block。和library cache类似,Oracle也是通过hash链进行快速定位和查询的。

图7.2-4 hash chain管理示意图

【数据库设计与实现】第7章:缓存与检查点 

如图7.2-4所示,整个缓冲区的hash管理由三部分组成:

  • buffer header:每个buffer header中有NXT_HASHPRV_HASH双向指针,将hash值相同的buffer header链接在一起,组成双向链表。实际上,hash值相同的buffer header有两种情况,分别是hash冲突和同一个block的多个CR版本;
  • buckethash桶,数量由参数_db_block_hash_buckets配置,默认为接近于db_block_buffers*2的一个2的幂数;
  • cache buffer chains latch:保护hash链表的latch,一般每个latch保护32bucket,即32hash链;

可见,hash链是全局的,不受限于某个具体的工作集。通过绝对文件号和块号计算hash值,就可以快速定位某个block是否在缓冲区中。下面详细看一下通过hash chain操作某个block过程

  • 通过文件号、块号、ClassNumber计算出hash值,定位到对应的cache buffer chain latchbucket
  • 申请该cache buffer chain latch
  • 遍历bucket对应的cache buffer chain,找到对应的buffer header,并pin住该buffer header
  • 释放该cache buffer chain latch
  • 操作找到的buffer block
  • 申请该cache buffer chain latch
  • unpin住该buffer header
  • 释放该cache buffer chain latch

整个互斥过程分为cache buffer chain latchpin两个部分。一个cache buffer chain latch需要保护多个cache buffer chain,同时数据库在操作具体buffer header时花费的时间可能会比较长,所以Oracle设计了latchpin两个阶段,有效地提升并发性。当然,这样也会引入额外的成本,一次buffer涉及两次latch申请(pinunpin),不过这和长时间占用latch相比仍然是有益的。在某些情况下,数据库如果预测到短期还会访问该block,会延后unpin动作,提升整体效率。

图7.2-5 pin管理示意图

【数据库设计与实现】第7章:缓存与检查点 

当多个session同时操作某个block时,pin机制就会发挥作用。pin有共享(S)和排它(X)两种模式,图7.2-5分别给出了共享和排它两种示例。每个buffer header有两对链表指针:

  • US_NXTUS_PRVUser’s List,用于将当前正在操作本buffer headersession链接在一起,即在User’s List中的session是已经拥有本buffer headersession
  • WA_NXTWA_PRVWaiter’s List,用于将等待本buffer headersession链接在一起,即在Waiter’s List中的session是阻塞等待的session

某个session操作某buffer header时,如果该buffer header尚未被其它session pin住,或者pin的模式是相容的,那么将本session加入到该buffer headerUser’s List中,本session可以操作该buffer header。如果该buffer header已经被其它session pin住,且pin的模式不相容,那么将本session加入到该buffer headerWaiter’s List中,本session阻塞等待。阻塞等待的事件为“buffer busy wait”(从10g以后,如果是因为等待其它session从磁盘读数据块,等待事件调整为“read by other session”)。

表7.2-4 pin模式相容矩阵

已经持有的模式

请求的模式

NEW

SHR

EXL

CR

CRX

NULL

NULL

Y

Y

Y

Y

Y

Y

SHR

N

Y

N

Y

Y

N

EXL

N

N

N

N

N

N

实际上Oracle内部是通过调用函数kcbget(descriptor, lock_mode)获得期望的buffer header,其中descriptor描述期望操作的具体块,lock_mode表示期望的加pin模式:

  • NEW:以排它方式访问新块;
  • SHR:以共享方式访问当前块;
  • EXL:以排它方式访问当前块;
  • CR:以共享方式访问CR块;
  • CRXCR模式的变体;
  • NULL:用于保持对块的引用,防止被换出缓存;

每个pin结构都会记录session的地址和期望的pin模式。pin结构又称为buffer handle,通过_db_handles_cached设置每个进程可以缓存的pin对象数(默认为5),_db_handles设置整个系统可以缓存的pin对象数(默认为_db_handles_cached*进程数),_cursor_db_buffers_pinned设置单个游标可以使用的最大pin对象数(默认为db_handles_buffers/进程数-2)。当User List中的session都完成操作后,会唤醒Waiter List中的session。该session被唤醒后会加入到User List中,并开始操作buffer header。出于死锁等健壮性考虑,session阻塞等待的默认时间为1秒(可通过隐藏参数_buffer_busy_wait_timeout调整)。当session超时自我唤醒后认为发生了死锁,报“buffer deadlock”等待时间(实际上并没有等待),然后释放该session已经占有的所有pin,之后再重新申请。

下面再回顾一下Oracle操作普通block的过程:

  • Step1:计算出bucket后,以独占方式申请对应的cache buffer chain latch
  • Step2:遍历cache buffer chain,找到对应的buffer header,并pin住该buffer header
  • Step3:释放该cache buffer chain latch
  • Step4:操作对应的buffer block
  • Step5:再次以独占方式申请该cache buffer chain latch
  • Step6unpinbuffer header
  • Step7:释放该cache buffer chain latch

按照上述步骤操作block的前提是step4消耗的时间比较长,应当仅可能地降低latch的持有时间。但是其代价也是非常明显的,两次latch操作且都是独占方式,同时还需要pinunpin。而有些场景下,step4是只读操作且非常快速,按照上述正常步骤操作反而影响性能。为此,Oracle设计了Examination方式(consistent get - examination):

  • Step1:计算出bucket后,以共享方式申请对应的cache buffer chain latch
  • Step2:遍历cache buffer chain,找到对应的buffer header
  • Step3:读取对应的buffer block的相关内容;
  • Step4:释放该cache buffer chain latch

可见,当step3的耗时极短时,examination方式的优势非常明显。一般情况下,对于根索引block索引非叶子block、唯一索引的叶子block、通过唯一索引访问的表blockundo block的读操作都会采用examination方式。

LRU/TCH

内存资源是有限的,所以缓冲区也是有限的。当缓冲区已经被buffer block占满,就需要一种机制将不常用的buffer block从缓冲区中换出,给其它buffer block腾出空间,这就是OracleLRU/TCH机制。和hash chain不同,为了提升性能LRU/TCH不是全局的,而是属于工作集的,即每个工作集都有一个独立的LRU/TCH,该LRU/TCH由相应的cache buffer lru chain latch保护。Oracle选择待置换出去的buffer block时,首先随机选择某个工作集,然后以立刻模式尝试获取该工作集的cache buffer lru chain latch。如果获得latch则在该工作集的LRU/TCH中寻找待置换的buffer block,否则按照某种顺序遍历各工作集直到获得某个工作集上的lru chain latch

图7.2-6 replacement主链示意图

【数据库设计与实现】第7章:缓存与检查点 

下面以单个工作集的LRU/TCH为例讲解OracleLRU机制,每个LRU/TCH由一个replacement主链和一个replacement辅链组成。replacement主链如图7.2-6所示,通过示例我们发现replacement主链由如下三部分组成:

  • MRU:工作集中的nxt_repl指向replacement主链的热端,即处于该端的buffer block属于最常用的block
  • LRU:工作集中的prv_repl指向replacement主链的冷端,即处于该端的buffer block属于最不常用的block
  • SCP:工作集中的cold_hd指向replacment主链的冷热隔离边界,一般取replacement主链的中间位置(由隐藏参数_db_percent_hot_default控制,默认50);

replacement主链实际上是由buffer header组成的双向链表,buffer header中有表征buffer block使用热度的TCHTIMTCH记录该buffer block被使用的次数,即buffer block每被复用一次,该buffer block对应的TCH就加一。然而,如果在某个极短的时间内大量反复使用某个buffer block,之后再也不使用该buffer block,会造成该buffer block假热。为此,Oracle引入了TIM,用于记录该buffer block上次更新TCH的时间。只有本次复用的时间减去上次更新的时间(即TIM)超过某个时间阈值(默认3秒),才会增加TCH,从而有效地避免了buffer block假热。

有了replacement主链的MRULRUSCP以及buffer header中的TCH/TIM这些概念之后,我们来看一下Oracle搜索replacement主链的算法。从LRU冷端开始搜索:

  • 如果遇到已经pin住的buffer header,直接跳过;
  • 如果遇到dirty状态且TCH小于_db_aging_hot_criteria(默认为2)的buffer header,将其转移到write主链中,从而加快冷dirty buffer block的写入速度,降低replacement主链的搜索长度;
  • 如果遇到TCH小于_db_aging_hot_criteriabuffer header,直接复用,复用后将该buffer headerTCH置为1,并转移到replacement主链的SCP位置;
  • 如果遇到TCH大于等于_db_aging_hot_criteriabuffer header,说明该块是热块,将其转移至replacement主链的MRU端(随着MRU端不断插入新的buffer header,就有相应的buffer header不断跨过SCP,一旦某buffer header跨过SCP,该buffer headerTCH降为1),并对TCH做如下调整:
    • 如果_db_aging_stay_count大于等于_db_aging_hot_criteria,将该块的TCH除以2
    • 如果_db_aging_stay_count大于等于_db_aging_hot_criteria,该块的TCH保持不变,并将该块的TCH赋给_db_aging_stay_count

通过上述方法从LRU端向MRU端搜索,只到找到可复用的buffer header。如果搜索了replacement主链的40%(可通过隐藏参数_db_block_max_scan_pct控制,默认40)仍然没有找到可复用的buffer header,说明系统存在太多脏页,Oracle会停止搜索,并向DBWR进程发送消息,让其尽快将脏块写入磁盘。每次都从replacement主链的LRU端开始搜索,有可能需要较长一段时间才能找到合适的buffer header。为了提高搜索效率,Oracle引入了replacement辅链。其本质是SMON进程每3秒醒来一次,会搜索replacement主链,提前将TCH小于_db_aging_hot_criteria且非dirty、非pinbuffer block转移到replacement辅链的MRU端。实际上,系统刚启动时,所有的buffer header都在replacement辅链中,replacement主链为空。随着数据不断被访问,replacement主链会越来越长,replacement辅链会越来越短。SMON进程会在后台维持replacement主链和辅链之间的平衡,一般维持在75:25的平衡比例。至于某个buffer headerreplacement主链还是辅链中,在主链的热区还是冷区中,可以通过buffer headerlru_flag查看。

不管是操作replacement主链还是replacement辅链,都需要在cache buffer lru chain latch保护下进行。当我们复用buffer block时,由于涉及实际block内容的更改,还需要更改该buffer header归属的hash chain,还需要申请cache buffers chain latch,这时就需要考虑latchlevel,防止死锁。

LRU/TCH通过上述机制将热数据保留在内存中,将冷数据及时从内存中换出。然而,对于全表扫描或者全索引扫描场景,由于涉及大规模数据的读取,如果不对LRU/TCH机制做调整,热数据很可能全部被清出内存。为此,Oracle对全表扫描和全索引扫描场景进行了优化。在扫描读操作实施前,会评估待扫描的总数据量:

  • 总数据量小于buffer cache2%:采用通用的LRU/TCH机制,不做特殊处理;
  • 总数据量在buffer cache2%~10%之间:TCH设置为0,并直接加入到replacement主链的LRU端,从而快速被移到replacement辅链中。当然,如果此时有其它session同时访问该buffer block,该buffer header有可能被移到replacement主链的MRU端;
  • 总数据量在buffer cache10%~25%之间:直接在replacement辅链上批量获取buffer header,读完后仍然加入到replacement辅链中,不增加TCH,不进入replacement主链;
  • 总数据量大于buffer cache25%:采用direct path read模式,不走LRU/TCH机制,直接在sessionPGA中进行,不消耗buffer cachelatch。当然该模式下,需要确保扫描设计的block在磁盘上都是最新的,所以在某段数据之前需要对该段数据做一次checkpoint,确保落盘(direct path read还有另外一个缺点,可能导致重复的延迟清理操作);

系统启动时或者执行flush cache后,所有的buffer header都挂在replacement辅链上。

CR与扫描

在前面章节我们知道,Oracle是通过latchpin来协调buffer block的读写的。Pin分为共享和独占两种模式,共享和共享是相容的,但共享和独占、独占和独占是不相容的,需要阻塞等待。不过,Oracle为了进一步提升并发性,将共享和独占也优化为不阻塞。共享和独占主要有两种情况:先读后写同一个block,或者先写后读同一个block。对于先读后写同一个blockOracle的优化过程如下:

  • Step1:计算出bucket后,以独占方式申请对应的cache buffer chain latch
  • Step2:遍历cache buffer chain,找到对应的buffer header,尝试以独占的方式pin住该buffer header,发现不相容,改为以共享的方式pin住该buffer header,并修改状态以表征此block正在被克隆(防止其它session同时做克隆);
  • Step3:释放该cache buffer chain latch
  • Step4:申请一个新的buffer header,将原block的内容克隆过来,并完成修改(写操作);
  • Step5:再次以独占方式申请该cache buffer chain latch
  • Step6:将新的buffer header加入到hash chain中,并将新buffer headerstate置为XCUR,表示当前版本;
  • Step7:将原buffer headerstate状态改为CR,表示是克隆版本,并将原buffer header中属于本自己的pin对象释放,以及其它阻塞等待的pin迁移到新的buffer header上;
  • Step8:释放该cache buffer chain latch

通过上述方法,Oracle将先读后写同一个block优化为不阻塞。对于先写后读,Oracle会优先寻找本blockCR

  • 如果存在CR,且CRscn等于读操作的scn,直接读取;
  • 如果存在CR,且CRscn大于读操作的scn,通过undo构造新的CR,然后读取;
  • 如果不存在CR,或者CRscn小于读操作的scn,在当前block上加共享pin,阻塞等待;

可见,先写后读的优化存在场景限制,需要等待前序的pin对象释放。不过不管是哪种情况,都是通过构造CR来提高并发性的。这样就会导致同一个block在内存中有多个版本,如果不做控制就会浪费内存。为此,Oracle定义了隐藏_db_block_max_cr_dba(默认为6),即blockCR版本数不允许超过_db_block_max_cr_dba。同时为了提高搜索消息,当前版本的block会放在所有CR版本的前面。

下面看看物理IO的情况。从磁盘读取数据的时间会比较长,为了防止多个session重复读取相同的数据,Oracle设计的物理IO过程如下:

  • Step1:计算出bucket后,以独占方式申请对应的cache buffer chain latch
  • Step2:遍历cache buffer chain,找不到对应的buffer header,需要从磁盘上读物读取。申请一个新的buffer header,初始化buffer header并加pin,然后加入到hash chain中;
  • Step3:释放该cache buffer chain latch
  • Step4:从磁盘读取该block数据;
  • Step5:再次以独占方式申请该cache buffer chain latch
  • Step6unpin
  • Step7:释放该cache buffer chain latch

虽然step4的时间会比较长,但对应的buffer header已经在hash chain中,这样其它session读相同的block时,就会发现已经有session在读,只要将这些session pin在该buffer header上即可,从而有效地防止重复读。对于物理IO来说,Oracle支持如下三种IO

  • db file sequence read:一次只读一个block,例如点查等场景;
  • db file parallel read:一次从多个位置上读取多个block,例如根据索引中间block读取多个叶子block
  • db file scattered read:一次从相邻位置读取多个block,例如顺序扫描;

Checkpoint

Oracle在运行过程中会持续不断地写redo日志,从而保证数据的持久性。不过数据block也需要在适当的实际刷入磁盘,从而保证磁盘中的数据仅可能地接近内存中的数据,这就是检查点机制。Oracle根据检查点目的的不同,设计了如下几种检查点:

  • 全量检查点:将数据库中的所有脏块全部刷入磁盘,例如shutdown instance、手工执行alter database close、手工执行alter system checkpoint local(单个RAC实例)、手工执行alter system checkpoint global(所有RAC实例)等等;
  • 文件检查点:将特定数据文件的所有脏块刷入磁盘,例如手工执行alter tablespace begin backup、手工执行alter tablespace offline等等;
  • 对象检查点:将特定某个对象的所有脏块刷入磁盘,例如手工执行drop table xxx purge、手工执行drop idnex xxx、手工执行truncate table xxx等等;
  • 并行查询检查点:并行查询走direct path read,数据不走buffer cache,所以需要提前将该对象相关的所有脏块刷入磁盘;
  • 增量检查点:就是根据active redo日志量的情况,持续不断地将脏块刷入磁盘,在写脏块和系统恢复时间之间取得平衡;
  • 日志切换检查点:和增量检查点类似,redo日志文件切换后,为了保证redo日志文件有足够的余量空间,需要将相关脏块刷入磁盘;

根据上述不同的检查点,Oracle设计了三种刷新队列:

  • checkpoint queue:按照redo日志的顺序组织dirty buffer blockbuffer header),用于降低实例异常重启的恢复时间;
  • file queue:按照数据文件组织dirty buffer blockbuffer header),用于tablespace backuptablespace offline等操作,可以高效地将属于某个数据文件的dirty block识别出来,并刷入磁盘;
  • object queue:按照对象组织dirty buffer blockbuffer header),用于drop tabletruncate tabledirect path read等操作,可以高效地将属于某个对象的dirty block识别出来,并刷入磁盘;

同一个脏块会同时出现在上述三个队列中,当然一旦完成持久化也会同时从这三个队列中摘除。三个队列的目的是从不同角度组织脏块,从而不同场景下都可以高效地找到对应的脏块,并完成持久化工作,所以三个队列也是由同一个checkpoint queue latch保护。

图7.2-7 checkpoint queue组织结构

【数据库设计与实现】第7章:缓存与检查点 

首先看Oracle关于checkpoint queue的设计,图7.2-7给出checkpoint queue的组织结构。session进程在修改数据block时,如果是第一个将该block从非脏块变为脏块,需要申请申请checkpoint queue latch,然后将该block从为尾部加入到checkpoint queue中。后继对该block的再次变更,不涉及脏块状态的变化,所以不需要操作checkpoint queue,也就不需要申请checkpoint queue latchBuffer header中的LRBA记录的是该buffer block的第一个redo日志的日志(相对于磁盘上的对应block),而HRBA记录的是该block的最近一条redo日志的地址。可见,checkpoint queue是按照LRBA的顺序组织起来的,按照checkpoint queue的顺序写脏块就可以基本反映redo日志的顺序,有效地实现了redo日志文件从active状态向inactive状态的转换。然而,同一个block可能被多次修改,导致HRBA大于LRBADBWR在持久化该block时,如果HRBA对应的redo日志已经持久化,DBWR正常持久化该block即可。否则由于WAL原则的约束,需要将HRBA的地址发送给LGW,让其尽快完成该地址之前的日志持久化,同时跳过该block,继续持久化队列中的后继block(刷完后继多个block还会回来检查跳过的block对应的redo日志是否已经完成持久化)。

7.2-7还给出了另外一个重要的信息,checkpoint queue很可能并不是按照工作集为粒度组织的,而是按照DBWR为粒度组织的(尚不清楚checkpoint queue队列首部的存放位置)。这是因为主要是前台session进程和DBWR进程操作checkpoint queue,而前台session进程只有在block第一次从非脏块变更为脏块时才需要操作checkpoint queue,所以checkpoint queuelatch冲突并不高。当然为了尽可能提高并发性,采用了双队列机制。当DBWR访问某个checkpoint queue时,前端session进程可以操作另外一个checkpoint queue,分散访问。正常情况下,前端session进程不会调整已经在checkpoint queue中的buffer header,然而当发生先读后写时,前端session进程会将原来的块变更为CR并创建新的当前块,这时就需要调整checkpoint queue,并申请checkpoint queue latch,即将checkpoint queue中的块置换为新的当前块。

增量checkpoint就是每次将部分脏块持久化。这些脏块每次不会太多,导致IO资源占用过大。也不会太少,导致active状态的redo日志量过大,既影响实例异常重启时恢复时间,也可能导致redo日志空间不足引起短暂性不可访问。DBWR3秒钟唤醒一次,计算出本次的目标redo日志地址(根据fast_start_mttr_targetfast_start_io_targetlog_checkpoint_timeoutlog_checkpoint_interval_target_rba_max_lag_percentage综合计算)。然后遍历归属于本DBWR的所有checkpoint queue。对于某个checkpoint queue而言,从队列头部开始遍历,如果buffer headerLRBA小于等于目标redo日志地址,该block就需要持久化。DBWR在遍历checkpoint queue时,以willing-to-wait方式申请checkpoint queue latch。实际上,DBWR在持久化脏块时会做相邻块合并,批量写入,从而进一步优化IO

DBWR在按批持久化脏块时,对持久化本身也会记录redo日志。日志内容主要包括每个脏块的地址、scn等。当系统崩溃恢复时,读取该日志就可以这些block是否需要应用redo日志,否则需要读取这些block,并拿这些block中的scnredo日志中记录的scn进行比较才知道是否需要应用redo日志。DBWRredo日志的过程和通用过程比较类似,需要申请copyallocation latch,然后给LGW发送消息,但不需要等待LGW真正完成日志持久化。

最后CKPT进程将checkpoint的完成情况写入到各数据文件以及控制文件中:

  • system checkpoint scn:系统检查点scn,存在于控制文件中,Oracle会根据checkpoint情况持续更新该scn(对于checkpoint队列第一个buffer headerLRBA);
  • datafile checkpoint scn:文件检查点scn,存在于控制文件中,每个数据文件一个,Oracle会根据checkpoint情况持续更新该scn(只读tablespace不更新);
  • datafile header start scn:文件检查点scn,存在于每个数据文件的文件头中,Oracle会根据checkpoint情况持续更新该scn

系统异常重启后分为两个阶段应用redo日志。第一阶段,从LRBA处开始扫描redo日志文件,列出所有潜在的block。同时根据批量写脏块日志将磁盘上已经是最新的block剔除,判断原则是批量写时该blockscn大于等于redo日志的scn。第二阶段,对仍然需要应用重做日志的block进行并行回放。在前面章节我们知道,正常运行期间,Oracle也是先创建redo日志,存放在日志缓冲区中,并将日志应用到记录上。可见,恢复期间的应用日志和运行态的应用日志是相同的代码,不同的知识redo日志源不同而已。

Write

Checkpoint主要按照崩溃恢复的时间决策将哪些脏块刷入磁盘,但有时我们还需要从其它维度来考虑。当replacement主链中有大量的TCH小于_db_aging_hot_criteria且为脏的块时,这些脏块不能移入replacement辅链。既导致replacement主链过长,影响搜索效率,又可能导致前端session进程搜索了_db_block_max_scan_pctbuffer header仍然找不到可用块,使得前端session阻塞等待。为此,Oracle在每个工作集中又设计了write主链和write辅链,工作集中的相关变量有NXT_WRITEPRV_WRITENXT_WRITEAXPRV_WRITEAXCNUM_WRITEANUM_WRITE

图7.2-8 Write主链生成示意图

【数据库设计与实现】第7章:缓存与检查点 

如图7.2-8所示,前端session进程通过扫描replacement辅链获得可用的buffer block。如果replacement辅链为空,则需要进一步扫描replacement主链。为了提高扫描replacement主链的效率,前端session进程对遇到的状态为脏、未被pin住且TCH小于2的块,会将该块从replacement主链上摘除,并加入到write主链上。当前端session进程扫描了_db_block_max_scan_pct仍然找不到可用的buffer block,会给DBWR进程发送消息,并将自己阻塞在free buffer waits事件上,等待DBWR进程尽快将脏数据写盘。当然,当write主链到达一定长度,虽然还未到达_db_block_max_scan_pct,前端session进程也会给DBWR进程发送消息,只不过不会阻塞在free buffer waits事件上。

DBWR进程被唤醒后,执行如下逻辑:

  • step1:从尾端开始扫描write主链,如果遇到的block对应的redo日志尚未写盘,跳过该block,并给LGW发送消息,让其尽快将相应的redo日志写盘。如果遇到的block对应的redo日志已经写盘,对该blockpin,然后从write主链中摘除,并加入到write辅链中;
  • step2:重复step1的动作,直到完成整个write主链的扫描,或者扫描的block数达到_db_writer_scan_depth_pct(默认25);
  • step3:将write辅链中的block添加到批量写结构中(该结构对多个DBWR进程开放,会按文件相邻性进行重组),然后触发异步写文件,并对本次写盘记redo日志;
  • step4:完成写盘后,遍历write辅链,对每个block,清理buffer headerdirty状态以及LRBA,并将blockwrite辅链和checkpoint queue中摘除,并加入到replacement辅链中;

DBWR不断地重复执行step1step4,直到归属于本DBWR的所有工作集中的write主链中的block都完成持久化。

表7.2-5 dirty block写优先级

写类型

优先级

写类型

优先级

LRU-PRAC

增量检查点

并行查询检查点

cold dirty buffers(老化)

LRU-XO

用户触发的检查点

LRU-XR

表空间检查点

实际上,除了write主辅链之外,Oracle还设计了LRU-PLRU-XOLRU-XR多个写队列,用于区分写优先级,不过这些队列也是由同一个cache buffer lru chain latch保护。这些队列的含义如下:

  • LRU-Pping-list,用于RAC,不过后期Oracle版本在实例间传递block时,已经不需要提前将脏块刷入磁盘;
  • LRU-XOreuse object list,用于将可复用的某对象相关脏块写入磁盘(drop/truncate);
  • LRU-XRreuse range list,用于将可复用的相关脏块写入磁盘(alter tablespace begin backupalter tablespace offlinealter tablespace read only);

同一个脏块不能同时在replacement链和write链上,但同一个脏块可能既会在write链上,同时也会在某个LRU-PLRU-XO或者LRU-XR上。write链是通用的写队列,LRU-PLRU-XOLRU-XR是针对某些特殊场景的写链。这些写链的机制是类似的,都采用主链和辅链机制,DBWR进程操作这些链上脏块的机制也是相同的。不同的是不同的链针对不同的场景,DBWR操作这些链的优先级也不同。具体优先级如表7.2-5所示,write链之外的其它写链优先级都比较高,都需要立刻写出。

最后,我们在总结一下DBWR的触发条件:

  • 3秒触发一次;
  • checkpoint触发;
  • 前端session进程扫描replacement主链找不到可用块触发;
  • write主链和辅链上的block达到_db_large_dirty_queue触发;
  • 用户命令触发,例如将表空间设置为离线、只读,对表空间进行备份等;

MySQL设计原理

缓冲区与Chunk

MySQL的缓冲区同样用于在内存中缓冲page,其大小由innodb_buffer_pool_size设置。和Oracle不同的是其只有一种类型缓冲区,且同时仅支持一种大小的page。虽然page大小可以通过innodb_page_size设置(支持4K8K16K32K64K),但不支持多种大小并存。

图7.3-1 缓冲区结构

【数据库设计与实现】第7章:缓存与检查点 

为了支持动态调整缓冲区大小,缓冲区是以chunk为单位组织的,每个chunk的大小由innodb_buffer_pool_chunk_size设置,默认128M。如图7.3-1所示,缓冲区对应于buf_pool_t结构,是由buf_chunk_t结构组成的数组,数组中每个buf_chunk_t结构中都有一个指针,指向其对应的chunk size的内存空间,用于缓存实际的page数据。因此,缓冲区的动态增加或减小是以chunk size为单位进行的。

图7.3-2 chunk内存的结构

【数据库设计与实现】第7章:缓存与检查点 

对于每个chunk而言,分为buf_block_tpage两个部分。如图7.3-2所示,buf_block_tpage一一对应,page用于缓冲数据页在内存中的映射,和innodb_page_size相等,而buf_block_tpage在内存中的控制结构。实际上,buf_block_t结构就是通过其中的frame指针指向page内存的。

缓冲区实例

为了解决高并发下缓冲区本身引起的并发冲突问题,MySQL同样对缓冲区进行了切分。每个子缓冲区就是一个缓冲区实例,对应一个buf_pool_t结构。缓冲区实例的数量可以通过配置项innodb_buffer_pool_instances进行设置,大小为innodb_buffer_pool_size的内存会以chunk为单位均匀地分配到各个缓冲区实例中。

对于任何page的访问,都可以基于表空间号和页号计算出哈希值,从而快速定位到对应的缓冲区实例,即某个特定page归属于的缓冲区实例是确定的。具体算法如下:

(space_id <<20+space_id+page_id>>6)%innodb_buffer_pool_instances

可见,该算法有如下特点:

  • 对于同一个表空间的连续page,会尽可能地放在同一个缓冲区实例中,即一个extent中的page缓存在同一个实例中,从而便于预读等优化;
  • 对于不同表空间的page,尽可能放在不同缓冲区实例中,从而便于降低冲突;

表7.3-1 buf_page_t部分关键信息

类型

含义

id

page_id_t

page的表空间号和页号

size

page_size_t

page的大小

buf_fix_count

uint32

page当前正在被引用的次数,正在被引用的page不能被置换出缓冲区

io_fix

buf_io_fix

正在进行中的IO类型

state

buf_page_state

page的当前状态

hash

buf_page_t*

单向指针,用于构建hash

newest_modification

lsn_t

page最近一次被修改时对应的lsn

oldest_modification

lsn_t

page第一次被修改时对应的lsn

list

UT_LIST_NODE_T

双向指针,将属于某个缓冲区实例的buf_page_t链接在一起,用于构建Free ListFlush   List,具体根据state区分

LRU

UT_LIST_NODE_T

双向指针,用于构建LRU   List

old

bool

page是否在LRU List的冷端

access_time

UNSIGNED

page被访问的时间戳,用于防止伪热page

表7.3-2 buf_block_t部分关键信息

类型

含义

page

buf_page_t

对应于buf_page_t结构

frame

byte *

指向本page对应的内存地址

lock

BPageLock

保护对应page的并发访问

mutex

BPageMutex

保护控制结构的并发访问

表7.3-3 buf_pool_t部分关键信息

类型

含义

mutex

BufPoolMutex

保护对应本缓冲区实例的并发访问

instance_no

ulint

缓冲区实例号

curr_pool_size

ulint

本缓冲区实例的大小

n_chunks

ulint

本缓冲区实例的chunk

chunks

buf_chunk_t*

构成本缓冲区实例的chunk数组

page_hash

hash_table_t*

本缓冲区实例的hash

flush_list

UT_LIST_BASE_NODE_T

Flush List双向链表,跟踪本缓冲区实例中的脏页,用于checkpoint

flush_list_mutex

FlushListMutex

保护Flush   List的并发访问

free

UT_LIST_BASE_NODE_T

Free List双向链表,跟踪空闲页

LRU

UT_LIST_BASE_NODE_T

LRU List双向链表,跟踪页的访问情况,用于将不经常访问的页置换出去

LRU_old

buf_page_t*

指向LRU List的冷热分界点

LRU_old_len

ulint

LRU冷端的长度

各缓冲区实例是互相独立的,即各自拥有独立的控制结构和互斥控制,从而最大化地提升并发性。对于每一个缓冲区实例,控制结构包括如下关键部分:

  • Page Hash:对应于buf_poo_t中的page_hash,用于快速定位某个page是否在本缓冲区实例中;
  • Free List:对应于buf_pool_t中的free,用于跟踪缓冲区实例中的空闲page,方便用户线程快速获得空闲page,由buf_pool_t.mutex提供互斥保护;
  • LRU List:对应于buf_pool_t中的LRU,用于按照LRU策略将缓存在本缓冲区实例中的page组织起来,从而方便淘汰不常用的page,由buf_pool_t.mutex提供互斥保护;
  • Flush List:对应于buf_pool_t中的flush_list,用于按照脏页产生的顺序将脏页组织起来,从而方便按照特定的策略进行脏页持久化,由buf_pool_t.flush_list_mutex提供互斥保护;

Page HashFree ListLRU ListFlush List的头部都在缓冲区实例的控制结构buf_pool_t中,具体如表7.3-3所示。Page HashFree ListLRU ListFlush List需要每个page的参与,因此buf_page_t中的hashlistLRU正是用于构建对应的单向或双向LIST,具体如表7.3-1所示。出现在Page Hash中的page一定在LRU List中,出现在LRU List中的page也一定在Page Hash中。出现在Flush List中的page一定在LRU List中,但在LRU List中的page不一定在Flush List,即LRU ListFlush List的超集。

Page Hash

当需要在缓冲区中查找某个特定的page时,首先通过表空间号和页号确定确定具体的缓冲区实例的,然后再通过该缓冲区实例的Page Hash定位到对应的page

图7.3-3 Page Hash结构

【数据库设计与实现】第7章:缓存与检查点 

如图7.3-3所示,Page Hash对应于hash_table_t结构,由如下三个部分组成:

  • buf_page_t:归属于buf_block_t结构,为缓冲区实例缓存page的控制结构,其中的hash指针用于将hash值相同的buf_page_t链接在一起;
  • buckethash桶,桶的数量为缓冲区实例中可容纳page数的2倍;
  • RW_LOCK:并发互斥量,用于保护buckethash链的并发访问,默认每个缓冲区实例16个互斥量,每个互斥量保护一段范围内的hash桶及其hash链;

MySQLOracle的不同之处在于Page Hash不是全局的,而是隶属于各个缓冲区实例的。通过空间号和页号可以快速定位某个page是否在缓冲区实例中,具体过程如下:

  • STEP1:通过空间号和页号确定page对应的缓冲区实例;
  • STEP2:通过空间号和页号查找缓冲区实例的Page Hash,定位到对应的RW_LOCKbucket
  • STEP3:对RW_LOCKR型锁;
  • STEP4:遍历hash链表,找到对应的buf_block_t结构;
  • STEP5:对buf_block_t中的buf_fix_count进行原子加1
  • STEP6:释放RW_LOCK锁;
  • STEP7:访问page中的数据;
  • STEP8:对buf_block_t中的buf_fix_count进行原子减1

上述是查找过程,所以对RW_LOCKR型锁。如果是修改Page Hash,就需要加W型锁。Page Hash仅保护hash的访问过程及buf_page_t的原子操作,即确保能够获取到对应的page。一旦获取完毕之后,就与Page HashRW_LOCK无关。Buf_fix_count确保对应page不会被缓冲区实例置换出去,而buf_block_t中的lockmutex分别提供page及其控制结构的访问保护。

LRU List

LRU List主要用于跟踪缓冲区实例中各page的访问频率(或冷热),当缓冲区实例中的空间不足时优先将冷数据置换出去。MySQL在申请新的page空间时,优先从Free List中获取。如果Free List已经为空,再从LRU List中获取。

图7.3-4 LRU List结构

【数据库设计与实现】第7章:缓存与检查点 

如图7.3-4所示,LRU List也是由buf_page_t构成的双向链表(具体参考表7.3-1和表7.3-3),其由如下三个部分组成:

  • Head:在buf_pool_t结构中(UT_LIST_BASE_NODE_T结构),指向LRU List的头部,表示最近或最频繁被访问的page
  • Tail:在buf_pool_t结构中(UT_LIST_BASE_NODE_T结构),指向LRU List的尾部,表示最旧或最不频繁被访问的page
  • LRU_old:在buf_pool_t结构中,指向LRU List的冷热分界位置。HeadLRU_old之前的部分称为Young sublist,为热数据。LRU_oldTail之间的部分称为Old sublist,为冷数据。Young sublistOld sublist之间的page数比例可以通过配置项innodb_old_blocks_pct配置,默认为63:37

当读取新的page时,将插入到LRU_old位置。如果在innodb_old_blocks_time时间之外再次访问该page,该page将被移动到Head位置。时间的判断正是通过buf_page_t中的access_time完成的,从而防止短时间内的频繁访问造成伪热数据页。早期版本的MySQL没有类似OracleTCH机制,pageLRU List中的移动非常频繁,影响性能。因此,MySQLYoung sublist中的page移动算法做了调整,即page只有在Old sublist中或Young sublist的后3/4位置中,再次被访问才会被移动的Head位置。这样对于已经在Young sublist中靠前位置的page,即使再次被访问也不会被移动到Head位置,从而在一定程度上缓解了频繁移动问题。

Free List中没有空闲页时,用户线程将搜索LRU List以获得待置换的page。具体算法如下:

  • 1轮遍历:从LRU List尾部开始遍历,最多遍历100page。一旦遇到可置换的page,将该page移入Free List,并退出遍历。如果没有找到可置换的page但存在可刷新的脏页,将该脏页持久化后移入Free List,并退出遍历;
  • 2轮遍历:如果第1轮遍历没有获得可置换的page,立刻进行第2轮遍历,其动作和第1轮类似,不同的是没有100page的限制,而是遍历整个LRU List
  • n轮遍历:如果第2轮遍历仍然没有获得可置换的page,则等待10ms后进行下一轮遍历,遍历过程和第2轮完全一致,不断重复直至获得可置换的page

可见,直接从Free List中获取的空闲页的效率是最高的,其次是在第1轮遍历中活动前可置换的page,而刷新脏页或第n轮遍历的效率都是相对较低的。为了解决这个问题,MySQL在后台引入了Page Cleaner线程,其会周期性地遍历LRU List,并将LRU List尾部的page转移到Free List中,从而提高效率。Page Cleaner线程的实现机制将在Flush List章节进一步讲解。

Flush List

page第一次从正常page变为脏页时,就会从头部插入到Flush List中。可见,Flush List就是一个由脏页组成的有序双向链表,头部pageoldest_modification大,尾部pageoldest_modification小。实际上,页面修改都被封装为一个mini-transactionmini-transaction提交时涉及的页面就会加入到Flush List中。PageLRU List中加入到Flush List中时不会从LRU List中删除,即Flush List中的page一定在LRU List。不过,LRU List中的page不一定在Flush List中。

MySQL引入了Page Cleaner线程,从而将脏页有节奏地刷入持久化设备,线程的数量可以通过配置项innodb_page_cleaners设置。Page Cleaner线程由一个协调线程和若干个执行线程组成(实际上,协调线程除了做协调工作之外本身也是一个执行线程),协调线程每1秒进行一次Flush,每次计算各缓冲区实例需要持久化的page数,然后通知各执行线程对各缓冲区实例中的page进行持久化。Page Cleaner线程和缓冲区实例不是一一对应的,而是轮训各缓冲区实例。如果某缓冲区实例没有被Page Cleaner线程处理过,Cleaner线程就会打上标签并开始处理,直至所有缓冲区实例都打上标签,表示本轮持久化工作执行结束。

下面来看协调线程是如何计算各缓冲区实例应当持久化的脏页数的。MySQL是从历史脏页刷新速度、REDO日志情况、脏页情况和系统IO能力4个维度进行综合评估的。历史刷脏页速度从pageREDO日志两个维度进行跟踪的,分别为avg_page_ratelsn_avg_rate,前者表示历史每秒刷脏页的数量,后者表示历史每秒REDO日志的产生数量。为了防止avg_page_ratelsn_avg_rate剧烈波动,每innodb_flushing_avg_loops秒(或次)才会更新一次。Avg_page_rate=(avg_page_rate+上个周期刷新的脏页数/上个周期刷新的耗时)/2,而lsn_avg_rate=(lsn_avg_rate+上个周期产生的日志数/上周周期刷新的耗时)/2,可见avg_page_ratelsn_avg_rate是根据上个周期的实际速度不断进行迭代更新的。

REDO日志情况是从活跃日志占比的角度进行跟踪的,活跃日志不能占总日志文件太大,否则很容易引起REDO满而等待。计算活跃日志占比pct_for_lsn的算法如下:

  • 当前的LSN减去Flush List中最旧的oldest_modification,记为age
  • 如果age/日志文件大小小于innodb_adaptive_flushing_lwmpct_for_lsn=0,否则进入下一步;
  • 如果innodb_adaptive_flushingfalsepct_for_lsn=0,否则进入下一步;
  • lsn_age_factor=age*100/日志文件大小,pct_for_lsn=(innodb_max_io_capacity / innodb_io_capacity)*(lsn_age_factor*sqrt(lsn_age_factor))/7.5

可见,pct_for_lsn已经考虑了系统IO能力。脏页情况是跟踪脏页的占比情况,脏页数不能占总页数太大,否则很容易引起无法获得可置换页而等待。计算脏页占比pct_for_dirty的算法如下:

  • 当前的脏页数除以总页数,记为dirty_pct
  • innodb_max_dirty_pages_pct_lwm等于0,表示不启动预刷新机制。此时,如果dirty_pct小于innodb_max_dirty_pages_pct,那么pct_for_dirty=0,否则pct_for_dirty=100
  • innodb_max_dirty_pages_pct_lwm不等于0,表示启动预刷新机制。此时,如果dirty_pct小于innodb_max_dirty_pages_pct,那么pct_for_dirty=0,否则pct_for_dirty=dirty_pct*100/(innodb_max_dirty_pages_pct+1)

有了上述信息之后,就可以计算出本次需要刷入的页数,记为n_pagesN_pages等于(avg_page_rate+pages_for_lsn+max(pct_for_dirty, pct_for_lsn)*innodb_io_capacity/100)/3,其中pages_for_lsn等于Flush Listoldest_modification小于当前已持久化的LSN+lsn_avg_rate的总page数。当然还需要考虑系统的IO能力,因此最终的n_pages=min(n_pages, innodb_max_io_capacity)。得到待刷新的总pagen_pages后,下一步就是将其分解到各个缓冲区实例中。如果pct_for_lsn大于30,说明REDO日志可能是潜在的瓶颈点,需要优先降低活跃日志数量,因此需要根据各缓冲区实例中的活跃日志比例,将n_pages分配给各缓冲区实例。如果pct_for_lsn小于30,比较简单,将n_pages均匀地分配给各缓冲区实例即可。

完成待刷新脏页数的计算后,各Page Cleaner线程开始并行实施持久化工作。对于每个缓冲区实例而言,主要包括刷新LRU ListFlush List两项工作。刷新LRU ListLRU List尾部开始遍历innodb_lru_scan_depthpage。如果page为非脏且没有其它线程在访问,将该page移到Free List中,从而提高用户线程获取空闲page的效率。如果page为脏页且没有其它线程访问或正在被刷新,刷新该page。刷新Flush ListFlush List尾部开始刷新特定数量的脏页。

Checkpoint

MySQL支持Sharp CheckpointFuzzy Checkpoint。当innodb_fast_shutdown配置为0或者1时,数据库关闭时会执行Sharp Checkpoint,即将所有脏页刷入持久化设备中。由于Sharp Checkpoint会刷新所有脏页,很可能影响正常的用户数据访问,所以数据库运行期间执行Fuzzy CheckpointFuzzy Checkpoint会综合考虑历史刷新速度、日志情况、脏页情况和系统IO能力,动态决定每次刷新脏页的数量,具体算法间Flush List章节。

Page Cleaner线程的任务是周期性地将脏页刷入持久化设备,而MASTER线程的任务之一就是周期性地(每1秒)将Flush List中的oldest_modification写入到REDO日志中,记为checkpoint LSN。当数据库重启后,会从REDO日志中获取最近一次的checkpoint lsn,并从该lsn开始应用重做日志。

总结与分析

OracleMySQL在缓冲区和检查点设计思想上是非常类似的,只是在实现精细化程度上有所不同。首先来看负载的多样性,在blockpage大小方面,Oracle可以同时支持2K4K8K16K32Kblock,而MySQL虽然也可以支持4K8K16K32K64Kpage,但同时支持能支持一种。Oracle在普通缓冲区的基础上,还可以同时支持KEEPRECYCLE类型的缓冲区,让用户在创建表是可以根据负载特点自行选择,只是MySQL所没有的。

在并发方面,Oracle根据CPU核数自动将缓冲区分割为多个工作集,MySQL需要根据用户配置的缓冲区实例数对缓冲区进行切割。不管是Oracle的工作集还是MySQL的缓冲区实例,都将缓冲区分解为多个子缓冲区,每个都有独立的控制结构和互斥量,从而提高并发性。不同之处是两者在Hash表的设计上,OracleHash表是独立于工作集设计的,是全局的,大约每32个桶有一个独立的互斥量。MySQLHash表是依附于缓冲区实例的,即每个缓冲区实例多有一个Hash表,是局部的,每个Hash表大约有16个互斥量。MySQL的局部Hash表设计需要引入两次Hash过程,既要将page均匀地Hash到各个缓冲区实例上,又要为了效率将属于同一个extent上的page放到同一个缓冲区实例中,两者存在一定的冲突。OracleHash表互斥量可以随着缓冲区的大小自动增加,而MySQLHash表只能跟踪缓冲区实例数的增加而增加。

LRU算法方面,Oracle有主链和辅链,MySQLLRU ListFree ListOracle在主链上有热区和冷区,MySQLLRU List也有热区和冷区。Oracle设计了TIM防止伪热块,MySQL也同样设计了access_time。不过在具体算法和设计方面Oracle更加精细化。例如,Oracle通过TCH机制更好地降低块在LRU链表上的移动,而MySQL只是做了适当的缓解。Oracle引入Write链区分LRU脏块和普通块,进一步提高效率,这是MySQL所没有的。Oracle根据负载对缓冲区的冲击情况做了不同的应对,这也是MySQL所不具备的。

在弹性方面,OracleMySQL都支持在线调节缓冲区大小的能力。Oracle的颗粒度为GranuleMySQL的颗粒度为Chunk。不同的是OracleSGA中大部分缓存都拉通了,且之间可以自动地、动态地调整,对业务基本没有影响。MySQL当前仅支持手动调整缓冲区大小,且对用户线程访问缓冲区有一定影响。

最后看checkpoint的平衡性,Oracle设计了Checkpoint QueueFile QueueObject Queue多个维度的队列,并引入了LRU-W的主链和辅链,从而根据负载的不同更加高效地识别出对应的脏块,并将脏块写入持久化设备。MySQL设计了Flush List(对应于OracleCheckpoint Queue),并结合LRU List进行脏页的持久化。OracleMySQL都综合考虑日志、脏页、Io等多个维度来确定脏页的写入速度,但Oracle很好地利用了统计信息,更加自动化地计算每次的脏页写入量。体现为在最简情况下Oracle可以仅设置系统启动时长fast_start_mttr_target,而MySQL需要设置innodb_flushing_avg_loopsinnodb_adaptive_flushing_lwminnodb_adaptive_flushinginnodb_max_dirty_pages_pct_lwminnodb_max_dirty_pages_pctinnodb_max_io_capacityinnodb_max_io_capacity等一系列配置项。

PDF文档下载地址:http://blog.itpub.net/69912723/viewspace-2732885/