Oracle内核技术揭秘 -- Buffer Cache

hash算法在内存中的应用:

传统的链表在内存中是不连续的,查找就得遍历链表直到找到目标块为止,浪费性能。

此时hash算法在内存中的应用就出现了,hash算法要求内存是相连的并且按照其语句的hash值除以三的余数排序

查找时将语句作为Hash Key计算出结果后除以3取余通过20000+1×300得到的数值就是其在内存中的位置

这几块内存称为Hash Bucket

假如计算1号HASH Bucket的位置:HASH表地址+HASH Bucket编号×Hash Bucket大小

以上所说的HASH Bucket并不储存真正的数据,只是记录了对应的Buffer地址

假如buffer cache的内存是10G 块大小是8k,那么Buffer Cache一共可以容纳1310720个块,对应的HASH Bucket大小为8字节(64个二进制位),1310720个8字节就是10MB

为了应对两个查询具有相同的HASH值,引入了HASH表+链表的概念,也就是在每个HASH Bucket后面建立一个链表。

BA:在HASH表中存放着的Buffer Cache的地址

逻辑读流程

1/根据需要访问块的文件号,块号计算HASH值

2/根据HASH值找到HASH Bucket

3/搜索Bucket后的链表找到目标BH

4/从BH中取出Buffer的BA

5/按BA访问Buffer

CBC Latch与Buffer Pin:

CBC Latch是Oracle在链表前加的一把锁,保证在找到目标BH前链表的一致性,当找到目标BH并修改其Buffer pin的状态,随后释放CBCLatch。

Buffer Pin是在BH上的锁,其为了保证在读或写时BH的状态不会改变。Buffer pin有两种模式:S 共享模式;X 独占模式。如果只是逻辑读,

进程将其设置为S,如果要进行DML则将其修改为X。

CBCLatch也有两种模式:共享和独占,但是并不是所有读都和Buffer Pin一样时共享的,除了有唯一引索外大多情况无论读还是写都以独占模式

获得,因为你还是需要修改其中BH的Buffer Pin锁的状态。在当Buffer Pin被修改是就会将独占的CBC Latch切换为共享的。

除了普通索引的根块、枝块外,在有唯一索引、索引唯一扫描时,索引的根块,枝块,页块,表块都将以共享的方式保护。

CBC latch争用:

两种情况:

1)多个进程以不兼容的模式访问某一CBC Latch保护的不同链表的不同BH-------此为热链竞争

2)多个进程以不兼容的模式访问某一CBC Latch保护的同一链表的同一BH-------此为热块竞争

对于热链竞争比较容易解决,修改参数_db_block_hash_buckets和_db_block_hash_latches之中的一个。这两个参数控制着HASH Bucket的数量和

CBC Latch的数量,这样一来BH和Hash Bucket的对应关系就会被重新计算。

但是需要说明的是,不推荐使用修改_db_block_hash_buckets和_db_block_hash_latches的方法,在无计可施的情况下才考虑使用这种方法。

对于热块竞争问题比较不好解决,大多都是SQL语句执行计划不合理导致的,只需要调优SQL即可。比如说创建了一个很小的表,没有创建索引

每次访问都是全表扫描。后来当查询压力逐渐加大时,这个表块上就出现了大量的CBC Latch竞争。

检查点队列:

脏块链表有两个:

1.检查点队列 CKPT-Q

2.LRUW

检查点队列如何产生:修改块时,块的状态会修改为脏块,这个工作是在CBC Latch的保护下进行的。之后的操作是,进程先持有checkpoint queue latch,然后将此修改几号文件的几号块加入到检查点队列(CKPT-Q),将修改产生的对应redo信息加上例如redo在19号redo文件1号块16字节处会写成19.1.16(这个地址叫RBA)现在又有一个块被修改了,他因为修改时间比之前的块晚,因此在检查点队列中排在之前的块之后。

也就是说检查点队列里面有文件号,块号,RBA按照上文所说,检查点队列的顺序和块变脏时产生的redo记录顺序是一致的,但是并不是每次修改块检查点队列都会发生改变。检查点队列只会记录块第一次修修改的信息(Low RBA--LRBA),不会记录块的最后一次修改记录(High RBA--HRBA);HRBA作用不大,LRBA作用巨大。

