揭密Oracle之七种武器之三:破译古老的谜题---共享CBC Latch的秘密

2020-06-19 00:00:00 索引 函数 都是 调用 共享
揭密Oracle之七种武器之三:破译古老的谜题---共享CBC Latch的秘密 (前两章地址 揭密Oracle之 七种武器 章 搭建测试环境(目前已到第三章) http://www.itpub.net/thread-1605241-1-1.html 揭密Oracle之七种武器二:DTrace语法:跟踪物理IO http://www.itpub.net/thread-1609235-1-1.html ) 从9iR2开始,Cache Buffers Chain(以下简称CBC)Latch就变成共享Latch了。从那时开始,我想当然的认为,如果我只有读操作,互相之间就不会阻 塞了。于是马上测试: declare myid number; begin for i in 1..10000000 loop select id1 into myid from a2_70m where rowid='AAACYJAAEAAAAAUAAA'; end loop; end; / 这段过程很简单,就是反复的逻辑读某一行。将这段过程在两个会话同时执行,我天真的认为,不会再看到CBC Latch等待。但是,查看等待事件的结果 ,令我深深的迷惑。为什么还是有等待呢?无论CBC 链还是数据块,我都没有修改,只是反复读取,为什么共享Latch不共享呢?从此,这个迷团一直困绕着我。 当然,还有其他一些谜团,比如索引和非索引在读扫描时的区别。普通的区别,是它们两个逻辑读不一样,索引比非少一个逻辑读 。但其实,它们两个的区别非常大。具体的区别在哪里?这些区别对于我们的选择,会有什么影响? 这些谜团很长一段时间内没有答案。 五、六年转眼即过,2011年初,因公司技术转型,我被迫从头学起GreenPlum。翻开几百页的英文文档,我不禁倦意袭来。再看会Oracle的资料,又不禁 精神百倍。于是,我退意蒙生。但是,这段经历,让我有一个意外的收获。阿里的GreenPlum,都是跑在Solaris下。接手GreenPlum运维,必先学会Solaris。在 学习Solaris时,看到有一本书用两页纸介绍了一个工具:DTrace语言,说是可以跟踪Solaris中的任何操作。当时我对Oracle的研究,也陷入了困境。能用的跟 踪事件都用了,很多原理还是无法搞清楚,只能跟着别人,人云亦云一下,自我感觉对Oracle了解甚为深入,已经没什么可以再学的了。但分析一些工作中奇怪 的问题,就总感觉似是,而非。 这种感觉让我想起来多年前,年青的时候我酷爱神秘文化。什么东西都信,曾在二月底初春时节跳入溥冰覆盖的河水中受洗,随身携带一本荒漠甘泉。 在被女神无情抛弃后,独自站在空旷的教堂祈祷:“仁慈的圣父啊,我知道这是您对我的庇护和煅炼,虽然您的孩子此刻心如刀绞,但我仍然感谢您、爱您。哈 利路亚,阿门。”不久之后,下一位女神出现,却是信佛的。于是我又到家乡的大相国寺,皈依佛祖,每逢初一、十五,烧香诵经:“南无西方琉璃药师佛 ,南无……”。 在诸多杂学之中,我精通的却还是周易。刚刚参加工作哪会,我为我们科室6个人占卦,算他们哪一年结婚、哪一年有小孩,6个人,只有一个算错了 。83%的准确率,很高了。但是,为什么有一次算错了呢?为什么其他的可以算对呢?这些问题我都答不上来,我对周易的理解,始终似是,而非。 易经这东西,真正的神人传下来的,几千年中,看懂的没几个。但是Oracle呢,我也无法真正的看“懂”它吗。对易经的理解似是而非,这我服气,但 对Oracle,我不想停留在似是而非的境界。 当看到这个DTrace后,我眼前顿时一亮,如果用DTrace跟踪Oracle,又会有怎样的效果呢?是否可以打破“似是而非”的僵局呢?于是我马上搜集资料 开始学习,这一下,没想到豁然为我打开一扇大门。于是我再也顾不得什么GreenPlum、什么KPI了。:) 好了,言归正传,这一节,从一个重要的提供器开始,PID进程提供器。Solaris在进程调用、退出每个函数时,都设置了Prob,进程提供器的作用就是 打开这些Prob。 我们可以写如下的脚本,打开PID提供器所有调用函数时的探针: pid1234:::entry { 动作; } 这个脚本的作用是打开1234进程所有函数调用处的探针。简单点说,1234进程每调一次函数,都会被触发。这个脚本还可以进一步改成这样: pid$1:::entry { 动作; } 用$1代替了1234。$1,这种写法是来自于Shell脚本编程,个参数。当然,我们也可以pid$2。 接下来,我们可以定义什么动作呢?当然还是观察了. 在我上传的《Solaris 动态跟踪指南》书中,P68页,列出了全部的内置变量,这次,我们使用这几个内置变量:probeprov, probemod, probefunc, probename,arg0和arg1…… probeprov:提供器名 probemod : 模块名 probefunc:函数名,这是我们要查看的重点。 probename:探针名,只有两个。entry,return,一个进入、一个是退出。 arg0,arg1,…… :调用函数时,传递给函数的参数。 这些内置变量,无需定义,可以直接使用。内置变量中保存了很多重要的值,在上篇文章已经有用到过。 好,我们的终脚本程序,是这个样子: 这个探针的使用很简单,我们总的脚本如下: #!/usr/sbin/dtrace -s -n dtrace:::BEGIN { i=1; } pid$1:::entry { printf("i=%d PID::entry:==%s:%s:%s:%s %x %x %x %x %x %x",i, probeprov, probemod, probefunc, probename,arg0,arg1,arg2,arg3,arg4,arg5); i=i+1; } 参数这块,我们也不知道每个函数都有几个参数,好在多输出参数DTrace并不会报错,所以,我们就多显示几个参数,我显示了前6个: arg0,arg1,arg2,arg3,arg4,arg5。都以%x,16进程格式显示。 将此脚本保存为all_func.d,授于执行权限,开始执行。 对了,别忘了,本章的目的,是观察CBC Latch。更进一步的,观察逻辑读的CBC Latch。 打开一个会话,查询出它对应的进程号: SQL> select c.sid,spid,pid,a.SERIAL# from (select sid from v$mystat where rownum<=1) c,v$session a,v$process b where c.sid=a.sid and a.paddr=b.addr; SID SPID PID SERIAL# ---------- ------------ ---------- ---------- 863 970 22 1 我的进程号是970。另外,在开始观察前,执行几次如下语句,让读是逻辑读: select * from a2_70m where rowid='AAACYJAAEAAAAAUAAA'; 如下运行脚本,观察970号进程: # ./all_func.d 970 > logic_read1.log dtrace: script './all_func.d' matched 124179 probes 根据显示结果,共有124179个探针被打开。十几万个探针,说明Oracle内部,有十几万个函数。C语言中,程序代码的复用,全靠函数了。C又被称为函 数语言吗。不过,Oracle内部竞然有十几万个函数,还是出乎我的意料。不过,函数分的越细,对我们调试、跟踪越好。在没有源代码的情况下,我们只能跟踪 到函数级别了。 跟踪结果会很多,为了便于观察,我将结果重定向到logic_read1.log文件中。 另外,由于会打开太多探针,有可能会超出DTrace的限制,报出错误,可以修改/kernel/drv/fasttrap.conf中fastrap-max-probes设置,在我的测试环 境中,我设置为fastrap-max-probes=1000000。 另外,如果在970进程执行期间,all_func.d脚本报内存不足,可以在脚本开头加上去内存大小或刷新频率的设置: #!/usr/sbin/dtrace -s -n -x switchrate=10hz -b 16m -x switchrate=10hz,设置刷新频率。DTrace会结果发送到输出终端,这个值可以理解为发送频率。在数据没有发送到输出终端前,DTrace会先保存到 自己的缓存中。因此,增加刷新频率,可以减少内存使用。 -b 16m , 修改缓存大小。 好了,来看结果吧,在970进程对应的会话中,再执行一次: select * from a2_70m where rowid='AAACYJAAEAAAAAUAAA'; 回到执行DTrace命令的窗口,按Ctrl+C。然后查看结果,先看一下有多少行输出吧: # cat logic_read1.log|wc -l 1211 1211行,这是运行一次软软解析,再加上对一个块逻辑读取出一行,Oracle所要调用的函数次数。这也是我们细粒度的跟踪级别了。比10046等任何一 个Event,都要细致的多。除非你去看源码,否则,不可能比这个更细、更深入了。 下面,让我们来看看结果都是什么吧: # cat logic_read1.log|more CPU ID FUNCTION:NAME 3 172611 memcpy:entry i=1 PID::entry:==pid970:libc.so.1:memcpy:entry 8047708 c0f2c28 1 c028934 c02a6dc 6 3 52316 kslwte_resmgr:entry i=2 PID::entry:==pid970:oracle:kslwte_resmgr:entry 100 62657100 1 0 8047708 c028894 3 174943 gethrtime:entry i=3 PID::entry:==pid970:libc.so.1:gethrtime:entry c07ad01 80461e4 80461e4 8dd9467 100 62657100 3 52313 kslwte_tm:entry i=4 PID::entry:==pid970:oracle:kslwte_tm:entry 100 62657100 1 0 cfacb398 1 3 111268 skgslnoop:entry i=5 PID::entry:==pid970:oracle:skgslnoop:entry c028934 c02a6dc 0 8046130 c0e7078 b0fc070 3 86139 kews_idle_wait:entry i=6 PID::entry:==pid970:oracle:kews_idle_wait:entry 8c9775bd 0 c028934 c02a6dc 0 8046130 3 174943 gethrtime:entry i=7 PID::entry:==pid970:libc.so.1:gethrtime:entry 8f1e27a0 8f18c820 8c9775bd a9c0001 c07ad9c 80460f0 3 86061 kewe_trace_level:entry i=8 PID::entry:==pid970:oracle:kewe_trace_level:entry 8f18c820 c028934 c02a6dc 0 8046130 c0e7078 3 52312 ksl_which_bucket:entry i=9 PID::entry:==pid970:oracle:ksl_which_bucket:entry 2325dd c028934 c02a6dc 0 8046130 c0e7078 3 53333 kskthewt:entry i=10 PID::entry:==pid970:oracle:kskthewt:entry c07ad01 80461e4 80461e4 8dd9467 100 62657100 3 172611 memcpy:entry i=11 PID::entry:==pid970:libc.so.1:memcpy:entry 8047714 c0f2c29 2 101 c028890 c0e7120 3 104873 kpuhhmrk:entry i=12 PID::entry:==pid970:oracle:kpuhhmrk:entry c028850 101 c028890 c0e7120 804773c 0 ………………………… ………………………… ………………………… 以行为例,pid970:libc.so.1:memcpy:entry,pid970是提供器名,libc.so.1是模块名,memcpy是函数名,entry是探针名。 我摘出前十几行,DTrace是能以很细的粒度跟踪Oracle,细致程度远超10046,但问题来了,我们如何解读跟踪结果。这是一个很重要的问题。 简单点说,这些函数都是干吗的。不要指望谁能告诉你,现在,进行这种探索的,还非常非常少。这方面的资料,就不要奢望了。来吧,Maoyeye教导我 们,自己动手,丰衣足食。 我们不需要、也可能能搞清楚这每一行函数调用都是干吗的。Oracle的代码量哪么庞大,估计Oracle的开发人员,也不可能搞清楚这每一行全部的意义 。我们只需要搞清楚,我们自己关心的就行了。比如,我一开始所说的,Oracle在什么时候加什么的Mutex、Latch、Pin、Lock,什么时候释放,会以怎样的形式 阻塞,等等。 我们今天,先以CBC Latch为例,说一下研究它的思路。其他的也都类似。我想做的,不是告诉你一个结果,而是这结果是怎么来的,让我们大家都可以 都可以用这种方式去研究。 每个Latch,都有一个地址,哪么,Oracle在调函数去获得、获放Latch时,应该会将此地址做为参数。好,马上,查找Latch的地址: 1、找出测试语句中ROWID在哪个文件哪个块: SQL> select dbms_rowid.ROWID_RELATIVE_FNO('AAACYJAAEAAAAAUAAA'),dbms_rowid.rowid_block_number('AAACYJAAEAAAAAUAAA') from dual; DBMS_ROWID.ROWID_RELATIVE_FNO('AAACYJAAEAAAAAUAAA') DBMS_ROWID.ROWID_BLOCK_NUMBER('AAACYJAAEAAAAAUAAA') --------------------------------------------------- --------------------------------------------------- 4 20 测试语句要查找的行在4号文件、20号块 2、在x$BH中,找到此块在哪个Latch的保护下: SQL> select file#,dbablk,tch,lower(HLADDR) from x$bh where file#=4 and dbablk=20; FILE# DBABLK TCH LOWER(HL ---------- ---------- ---------- -------- 4 20 3 8ea1d750 4号文件20号块,是受地址为8ea1d750的Latch保护。 3、在跟踪结果文件中查找相关的: # cat logic_read1.log|grep 8ea1d750 3 111575 sskgslcas:entry i=517 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 20000016 fdc3f1e4 fdc3f18c fdc3f1e4 3 111578 sskgsldecr:entry i=526 PID::entry:==pid970:oracle:sskgsldecr:entry 8ea1d750 20000016 fdc3f1e4 fdc3f18c fdc3f1e4 804544c 3 111575 sskgslcas:entry i=552 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 20000016 1 fdc3f17c 81e1c064 3 57740 kcbzar:entry i=557 PID::entry:==pid970:oracle:kcbzar:entry 8ef9a5b4 8ea1d750 108000 8045368 1 fdc3f17c 3 101760 slmxnoop:entry i=558 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18 3 101760 slmxnoop:entry i=559 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18 3 101760 slmxnoop:entry i=560 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18 3 101760 slmxnoop:entry i=561 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18 3 101760 slmxnoop:entry i=562 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18 3 101760 slmxnoop:entry i=564 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18 3 111578 sskgsldecr:entry i=566 PID::entry:==pid970:oracle:sskgsldecr:entry 8ea1d750 20000016 1 fdc3f17c 81e1c064 8045510 3 52568 kssrmf:entry i=568 PID::entry:==pid970:oracle:kssrmf:entry 8ef9a590 8e94811c 81ff1de4 20000016 8ea1d750 8ef9a5b4 和这个地址相关的有这十几行。在这里,有一点编程习惯再说一下,要申请某一个地址处的Latch,这个Latch的地址,是这个函数的重要的参数,因 此,Oracle会把它排在位,也就是说,以上这十几行中,个参数不是8ea1d750的,基本可以排队掉了。 所以,我们只剩这些行需要关注: # cat logic_read1.log|grep "entry 8ea1d750" 3 111575 sskgslcas:entry i=517 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 20000016 fdc3f1e4 fdc3f18c fdc3f1e4 3 111578 sskgsldecr:entry i=526 PID::entry:==pid970:oracle:sskgsldecr:entry 8ea1d750 20000016 fdc3f1e4 fdc3f18c fdc3f1e4 804544c 3 111575 sskgslcas:entry i=552 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 20000016 1 fdc3f17c 81e1c064 3 111578 sskgsldecr:entry i=566 PID::entry:==pid970:oracle:sskgsldecr:entry 8ea1d750 20000016 1 fdc3f17c 81e1c064 8045510 这四行,两个函数调用,sskgslcas、sskgsldecr,个参数都是Latch的地址:8ea1d750。我相信这不是巧合,它们肯定是申请、释放Latch的函数。 i=517这行,Oracle调用sskgslcas持有Latch,在i=526这行,调用sskgsldecr释放,接下来在i=552又一次调用sskgslcas持有Latch,在i=566处调用 sskgsldecr释放。一次逻辑读对应两次Latch调用。 结果是这样吗,让我们继续验证,Oracle的Oradebug可以调用某个Oracle自身的函数,就有它来验证吧: SQL> oradebug setmypid Statement processed. SQL> oradebug call sskgslcas 0x8ea1d750 0 0x20000016 0xfdc3f1e4 Function returned 1 SQL> sskgslcas参数的取值,就是我们上面的跟踪结果。我只用了4个参数,其实应该只有3个参数。但是,用Oradebug时,多传了参数也无所谓。 Function returned 1,这一行说明我们的调用是成功的。 回到970进程对应的会话,再次执行如下语句: SQL> select * from a2_70m where rowid='AAACYJAAEAAAAAUAAA'; 被Hang住了,在另一个会话中查看等待事件(970号进程对应的会话ID是863): SQL> select sid,event,p1raw,p2 from v$session where sid=863; SID EVENT P1RAW P2 ---------- ---------------------------------------------------------------- -------- ---------- 863 latch: cache buffers chains 8EA1D750 122 863果然在等待CBC Latch,而且根据P1RAW列的值,所等的Latch就是8EA1D750。接着,sskgsldecr是释放Latch,继续验证此点,在刚才Oradebug的会话 中继续执行: SQL> oradebug call sskgsldecr 0x8ea1d750 0x20000016 Function returned 20000016 同样,sskgsldecr 0x8ea1d750 0x20000016,这个函数的参数来自于我们的跟踪文件。我们这样手动调用结束,刚才被Hang的会话,已经可以顺利执行 下去了。说明Latch已经被释放。 看,我们很轻松就已经找到了Oracle申请、释放CBC Latch的函数。一切都是如此简单。 到这里,可能有人会有不同意见了。如果你看过其他一些牛人的书,包括Oracle的DSI405,都说到Latch的调用、释放,是用kslgetl(独占)、 kslgetsl(共享)和kslfre,怎么我又说申请、释放Latch是另外的函数呢。 这很容易理解,DSI405是讲9i的。其他牛人说的也没错,kslgetl(独占)、kslgetsl(共享)和kslfre的确也是Latch相关的函数。物理读一个块时, Oracle也会用这三个函数来加、释放CBC Latch,但逻辑读不是。 这很容易理解,逻辑读是繁忙的操作,Oracle专门为它开个小灶、做做优化不是很正常吗。而且,提前说一下,Mutex也是用sskgslcas申请的(释放 不是用sskgsldecr),关于Mutex内幕,我们到后几章再详细说,顺便说一句,要想揭开Mutex内幕,也只有D&G(DTrace+GDB)了。 我们还要再接着研究。CBC Latch的地址是8ea1d750,在这个地址处,Oracle都放了什么呢。有两种方式可以观察这个,用Oradebug,或者,改写我们的 DTrace脚本。我用后一种方式吧,这种方式早晚要熟练掌握的,而且并不是每个要观察的值,都可以用Oradebug。 使用DTrace,如果参数是地址的话,将地址的址读出来,这种方法在上一章中已经有描述了,如下修改脚本程序: #!/usr/sbin/dtrace -s -n char *memnr; int latchaddr; dtrace:::BEGIN { i=1; latchaddr=0; } pid$1::sskgslcas:entry { memnr=copyin(arg0,12); latchaddr=arg0; printf("[%2x%2x%2x%2x|%2x%2x%2x%2x|%2x%2x%2x%2x]",memnr[3],memnr[2],memnr[1],memnr[0],memnr[7],memnr[6],memnr[5],memnr[4],memnr [11],memnr[10],memnr[9],memnr[8]); printf("i=%d PID::entry:==%s:%s:%s:%s %x %x %x %x %x %x",i, probeprov, probemod, probefunc, probename,arg0,arg1,arg2,arg3,arg4,arg5); i=i+1; } pid$1::sskgslcas:return { memnr=copyin(latchaddr,12); printf("[%2x%2x%2x%2x|%2x%2x%2x%2x|%2x%2x%2x%2x]",memnr[3],memnr[2],memnr[1],memnr[0],memnr[7],memnr[6],memnr[5],memnr[4],memnr [11],memnr[10],memnr[9],memnr[8]); printf("i=%d PID::entry:==%s:%s:%s:%s %x %x %x",i, probeprov, probemod, probefunc, probename,latchaddr,arg0,arg1); i=i+1; } 在这个脚本中,我只观察CBC的申请和释放。copyin函数的使用,上一章有,不再重述。需要注意的时,我在pid$1::sskgslcas:entry中,执行了这样一 行:latchaddr=arg0;目的是将Latch的地址保存到全局变量latchaddr中。然后,在sskgslcas申请Latch后,再观察一下此地址中的值。 看一下观察结果吧: # cat logic_read2.log|grep "8ea1d750" 0 111575 sskgslcas:entry [ 0 0 0 0| 0 0 291| 0 0 07a]i=3 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 20000016 fdc1a2dc fdc1a284 fdc1a2dc 0 175725 sskgslcas:return [20 0 016| 0 0 291| 0 0 07a]i=4 PID::entry:==pid970:oracle:sskgslcas:return 8ea1d750 16 1 0 111575 sskgslcas:entry [ 0 0 0 0| 0 0 292| 0 0 07a]i=5 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 20000016 1 fdc1a274 81e1c064 0 175725 sskgslcas:return [20 0 016| 0 0 292| 0 0 07a]i=6 PID::entry:==pid970:oracle:sskgslcas:return 8ea1d750 16 1 我显示了latch地址处的12个字节,我将结果整理一下: 进入sskgslcas函数时:[ 0 0 0 0| 0 0 291| 0 0 07a] 从sskgslcas返回时 :[20 0 016| 0 0 291| 0 0 07a] 进入sskgslcas函数时:[ 0 0 0 0| 0 0 292| 0 0 07a] 从sskgslcas返回时 :[20 0 016| 0 0 292| 0 0 07a] 我一共显示了12个字节。后4个节字,7A,10进制是122。这个是Latch编号。中间4个字节,291、292,明显是我访问的次数。这些可以从v $latch_children视图中得到。后4个字节是LATCH#列,中间4个字节,就是GETS列了。 前面4个字节,20000016,正好是sskgslcas的第三个参数。我觉得这个应该是模式。 看来,sskgslcas的作用,应该就是将第三个参数的值“20000016”交换到Latch 地址所指向的内存中。然后访问次数加1。 接下来,该如何确定20000016是否是模式呢?这个,从这里就看不出来了,我们要找个索引试试。 在我的测试表a2_70m,ID1列上有个索引,索引名是A2_70M_ID1。我使用如下测试语句: SQL> select * from a2_70m where id1=1; ID1 ID2 CC1 ---------- ---------- ------------------------------ 1 10 A----------------------------- 以上语句,多执行个几次,在另一个会话中,查看索引的块和Latch地址: SQL> set pagesize 50000 SQL> set linesize 10000 SQL> select file#,dbablk,tch,ba,HLADDR from x$bh a,dba_objects b where a.obj=b.data_object_id and object_name='A2_70M_ID1' order by FILE#,DBABLK; FILE# DBABLK TCH BA HLADDR ---------- ---------- ---------- -------- -------- 5 23449 0 8189E000 8E98DAD4 5 23450 0 81A74000 8EAF0390 5 23451 0 8189C000 8EA150C8 5 23452 3 81A78000 8EB77E00 5 23453 3 81A76000 8EA9CB38 5 23454 0 81A72000 8E9C13F4 5 23455 0 8189A000 8EB2412C 5 23456 0 81A70000 8EA48E64 6 5695 3 818A0000 8EACBC98 多执行几次测试语句,找出TCH值不断在增加的,这些块就是索引扫描时相关的块了。我这里是5号文件23452、23453块,和6号文件5695块。索引的root 块,都是段头的下一个块,我们可以如下确认一下: SQL> select segment_name,header_file,header_block from dba_segments where segment_name=upper('A2_70M_ID1'); SEGMENT_NAME HEADER_FILE HEADER_BLOCK ------------------------------ ----------- ------------ A2_70M_ID1 5 23451 段头是23451块,哪么23452就是root块了。提一个注意事项,索引扫描在10.2.0.2后是不用读段头的,真接Root、枝、叶。但在10.2.0.1,有时还是需 要读段头的。 好,用我们刚才的脚本,开始观察吧。 先执行脚本: # ./all_func.d 970 > logic_read3.log dtrace: script './all_func.d' matched 3 probes 再执行测试SQL,显示logic_read3.log内容,观察结果,先看根块吧: # cat logic_read3.log|grep 8eb77e00 1 111575 sskgslcas:entry [ 0 0 0 0| 0 0 721| 0 0 07a]i=3 PID::entry:==pid970:oracle:sskgslcas:entry 8eb77e00 0 1 fdc1a3bc fdc1a3b4 fdc1a278 1 175725 sskgslcas:return [ 0 0 0 1| 0 0 721| 0 0 07a]i=4 PID::entry:==pid970:oracle:sskgslcas:return 8eb77e00 16 1 根块Latch的地址是8eb77e00,先只看一下根块。注意第三个参数,不是20000016,而是1。我们自己调一下试试: SQL> oradebug call sskgslcas 0x8eb77e00 0 1 Function returned 1 (释放是: SQL> oradebug call sskgsldecr 0x8eb77e00 1 Function returned 1 ) 再到另一个会话执行测试SQL,不会被阻塞。看来这才是共享模式啊。再往下看跟踪文件,8eacbc98是root块后接着申请的一个Latch,它对应6号文件 5695号块。看来它是枝块了。 # cat logic_read3.log|grep 8eacbc98 2 111575 sskgslcas:entry [ 0 0 0 0| 0 0 784| 0 0 07a]i=5 PID::entry:==pid970:oracle:sskgslcas:entry 8eacbc98 0 1 fdc3f2c4 fdc3f2bc fdc3f180 2 175725 sskgslcas:return [ 0 0 0 1| 0 0 784| 0 0 07a]i=6 PID::entry:==pid970:oracle:sskgslcas:return 8eacbc98 16 1 枝块获得CBC Latch,也是共享的。 那么5号文件23453块,它应该是叶块了,查看它的获取Latch情况: # cat logic_read3.log|grep 8ea9cb38 2 111575 sskgslcas:entry [ 0 0 0 0| 0 0 783| 0 0 07a]i=7 PID::entry:==pid970:oracle:sskgslcas:entry 8ea9cb38 0 1 fdc3f2c4 fdc3f2bc fdc3f180 2 175725 sskgslcas:return [ 0 0 0 1| 0 0 783| 0 0 07a]i=8 PID::entry:==pid970:oracle:sskgslcas:return 8ea9cb38 16 1 2 111575 sskgslcas:entry [ 0 0 0 0| 0 0 784| 0 0 07a]i=13 PID::entry:==pid970:oracle:sskgslcas:entry 8ea9cb38 0 20000016 ffffffff fdc3f2c4 fdc3f17c 2 175725 sskgslcas:return [20 0 016| 0 0 784| 0 0 07a]i=14 PID::entry:==pid970:oracle:sskgslcas:return 8ea9cb38 16 1 2 111575 sskgslcas:entry [ 0 0 0 0| 0 0 785| 0 0 07a]i=15 PID::entry:==pid970:oracle:sskgslcas:entry 8ea9cb38 0 20000016 c030e14 fdc3f180 fdc3f2bc 2 175725 sskgslcas:return [20 0 016| 0 0 785| 0 0 07a]i=16 PID::entry:==pid970:oracle:sskgslcas:return 8ea9cb38 16 1 2 111575 sskgslcas:entry [ 0 0 0 0| 0 0 786| 0 0 07a]i=17 PID::entry:==pid970:oracle:sskgslcas:entry 8ea9cb38 0 20000016 0 fdc3f2c4 fdc3f2b8 2 175725 sskgslcas:return [20 0 016| 0 0 786| 0 0 07a]i=18 PID::entry:==pid970:oracle:sskgslcas:return 8ea9cb38 16 1 它一共获取了4次,次是共享的,后面三次,是独占的。后还可以再看一下表块,表块要获得两次,都是独占的。这样看来,索引叶块的CBC Latch的争用,要比表块多啊。建议索引的PCTFREE可以调的比表高些,既能减少中间块分裂的总次数。块中行更少,又能分散争用。 但这样做会使索引树层数升高,增加索引访问时的逻辑读。对于解决索引块上的CBC Latch争用,这样做还是非常值得的。因为同样是逻辑读,消耗的资 源可是不以同日而语的。索引枝块只需要一次CBC Latch,而且是共享的,并且,不需要把数据拷贝到PGA中,只在Buffer Cache中比较一下Key值,取出下一层块 的位置。这种逻辑读,不会造成争用,因为从头到尾,所有资源都是共享的,所耗资源比表块逻辑读也少的多。而且大的PCTFree,还可以减少索引块分裂次数。 因此,使用这种方式,减少索引叶块的CBC Latch争用,是可行的。 好,经过上面的测试,本章开头提到个问题,已经有了答案。为什么共享的CBC Latch会有争用,答案是因为Oracle以独占的方式持有了它。 在文章开头,我还提到过一个问题,就是索引和非索引读扫描时的区别,刚才我的测试索引,不是非的,我把它重建为索引试试,我 们可以比较下,区别还是非常大的: SQL> drop index a2_70m_id1; Index dropped. SQL> CREATE unique INDEX a2_70m_id1 on a2_70m(id1); Index created. 我们的测试语句和刚才相同,只不过这次它的访问路径是索引扫描。 索引的测试结果,和非有很大不同: # cat logic_read3.log CPU ID FUNCTION:NAME 1 111575 sskgslcas:entry [ 0 0 0 0| 0 0 01d| 0 0 0 0]i=1 PID::entry:==pid970:oracle:sskgslcas:entry 87d88194 0 35f0001 8886a9c8 87d88194 888f7c48 1 175725 sskgslcas:return [ 35f 0 1| 0 0 01d| 0 0 0 0]i=2 PID::entry:==pid970:oracle:sskgslcas:return 87d88194 16 1 1 111575 sskgslcas:entry [ 0 0 0 0| 0 0 c67| 0 0 07a]i=3 PID::entry:==pid970:oracle:sskgslcas:entry 8eb77e00 0 1 804520c 8045204 fda522f8 1 175725 sskgslcas:return [ 0 0 0 1| 0 0 c67| 0 0 07a]i=4 PID::entry:==pid970:oracle:sskgslcas:return 8eb77e00 16 1 1 111575 sskgslcas:entry [ 0 0 0 0| 0 0 bc3| 0 0 07a]i=5 PID::entry:==pid970:oracle:sskgslcas:entry 8eafa97c 0 1 804520c 8045204 fda522f8 1 175725 sskgslcas:return [ 0 0 0 1| 0 0 bc3| 0 0 07a]i=6 PID::entry:==pid970:oracle:sskgslcas:return 8eafa97c 16 1 1 111575 sskgslcas:entry [ 0 0 0 0| 0 0 c38| 0 0 07a]i=7 PID::entry:==pid970:oracle:sskgslcas:entry 8ea9cb38 0 1 804520c 8045204 fda522f8 1 175725 sskgslcas:return [ 0 0 0 1| 0 0 c38| 0 0 07a]i=8 PID::entry:==pid970:oracle:sskgslcas:return 8ea9cb38 16 1 1 111575 sskgslcas:entry [ 0 0 0 0| 0 0 bdc| 0 0 07a]i=9 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 1 fda52660 fda52658 fda52600 1 175725 sskgslcas:return [ 0 0 0 1| 0 0 bdc| 0 0 07a]i=10 PID::entry:==pid970:oracle:sskgslcas:return 8ea1d750 16 1 1 111575 sskgslcas:entry [ 0 0 0 1| 0 0 01e| 0 0 0 0]i=11 PID::entry:==pid970:oracle:sskgslcas:entry 87d88194 1 35f0000 c030d18 87d88194 888f7c48 1 175725 sskgslcas:return [ 35f 0 0| 0 0 01e| 0 0 0 0]i=12 PID::entry:==pid970:oracle:sskgslcas:return 87d88194 16 1 索引还是占了同样的数据块,所以对应的Latch不变。可以看到,从根块到叶块,再到数据块,竞然都不是独占的,全是共享的,而且都只需要申请一次 。可以用个匿名块验证一下: declare myid number; begin for i in 1..10000000 loop select id1 into myid from a2_70m where id1=1; end loop; end; / 和开头的存储过程不同的是,select id1 into myid from a2_70m where id1=1 ,这条语句不再直接用ROWID访问,换成索引。在两个会话中分 别执行此段过程,终查看了一下: SQL> select event from v$session_event where sid=862; EVENT --------------------------------------------- db file sequential read cursor: pin S wait on X SQL*Net message to client SQL*Net message from client SQL*Net break/reset to client events in waitclass Other 6 rows selected. 果然没有CBC Latch的竞争。看到没,区别可是非常之大啊。如果不用DTrace分析,恐怕很难准确的发现这点。看来INDEX UNIQUE SCAN和INDEX RANGE SCAN,不同的访问路径,Oracle实现起来的方法大相庭径啊。而且,由不由的访问路径起始,上层的操作也会不一样。 比如同样是TABLE ACCESS BY INDEX ROWID,下层是INDEX UNIQUE SCAN的话,表块将只有共享Latch。下层是INDEX RANGE SCAN的话,表块上将有独占 Latch。 比较一下索引和非索引的区别: 非 ------ -------- ---------------- 根 1次共享 1次共享 枝 1次共享 1次共享 叶 1次共享 1次共享 3次独占 表块 1次共享 2次独占 非索引共需8次CBC Latch,其中5次是独占。看来,在读远高于写的环境,想解决CBC Latch竞争问题吗,那就如果可能的话,使用索引吧。 (当然,出现CBC Latch争用,一般都是SQL惹的祸,调SQL即可。这个结论,是说如何从宏观上减少CBC Latch争用) 顺便测一下DML,索引时,即使修改索引列,索引的访问不变,都是共享Latch。但表块是独占Latch。其他UNDO块、DUNO段头了等等Latch的持有访 问,我就不再演示了,有兴趣自己测吧。 其实还有一个问题,就是为什么用Rowid访问一个表块,或者非索引的叶块、表块,Oracle不会以共享的方式获得Latch呢?要解答这个问题,先要 搞清楚一点,为什么用ROWID的形式,访问表块的时候,要申请2次CBC Latch。而根块、枝块只要一次,索引以INDEX UNIQUE SCAN形式访问,所有块都只需 要一次共享的CBC Latch。 这个问题又可以写一篇很长的文章分析了。不知道放在这里是否合适,因为这篇文章已经有点长了。但我觉得,如果你掌握了今天我们所用的方法,继 续这样的分析难度不大。我先简单描述一下,后面再另起一章详细解剖。可以使用我们个脚本: #!/usr/sbin/dtrace -s -n dtrace:::BEGIN { i=1; } pid$1:::entry { printf("i=%d PID::entry:==%s:%s:%s:%s %x %x %x %x %x %x",i, probeprov, probemod, probefunc, probename,arg0,arg1,arg2,arg3,arg4,arg5); i=i+1; } 拦截所有操作,你可以发现通过ROWID访问,形式如下: 1、调用sskgslcas获得Latch 2、进行一些未知操作 3、调用sskgsldecr释放Latch 4、未知操作 5、memcpy拷贝内存,从SGA向PGA 6、未知操作 7、调用sskgslcas获得Latch 8、进行一些未知操作 9、调用sskgsldecr释放Latch 第5步拷贝内存,其实就是真正的逻辑读过程,把数据从SGA中的Buffer Cache,拷贝到PGA,我跟踪出的Memcpy函数形式如下: 2 172791 memcpy:entry i=663 PID::entry:==pid972:libc.so.1:memcpy:entry fdad1b10 82c61fde 1e fdad2f94 886f2bf8 8045478 第二个参数82c61fde , 是Buffer Cache中行的位置,我们可以如下确定: SQL> select file#,dbablk,tch,lower(HLADDR),ba from x$bh where file#=4 and dbablk=20; FILE# DBABLK TCH LOWER(HL BA ---------- ---------- ---------- -------- -------- 4 20 7 8ea1d750 82C60000 BA列,82C60000开始的8K,也就是从82C60000到82C62000,都是4号文件20号块的Buffer。memcpy第二个参数82c61fde,正是在这个范围之间。证明是从 4号文件20号块中拷贝数据。个参数地址fdad1b10,它不在任何内存池地址空间范围之内,它是进程自身的内存,可以认为是PGA。第三个参数1e,十进制是 30,是拷贝数据的长度。查看表的定义: SQL> desc a2_70m; Name Null? Type ----------------------------------------- -------- ---------------------------- ID1 NUMBER(38) ID2 NUMBER(38) CC1 VARCHAR2(30) 拷贝30个字节,其实就是将CC1列的数据读到PGA中。 另外,还有一点,先说明一下,到下一章再详细讲。上面步骤1至3中间的未知操作,和7至9中的未知操作,其实是加Buffer Pin和释放Buffer Pin。其 实,上面那9个步骤,我们可以简化一下: 1、调用sskgslcas获得独占Latch 2、加Buffer Pin 3、调用sskgsldecr释放Latch 5、memcpy拷贝内存,从SGA向PGA 7、调用sskgslcas获得独占Latch 8、释放Buffer Pin 9、调用sskgsldecr释放Latch 但在索引访问时,形式是这样的: 1、调用sskgslcas获得共享Latch 2、memcpy拷贝内存,从SGA向PGA 3、调用sskgsldecr释放Latch 和ROWID访问的不同之处,没有了Buffer Pin。一个CBC Latch,从逻辑读开始到逻辑读结尾。 为什么索引Root块、枝块的访问,只需要一次共享CBC Latch,叶块、表块需要多次独占。这个问题,现在可以回答了。Oracle认为根块、枝块不会经常 修改,因为,用一个共享CBC Latch,保护逻辑读所有操作。虽然Latch持有时间长,但由于是共享的,不会有争用。而对于叶块和表块,Oracle认为有可能会频 繁修改,所以,用独占Latch保护,获得Buffer Pin,在Pin的保护下,读取、修改Buffer数据。 而至于索引,INDEX UNIQUE SCAN的访问路径,Oracle单独做了处理,也依照根块、枝块的方式访问。这说明如果是索引,对表有大量读写混合 的操作,那么CBC Latch竞争会激烈些,因为没有了Buffer Pin,读持有CBC Latch的时间会较长。但对于读远远多于写的环境,由于读都是共享Latch,反而可以 大大减少CBC Latch的争用。 好了,先到这里吧。已经有点长了。 本章内容,难度稍高,有兴趣的兄弟还是要好好测测。这章内容是后面的基础,如果这一章没问题,那后理解Mutex等等内容就方便了。 由于本章长度有限,有些问题,比如Buffer Pin的问题。我们交到以后解决,这里先提出来,有兴趣可以自己动手分析、测试下。 好,今天就到这里为止了,后续更精彩,敬请期待。

相关文章