揭密Oracle之 七种武器

揭密Oracle之 七种武器 第一章 搭建测试环境

vage


揭密Oracle之 七种武器 第一章 搭建测试环境


长生剑,碧玉刀,刀剑合璧,天下无敌。


(揭密Oracle之七种武器第二章地址:DTrace语法:跟踪物理IO
http://www.itpub.net/thread-1609235-1-1.html


揭密Oracle之七种武器之三:破译古老的谜题---共享CBC Latch的秘密
http://www.itpub.net/thread-1617245-1-1.html)


第一章 测试环境的搭建
工欲善其事,必先利其器,这是老话了,也是实话。
面对越来越封闭的Oracle,你想揭开它神秘的面纱吗。
奇奇怪怪的问题,总是查不出原因,你想揭开它背后的秘密吗。
还等什么,DTrace+GDB,长生剑,碧玉刀,刀剑合璧,天下无敌。


另外,经过朋友 ysping 提醒,我觉得有必要说明下,虽然我们只能在Solaris测试,但Oracle的工作原理,在


大部分平台下,是一样的 (可能Windows会有点不同吧)。我们用Dtrace分析Oracle的原理,比如在什么时候


、加什么样的锁、Pin、Latch或Mutex,加到什么操作为至释放,会以怎样的形式阻塞,等等,用这个原理,去


诊断其他平台的问题。


1、安装Solaris
首先你要有个测试环境,DTrace只有Solaris下有,我们就搭个Solaris的测试平台吧。到Oracle官网上下个


Solaris,现在已经有11了,我下的是10,这是10的
链接:http://www.oracle.com/technetwork/server-storage/solaris10/overview/index.html 。如果你想用


11,当然也可以。对于我们测试Oracle,10和11差别不大
的。
下载的文件,是个ISO文件,直接加载到虚拟机里安装就可以了,安装过程我不再说了,非常简单。考虑到以后


我还要装Oracle、建库,磁盘最好搞大点,20G吧
。对了,我的虚拟机是VMWare 8.0.0 build-471780。你也可以选择其他版本,或其他虚拟机。
2、安装Oracle
(1)、下载Oracle
我们最好装两个版本,10GR2和11GR2。10G现在用的还比较多,而且10G相关DTrace和GDB的资料更多些,因为我


一直在用DTrace+GDB研究10G,后面我会逐步总结
一些我的研究结果。11GR2作为未来必定会流行的版本,有必要早做准备。
注意,11GR2在Solaris平台,只有64位版本的。要求你的电脑要是64位才行。
(1)、创建Oracle用户和Oinstall、DBA组
groupadd dba
groupadd oinstall
useradd -g oinstall -G dba -d /export/home/lhb -m oracle
(2)、设置内核参数
编辑/etc/system文件增加如下行:
set shmsys:shminfo_shmmax=10737418240
set shmsys:shminfo_shmmin=1048576
set shmsys:shminfo_shmseg=1000
set shmsys:shminfo_shmmni=100
set semsys:seminfo_semmns=700
set semsys:seminfo_semmni=100
set semsys:seminfo_semmsl=256
我10G、11G都是这样写的。
(3)、设置oracle用户下的.profile
我的.profile只有这些内容
ORACLE_BASE=/export/home/oracle/opt/
ORACLE_HOME=$ORACLE_BASE/product/10.2.0/
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$ORACLE_HOME/lib:/usr/lib
PATH=$PATH:/usr/ccs/bin:$ORACLE_HOME/bin
ORACLE_SID=h1
export ORACLE_BASE
export ORACLE_HOME
export LD_LIBRARY_PATH
export PATH
export ORACLE_SID
(4)、安装和建库
解压下载的Oracle安装文件,开始安装。
11G安装画面变了很多,但过程和10G大致上相差无几。我安装11GR2的时候,在环境检查时,报swap和tmp不够


大,直接忽略过去就行了。还有,在安装的83%时,停了很
久。到底多久不知道,因为等了太久,都晚点12点多来,我一直有早睡早起的习惯,直接去睡了,第二天早上


起来一看,已经装好了。
库你怎么建都行,我一般是手动建,你当然可以选择在安装的时候一起建,这些基本的我就不再说了。
3、安装GDB
到http://www.sunfreeware.com中,下载GDB的库,我下载了这五个库:
expat-2.0.1-sol10-x86-local.gz
gdb-6.8-sol10-x86-local.gz
libiconv-1.14-sol10-x86-local.gz
libintl-3.4.0-sol10-x86-local.gz
ncurses-5.7-sol10-x86-local.gz
用如下命令,依次安装5个库。注意,最好安照我上面的顺序安装,
gunzip expat-2.0.1-sol10-x86-local.gz
pkgadd -d expat-2.0.1-sol10-x86-local.gz
安装完后,将gdb的路径,加入到/etc/profile中:
在/etc/profile中增加如下行:
PATH=$PATH:/usr/sfw/bin:/usr/local/bin
LD_LIBRARY_PATH=/usr/sfw/lib
export LD_LIBRARY_PATH
export PATH
好了,等测试环境建好后,我们来一步一步,用DTrace+GDB,刀剑合璧,让Oracle没有秘密。


========

揭密Oracle之七种武器二:DTrace语法:跟踪物理IO

vage


揭密Oracle之七种武器二:DTrace语法:跟踪物理IO


(第一章地址:
揭密Oracle之 七种武器 第一章 搭建测试环境
http://www.itpub.net/thread-1605241-1-1.html
或参考我的BLOG www.MythData.com


揭密Oracle之七种武器之三:破译古老的谜题---共享CBC Latch的秘密
http://www.itpub.net/thread-1617245-1-1.html





其实本章是以物理IO为例,介绍DTrace的基本语法。本篇内容还是比较多的,应该足够有兴趣的朋友


学习一周了。下周,再推出我的第三章。
另外说明一下,我并不是DTrace专家,我用DTrace主要目的是研究Oracle。我们不会对DTrace作过多


深入的介绍(不过DTrace本身也没有深入的东西,调试工具吗,复杂的是要调试的目标),我们的主要内容


还是逐步深入,介绍如何用DTrace分析、研究Oracle方法
授人以鱼,不如授人以渔。我相信,方法比结果重要。


本篇内容会用到一本书,Solaris 动态跟踪指南,可以到这里下载。 D语言全面介绍.pdf (1.83


MB,下载次数: 1840) 这个是Solaris官方提供的文档,而且是中文的。当字典来查,非常不错。




第二章 DTrace语法:跟踪物理IO


第一节 Hello World
DTrace是什么,这个问题很容易回答:调试工具。具体我们可以这样理解,Solaris在其内部,增加


N多的触发器,这些触发器平时是Disable的,对性能没有任
何影响。你可以通过DTrace,Enable某些触发器。并且,在Enable的同时,还可以为这些触发器指定动作。
比如,有一个I/O触发器,你用DTrace Enable了它,同时,你定义动作,“每次发生I/O时,显示


I/O大小”。当然,还可以定义更复杂的动作,显示I/O的内容
、甚至修改I/O数据。进程想往磁盘中写个A,你可以用DTrace,将A换成B。当然,我们调试进程,一般不需要


修改,只需要观察。
换成DTrace中的术语,触发器就是Probe,可以译为探针。探针并时都是关闭的,也就是Disable的。


我们可以使用DTrace,打开探针,并为探针指定动作。当探针
被触发,你通过DTrace为探针定义的动作,就会被执行。
好,闲言少叙,试试我们的第一个DTrace程序吧。曾几何时,无论我们学习什么语言,第一个程序总


是“Hello World”,让我们也来个DTrace版的“Hello World
”吧。
在root下,vi test1.d,输入如下命令:
BEGIN
{
printf("hello world,www.MythData.com ";
exit(0);
}
如下执行此脚本:
# dtrace -s test1.d
结果显示
dtrace: script 'test1.d' matched 1 probe
cpu ID FUNCTION:NAME
3 1 :BEGIN hello world,www.MythData.com
在此加上我的BLOG地址,我的那篇突然35岁,虽然被转载的到处都是,但被截掉了我的BLOG地址。唉


。如果有人转载的话,应该也会有人截掉的。
闲言少叙,上面就是显示结果了。对于我的程序和输出结果,略加说明。
1、BEGIN:它是DTrace的探针之一。也是最简单的探针。它不和操作系统任何操作关联,一般它用来


做DTrace程序运行的初始化工作。BEGIN探针中的代码,会在
DTrace程序开始时运行。
2、大括号:如我们所见,探针名之下,就是大括号。这足以说明DTrace的设计者是C语言迷,将C语言


的格式带入到了DTrace中来。
3、大括号中间的语句:这就是我们为BEGIN探针定义的动作了。包含两条语句,显示退出。每条语


句之后以;号结尾。
4、关于这两条语句,我就不再多说了,printf,在此的使用方法,完全和C语言一样。
5、两注意事项,(1)、大小写是敏感的。(2)、如果不加exit(0)的话,此程序运行完将不会退出


。可以手工Ctrl+C退出
输出结果的话:
1、cpu列为3,说明此DTrace程序在运行时,刚好在3号cpu上执行命令。
2、ID列是探针编号。
3、FUNCTION:NAME,:BEGIN,探针名相关信息,这个后面再详细说。
4、最后无列名的部分,hello world,www.MythData.com,就是我们程序的输出结果了。
最后,每次运行此程序时,都要dtrace -s,太麻繁了。我们可以添加一个#!/usr/sbin/dtrace -s在


程序头,如下所示:
#!/usr/sbin/dtrace -s
BEGIN
{
printf("hello world,www.MythData.com ";
exit(0);
}
保存,使用chmod 755 test1.d,赋上去可执行权限,如下方式执行:
# ./test1.d
dtrace: script './test1.d' matched 1 probe
cpu ID FUNCTION:NAME
0 1 :BEGIN hello world,www.MythData.com


第二节 详述探针(Probe)


完整的探针描述符,绝对不至上节我们遇到的BEGIN这么简单。它包括PROVIDER(提供器),MODULE(


模块名),FUNCTION(函数名)和NAME(探针名称)四部分
。BEGIN只是最简单的一个特例。
PROVIDER是最上层的称号,比如有IO PROVIDER,进程PROVIDER,等等。每种PROVIDER根据其包含的探


针不同,又分为N种MODULE。MODULE之中又包含各种FUNCTION
,最后的NAME是探针名,通常是进入、开始、退出、完成这些东西,在进入一个FUNCTION(函数)、退出函数


完成函数等等动作发生时被触发。
我们以IO为例,这应该是我们最关心的话题。操作系统中大部分IO事件的开始处,有这样一个探针:
io:genunix:bdev_strategy:start
io是PROVIDER,genunix是MODULE。bdev_strategy是FUNCTION,所有串行磁盘I/O事件将调用


bdev_strategy函数完成。最后一个,start,bdev_strategy函数入口
处的探针。
我们可以这样称呼它,io提供器下的genunix模块中的bdev_strategy函数上的start探针。我们可以理


解为某数据库下某Schema中的某个表上的某某触发器。一个
探针的称呼其实无所谓。了解Solaris一供为我们提供了什么PROVIDER(提供器),这些提供器下都有什么


MODULE(模块),这些模块中都有什么FUNCTION(函数),以及
这些函数上都有什么探针,这才是重要的。关于这点,我们可以参考《Solaris 动态跟踪指南》,这是本书像


一本字典,详细介绍了所有的提供器、模块等等。
好,先以io:genunix:bdev_strategy:start为例子,测试一下吧:
vi test2.d
#!/usr/sbin/dtrace -s
BEGIN
{
i=0;
}
io:genunix:bdev_strategy:start
{
printf("%d number physio IO",i);
i++;
}
保存,chmod 755 test2.d,这是执行的结果:
# ./test2.d
dtrace: script './test2.d' matched 2 probes
cpu ID FUNCTION:NAME
1 781 bdev_strategy:start 0 number physio IO
1 781 bdev_strategy:start 1 number physio IO
1 781 bdev_strategy:start 2 number physio IO
1 781 bdev_strategy:start 3 number physio IO
1 781 bdev_strategy:start 4 number physio IO
1 781 bdev_strategy:start 5 number physio IO
……………………
每有一次IO,程序会都会显示一行,“ 1 781 bdev_strategy:star”,这一部分


是固定输出,这一部分其实可以用一个参数关掉。参数我们以
后再说。后面“ 0 number physio IO”,是我们程序的输出结果。
如果我们不按Ctrl+C,程序会一直显示下去。每有一次串行IO发生,准确说是每调用一次


bdev_strategy函数,探针被触发一次,就会显示一行。
显示IO的次数,也没啥意义。其实我们可以显示更多的东西。但要对IO类探针进一步了解些。
bdev_strategy既然被叫作函数,是函数的话,当然有参数。它一共有3个参数,参数1是bufinfo_t型


的结构,参数2是devinfo_t型结构,参数3是fileinfo_t型结
构。可以参见《Solaris 动态跟踪指南》 356页。
另外,结构,Struct,C语言的基本东西。不会的话,去看看潭浩强的C语言吧。二级C语言,我想我们


都应该没啥问题吧。确定写DTrace脚本,连二级C都不需要,
只需要对C语言有最基本的了解即可。
这三个结构当中,fileinfo_t包含的有I/O所针对的文件名,请允许我粘一段《Solaris 动态跟踪指南


》 359页的内容,fileinfo_t结构的定义:
typedef struct fileinfo
{
string fi_name; /* name (basename of fi_pathname) */
string fi_dirname; /* directory (dirname of fi_pathname) */
string fi_pathname; /* full pathname */
offset_t fi_offset; /* offset within file */
string fi_fs; /* filesystem */
string fi_mount; /* mount point of file system */
} fileinfo_t;
在此,照顾一下不会C语言的人,简单说明一下,如果我们要访问结构中的内容,格式是“结构名.域


”,或者“结构指针->域”。在DTrace中,我们得到的一般都
是指针。
好,下面我们改一下脚本程序:
io:genunix:bdev_strategy:start
{
printf("%s",args[2]->fi_pathname);
}
args[2],是bdev_strategy函数的第三个参数,这是Dtrace中的固定用法。DTrace中还会有一些类似


的固定用法,可以参考《Solaris 动态跟踪指南》P68页,内
置变量。我们以后还会用到一些其他的。
在bdev_strategy函数中,第三个参数是fileinfo_t型的指针,也就是说,我们可以用“args[2]->域


”的格式,访问fileinfo_t型结构中的域。我们此外访问的域
是fi_pathname,也就是文件的完整路径加名字,形式就是如上面所示:args[2]->fi_pathname。
这是我执行后的结果:
# ./test2.d
dtrace: script './test2.d' matched 2 probes
cpu ID FUNCTION:NAME
2 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl
2 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl
0 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl
0 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl
1 781 bdev_strategy:start <none>
1 781 bdev_strategy:start <none>
1 781 bdev_strategy:start <none>
1 781 bdev_strategy:start <none>
1 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl
1 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl
1 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl
可以看到,有很多控制文件的写。随带说一下,我这个测试库很闲,没有任何操作。但你可以看着表


统计一下,不超过3秒,肯定会有一次控制文件的IO操作。原
因是什么,我就不用再说了吧。
再进一步的,Oracle每次控制文件的IO是多大呢? IO的大小在bufinfo_t结构中的b_bcount域,你可


以查看《Solaris 动态跟踪指南》 356页,为了节省篇幅,我
就不再粘过来了。bufinfo_t结构的指针,是bdev_strategy的第一个参数,也就是args[0]。因此,我们可以如


下再次修改代码


io:genunix:bdev_strategy:start
{
printf("%s %d",args[2]->fi_pathname,args[0]->b_bcount);
}
这是我的执行结果:
1 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl 8192
2 781 bdev_strategy:start /var/tmp/Exwla4xc 8192
2 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/redo03 4096
1 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl 8192
1 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl 8192
1 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl 8192
控制文件的IO大小,很整齐的都是8192字节,8K,控制文件的块大小。出乎我意料的是,有一个Redo


文件:redo03,它的IO大小是4096。我以前一直以为,应该是
512才对,因为Solaris下,Redo的块大小就是512字节啊(我碰到的系统,好像只有HP的不是512)。
不急,我们还没有搞清楚这些IO是读还是写呢,说不定是归档的读Redo IO呢。还是bufinfo_t结构,


b_flags域,说明了IO类型。关于这个域,在操作系统内部定
义了几个标志(就是用#define 定义的),B_WRITE代表IO是写,B_READ代表是读,还有些其他的,自己到357


页查吧。
我将代码修改如下,添加上去IO类别的判断:
io:genunix:bdev_strategy:start
{
printf("%s %d %s",args[0]->b_bcount,args[0]->b_flags&B_READ?"R":"W" );
}
args[0]->b_flags&B_READ?"R":"W",这种使用形式,条件表达式,是我们以后常用的形式,因为


DTrace中没有if、while等流程控制语句,所以条件表达式将是if
的常用替代者,但它必竟替代不了复杂的控制语句。
仍然是为了程序不太好的人,介绍一下这个条件表达式:“条件?值1?值2”,将条件为True,值1为


整个条件表达式的值。否则,值2为整个条件表达式的值。
这是执行结果:
3 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl 8192


W
3 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl 8192


W
2 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/redo01 4096 W
1 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl 8192


W
1 781 bdev_strategy:start /export/home/oracle/opt/dbdata/h1/control01.ctl 8192


W
………………
控制文件IO也全是写,还有,中间的Redo文件IO,是我专门修改1行,Commit一下产生的,IO大小是


4096字节,而且是写IO。看来,的确是LGWR在写Redo01。我们
可以观察一会儿,不会发现Redo文件有512字节IO,这也说明了一个我之前一直忽视的问题,先卖个关子,我们


待会再讨论。

我们为io:genunix:bdev_strategy:start处的探针定义了动作,当运行此DTrace脚本时,我们就开启


了探针io:genunix:bdev_strategy:start。但有些IO,并不是
通过bdev_strategy函数完成的,探针io:genunix:bdev_strategy:start捕获不得这些IO。为了开启更多的探针


、捕获更多的IO操作,在完整的探针描述符中,我们可以确
实部分内容,下面,如下修改程序:
io:genunix::start
{
printf("%s %d %s",args[0]->b_flags&B_READ?"R":"W" );
}
脚本程序运行后的提示
# ./test2.d
dtrace: script './test2.d' matched 4 probes
cpu ID FUNCTION:NAME
2 782 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/control01.ctl


8192 W
2 782 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/control01.ctl


8192 W
2 782 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/control01.ctl


8192 W
其中,第一行“dtrace: script './test2.d' matched 4 probes”,说明一共开启了4个探针,比之


前的测试,多开启了两个探针。
我们还可以进一步省略,io:::start,这将开启7个探针。我就不再测试了。但我们不能写成:::start


,或io:::这样的形式。
我们还可以使用通配符,如“i*:::start”,这就是打开所有i开头的提供器中的所有模块、所有函数


的Start探针。当然,我们也可以在模块、函数名中,使用通
配符,但不能在探针名中使用通配符。比如,这样将是错误的:“io:::st*”。通配符还可以是问号,比如:


“i?:::start”。 * 号代表所有字符,一个 ? 号,只能代表
一个字符。
探针的使用,说的也就差不多了,最后再来一个总结,我们如何知道Solaris有哪些探针,当然,我们


可以查看《Solaris 动态跟踪指南》。除了这个之外,
dtrace -l 命令可以查看所有的探针:
# dtrace -l|wc -l
51805
我使用的Solaris中,一共有5万多个探针。
我们还可以显示某一个提供器下所有探针,这样更有针对性,比如,显示io提供器下有什么模块、函


数、探针:
# dtrace -lP io
ID PROVIDER MODULE FUNCTION NAME
767 io genunix biodone done
768 io genunix biowait wait-done
769 io genunix biowait wait-start
780 io genunix default_physio start
781 io genunix bdev_strategy start
782 io genunix aphysio start
2530 io nfs nfs4_bio done
2531 io nfs nfs3_bio done
2532 io nfs nfs_bio done
2533 io nfs nfs4_bio start
2534 io nfs nfs3_bio start
2535 io nfs nfs_bio start
不多,io提供器下,只有13个探针。
好了,探针的内容我们就说到这儿,下面,下一项内容:谓词。


第三节 谓词


谓词其实就是条件语句,减化版的条件语句。比如,我们继续前面的例子,我只想观察一下针对Redo


文件的IO,我们可以这样做,先用如下命令确认一下当前Redo
文件是谁:
set linesize 10000
select * from v$log;
GROUP# THREAD# SEQUENCE# BYTES MEMBERS ARC STATUS FIRST_CHANGE#


FIRST_TIM
---------- ---------- ---------- ---------- ---------- --- ---------------- -------------


---------
1 1 470 62914560 1 YES ACTIVE 9387236 05-


MAY-12
2 1 471 62914560 1 NO CURRENT 9388037 05-


MAY-12
3 1 469 62914560 1 YES INACTIVE 9386469 05-


MAY-12
2号Redo文件是当前Redo文件,再如下确认一下2号Redo文件是谁:
select member from v$logfile where group#=2;
MEMBER
-------------------------------------------------------------------------------------------------


-----------------------------------------------------
/export/home/oracle/opt/dbdata/h1/redo2.log
好,如下修改脚本:
io:::start
/ args[2]->fi_pathname=="/export/home/oracle/opt/dbdata/h1/redo2.log" /
{
printf("%s %s %d %s\n",args[1]->dev_statname,args


[0]->b_flags&B_READ?"R":"W" );
}
探针描述符我简写为:io:::start,我开启了所有IO开始处的探针,拦截io提供器下所有的IO函数


用。在探针描述符之下的 / ………… / ,就是本节所要介绍
的谓词。简单点说,就是条件。当探针被触发后,只有满足谓词条件,才会执行探针下的动作。
结合上面的程序,也就是当有IO发生时,只有当IO是针对redo2.log这个文件的,才会执行下面的


Printf命令。
开一个数据库会话,使用update命令更新一行,观察一下行针对Redo的IO。
我针对一个很小的表,执行如下命令:
sql> update a1 set id=id+0 where id=1;
1 row updated.
sql> commit;
Commit complete.
观察结果, update a1 set id=id+0 where id=1时,并没有Redo的IO,这是因为在全新的IMU模式下


,Redo机制Oracle做了很大调整,DML语句执行时,只要没有
超过private strand大小,日志不会被写进Log Buffer。关于这些内容,后面我们专门开个帖子详细讨论吧。


在Commit时,就有了如下输出
# ./test2.d
dtrace: script './test2.d' matched 7 probes
cpu ID FUNCTION:NAME
2 782 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/redo2.log


4096 W
有4096字节的针对Redo2.log的写IO。但是,注意,并不是每次提交时,都会有IO。下面是我做的一个


完整测试,我一共执行了上面的Update、commit命令共10次
,有6次commit时有IO:
# ./test2.d
dtrace: script './test2.d' matched 7 probes
cpu ID FUNCTION:NAME
2 782 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/redo3.log


4096 W
2 782 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/redo3.log


4096 W
3 782 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/redo3.log


4096 W
1 782 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/redo3.log


4096 W
3 782 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/redo3.log


4096 W
2 782 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/redo3.log


4096 W
我们可以用truss 跟踪一下lgwr进程,每次提交时,lgwr都会调用pwrite64函数,写Redo文件。写的


大小不等,最小时会是512字节。而且,每次提交时,都会写
Redo文件。这就是Oracle的运行法则之一吗,提交时,一定会写Redo文件。但是,如果你使用的是文件系统,


对日志文件的写,操作系统有可能只是暂时先写进主机的缓存
当中。
当然,在Solaris下,我们可以mount -o directio 这样Mount磁盘,所有IO都将是直接IO了。
使用文件系统是危险的。我只了解Oracle,不了解各种文件系统。严谨点的说法,使用某些文件系统


存放Redo文件是危险的。有可能造成提交后的数据丢失。如果
你决定使用文件系统,一定要详细了解下你的文件系统,针对Redo文件,是否是直接IO,或者在突然当机后的


处理,有没有类似Oracle的实例恢复机制。
我们可以测试一下,做一个DML操作,提交,然后马上关掉主机电源(我是Power Off掉虚拟机的电源


),再重新启动虚拟机、数据库,查看刚才已经提交DML的结
果,你会发现,有时DML的结果会丢失掉的。注意,是已提交的DML结果会丢失。
在谓词中,我们可以使用常见的||、&&、!,就是或、与、非。比如,我共有三个日志文件,上面的


谓词我捕获一个日志文件redo2.log的IO,为了三个文件都捕
获,可以如下改写:
io:::start
/ args[2]->fi_pathname=="/export/home/oracle/opt/dbdata/h1/redo1.log" ||
args[2]->fi_pathname=="/export/home/oracle/opt/dbdata/h1/redo2.log" ||
args[2]->fi_pathname=="/export/home/oracle/opt/dbdata/h1/redo3.log" /
{
printf("%s %s %d %s\n",args


[0]->b_flags&B_READ?"R":"W" );
}
谓词中我使用两个||符号,也就是“或”,将三个条件连接起来。只要有任意一个条件满足,都会执


行下面的printf命令。这样,无论日志怎么切换,我们都可以
观察Redo的IO情况了。
那么,ASM会不会有这样的情况呢,我们可以测试下。
首先,配置ASM。我们先在10G下测试吧,10G的ASM配置比较简单,网上资料也比较多。11GR2的,我们


以后再配置、观察。
(1)、配置ASM
# export ORACLE_HOME=/export/home/oracle/opt//product/10.2.0/
# cd $ORACLE_HOME/bin
# ./localconfig add
(2)、准备ASM参数文件
$ cat asmpfile.ora
instance_type=asm
cluster_database=false
asm_diskgroups=dg1
asm_diskstring='/dev/rdsk/c*'
processes=200
remote_login_passwordfile=exclusive
core_dump_dest=/export/home/oracle/opt/admin/myasm/cdump
user_dump_dest=/export/home/oracle/opt/admin/myasm/udump
background_dump_dest=/export/home/oracle/opt/admin/myasm/bdump
(3)、启动ASM实例
先要保证参数文件中background_dump_dest几个参数中所指定的位置是有的,如下启动ASM实例即可:
ORACLE_SID=myasm
sqlplus / as sysdba
create spfile from pfile='/export/home/oracle/asmpfile.ora';
startup nomount;
在我博客上有一篇文章,专门讲如何配置ASM的:http://space.itpub.net/321157/viewspace-718805
(4)、给虚拟机添加新的磁盘。
这个我就不多说了,要先关掉虚拟机,添加完磁盘再重启。先添加两块吧,大小不用太大。
(5)、格式化
格式化的步骤比较长,我写的详细些:
# format <----------------------------------format ,格式化命令。
Searching for disks...done
AVAILABLE DISK SELECTIONS:
0. c0d1 <DEFAULT cyl 2085 alt 2 hd 255 sec 63>
[url=]/pci@0,0/pci-ide@7,1/ide@1/cmdk@1,0[/url]
1. c1t1d0 <DEFAULT cyl 98 alt 2 hd 64 sec 32>
[url=]/pci@0,0/pci15ad,1976@10/sd@1,0[/url]
2. c1t3d0 <DEFAULT cyl 98 alt 2 hd 64 sec 32>
[url=]/pci@0,1976@10/sd@3,0[/url]
3. c2t0d0 <DEFAULT cyl 98 alt 2 hd 64 sec 32>
[url=]/pci@0,790@11/pci15ad,1976@3/sd@0,0[/url]
4. c2t1d0 <DEFAULT cyl 98 alt 2 hd 64 sec 32>
[url=]/pci@0,1976@3/sd@1,0[/url]
Specify disk (enter its number):1 <-------------------------除了c0d1是我安装系统时,原始创建的


磁盘外,剩下四个是后面添加的。此处我选择1号磁盘,
c1t1d0。
selecting c1t1d0
[disk formatted]


FORMAT MENU:
disk - select a disk
type - select (define) a disk type
partition - select (define) a partition table
current - describe the current disk
format - format and analyze the disk
fdisk - run the fdisk program
repair - repair a defective sector
label - write label to the disk
analyze - surface analysis
defect - defect list management
backup - search for backup labels
verify - read and display labels
save - save new disk/partition definitions
inquiry - show vendor,product and revision
volname - set 8-character volume name
!<cmd> - execute <cmd>,then return
quit
format> fdisk <------------------------应该先fdisk一下
No fdisk table exists. The default partition for the disk is:
a 100% "SOLARIS System" partition
Type "y" to accept the default partition,otherwise type "n" to edit the
partition table.
y <-----------------------------此处输入y
format> partition <---------------------进入分区命令
PARTITION MENU:
0 - change `0' partition
1 - change `1' partition
2 - change `2' partition
3 - change `3' partition
4 - change `4' partition
5 - change `5' partition
6 - change `6' partition
7 - change `7' partition
select - select a predefined table
modify - modify a predefined partition table
name - name the current table
print - display the current table
label - write partition map and label to the disk
!<cmd> - execute <cmd>,then return
quit
partition> print <-------------------查看分区状态
Current partition table (original):
Total disk cylinders available: 98 + 2 (reserved cylinders)
Part Tag Flag Cylinders Size Blocks
0 unassigned wm 0 0 (0/0/0) 0
1 unassigned wm 0 0 (0/0/0) 0
2 backup wu 0 - 97 98.00MB (98/0/0) 200704 <-------2号分区已经被系统使用
3 unassigned wm 0 0 (0/0/0) 0
4 unassigned wm 0 0 (0/0/0) 0
5 unassigned wm 0 0 (0/0/0) 0
6 unassigned wm 0 0 (0/0/0) 0
7 unassigned wm 0 0 (0/0/0) 0
8 boot wu 0 - 0 1.00MB (1/0/0) 2048 <-------8号分区也已经被系统使



9 unassigned wm 0 0 (0/0/0) 0
partition> 0 <--------------------------------------选择第0号分区,也可以选择1号、3号或其他的,


但不能选择2号、8号这两个已经占用的。
Part Tag Flag Cylinders Size Blocks
0 unassigned wm 0 0 (0/0/0) 0
Enter partition id tag[unassigned]: <-------- 缺省,直接回车
Enter partition permission flags[wm]: <-------- 缺省,直接回车
Enter new starting cyl[0]:1 <-------- 如果要用ASM,此处要从1开始,否则可以从0开始
Enter partition size[0b,0c,0e,0.00mb,0.00gb]: 95mb <-------- 输入大小
partition> label <-------------设置label
Ready to label disk,continue? y <------------输入y
partition> quit <-------------退出partition,回到Format
FORMAT MENU:
disk - select a disk
type - select (define) a disk type
partition - select (define) a partition table
current - describe the current disk
format - format and analyze the disk
fdisk - run the fdisk program
repair - repair a defective sector
label - write label to the disk
analyze - surface analysis
defect - defect list management
backup - search for backup labels
verify - read and display labels
save - save new disk/partition definitions
inquiry - show vendor,then return
quit
format> type <----------------------------选择类型
AVAILABLE DRIVE TYPES:
0. Auto configure
1. DEFAULT
2. other
Specify disk type (enter its number)[1]: 1 <---------------------此处选DEFAULT
selecting c1t1d0
[disk formatted]
format> save <-----------------------------此处,输入save,保存刚才做的所有工作
Saving new disk and partition definitions
Enter file name["./format.dat"]: <-----------------------此处直接回车
format> quit <---------------------退出
#
然后到刚才启动的ASM实例中,确定asm_diskstring参数的值为/dev/rdsk/c*:
sql> show parameter string
NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
asm_diskstring string /dev/rdsk/c*
查看我们刚才Format的磁盘:
sql> select path from v$asm_disk;
PATH
--------------------------------------------------------------------------------
/dev/rdsk/c0d1s0
/dev/rdsk/c0d1s1
/dev/rdsk/c0d1s7
/dev/rdsk/c0d1s9
/dev/rdsk/c1t1d0s0
/dev/rdsk/c1t1d0s0,即为我们刚才Format的磁盘。c0d1s9这些不是,这些是装系统时建的盘。
说到这儿,补充一下Solaris下磁盘名的格式:
/dev/[r]dsk/cXtXdXsX
c:逻辑控制器号(逻辑控制器)
t:物理总线目标号
d:磁盘或逻辑单元号(LUN)
s:分区号
如c1t1d0s0表示1号控制器,1号磁盘,0号LUN,0号分区。

分完区后,不需要设置属主,ASM就可以自动识别到了。再把其他的盘也Format一下,让ASM可以识别


到。
我最终选择了这两块盘创建DG:
sql> create diskgroup dg1 external redundancy disk '/dev/rdsk/c2t1d0s3','/dev/rdsk/c1t3d0s3';
Diskgroup created.

然后我们把Redo挪到ASM的dg1中,我在asmcmd中,在DG1里面建了一个redo目录,然后到数据库实例中


,将组2、组3挪进了ASM。在删除组1、再添加组1到ASM中时
,报ASM空间不足。算了,我的磁盘太小了。
alter database drop logfile group 2;
alter database add logfile group 2 ('+DG1/redo/redo2.log') size 60m reuse;
alter database drop logfile group 3;
alter database add logfile group 3 ('+DG1/redo/redo3.log') size 60m reuse;
alter database drop logfile group 1;
现在我只有两组日志,都在ASM中,对于我们测试来说,两组已经够了:
sql> select * from v$log;
GROUP# THREAD# SEQUENCE# BYTES MEMBERS ARC STATUS FIRST_CHANGE#


FIRST_TIM
---------- ---------- ---------- ---------- ---------- --- ---------------- -------------


---------
2 1 489 62914560 1 NO CURRENT 9400623 07-


MAY-12
3 1 0 62914560 1 YES UNUSED 0
好了,开始我们的测试吧。修改一下脚本:
io:::start
{
printf("%s %s %d %s\n",args


[0]->b_flags&B_READ?"R":"W" );
}
将上面所加的args[2]->fi_pathname=="/export/home/oracle/opt/dbdata/h1/redo1.log" 这


种谓词去掉。
# ./test2.d
dtrace: script './test2.d' matched 7 probes
cpu ID FUNCTION:NAME
1 3092 default_physio:start sd5 <none> 4096 W
1 3092 default_physio:start sd2 <none> 4096 W
3 3093 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/control01.ctl


8192 W
3 3093 bdev_strategy:start cmdk0 /export/home/oracle/opt/dbdata/h1/control01.ctl


8192 W
……………………………………
……………………………………
……………………………………
由于打开了7个探针,所有IO开始时的操作都会被我们捕获,因此,输出信息会比较多。
在Oracle 会话中,修改一行数据,再提交,可以观察到这样的IO:
2 3094 aphysio:start sd3


[url=]/devices/pci@0,0:d,raw[/url] 1024 W
[url=]/devices/pci@0,raw[/url],这一串东西是啥?只要显示一下


ASM中的磁盘就明了了,我的ASM DG1 中包含这两块盘/dev/rdsk/c2t1d0s3,
/dev/rdsk/c1t3d0s3。


# ls -lFrt /dev/rdsk/c2t1d0s3
lrwxrwxrwx 1 root root 64 Mar 17 10:03 /dev/rdsk/c2t1d0s3 ->


../../devices/pci@0,raw*
# ls -lFrt /dev/rdsk/c1t3d0s3
lrwxrwxrwx 1 root root 50 Mar 17 10:03 /dev/rdsk/c1t3d0s3 ->


../../devices/pci@0,raw
经过观察,可以发现,刚才的IO,其实是针对/dev/rdsk/c1t3d0s3的,这正是DG1中的一块盘。接下来


我们如下修改脚本:
io:::start
/ args[2]->fi_pathname=="[url=]/devices/pci@0,raw[/url]" ||
args[2]->fi_pathname=="[url=]/devices/pci@0,raw[/url]"


/
{
printf("%s %s %d %s\n",args


[0]->b_flags&B_READ?"R":"W" );
}
我们只观察针对这两个磁盘的IO。执行一条简单的DML,修改一行,我执行如下命令:
sql> update a1 set id=id+0 where id=2;
1 row updated.
sql> commit;
Commit complete.
在提交时,可以发现会有如下的IO:
3 783 aphysio:start sd5


[url=]/devices/pci@0,raw[/url] 1024 W
每次提交时,都会有IO发生,这是和文件系统的最大区别。这样的话,我们提交的数据,就不会丢失


了。有时,也可以观察到512字节的IO。
再进一步的,我们知道,在ASM中,AU的大小通常是1M,条带大小也为1M。但Redo和控制文件Oracle会


将它们设置为“细粒度条带”,条带大小为128K。这个128K
,到底有什么意义呢? 为什么我上面一定要在ASM DG1中,至少搞两块磁盘,就是为了观察这个“细粒度条带


”。我们继续。

经过观察,我测试用的两条命令,update a1 set id=id+0 where id=2;,commit,每次执行,都会产


生1K的Redo。这种说法也不太准备,更准确应该是这两条命
令,每次执行会触发Lgwr 执行1K的IO写操作。其实这两条语句产生的Redo只有700多字节,但提交的时候,


LGWR写Redo数据,只能以块为单位去写。700多字节,正好占两
个日志块,1K字节。
脚本再做一下修改,完整的脚本如下:
#!/usr/sbin/dtrace -s
BEGIN
{
i=0;
}
io:::start
/ args[2]->fi_pathname=="[url=]/devices/pci@0,raw[/url]"


/
{
printf("IO number:%d %s %s %d %s\n",i,args


[0]->b_bcount,args[0]->b_flags&B_READ?"R":"W" );
i++;
}
我把之前用过的i++加了进来,主要用来观察IO的次数。好,运行这个程序,然后反反复复的执行上面


的DML和提交语句。
# ./test2.d
dtrace: script './test2.d' matched 7 probes
cpu ID FUNCTION:NAME
3 783 aphysio:start IO number:0 sd5


[url=]/devices/pci@0,raw[/url] 1024 W
3 783 aphysio:start IO number:1 sd5


[url=]/devices/pci@0,raw[/url] 1024 W
…………………………………………省略部分内容…………………………………………
0 783 aphysio:start IO number:99 sd5


[url=]/devices/pci@0,raw[/url] 1024 W
2 783 aphysio:start IO number:100 sd3


[url=]/devices/pci@0,raw[/url] 1024 W
2 783 aphysio:start IO number:101 sd3


[url=]/devices/pci@0,raw[/url] 1024 W
…………………………………………省略部分内容…………………………………………
0 783 aphysio:start IO number:227 sd3


[url=]/devices/pci@0,raw[/url] 1024 W
0 783 aphysio:start IO number:228 sd5


[url=]/devices/pci@0,raw[/url] 1024 W
0 783 aphysio:start IO number:229 sd5


[url=]/devices/pci@0,raw[/url] 1024 W
…………………………………………省略部分内容…………………………………………
0 783 aphysio:start IO number:354 sd5


[url=]/devices/pci@0,raw[/url] 1024 W
0 783 aphysio:start IO number:355 sd5


[url=]/devices/pci@0,raw[/url] 1024 W
0 783 aphysio:start IO number:356 sd3


[url=]/devices/pci@0,raw[/url] 1024 W
0 783 aphysio:start IO number:357 sd3


[url=]/devices/pci@0,raw[/url] 1024 W
^C
上面的结果,注意IO number:0的值,从IO number:100到IO number:227为止,共128次1K的IO,都是


针对,磁盘设
备“/devices/pci@0,raw”,从228次IO到355次IO,共128次1K IO,针对另一磁


盘设
备“/devices/pci@0,raw”。
这就是细粒度条带的意义。虽然AU大小仍为1M,文件仍是按1M来分配空间。文件的第一个1M,在磁盘1


,第二个1M,在磁盘2,等等。但在使用空间的时候,却是按
128K。先在磁盘1写128K,再在磁盘2中写128K,等等。
最后,还有一个问题,比如第100和101次IO:
2 783 aphysio:start IO number:100 sd3


[url=]/devices/pci@0,raw[/url] 1024 W
这是两次IO,每次IO的大小是1K,但是,我们如何确定这两次1K大小的IO,一共写了4个日志块?有没


有可能100次IO,写了1、2号日志块,101次IO,写了2、3号
日志块,两次1K大小的IO,总共写了1.5K数据?
之所以有这样的疑问,是因为我的那条DML加提交,产生的Redo量是700多字节。虽然要占两个块,但


是占不满两个块。那么,接下来的一次针对Redo的IO,是从没
占满的块继续往下写,还是新占一个块?
在资料视图中,有一个资料redo wastage,Redo浪费空间。如果两次IO每次各占两个块,redo


wastage会不会有增加呢? 我用如下语句查了一下
sql> col name for a20
sql> select name,value from v$sesstat a,v$statname b where a.statistic#=b.statistic# and b.name


in( 'redo size','redo wastage') and sid=855;
NAME VALUE
-------------------- ----------
redo size 774092
redo wastage 0
855号会话,就是我反反复复执行那条DML、提交语句的会话。我们没有观察到这个会话中有Redo浪费


。通过这个结果,我上面的测试,好像两次相邻的、1K大小的
IO,应该并没有产生共2K的Redo,应该只有1.5K。
但是,如果是这样的话,我们上面关于“细粒度条带”的结论,就有问题了。因为它是建立在连续128


次1K大小的IO,共写了128K Redo基本上的。或者说,是建立
在两次相邻的、1K大小的IO,一定会产生2K的Redo这个基础上。但通过redo wastage分析的结果,好像应该只


有1.5K才对。
到底结果如何,如果要确定的话,我们只能分析每次Redo IO的内容了。请看下节:“获取进程数据”


第四节 获取进程数据


不久前才看过一个笑话,从哪儿来的我也忘了。说是一名生物学家研究螃蠏的听觉系统,对着螃蠏大


吼一声,螃蠏马上跑了。将螃蠏的腿截掉,对着螃蠏再次大吼
一声,螃蠏没有跑。因此证明螃蠏的听觉系统是在腿上。
我们做测试,千万不要无意中成为这名生物学家。用不严谨的测试、得到错误的结论,然后再用这个


结论去分析碰到的问题,可想而知,这会是什么结果。
好,言归正传,继续上节中的测试,为了得到准确的结论,我们需要获得IO的内容
继续做这个分析,需要我们对Redo的块格式,有简单的了解。
512字节的一个Redo块,前16字节,是块头。块头中第8到11字节,是块所属于日志序列号,第4到7字


节,是块编号。接下来要做的,在Start探针中,读出块头,
将每次IO的日志序列号、块号显示出来。
DTrace中,可以使用copyin,《Solaris 动态跟踪指南》P155页,有这个函数的介绍。P411页,还有


更详细的介绍。
简单点说,某个进程的数据,我们不能直接显示,要使用copyin拷贝到DTrace的缓存中,才能显示


如下修改我们的脚本:
#!/usr/sbin/dtrace -s
char *rd;
char bn[4];
char seq[4];
BEGIN
{
i=0;
}
io:::start
/ args[2]->fi_pathname=="[url=]/devices/pci@0,raw[/url]"


/
{
rd=copyin((uintptr_t )(args[0]->b_addr),16);
bn[0]=rd[4];
bn[1]=rd[5];
bn[2]=rd[6];
bn[3]=rd[7];
seq[0]=rd[8];
seq[1]=rd[9];
seq[2]=rd[10];
seq[3]=rd[11];
printf("IO number:%d %s %s %d %s Seq:%d,Block:%d\n",args[2]-


>fi_pathname,args[0]-
>b_flags&B_READ?"R":"W",*((int *)&seq[0]),*((int *)&bn[0]));
i++;
}
解释一下这段脚本,首先,在开头的三个变量定义:
char *rd;
char bn[4];
char seq[4];
我们在BEGIN探针中,也用过变量:i。在io:::start探针中,还对i做了自增操作。这三个变量与i的区


别有两点,一它们是全局变量,二它们有类型。
只有这样的全局变量,才需要专门在程序开头定义,也只有它们,才可以有类型。可以使用的类型,


同C语言一样。变量i我们没有定义,直接就开始使用。DTrace
根据它的值来决定它的类型。

然后,这行是这个脚本程序的关键:
rd=copyin((uintptr_t )(args[0]->b_addr),16);
(uintptr_t )(args[0]->b_addr),这是我们的地址,我们从这个地址中,考贝16个字节,到rd中。rd


是一个字符型指针,它的空间在DTrace缓存中。args[0]-
>b_addr这个域的值,就是存放IO内容的缓存区地址。我们在这个地址前加一个(uintptr_t ),强制类型转换。


因为在《Solaris 动态跟踪指南》P155页,我们可以看到:
void *copyin(uintptr_t addr,size_t size)
copyin要求的地址类型是uintptr_t型。但(args[0]->b_addr)的类型并不是这个,因此,我们需要加


个强制类型转换。将(args[0]->b_addr)转成uintptr_t型。
在这里我拷贝了16个字节,就是整个块头了。下面的赋值语句:
bn[0]=rd[4];
bn[1]=rd[5];
bn[2]=rd[6];
bn[3]=rd[7];
seq[0]=rd[8];
seq[1]=rd[9];
seq[2]=rd[10];
seq[3]=rd[11];
将块头中的4到7字节,传到bn中,这是块号。将8到11字节,传到seq中,这是日志序列号。最后一步


输出时,是这样操作bn和seq这两个数组的:
…………,*((int *)&seq[0]),*((int *)&bn[0]);
&seq[0],取出seq数组中0号元素的地址。(int *),将这个地址转成整型,(int *)&seq[0],是一个


整型的地址,指向seq的开头。在它之前再加一个*,取出这个
整型地址中值。其实根本含意就是,将bn数组中的4个字节,当作一个整型变量输出。这是C语言中指针的常见


玩法。在使用copyin时,可能会用的比较多。
好了,运行脚本,执行DML+提交,观察输出结果:
……………………
3 783 aphysio:start IO number:93 sd3


[url=]/devices/pci@0,raw[/url] 1024 W Seq:495,Block:513
3 783 aphysio:start IO number:94 sd3


[url=]/devices/pci@0,Block:515
3 783 aphysio:start IO number:95 sd3


[url=]/devices/pci@0,Block:517
3 783 aphysio:start IO number:96 sd3


[url=]/devices/pci@0,Block:519
……………………
可以看到第93次IO写的是495号日志第513号块,大小1K。94次IO是495号日志515号块,大小1K,等等



也就是说,两次相邻Commit触发的IO,各自写各自的块。两次1K的IO,共写了4个块。
也就是说,我们对“细粒度条带”的分析是不错的。但redo wastage资料中的值就有问题了,不知道


它衡量的是什么浪费。我每次只产生700多字节Redo,用了
1024字节空间,但redo wastage的值为0。


好了,我们的揭密Oracle之七种武器之二,先到这里吧。第三章将继续介绍DTrace语法,同时结合


Oracle的观察结果。
用一句广告语:精彩继续,敬请期待。
========

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

vage




揭密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",probeprov,


probefunc,probename,arg0,arg2,arg3,arg4,arg5);
i=i+1;
}
参数这块,我们也不知道每个函数都有几个参数,好在多输出参数DTrace并不会报错,所以,我们就


显示几个参数,我显示了前6个:
arg0,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:==pid970racle: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:==pid970racle:kslwte_tm:entry 100


62657100 1 0 cfacb398 1
3 111268 skgslnoop:entry i=5 PID::entry:==pid970racle:skgslnoop:entry c028934


c02a6dc 0 8046130 c0e7078 b0fc070
3 86139 kews_idle_wait:entry i=6 PID::entry:==pid970racle: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:==pid970racle:kewe_trace_level:entry


8f18c820 c028934 c02a6dc 0 8046130 c0e7078
3 52312 ksl_which_bucket:entry i=9 PID::entry:==pid970racle:ksl_which_bucket:entry


2325dd c028934 c02a6dc 0 8046130 c0e7078
3 53333 kskthewt:entry i=10 PID::entry:==pid970racle: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:==pid970racle: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:==pid970racle:sskgslcas:entry


8ea1d750 0 20000016 fdc3f1e4 fdc3f18c fdc3f1e4
3 111578 sskgsldecr:entry i=526 PID::entry:==pid970racle:sskgsldecr:entry


8ea1d750 20000016 fdc3f1e4 fdc3f18c fdc3f1e4 804544c
3 111575 sskgslcas:entry i=552 PID::entry:==pid970racle:sskgslcas:entry


8ea1d750 0 20000016 1 fdc3f17c 81e1c064
3 57740 kcbzar:entry i=557 PID::entry:==pid970racle:kcbzar:entry 8ef9a5b4


8ea1d750 108000 8045368 1 fdc3f17c
3 101760 slmxnoop:entry i=558 PID::entry:==pid970racle:slmxnoop:entry


81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
3 101760 slmxnoop:entry i=559 PID::entry:==pid970racle:slmxnoop:entry


81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
3 101760 slmxnoop:entry i=560 PID::entry:==pid970racle:slmxnoop:entry


81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
3 101760 slmxnoop:entry i=561 PID::entry:==pid970racle:slmxnoop:entry


81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
3 101760 slmxnoop:entry i=562 PID::entry:==pid970racle:slmxnoop:entry


81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
3 101760 slmxnoop:entry i=564 PID::entry:==pid970racle:slmxnoop:entry


81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
3 111578 sskgsldecr:entry i=566 PID::entry:==pid970racle:sskgsldecr:entry


8ea1d750 20000016 1 fdc3f17c 81e1c064 8045510
3 52568 kssrmf:entry i=568 PID::entry:==pid970racle: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:==pid970racle:sskgslcas:entry


8ea1d750 0 20000016 fdc3f1e4 fdc3f18c fdc3f1e4
3 111578 sskgsldecr:entry i=526 PID::entry:==pid970racle:sskgsldecr:entry


8ea1d750 20000016 fdc3f1e4 fdc3f18c fdc3f1e4 804544c
3 111575 sskgslcas:entry i=552 PID::entry:==pid970racle:sskgslcas:entry


8ea1d750 0 20000016 1 fdc3f17c 81e1c064
3 111578 sskgsldecr:entry i=566 PID::entry:==pid970racle: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",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[8]);
printf("i=%d PID::entry:==%s:%s:%s:%s %x %x %x",


probename,latchaddr,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:==pid970racle: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:==pid970racle:sskgslcas:return 8ea1d750 16 1
0 111575 sskgslcas:entry [ 0 0 0 0| 0 0 292| 0 0 07a]i=5


PID::entry:==pid970racle: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:==pid970racle: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#,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:==pid970racle: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:==pid970racle: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:==pid970racle: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:==pid970racle: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:==pid970racle: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:==pid970racle:sskgslcas:return 8ea9cb38 16 1
2 111575 sskgslcas:entry [ 0 0 0 0| 0 0 784| 0 0 07a]i=13


PID::entry:==pid970racle: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:==pid970racle:sskgslcas:return 8ea9cb38 16 1
2 111575 sskgslcas:entry [ 0 0 0 0| 0 0 785| 0 0 07a]i=15


PID::entry:==pid970racle: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:==pid970racle:sskgslcas:return 8ea9cb38 16 1
2 111575 sskgslcas:entry [ 0 0 0 0| 0 0 786| 0 0 07a]i=17


PID::entry:==pid970racle: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:==pid970racle: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:==pid970racle: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:==pid970racle:sskgslcas:return 87d88194 16 1
1 111575 sskgslcas:entry [ 0 0 0 0| 0 0 c67| 0 0 07a]i=3


PID::entry:==pid970racle: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:==pid970racle:sskgslcas:return 8eb77e00 16 1
1 111575 sskgslcas:entry [ 0 0 0 0| 0 0 bc3| 0 0 07a]i=5


PID::entry:==pid970racle: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:==pid970racle:sskgslcas:return 8eafa97c 16 1
1 111575 sskgslcas:entry [ 0 0 0 0| 0 0 c38| 0 0 07a]i=7


PID::entry:==pid970racle: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:==pid970racle:sskgslcas:return 8ea9cb38 16 1
1 111575 sskgslcas:entry [ 0 0 0 0| 0 0 bdc| 0 0 07a]i=9


PID::entry:==pid970racle: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:==pid970racle: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",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#,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的问题。我们交到以后解决,这里先提出来,有兴趣


可以自己动手分析、测试下。
好,今天就到这里为止了,后续更精彩,敬请期待。
========

揭密buffer Cache中的链表补遗

vage 揭密buffer Cache中的链表补遗: (揭密Oracle之七种武器之四:揭密Buffer Cache中的链表 http://www.itpub.net/thread-1631537-1-1.html) 补充两个问题: 1、如果一个脏块在CKPT-Q上,当此脏块被移到LRUW时,会从CKPT-Q上去掉吗? 回答:不会。直到从LRUW上被写到磁盘上后,才会从CKPT-Q上去掉。 测试过程很简单,搞一个脏块,然后再运行一个需要大量扫描LRU的操作,我是这样的: set autot trace update a2_70m set id2=id2+0 where id1=1; commit; select * from a4_70m; (测试环境和前面所述一致,Buffer Cache 100M,a4_70m 80M,但它被设为了Cache) 然后在另一会话中不停运行如下几条语句: alter session set events 'immediate trace name SET_TSN_P1 level 5'; set pagesize 50000 set linesize 10000 select file#,lru_flag,decode(state,'free',1,'xcur',2,'scur',3,'cr',4,'read',5,'mrec',6,'irec',7,'write',8,'pi',9,'memory',10,'mwrite',11,'donated'),decode(bitand(flag,1),'N','Y') dirty,NXT_REPL,PRV_REPL,WA_NXT,WA_PRV,ts#,HLADDR from x$bh a where file#=4 and dbablk=20 order by FILE#,DBABLK; select CNUM_SET,CNUM_REPL,ANUM_REPL,CNUM_WRITE,ANUM_WRITE from x$kcbwds where cnum_set>0; alter session set events 'immediate trace name BUFFER level 0x01000014'; 在DUMP结果中,可以找到如下内容: BH (7c3f497c) file#: 4 rdba: 0x01000014 (4/20) class: 1 ba: 7c298000 ……………………(省略部分无意义内容)………………………………………………………… hash: [8e96e068,8e96e068] lru: [80bf9cc8,7c7efffc] obj-flags: object_ckpt_list ckptq: [7bfe8140,7dfea5d0] fileq: [8ea707ec,8ea707ec] objq: [88c3d034,88c3d034] ……………………(省略部分无意义内容)………………………………………………………… Hex dump of block: st=0,typ_found=1 Dump of memory from 0x7C298000 to 0x7C29A000 ……………………(省略部分无意义内容)………………………………………………………… BH (7c3f497c) file#: 4 rdba: 0x01000014 (4/20) class: 1 ba: 7c298000 ……………………(省略部分无意义内容)………………………………………………………… hash: [8e96e068,8e96e068] lru-write: [8ea63e58,88c3d034] ……………………(省略部分无意义内容)………………………………………………………… 上一次DUMP的时候,LRU链前后块的指针为lru: [80bf9cc8,7c7efffc],下一次DUMP时,已经变成lru-write: [8ea63e58,7c7efffc],但检查点队列相关的信息没变,都是ckptq: [7bfe8140,7dfea5d0]。 当脏块写完成时,BH中信息变为这样: BH (7c3f497c) file#: 4 rdba: 0x01000014 (4/20) class: 1 ba: 7c298000 ……………………(省略部分无意义内容)………………………………………………………… hash: [8e96e068,8e96e068] lru: [7c7efffc,7c3f2f18] lru-flags: on_auxiliary_list ckptq: [NULL] fileq: [NULL] objq: [88c3d02c,88c3d02c] ……………………(省略部分无意义内容)………………………………………………………… 从lru-write: [8ea63e58,7c7efffc],变为了lru: [7c7efffc,7c3f2f18],从lru-flags可以看到,已经被放到 辅助链表中了。同时,ckptq已经是NULL了。写磁盘完成时,才从CKPT-Q上摘掉。 2、根据上面的测试结果,脏块可能会同时存在于两个链表:LRUW和CKPT-Q。块从LRUW写磁盘后,会从CKPT-Q上 摘掉。但反过来呢?从CKPT-Q中写磁盘,写完后会从LRUW上摘掉吗? 答案是,会的。 如何验证呢?我是通过Latch的获取来验证的。 脏块通过CKPT-Q写到磁盘后,其所处的LRU位置不变,这一点我在前文中已经提到过,也很容易验证这点,从x $BH中的NXT_REPL,PRV_REPL两列,就可以验证此点。也就是说,从CKPT-Q写脏块,是和LRU链表无关的,也就是 不需要获得LRU Latch。如果从CKPT-Q写脏块申请了LRU Latch,哪一定和LRUW有关。 将检查点超时参数设为很小的值,写个简单的DTrace脚本,跟踪一下DBWR进程Latch的获得情况。发现每次从 CKPT-Q写脏块时,DBWR都要按如下顺序申请Latch: 获得cache buffers chains Latch 获得LRU Latch 释放LRU Latch 释放cache buffers chains Latch 获得checkpoint queue latch 释放checkpoint queue latch 获得cache buffers lru chain 释放cache buffers lru chain 也就是说,从CKPT-Q写脏块时,不但要获得checkpoint queue latch,还要LRU Latch。根据前面的分析,从 CKPT-Q写脏块时,获取LRU Latch的目的,只能是为了访问LRUW,因为CKPT-Q写不改变块在LRU的位置,不必要 访问LRU。哪么,CKPT-Q写访问LRUW的目的是什么,可以推论,目的是为了检查脏块是否在LRUW、并摘掉它。 还有一点,由于块已经从LUR移到了LURW,从CKPT-Q写完成后,虽然不是从LURW写的,块应该仍会被放入辅助 LRU,这个就是猜想了。我尽量找个测试实际验证下。 ========

相关文章

数据库版本:11.2.0.4 RAC(1)问题现象从EM里面可以看到,在23号早上8:45~8:55时,数据库等待会话暴增...
(一)问题背景最近在对一个大约200万行数据的表查看执行计划时,发现存在异常,理论上应该返回100多万...
(一)删除备份--DELETE命令用于删除RMAN备份记录及相应的物理文件。当使用RMAN执行备份操作时,会在RM...
(1)DRA介绍 数据恢复顾问(Data Recovery Advise)是一个诊断和修复数据库的工具,DRA能够修复数据文...
RMAN(Recovery Manager)是Oracle恢复管理器的简称,是集数据库备份(backup)、修复(restore)和恢复...
(1)备份对象 可以使用RMAN进行的备份对象如下: --整个数据库:备份所有的数据文件和控制文件; --数...