buffer cache中的所有buffer变脏时都会立即被链接到检查点队列中。块变脏到被连接到检查点队列中是修改操作的一部分,只有当连接操作完成才算修改完成。回到主题,脏块什么时候写入?每当3s,dbwn就会被唤醒,查看检查点队列的长度,也就是查看脏块的数量,如果脏块太多,就会触发写。DBWR会按照检查点队列从头到尾写入,事实上DBWR并不会一次性把所有检查点队列上的脏块全部写入,这里不做讨论。当DBWR扫描检查点队列时先获取 checkpoint queue latch,按顺序确定要写的脏块将其从检查点队列中移走到对象队列(OBJ-Q)中,这部分也是以后再讲,待移走后会立马释放checkpoint queue latch,所以在脏块被写入磁盘前他就不在检查点队列了。

Oracle实例恢复的目标就是检查点队列中所有的脏块。恢复时只需找到检查点队列头对应的redo记录从此开始顺序向下恢复。由于检查点队列也在内存中,出事了也会丢掉,所以CKPT进程每3s将对应的LRBA记录到控制文件中。

总结:

1.块被修改时会产生Redo记录。

2.块由不脏变为脏时会被链接到检查点队列中

3.检查点队列中块的排列顺序和Redo的记录顺序基本一致。

4.DBWR每3S检查一次检查点队列的长度,也就是脏块数。如果队列过长就会触发写脏块。

5.DBWR会沿着检查点队列的顺序写脏块

6.CKPT每3S一次将检查点队列头对应的LRBA写入控制文件。

7.如果发生崩溃、宕机等情况,实例恢复的起点就是控制文件中记录的检查点队列头所对应的LRBA。

8.实例恢复开始时,Oracle找到此LRBA,再定位到某个Redo文件的某个位置,开始一次进行实例恢复。

dbwr如何写脏块:先理解参数 fast_start_mttr_target(简称MTTR) 对此参数的解释是用户希望多久完成实例恢复,300就是300s;可以通过设置MTTR参数增大减少期望的实例恢复时间其次DBWR决定写脏块时,会对要写的脏块进行整合,从而将相邻的脏块合并。为了将几个小块合并成一个大I/O,写脏块到磁盘前,会将连续的脏块写道共享池这块专门的缓冲区中。在这里块物理上是相连的,然后从共享池中将整块缓存到磁盘中但是一次不可能把所有的脏块都进行合并,需要分批进行,把脏块分成多个Batch,分别对每个Batch合并。DBWR会从第一个Batch开始,轮流将Batch的脏块移出检查点在写入第一个Batch之前DBWR会记录一个等待事件 db file parallel write,直到这一个Batch所有脏块的写操作都完成,此等待事件结束。假如有3个Batch,那么等所有脏块都写入完毕后 db file parallel write 会增加3次。

如何判断DBWR的写I/O是否存在问题主要依据以下三个指标

1.写I/O总块数:通过v$sysstat中physical writes资料可得到写入总块数

2.写I/O总次数:通过v$filestat视图中的phywrts可以得到总次数。或查看v$sysstat中的physical write IO requests资料值也可了解所有数据文件写入总次数

3.Batch的总次数:通过等待事件 db file paraller write 的次数就可以知道Batch的个数和完成时间。

所以,单个Batch越大、Batch总数越少,写I/O的性能也就越好。

提高DBWR写效率:减少块数、次数和“批数” 。块数和次数和应用相关,很难减少,批数很容易。隐藏参数_db_writer_coalesce_area_size。理解为为DBWR写合并分配的内存大小,这个值越大Batch就会越少,单个Batch就会越大。

LRU队列

一组LRU包含4个链表:主LRU、辅助LRU、主LRUW和辅助LRUW。

其中主LRU和辅助LRU用于在buffer cache中寻找可覆盖的buffer块。主LRUW和辅助LRUW的作用是和检查点队列相辅相成,共同完成写脏块机制。

主LRU、辅助LRU链表

LRU会将buffer cache中所有的buffer都链接在一起。主LRU有冷端、热端两个部分,同时每个buffer有一个访问计数TCH,以3s为一个阶段,每个阶段只要有并且不管有多少进程访问,他的数值就会加1,buffer在冷端还是热端全看这个TCH首先要明白,Oracle内存中的任何链表,都会有专门的Latch保护它。一组LRU链表(上文说的那四个)为一个workset(工作组)用一个Latch保护:cache buffer lru chain latch一般25%的buffer在辅助LRU中,而且不一定所有的buffer都在LRU中,因为还有LRUW

