Author: Lv, Haibo.
前言
“多年之后,面对枪决行刑队,奥雷良诺•布恩迪亚上校将会想起,他父亲带他去见识冰块的那个遥远的下午”。
《百年孤独》的这个开头堪称经典。倒序的方法,加上时间状语“多年之后”,苍桑感一下子就出来了。何况又是面对行刑队,悲剧感也很强烈。要知道文学作品最能打动人心的,就是悲剧。比如,我至今还记得最短、最悲伤的爱情小说:
“我爱你!!!”
“滚。”
全篇只有四个字,但悲剧色彩强烈。一下子让我想到了我哪坎坷的青少年时期。
好了,不扯了,大家没有看错,这是一篇技术文章。我们言归正传。
“多年之后,麻木的面对一个个一筹莫展的Oracle难解之谜时,我总会想起来,当年我是如何积极的想成为一名DBA”。
第一节 调试Oracle:DBA的新领域
职业的发展,总会经历一段特殊的时间段,有叫“舒适区”,有叫“天花板”。总之,就是你感觉前进困难、进步无门,而待在原地则很舒适,那么恭喜你,你就在处在这段区域中了。
我是2004年开始转DBA,2005年正式成为DBA。5年后,2010年,我进入了我的“舒适”区。我感觉处理Oracle故障也就哪么几种方式,搞不定的问题丢给Oracle售后。我也想着进一步去钻研Oracle的原理、进一步提升自己的技能。但毕竟看不到源码,只能根据有限的几条Dump命令去探索,如同隔靴搔痒、雾里看花一般,这样研究出来的“底层原理”,准确性都不好确定,更别谈用这样的原理去排故、调优。
好像有一道无形的天花板,挡住了我的前进之路一般。既然前进无门,原地踏步又很舒适,还有谁会想着前进呢?
这时回想当年自己彻夜钻研Oracle技术,好似隔世一般。
当年我热火朝天的从开发转到DBA时,一定没有料到最终会是这样的结果。我就像奥雷良诺•布恩迪亚上校,被枪决在技术的天花板前。或者,也可以换种好听点的说法,成为在舒适区的温水中,被煮死的青蛙。
微博上有人辟谣,温水煮不死青蛙。我也是这样认为的,青蛙又不傻,当水温升高到一定程度时,难道它不会跳出来?
无论温水煮不煮的死青蛙,但温水一定煮的死人。我不想被温水煮死。
还有人为鸵鸟辟谣,传说当危险来临时,鸵鸟会把头埋到沙子中。由此还产生了一个新词:鸵鸟心态。我也一直认为,鸵鸟如果真的哪么傻,它是如何在这个充满竞争的星球存活亿万年的。
同样,鸵鸟有没有鸵鸟心态不太清楚,人多多少少都会有的。
我想打破我的天花板,我不想用鸵鸟心态去面对问题,我更不想待在“温水舒适区”等死。如何跳离“温水”呢?
答案就是:“调试Oracle”。
调试Oracle真的有这么神奇吗。是的。它可以有助于我们打破天花板,帮助我们跳离“温水”。当然前提是你要先有跳出温水的意愿。
如果非要让我用一句话总结“调试Oracle”的最大用途,我觉得,就是这7个动人的字:“明明白白我的心。”
维护Oracle这种大型软件,仅仅会运用它提供的命令是远远不够的,必须要了解它底层的原理。
比如,我知道体温超过38度可以吃两片白加黑,如果咳嗽的话可以来一瓶川贝枇杷膏,……,根据常识,我知道身体常见的症状要吃什么药,我能当医生吗?我当了医生你敢让我看病吗?
多多少少,我们要了解一下身体的运行原理,才能当医生。
比如体温升高我们可以去验个血,看一下白细胞比例,如果偏高,说明有病菌,属于病毒性感冒。原理是白细胞是杀死病菌的,如果体内病菌增多,人体就会产生更多的白细胞,杀死病菌。所以观察到白细胞增多,就意味着身体检测到病菌入侵了。
这种情况下如果体质好的话,就不必吃药,等着体内的白细胞自己杀掉所有的病菌。如果体质不好,可以吃一些抗菌药,等等。
我不是医生,也没学过医,上面这些都是道听途说,不知道对不对。只是拿来做个比喻。
Oracle的运维,道理差不多的。会使用Oracle提供的命令只是学习Oracle的第一步,必须要懂得原理,才能慢慢提高自己。
但Oracle毕竟是闭源软件,又看不到源代码,要如何去分析、挖掘它的原理呢?“调试Oracle”就是用来打开这个黑盒子的,掌握了它,我们就可以随意挖掘你感兴趣的原理。
本来我想用一两个例子,快速展示一下调试Oracle的神奇之处。但发现这可能比较困难,因为我必须从头说起,如果没有一点基础,你可能很难明白我们接下来要说的案例。下面,我们先来了解一下DTrace的基础。
DTrace是一门简单的语言,了解任何语言,都要先从Hello World开始,要不然,就会显的你很不专业。我当然不能坏了规矩。
第二节 DTrace基础: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。当然,我们调试进程,一般不需要修改,只需要观察。(偶而也需要修改,比如,有时我们要修改DBWR、LGWR等Oracle核心进程的触发时间,从3秒改成我们需要的时间。)
这就是DTrace的原理。我们将上文中的术语换成DTrace中的术语,触发器就是Probe,可以译为探针。探针并时都是关闭的,也就是Disable的。我们可以使用DTrace,打开探针,并为探针指定动作。当探针被触发,你通过DTrace为探针定义的动作,就会被执行。
好了,让我们从Hello World开始吧。下面是DTrace版的“Hello World”:
在root下,vi test1.d,输入如下命令:
BEGIN
{
printf("hello world,www.ebay.com ");
exit(0);
}
如下执行此脚本:
# dtrace -s test1.d
结果显示:
dtrace: script 'test1.d' matched 1 probe
CPU ID FUNCTION:NAME
3 1 :BEGIN hello world,www.ebay.com
上面就是显示结果了。下面对于脚本程序和输出结果,略加说明:
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.ebay.com,就是我们程序的输出结果了。
最后,每次运行此程序时,都要dtrace -s,太麻繁了。我们可以添加一个#!/usr/sbin/dtrace -s在程序头,如下所示:
#!/usr/sbin/dtrace -s
BEGIN
{
printf("hello world,www.ebay.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.ebay.com
第三节 详述探针(Probe)
完整的探针描述符,绝对不止上节我们遇到的BEGIN这么简单。它包括PROVIDER(提供器),MODULE(模块名),FUNCTION(函数名)和NAME(探针名称)四部分。BEGIN只是最简单的一个特例。
PROVIDER是最上层的名称,比如有IO PROVIDER,进程PROVIDER,等等。每种PROVIDER根据其包含的探针不同,又分为N种MODULE。MODULE之中又包含各种FUNCTION,最后的NAME是探针名,通常是进入、开始、退出、完成这些东西,在进入一个FUNCTION(函数)、退出函数、完成函数等等动作发生时被触发。
简单点说,PROVIDER、MODULE、FUNCTION、NAME就像姓、名、字、号,姓李,名白字太白,号诗仙。PROVIDER为李,MODULE为白,FUNCTION为太白,NAME为诗仙。
也就是说,它们都是“名字”的不同部分,不必理解的太为复杂。
我们以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探针。或者,直接称它为io:genunix:bdev_strategy:start探针。
一个探针的称呼其实无所谓。了解Solaris一供为我们提供了什么PROVIDER(提供器),这些提供器下都有什么MODULE(模块),这些模块中都有什么FUNCTION(函数),以及这些函数上都有什么探针,这才是重要的。关于这点,我们可以参考《Solaris 动态跟踪指南》,这是本书像一本字典,详细介绍了所有的提供器、模块等等。其实它就是DTrace的官方文档。难得Oracle还有翻译的如此好的文档。
好,先以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
1 781 bdev_strategy:start
1 781 bdev_strategy:start
1 781 bdev_strategy:start
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[2]->fi_pathname,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,这是Oracle最小的I/O了。
我们为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[2]->fi_pathname,args[0]->b_bcount,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个探针。
好了,基础就补到这。其实从这里的简单例子,我们已经可以分析挖掘很多的东西了。但这些东西找个其他的IO跟踪工具也可以跟踪出来。下面,正式内容开始,我们开始调试Oracle。这是别的工具所完成不了的。
精彩内容,请期待“调试Oracle之二:实例篇。”
* 本文版权和/或知识产权归eBay Inc所有。如需引述,请和联系我们[email protected]。本文旨在进行学术探讨交流,如您认为某些信息侵犯您的合法权益,请联系我们[email protected],并在通知中列明国家法律法规要求的必要信息,我们在收到您的通知后将根据国家法律法规尽快采取措施。