揭密Oracle之七种武器之三:破译古老的谜题---共享CBC Latch的秘密
揭密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的问题。我们交到以后解决,这里先提出来,有兴趣可以自己动手分析、测试下。
好,今天就到这里为止了,后续更精彩,敬请期待。
分享好友
分享这个小栈给你的朋友们,一起进步吧。
订阅须知
• 所有用户可根据关注领域订阅专区或所有专区
• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询
• 专区发布评论属默认订阅所评论专区(除付费小栈外)