物理读:

1.先获得cache buffer lru chain latch 然后进程从辅助LRU尾端搜索可以覆盖的Buffer。

2.可以覆盖的标准时:一不是脏块,二TCH值小于2。

3.辅助LRU尾端的H可以覆盖,它会被转移到主LRU的冷端头。

//其中如果发现TCH大于2的,那么他将会被移动到热端头并且TCH清零。

//LRU的热端不会加入循环中,但是随着新的TCH>2的块被放到热端头,其中也有块被挤出加入循环。

//SMON会抽空在辅助LRU上准备好一些可以覆盖的牺牲者,以减少cache buffer lru chain latch

//全表扫描大表时物理读的块不会进入主LRU,只会使用辅助LRU的空间;块的TCH值为0 并且大表的全表扫描在11gR2后自动使用直接路径读,不在进入buffer cache

//cache buffer lru chain latch的数量和工作组有关,工作组的参数通常来自于cpu_count参数,也就是有几个CPU就有几个工作组。

LRU-flags的意义与怎样查找:

LRU-flags是为了让我们知道一个buffer是在主LRU还是辅助LRU中,可以查找x$bh中的相关信息与buffer caache内存DUMP进行搜索。除了物理读还有一种操作会在LRU链表搜索可覆盖的Buffer,那就是CR块的构造,其也会申请获得cache buffer lru chain latch。如何判断是哪种情况导致的争用可以在v$latch_misses中或AWR中看到Latch在“kcbo_link_q”和“kcbzgws”处有无争用,如果是cr块构造的太多造成的竞争,将只会在’kcbzgws‘处争用,若是物理读造成的则两处都有

脏链表LRUW:

LRUW也是存放脏块的,从作用上说,他和检查点队列有点重复,他们都是放脏块的。但实际上,他们两个相辅相成,共同构成了Oracle的脏块刷新机制。

首先要清楚的是并不是块一变脏就会进入LRUW队列,而是进程在主LRU的冷端扫描时将TCH小于2的脏块移动到LRUW链表。

当有脏块进入LRUW链表时,并不会马上落盘,而是等DBWR进程每3S醒来。下次当DBWR醒来后,他会将脏块移动到辅助LRUW处,从辅助LRUW落盘。当DBWR醒来后不管LRUW中不管有多少(一个或者多个)脏块都会被写道磁盘中。当要写多个脏块时DBWR就会进行合并,将相邻的脏块合并为一个大脏块,这样虽然没有减少写I/O的块数,但可以减少I/O次数。

这里说明一点,由于所有的脏块都在检查点队列中,从LRUW写的脏块,当写完成时,要从检查点队列中去掉。而且写脏块并不是清除操作,写完脏块后它并不会从Buffer Cache中清除,而是等着被覆盖。

脏块G被处理的流程如下:

1.它被服务器进程从主LRU链表移到主LRUW链表。

2.一旦LRUW有脏块,DBWR3秒后醒来,发现LRUW中有脏块,它会将脏块从主LRUW移动到辅助LRUW。

3.脏块被写磁盘

4.写磁盘完成后,脏块被从检查点队列中去除。

5.写完成的脏块已经不脏了,他被放入辅助LRU链表尾端,等待被下次物理读或CR块相关操作覆盖。

服务器进程在扫描主辅LRU时,会将发现的脏块移到LRUW链表中,这时并不会唤醒DBWR写脏块。DBWR 3 秒醒来后,会做两件事:检查CKPT-Q(检查点队列)的长度和Redo量、检查LRUW中是否有脏块。

Oracle中的各个核心进程(DBWR、LGWR、PMON、SMON、CKPT)都有一个3s超时机制,这个机制就是通过semtimedop函数实现的。可以通过man semtimedop查看此函数的详细说明。

Free Buffer Waits

_db_large_dirty_queue参数是用来缓解Free Buffer Waits的。

Free Buffer Waits等待事件是,当服务器进程扫描LRU链表寻找可用块时,如果找了40%的Buffer(40%是受隐藏参数_db_block_max_scan_pct控制)都没有找到可以覆盖的Buffer,进程将停止继续扫描LRU,唤醒DBWR写脏块,同时进程转入睡眠,开始等待Free Buffer Waits。

这些不可被覆盖的Buffer包含以下3种类型:

1)正在被其他进程加Buffer Pin Lock的(也就是正在被Pin的)

2)TCH大于等于2的

3)还有脏块的

也就是说,服务器进程扫描了LRU链的40%没找到可覆盖的Buffer,有可能将很多很多Buffer移动到LRUW链中。随后服务器进程将停止扫描LRU,唤醒DBWR进程将LRUW中的所有脏块赶快写入磁盘。然后服务器进程将为自己设置一个超时时间,进而转入睡眠状态,等待超时过后再次醒来,重新搜索主辅LRU链表。**从服务器进程进入睡眠开始,找到可覆盖的Buffer**,等待时间就是Free Buffer Waits了。

如果一个系统中存在太多的物理读使Free Buffer Waits过多,则可以适当增加DBWR写脏块的频率,减少此等待事件的增加,此机制与参数**_db_large_dirty_queue**参数相关。

服务器进程在扫描LRU时,会将遇到的脏块移到LRUW中。当进程完成LRU扫描,找到可覆盖的Buffer时,在进入下一流程是会判断总块数,如果脏块数小于总Buffer数的25%则等DBWR超时醒来写,如果即将达到或超过25%则直接唤醒DBWR,这25%就是由**_db_large_dirty_queue**设定的。至于服务器检查脏块数环节是检查检查点队列的长度,因为CKPT-Q就有所有未写入的脏块。

谁“扣动”了DBWR的“扳机”

DBWR会在以下状况中醒来:

1. DBWR自身的3秒超时。会做两件事:检查LRUW是否有脏块;查看检查点队列长度、Redo Recorder数量,以决定是否写脏块,这是增量检查点机制。如果这两个队列都不足以触发写,则在3秒超时时啥都不做。
2. 服务器进程扫描LRU后,发现脏块数超过25%,会唤醒DBWR。
3. 若服务器进程扫描超过40%的Buffer,没找到可覆盖的Buffer,会等待Free Buffer Waits事件,唤醒DBWR写脏块。
4. 以下其他情况

1. 用户发出完全检查点命令
2. 用户发出正常关闭数据库
3. 日志切换
4. 表空间或数据文件Offline时
5. Drop、Truncate或直接路径读某个对象时

如果检查点写比例大幅下降,必然导致v$sysstat中dirty buffers inspected资料值上升,进程持有cache buffers lru chain latch时间加长。

如果检查点写比例上升,因为热块反复写,会使物理写块数比以前增加。

日志切换与写脏块

日志切换时的检查点,不是增量检查点,只能说日志切换有可能触发增量检查点。没有等待的日志切换并不会触发DBWR立即写脏块。只是唤醒DBWR,并告知DBWR已经发生了日志切换。写不写脏块由DBWR自己判断

日志切换时,CKPT找到19号Redo File在检查点队列中RBA最大的脏块,将它的SCN、RBA通知DBWR。

然后DBWR仍然按照原来的增量检查点机制写脏块,CKPT也仍然3秒一次检查DBWR的写进制。

当CKPT醒来发现DBWR没有将对应的脏块全部写完,那么这个Redo File的状态仍然保留为Active。当CKPT又一次醒来发现DBWR已经写完了,这时CKPT会做如下工作:

1. 修改控制文件中当前Redo File的状态,新状态为Inactive
2. 修改控制文件中数据文件的SCN
3. 修改数据文件头的SCN和RBA

I/O总结

只靠Buffer Cache就能完成的I/O是逻辑I/O,也就是逻辑读/写;针对磁盘设备的I/O,是物理I/O。

逻辑读又被细分为consistent gets(一致读)和db block gets(当前读),可以在v$sysstat资料视图中查看它们的值。它们两个的一致读针对select,而当前读则针对DML或DDL。简单点说,一致读是纯粹的读,当前读是为修改而产生的读。

关于这两种读的标准定义,就是Buffer Header上的_Buffer Pin锁。它有多种模式,常见的三种:共享、独占、类似共享的模式。其中共享就是一致读。独占就是修改——逻辑写。类似共享就是当前读。

 

END

摘自《Oracle内核技术解密》 ---吕海波

posted @   guapisama  阅读(146)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示