原址如下:
VAGE的探索频道:Oracle时间之旅
http://www.itpub.net/thread-1842611-1-1.html
本文较长,一万多字。了解我的人都知道,这是我的一贯风格,虽然我文章不多,但每一篇都比较长。了解我的人也知道,我的技术文章经常会颠覆一些常识,这篇文章也是。
春节了,趁休息时间不妨看看此文,思考一下自己的技术方向。我的主要目的,还是为大家提供一条通向超级DBA之路。如果对此实在无兴趣,无妨,MySQL、Hadoop、Linux、安全、……,等等,还有诸多的方向可供选择。
本文主要记录这前段时间的研究。
前段时间我花4天研究了一下Oracle中的时间机制。虽然得到的有实际意义的结论只有两条,但其实花的时间也不多,每天也只两、三小时而已。但也搞明白了Oracle中时间的秘密,基本上搞清楚了神奇的OWI(Oracle Wait Inface,也就是等待事件)中的记时机制。这4天时间,还是挺有意义的。
我把这4天的研究过程记录在此,是想告诉大家,深入研究并没有想像中的哪么难,哪么耗时。
至于深入研究有什么意义,这个话题一直都十分有争议。说白了,就是专与博的方向选择问题。
专与博,到底怎么做,才对个人发展有利呢?
其实两者不矛盾的,专也行,博也行。
我遇到过靠“专”成功的,比如阿里巴巴DBA界早期的几位牛人。他们用当时的技术,把Oracle研究的非常深入。到现在还有很多人在看他们当年的帖子,现在他们大都转入管理岗位,结束了做为技术人员的苦逼日子。
我也遇到过靠“博“上位的,他凭OCM进的公司,对Oracle熟悉。一开始级别不高,中等偏上吧。他当时考OCM时还没有题库,能考上OCM,Oracle水平应该还不错,但谈不上精通。但他的优势在于会的多。除Oracle外,Hadoop、GreenPulm,Java、甚至UML,等等著多技术都懂。这样的人可以敏感的发现用户需求,并通过各种技术的组合,为用户解决问题。在多次为用户解决实际需求问题后,级别逐渐升了上去,再跳个槽,华丽转身为中高层管理人员。
虽然专与博都是不错的发展方向,但相较来说,国内“博”方面的人才很多。
比如同时拥有OCM、RHCA等多个顶级证书的人,在前些年很少,现在已经有不少了。甚至Oracle OCM+MBA,前些年如凤毛麟角,现在,已经听说有一些OCM都去考了MBA。大家都在横向发展。
而“专”的人才,相对来说较少。别行业的不说,就拿Oracle为例,国外的大神们早就开始用“动态跟踪语言”研究Oracle了,而国内呢?很多DBA应该还都不知道“动态跟踪语言”为何物。
Solaris下的动态跟踪语言就是Dtrace。Liews在他的新书《Oracle内核》中有提到Dtrace。但在书中他并没有讲如何用Dtrace研究Oracle,因为这可能难度有点高。
我通常更倾向于走人少的路,因此,才有本文。本文就是以Dtrace为主,mdb为辅,还有一点点反汇编代码,为我们分析Oracle的时间机制。
好了,我是否像唐僧一样啰嗦了,前言部分有点长了。下面,开始吧。
周日 晴 有mai 空气重度污染
上午无所事事,背了一课新概念英语,玩了一局保护萝卜,觉得时间不能如此浪费,想起一直以来都有个疑问,闲着也是闲着,正好现在测试一下。疑问是:Mutex到底比Latch快多少倍。
都说Mutex快,Mutex好,Latch迟早完蛋。到底Mutex比Latch快多少倍呢?毛爷爷教导我们:“实践是检验真理的唯一标准。”,于是我准备测试一下。
测试这东西,还是用Dtrace最方便,代码简单。但还是只能用Solaris下的Dtrace,Linux下的Dtrace暂时还没有PID探针。无法探测Oracle内部函数的调用。
在11GR2下,Latch的函数有:
1)、ksl_get_shared_latch:获得共享Latch
2)、kslgetl:获得独占Latch
3)、kslfre:释放共享或独占Latch
Mutex的获取函数,
1)、共享kgxShared、kgxSharedExamine、……
2)、独占kglGetMutex、kglGetBucketMutex、……
3)、释放:kglReleaseMutex、kgxRelease、……
至于这些函数是如何获得的,参看我去年的文章吧。这是我的Dtrace脚本:
bash-3.2# catlatch_mutex_time.d
#!/usr/sbin/dtrace-s -n
dtrace:::BEGIN
{
i=1;
printf("Start...\n");
}
pid$1::kslgetl:entry,
pid$1::kgxShared:entry,
pid$1::kglGetMutex:entry,
pid$1::kglGetBucketMutex:entry,
pid$1::kgxDecrementExamine:entry,
pid$1::kgxExclusive:entry,
pid$1::kslfre:entry,
pid$1::kglReleaseMutex:entry,
pid$1::kgxRelease:entry,
pid$1::kglReleaseBucketMutex:entry,
pid$1::kgxSharedExamine:entry,
pid$1::ksl_get_shared_latch:entry
{
tm1[probefunc]=vtimestamp;
}
pid$1::kslgetl:return,
pid$1::kgxShared:return,
pid$1::kglGetMutex:return,
pid$1::kglGetBucketMutex:return,
pid$1::kgxDecrementExamine:return,
pid$1::kgxExclusive:return,
pid$1::kslfre:return,
pid$1::kglReleaseMutex:return,
pid$1::kgxRelease:return,
pid$1::kglReleaseBucketMutex:return,
pid$1::kgxSharedExamine:return,
pid$1::ksl_get_shared_latch:return
{
tm2=vtimestamp-tm1[probefunc];
printf("%3d tm=%d%s\n",i,tm2,probefunc);
i=i+1;
@times[probefunc]=avg(tm2);
@counts[probefunc]=count();
@maxone[probefunc]=max(tm2);
@minone[probefunc]=min(tm2);
tm1[probefunc]=0;
}
dtrace:::END
{
trace("-----------------------------");
printa(@times,@counts,@maxone,@minone);
}
这个脚本用内置变量vtimestamp统计时间,单位是纳秒。和timestamp的区别是,vtimestamp将去掉Dtrace脚本谓词对时间的影响。
另外,在函数的入口探针处,使用如下命令:
tm1[probefunc]=vtimestamp;
tm1[probefunc]被称为关联数组。在我之前发到Pub上的《Solaris动态跟踪指南》第60页有介绍。
还有,函数出口探针处的“@times[probefunc]=avg(tm2);”,还有它的下三行,都是聚合函数,在《Solaris动态跟踪指南》第9章有介绍。
好,可以开始测试了。新建一个测试会话:
-bash-3.2$sqlplus lhb/a
SQL*Plus:Release 11.2.0.1.0 Production on Fri Nov 22 09:13:40 2013
Copyright (c)1982, 2009, Oracle. All rights reserved.
Connected to:
Oracle Database11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
With thePartitioning, Automatic Storage Management and Data Mining options
SQL> selectc.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#
---------------------------------- ---------- ----------
247 1265 21 11
测试会话的SID是247,对应的服务器进程号是1265。先来试试软软解析:
SQL> select *from vage where id=1;
ID NAME
--------------------------------------------------
1 ddddd
SQL> /
ID NAME
--------------------------------------------------
1 ddddd
SQL> /
ID NAME
--------------------------------------------------
1 ddddd
这条SQL已经执行了三次,已经被Cache了。下面,运行我们的Dtrace脚本:
bash-3.2#./latch_mutex_time.d -q -x switchrate=5hz -b 2m 1265
Start...
在247会话中,再次执行测试SQL。latch_mutex_time.d脚本输出如下内容:
1 tm=7890 ksl_get_shared_latch
2 tm=5875 kslfre
3 tm=4451 ksl_get_shared_latch
4 tm=2687 kslfre
5 tm=2055 ksl_get_shared_latch
6 tm=2075 kslfre
7 tm=3710 kgxSharedExamine
8 tm=2902 ksl_get_shared_latch
9 tm=2677 kslfre
10 tm=5038 ksl_get_shared_latch
11 tm=3027 kslfre
12 tm=5639 kslgetl
13 tm=2482 kslgetl
14 tm=2676 kslfre
15 tm=2366 kslfre
16 tm=4085 kgxDecrementExamine
17 tm=3234 ksl_get_shared_latch
18 tm=2646 kslfre
再按一个Ctrl+C,将显示聚合函数的结果:
^C
-----------------------------
kslfre 3003 8 5875 2075
kgxSharedExamine 3710 1 3710 3710
kslgetl 4060 2 5639 2482
kgxDecrementExamine 4085 1 4085 4085
ksl_get_shared_latch 4261 6 7890 2055
bash-3.2#
结果不用我说了,你可以自己试一下。Mutex和Latch速度并无明大的区别,有的Latch比Mutex快,有的Latch比Mutex慢。平均下来,Latch比Mutex慢一点。
这是软软解析,再看看硬解析会怎样。运行Dtrace脚本,在247会话中执行如下SQL:
SQL> select *from vage where id=2;
ID NAME
--------------------------------------------------
2 aaaaa
条件为id=2的SQL之前没有执行过,这是一个硬解析。看看跟踪结果是怎么样的:
bash-3.2#./latch_mutex_time.d -q -x switchrate=5hz -b 2m 1265
Start...
1 tm=7790 ksl_get_shared_latch
2 tm=6878 kslfre
3 tm=2868 ksl_get_shared_latch
……………………
413 tm=2015 kslfre
414 tm=2020kslfre
415 tm=2246ksl_get_shared_latch
416 tm=2095kslfre
^C
-----------------------------
kglReleaseMutex 1948 57 4817 748
kgxExclusive 2148 58 7629 758
kglReleaseBucketMutex 2192 12 4457 818
kslfre 2330 83 6878 779
kgxShared 2376 5 2968 1910
kgxRelease 2706 63 44664 779
kslgetl 2926 76 44659 792
kgxDecrementExamine 3303 1 3303 3303
ksl_get_shared_latch 3908 7 7790 2236
kglGetMutex 5100 45 9594 882
kglGetBucketMutex 9021 9 28312 2383
一次硬解析,Mutex和Latch的获取次数有两百次左右了。这其中平均时间最慢的kglGetBucketMutex、kglGetMutex,都是Mutex。
也就是说Mutex并不比Latch快,它们从获取、释放时间上说,都是在一个数量级的。Mutex的优势在于,它占的内存少,数量可以更多。这样,竞争就分散了。
比如,原来由Library CacheLock保护句柄、由Library Cache Pin保护堆。但是堆有好几个呢,比如SQL语句子游标就有两个堆:Heap 0和Heap 6。以前时候这两个堆用一个Library Cache Pin保护,现在它们各自用自己的Mutex保护。
甚至,同一个堆内的不同部分,也可以分别由不同的Mutex保护。比如父游标堆0中,有一部分信息是所有子游标句柄组成的HASH表,这一部分和父游标堆0中的其它信息原来都是受同一Library Cache Pin保护。现在,则不一样了。父游标堆0中其他信息用Cursor Mutex型Mutex保护,子游标句柄HASH表则是由另一种Hash Table型Mutex保护。
更多的Mutex、更细的粒度,肯定可以使并发更高、竞争更少,这才是Mutex的最终目的。当没有竞争时,只有一个进程在跑时,不使用Mutex可能会更节省CPU。但当并发高时,当然还是Mutex更有利于减少竞争。简单的说,Mutex增加了Oracle的Scale能力。
Oracle的代码一代代发展,Scale能力不断加强,这不是其他数据库短时间可以赶上的。除了价格是它的劣势,目前在OLTP数据库中,Oracle的确是没有对手的。
Liews在他的新作《Oracle内核技术》的前言中,进一步发展了二、八理论。提出了90%、9%、1%。
90%的问题都属于基本问题,对于Oracle,我们搞明白90%就可以了。更进一步的,可以搞明白剩下的9%。最后的1%,实在没有必要去研究了,因为要花很多精力,最后的收获只有1%。
但是,注意我们不是Liews这等Oracle界神人,当有时候要提出一些不一样的言论时,最好连哪1%都做了。否则,不会让人信服。
我的Dtrace脚本连空行也算上,一共52行,很简单。写个小脚本,解答一下心中的疑问,这是很简单事情。一共也就花个二、三十分钟的事。
但是我得到的这个结论,有点让大家吃惊了。如果不继续再深入点,很多人应该会有不同的看法。
“相比Latch,Mutex的代码路径更短,执行更迅速”。这样的描述,到处都有,而且深入人心。下面,让我们打破这种说法。
方法很简单,反汇编一下就可以了。Latch和Mutex都是核心代码,反汇编的结果都应该很短,不会太难以阅读的。而且,我们也没必要逐行去阅读它们的反汇编代码。
Latch我以kslgetl为例,它是获得独占Latch的函数。Mutex以kglGetBucketMutex为例吧,它取代了LibraryCache Latch,保护共享池HASH表后的HASH链。
下面,先来比比它们的长度。先来看看kslgetl。随便找个Oracle服务器进程,用mdb调试它,输入“kslgetl::dis”,就可以反汇编kslgetl函数:
bash-3.2# mdb -p1306
Loading modules:[ ld.so.1 libc.so.1 ]
>
>kslgetl::dis
kslgetl: pushq %rbp
kslgetl+1: movq %rsp,%rbp
kslgetl+4: pushq %rbx
kslgetl+5: pushq %r12
……………………
……………………
kslgetl+0x1485: xorl %eax,%eax
kslgetl+0x1487: call -0x6b097 <ksesec0>
kslgetl+0x148c: movl $0x0,-0x2b8(%rbp)
kslgetl+0x1496: jmp -0x10b0 <kslgetl+0x3e6>
一直按空格,将这个函数的反汇编代码全部显示完,一共1048行,大小共0x1496字节,也就是5270字节。
再看kglGetBucketMutex:
>kglGetBucketMutex::dis
kglGetBucketMutex: pushq %rbp
kglGetBucketMutex+1: movq %rsp,%rbp
kglGetBucketMutex+4: pushq %rbx
………………………………
kglGetBucketMutex+0x1fe: jmp -0xf2 <kglGetBucketMutex+0x10c>
kglGetBucketMutex+0x203: movl $0x1,%eax
kglGetBucketMutex+0x208: jmp -0xfc <kglGetBucketMutex+0x10c>
>
汇编代码只有138行,大小0x208字节,正好520字节。
从函数大小上看,的确没有说错,Mutex比Latch的代码要短十倍。哪为什么kglGetBucketMutex所占用的时间会比kslgetl要长呢?无论什么样的测试环境,简单的程序都会比复杂的程序跑的更快啊。
下面,只能单步跟踪了。
先从Mutex开始吧。在kglGetBucketMutex函数处设置断点:
>kglGetBucketMutex:b
> :c
:c是继续执行的意思。然后在1306进程对应的会话中,随便执行条SQL,硬、软解析都可以,但不能是软软解析,因为软软解析时不会调用此函数。
mdb: targetstopped at:
kglGetBucketMutex: pushq %rbp
>
在显示如上信息,在断点处停下来后,使用::step,一步步执行。
> ::step
mdb: targetstopped at:
kglGetBucketMutex+1: movq %rsp,%rbp
> ::step
mdb: targetstopped at:
kglGetBucketMutex+4: pushq %rbx
> ::step
mdb: targetstopped at:
kglGetBucketMutex+5: pushq %r12
…………………………
> ::step
mdb: targetstopped at:
kglGetBucketMutex+0x11a:ret
> ::step
mdb: targetstopped at:
kglhdgn+0x1be: movzbq %r15b,%r8
在执行了125条指令后,kglGetBucketMutex函数执行完成,退出到了它的上层函数kglhdgn中。
对kslgetl也如法炮制,可以得到,kslgetl只调用了85条汇编指令,就完成了加Latch操作。
这就不难解释为什么Latch会比Mutex更快了,它的调用只需要更少的汇编指令。但为什么kslgetl会比kglGetBucketMutex复杂哪么多呢?我觉得这就是Latch和Mutex机制上的不同了,Latch的Latch Miss比较详细,而Mutex的Miss较难获得。
并不是所有Mutex所需指令都比Latch多,kgxSharedExamine是软软解析时获取的共享Mutex的函数,它只需要54条汇编指令。考虑到Latch的获得要维护v$latch、v$latch_children下层的X$视图,而Mutex则不需要。Kslgetl中实际干活(而非维护下层x$视图)的指令比kgxSharedExamine实际多不了多少。
看,简单吧,这些函数具体的汇编代码现在没必要去阅读,我们只是单步跟踪一下,就打破了“Mutex代码路径比Latch短”这种说法。要知道这可是在Oracle大神Liews的《Oracle内核技术》中,黑纸白字写了的。只需要单步跟踪一下,一切自有分晓。
(当然,这里我不是刻意要针对Liews,这篇文章我已经构思很久,Mutex比Latch快多少也一直是我心头的疑惑,当时《Oracle内核技术》中文版还没有出版,英文版的《Oracle Core》我并没有详细阅读。直到上周,我去书店休逛,发现《Oracle内核技术》中文版已经有的卖了,拿起来顺手翻了一下,才看到Liews说Mutex的代码路径要比Latch少,于是,我又将本文扩充了一下,加上了后面单步执行的统计)
最后,还有一点补充,CBC Latch(cachebuffers chain latch)在很多时候都是数据库中最为繁忙的一种锁,比其他的锁包括Mutex、其他Latch、各种队列锁、……都要繁忙。因此Oracle对它专门设计,它是所有锁机制中代码路径最短、CPU消耗最少的。
Latch和Mutex的比较先说到这儿吧,说话间,已经是中午了。我一向有午睡的习惯,所谓中午不睡,下午崩溃。但是很遗憾,午睡完后不能继续了,因为要陪老婆逛街。我一向是家庭第一、事业第二。
逛街过程略过,这是Oracle研究日记,不是逛街日记。
晚上,夜深人静,这是IT人最爱的时刻。你的电脑屏幕上显示的东西,决定了未来你的生活。如果你电脑上是满屏的代码,过几年你将是资深程序员。如果是苍老师视频,过几年你将是资深水管工。
开句玩笑。开始正题。今天的日记之所以还没有结束,原因我还有一个疑问。所以逛完街回来,我要继续测试。我们知道,一条SQL最基本执行的步骤有:
1、解析
2、执行
3、抓取
4、传送数据到Client
这些步骤中,哪些步骤耗CPU最多呢?
我以如下SQL为例:
SQL> select *from vage where rowid='AAADf5AAMAAABMjAAA';
ID NAME
--------------------------------------------------
1 ddddd
Vage表只有两列,当反复多次执行此SQL,已经软软解析后。再次执行此SQL,哪个步骤最耗CPU。
在开始测试前,不仿猜一猜,谁最耗CPU?
我认为是抓取。这个阶段包含了逻辑读,还要把逻辑读的结果拷贝到PGA中。相比之下,软软解析只是根据PGA中缓存的地址,找到SQL执行计划而已。
至于执行阶段吗,说实话,对于Select来说,我一直不清楚这一阶段具体都干了啥。逻辑读、数据读取这些步骤是抓取,执行,应该是很简单的步骤。传送数据到客户端进程,这个就要看网络了。我是本地连接,没通过监听,因此这个步骤也应该只消耗很少时间才对。
但,实事上最终测试的结果,完全超出了我的预料。
不过,我已经习惯了。自从开始“调试Oracle”以来,已经有太多结果,颠覆了我原来对Oracle的认知。再多一个也不嫌多了。
开始测试吧。我一开始的想法,仍然使用Dtrace中的内置变量vtimestamp,记录Oracle所有函数调用时间。我的脚本如下:
bash-3.2# catall_time.d
#!/usr/sbin/dtrace-Fs -n
int tm2;
dtrace:::BEGIN
{
i=1;
}
pid$1:::entry
/probefunc[0]!=0x6d& probefunc[1]!=0x65 & probefunc[2]!=0x6d /
{
printf("i= %d tm= %d %senter------------- ",i,timestamp,probefunc);
i=i+1;
tm1[probefunc]=timestamp;
}
pid$1:::return
/probefunc[0]!=0x6d& probefunc[1]!=0x65 & probefunc[2]!=0x6d /
{
printf("i= %d tm= %d %sreturn",i,timestamp-tm1[probefunc],probefunc);
i=i+1;
tm1[probefunc]=0;
}
syscall::tim*:*
{
printf("%s %s %x %x%x",probefunc,probename,arg0,arg1,arg2);
}
dtrace:::END
{
trace(i);
}
注意这个脚本第一行有一点变化:“#!/usr/sbin/dtrace-Fs –n”,注意这里,比之前多了一个F。这是Dtrace的推理跟踪,在《Solaris动态跟踪指南》书中第202页,第16章“选项和可调参数”中有介绍。它的作用是根据函数的调用、返回产生缩进。
还有,注意这个谓词:“/probefunc[0]!=0x6d& probefunc[1]!=0x65 & probefunc[2]!=0x6d /”,0x6d、0x65、0x6d的ASCII码是mem,这里我把函数名以mem开头的函数过虑掉了。这些函数有memcpy、memcmp、memset、……。这些函数退出的方式我不太清楚,它们将会破坏掉缩进的规则。所以将这些函数去掉。
而加上“syscall::tim*:*”的目的,主要是我想看看Oracle的哪些函数会调用OS系统调用time。
我的测试语句仍然是这个“select *from vage where rowid='AAADf5AAMAAABMjAAA';”,为了和Dtrace结果验证,我使用时间模型显示了一下这条SQL的时间,步骤如下:
步1:显示SQL执行前的时间:
SQL> selectsid,stat_name,value/1000 from V$SESS_TIME_MODEL where sid in (248) andSTAT_NAME in
2 ('DB CPU','DB time','parse time elapsed','sql execute elapsed time')order by sid,stat_name;
SID STAT_NAME VALUE/1000
------------------------------------ ----------
253 DB CPU 360
253 DB time 1365.688
253 parse time elapsed 826.905
253 sql execute elapsed time 860.494
步2:运行Dtrace脚本
bash-3.2#./all_time.d -x switchrate=5hz -b 8m 1212 > tm1.log
dtrace: script'./all_time.d' matched 305417 probes
步3:再次运行测试SQL
SQL> /
ID NAME
--------------------------------------------------
1 ddddd
步4:结束Dtrace脚本,并再次显示时间模型中的时间。
SQL> /
SID STAT_NAME VALUE/1000
------------------------------------ ----------
253 DB CPU 390
253 DB time 1392.465
253 parse time elapsed 830.397
253 sql execute elapsed time 866.66
这一次解析花了3.492毫秒,执行共花了6.166毫秒。V$SESS_TIME_MODEL视图中的时间单位是微秒,我将它除了1000,化为毫秒。实事上这样做没有意义,因为Dtrace中的结果是纳秒。
还有一点要说明下,这里的执行,其实包括了执行和抓取两个阶段。关于这一点,很容易测试验证,这里不再补充。
----------------------------------------------
注意:因为使用Dtrace打开了30多万探针,因此这里的解析、执行时间都比平常要慢些。
----------------------------------------------
最后一步,根据时间模型视图的结果,在Dtrace跟踪结果中查找解析、执行、抓取分别用了多久时间。
这一步可以很耗时间的。我当天晚上并没有做完。一直到第二天才完成。
好了,第一天的日记就到这里了,开始第二天吧。
周一 风 空气还好
今天白天一直没有得闲,直到晚上,才开始继续发掘昨天的跟踪结果。在跟踪结果中,首先注意到这一块:
-> kksParseCursor i= 257 tm= 5188341428755kksParseCursor enter-------------
-> kkspsc0 i= 258 tm= 5188341453144 kkspsc0 enter-------------
-> kgscComputeHash i=259 tm= 5188341498854 kgscComputeHash enter-------------
-> kggmd5Update i=260 tm= 5188341532407 kggmd5Update enter-------------
……………………………………………………………………………………
<- kglHandleLoads i=354 tm= 11775 kglHandleLoads return
-> kglHandleInvalidations i=355 tm= 5188344426677 kglHandleInvalidations enter-------------
<- kglHandleInvalidations i=356 tm= 11386 kglHandleInvalidations return
<- kkspsc0 i= 357 tm= 3021130 kkspsc0 return
<- kksParseCursor i= 358 tm= 3055994kksParseCursor return
kksParseCursor,看这个名字就知道它是解析了,它一共用了3055994纳秒,化为毫秒3.056毫秒。这和时间模型视图中的3.492还差了点,
在这个函数之后不久,就是opiexe函数了。从名字上看,我相信它对应“执行”阶段。kksParseCursor 之外,是opiosq0函数,它调用了kksParseCursor。opiosq0的执行时间是3.825毫秒。
3.056毫秒和3.825毫秒这两个时间都比时间模型视图中的3.492差了点。这是为什么呢?
要了解这个问题,就要进一步了解Oracle的时间机制。Oracle是如何知道一个操作执行了多少时间的呢?Oracle的计时机制到底是如何实现的呢?
千万不要把Oracle想象的太神秘,需知Oracle也是程序,也是由程序员开发出来的,仅此而已。如果你是程序员,如果你要对程序中某个操作计时,你如何实现?我想,代码无外乎如下形式:
Main()
{
……
t1=当前时间();
开始操作();
操作占用时间=当前时间()-t1;
………………
}
只要找到Oracle的“当前时间()”函数是什么,我们就可以在Dtrace跟踪结果中,计算出Oracle的各种时间。时间模型的,甚至等待事件的。
这样以来,我们可以更加准确理解Oracle中时间相关的资料,甚至Oracle时间模型的缺陷。这对于查看AWR、日常健康检查有很重要的意义。
在我的脚本中,我专门打开了OS系统调用time的探针:“syscall::tim*:*”,在跟踪结果中可以发现类似如下形式的调用:
->slcpums i= 267 tm=14293149957503 slcpums enter-------------
=> times times entry fffffd7fffdfb230 0 0
<= times times return 15c766 15c766 0
<-slcpums i= 268 tm= 63286slcpums return
-> slcpu i= 271 tm= 14293150108774 slcpuenter-------------
=> times times entry fffffd7fffdfb230fffffd7ffc9e3ab0 0
<= times times return 15c766 15c766 0
<- slcpu i= 272 tm= 41474 slcpu return
Slcpu和slcpums函数,都调用了系统调用time(),这两个函数都获得过系统时间,但经过分析,我觉得这两个函数都不是时间模型和等待事件所采用的计时函数。
正在疑惑间,我想起有一个函数:gethrtime。这个函数在我已前的跟踪结果中很常见。很奇怪,在这次的跟踪结果中,没有gethrtime函数的调用。我也不知道这是为什么,可能是使用了选项“-F”的原因。
我反汇编了这两个函数:
bash-3.2# mdb -p 1212
Loading modules:[ ld.so.1 libc.so.1 ]
> gethrtime::dis
libc.so.1`gethrtime: movl $0x3,%eax
libc.so.1`gethrtime+5: int $0xd2
libc.so.1`gethrtime+7: ret
>
>
> time::dis
libc.so.1`time: pushq %rbp
libc.so.1`time+1: movq %rsp,%rbp
libc.so.1`time+4: pushq %rbx
libc.so.1`time+5: movq %rdi,%rbx
libc.so.1`time+8: subq $0x8,%rsp
libc.so.1`time+0xc: call +0x18264 <libc.so.1`__time>
libc.so.1`time+0x11: testq %rbx,%rbx
libc.so.1`time+0x14: je +0x5 <libc.so.1`time+0x19>
libc.so.1`time+0x16: movq %rax,(%rbx)
libc.so.1`time+0x19: addq $0x8,%rsp
libc.so.1`time+0x1d: popq %rbx
libc.so.1`time+0x1e: leave
libc.so.1`time+0x1f: ret
> ::quit
Gethrtime()比time()要简单的多,它只是简单的调用BIOS中0xd2号中断程序。从节省时间的角度上说,gethrtime()应该会比time()要快。
下面,修改一下Dtrace脚本如下:
bash-3.2# catall_time.d
#!/usr/sbin/dtrace-Fs -n
int tm2;
dtrace:::BEGIN
{
i=1;
}
pid$1::gethrtime:entry
{
printf("i= %d tm= %d %s enter-------------",i,timestamp,probefunc);
i=i+1;
tm1[probefunc]=timestamp;
}
pid$1::gethrtime:return
{
printf("i=%dPID::entry:==%s:%s:%s:%s %x %d %d",i, probeprov, probemod, probefunc,probename,arg0,arg1,arg1-tm2);
tm2=arg1;
i=i+1;
}
pid$1:::entry
/probefunc[0]!=0x6d& probefunc[1]!=0x65 & probefunc[2]!=0x6d /
{
printf("i= %d tm= %d %senter------------- ",i,timestamp,probefunc);
i=i+1;
tm1[probefunc]=timestamp;
}
pid$1:::return
/probefunc[0]!=0x6d& probefunc[1]!=0x65 & probefunc[2]!=0x6d /
{
printf("i= %d tm= %d %sreturn",i,timestamp-tm1[probefunc],probefunc);
i=i+1;
tm1[probefunc]=0;
}
syscall::tim*:*
{
printf("%s %s %x %x%x",probefunc,probename,arg0,arg1,arg2);
}
dtrace:::END
{
trace(i);
}
其实和之前的脚本相比,只是增加了如下内容:
pid$1::gethrtime:entry
{
printf("i= %d tm= %d %senter------------- ",i,timestamp,probefunc);
i=i+1;
tm1[probefunc]=timestamp;
}
pid$1::gethrtime:return
{
printf("i=%dPID::entry:==%s:%s:%s:%s %x %d %d",i, probeprov, probemod, probefunc,probename,arg0,arg1,arg1-tm2);
tm2=arg1;
i=i+1;
}
需要说明下,可以使用Dtrace内置变量arg1,获得gethrtime返回的时间。我在“pid$1::gethrtime:return”中,除了显示一下这个值外,顺便计算了一下每两次调用gethrtime的差。
再次用这个脚本跟踪。这次解析、执行的时间分别是:
SQL> /
SID STAT_NAME VALUE/1000
-------------------------------------------------------------------------- ----------
253 DB CPU 410
253 DB time 1403.862
253 parse time elapsed 832.161
253 sql execute elapsed time 868.591
SQL> /
SID STAT_NAME VALUE/1000
-------------------------------------------------------------------------- ----------
253 DB CPU 440
253 DB time 1430.077
253 parse time elapsed 835.595
253 sql execute elapsed time 874.776
上面的两次显示结果,分别是执行测试SQL前和后的,两个相减:
SQL> select835.595-832.161 from dual;
835.595-832.161
---------------
3.434
解析的时间是3.434毫秒,和上次差不多。
好了,要到跟踪结果中寻找了。在opiosq0函数之下,我们就可以找到gethrtime:
->opiosq0 i= 258tm= 14293149718316 opiosq0 enter-------------
-> kkdlcus i= 259 tm=14293149741833 kkdlcus enter-------------
<- kkdlcus i= 260 tm= 10182 kkdlcusreturn
-> kposrc i= 261 tm=14293149771034 kposrc enter-------------
<- kposrc i= 262 tm= 8297 kposrcreturn
-> gethrtime i= 263 tm= 14293149849345gethrtime enter-------------
<- gethrtime i=264PID::entry:==pid1212:libc.so.1:gethrtime:return 7 14293149859571 1589238
……………………
此时用gethrtime获得的时间是14293149859571,我以这个时间为起点,向下寻找,在opiosq0函数的结尾处,kksParseCursor函数调用结束之后,终于找到目标:
…………………………………………
<- kksParseCursor i= 370 tm= 2901315 kksParseCursorreturn
-> slcpu i= 371 tm= 14293153135614slcpu enter-------------
=> times times entry fffffd7fffdfb230ceed7f8 0
<= times times return 15c767 15c767 0
<- slcpu i= 372 tm= 60944 slcpureturn
-> gethrtime i= 373 tm= 14293153232071 gethrtimeenter-------------
<- gethrtime i=374PID::entry:==pid1212:libc.so.1:gethrtime:return 7 14293153240278 3163728
-> gethrtime i= 375 tm= 14293153286494gethrtime enter-------------
<- gethrtime(此行就是目标)i=376PID::entry:==pid1212:libc.so.1:gethrtime:return 7 14293153293823 53545
-> gethrtime i= 377 tm= 14293153316828gethrtime enter-------------
<- gethrtime i=378PID::entry:==pid1212:libc.so.1:gethrtime:return 7 14293153323952 30129
-> slcpums i= 379 tm= 14293153353785slcpums enter-------------
=> times times entry fffffd7fffdfb230ceed7f8 0
<= times times return 15c767 15c767 0
<- slcpums i= 380 tm= 43810 slcpumsreturn
-> kglHandleInvalidations i= 381 tm= 14293153416961kglHandleInvalidations enter-------------
<- kglHandleInvalidations i= 382 tm= 13771 kglHandleInvalidationsreturn
-> kglHandleLoads i= 383 tm= 14293153466856kglHandleLoads enter-------------
<- kglHandleLoads i= 384 tm= 11942 kglHandleLoadsreturn
-> kksGetStats i= 385 tm= 14293153494173kksGetStats enter-------------
<- kksGetStats i= 386 tm= 8798 kksGetStats return
-> kzaAudit i= 387 tm= 14293153614303kzaAudit enter-------------
<- kzaAudit i= 388 tm= 12948 kzaAuditreturn
<-opiosq0 i= 389 tm=3920416 opiosq0 return
用目标行的14293153293823减去开始处的14293149859571:
SQL> select14293153293823-14293149859571 from dual;
14293153293823-14293149859571
-----------------------------
3434252
化成毫秒就是3.434。这和时间模型视图中的结果是完全一致的。
找到了解析,一天又过去了。其实这花了我不多时间,两个多小时而已。只不过今天比较忙,只有两个多小时时间研究Oracle。
(未完待续)
周二 风
这是我在北京最后一天了,感谢京东给了我这么高的级别。当年到阿里的时候,我是P8,虽是大P,但比我级别更高的技术人员仍有几十个,到了京东,这个数字已经下降到十个左右。
这是大家对我技术研究方向的认可,我对前方的路也越来越有信心,我相信会有越来越多的人加入到“调试Oracle”阵营。
很遗憾,我在京东的只待了两周。我自身的原因是起因,北京这个奇怪的城市是主要原因之一。我老婆对北京抱怨不已,让我在最后关头放弃了努力。仿佛命运的召唤,让我选择离开。
虽只两周时间,但京东给我的感觉和阿里很像。都是大型电商,技术部门都以年轻人为主,虽然少不了部门间的推萎,但总的效率还是高的。阿里对员工上班迟到这事,没有强制限制,不用打卡,只要别做的太过份就行。京东也类似,虽有打卡机制,但监管并不严格。不过还好,我作为高管级别的技术人员是不用打卡的。
一直在思索后面要做什么事,朋友力荐我和他合作,一起搞Oracle的培训、咨询。我还没有最终下定决心。
今天又帮朋友处理点问题,他的数据库碰到了07445错误,这是一类很常见的错误。多数情况下是BUG,大部分时候可以绕过,有些情况则一定要打补丁。
我一直对这类错误很感兴趣,一直希望在11GR2、Solaris平台,能找到可反复重现的07445错误,这样我就可以使用mdb和Dtrace分析了,但很遗憾,我收到的07445基本都不可手动重现。只从事后的跟踪文件来分析,大部分时候不得要领,只得求助MOS了。
这一部分将是我下步的研究重点,因为,很多时候,MOS的响应并不即时,而且,并不是每个MOS的原厂工程师都很负责,再者,多数MOS上的BUG描述都是简短的一小段英文,好像是在说:“拿这段英文去向你的领导、客户交差吧,就说这是Oracle的回复”。
如果不是看在MOS可以下补丁的份上,真觉得它的其他作用也有限。
好,继续我们的研究吧。再看时间模型中的执行:
SQL> select 874.776-868.591from dual;
874.776-868.591
---------------
6.185
6.185毫秒。在Dtrace跟踪结果中,我觉得执行应该是从opiexe开始的:
-> opiexe i= 394 tm= 14293153737520opiexe enter-------------
-> ksuvrl i= 395 tm= 14293153752896ksuvrl enter-------------
-> slcpu i= 396 tm= 14293153775274slcpu enter-------------
=> times times entry fffffd7fffdfa6d0fffffd7ffc9df990 0
<= times times return 15c767 15c767 0
<- slcpu i= 397 tm= 56398 slcpu return
<- ksuvrl i= 398 tm= 90351 ksuvrlreturn
-> gethrtime i= 399 tm= 14293153880124gethrtime enter-------------
<- gethrtime i=400PID::entry:==pid1212:libc.so.1:gethrtime:return 7 14293153887824 563872
………………………………
………………………………
………………………………
<- audStatement i= 524 tm= 12001 audStatement return
-> kskmkinact i= 525 tm= 14293156425015 kskmkinactenter-------------
<- kskmkinact i= 526 tm= 11350 kskmkinact return
-> gethrtime i= 527 tm= 14293156479463 gethrtimeenter-------------
<- gethrtime i=528PID::entry:==pid1212:libc.so.1:gethrtime:return 7 14293156487389 360625
<- opiexe i= 529 tm= 2772282 opiexereturn
用opiexe结尾的gethrtime减去开头的:
SQL> select14293156487389-14293153887824 from dual;
14293156487389-14293153887824
-----------------------------
2599565
Opiexe中的时间是2.6毫秒。
在opiexe之后,Oracle紧接着调用了kpodny函数。这个函数在运行期间都没有调用过gethrtime函数。因此它应该是不计算在时间模型的执行阶段的。
如何确认下这点呢?简单,使用mdb调试服务器进程,在kpodny处设置断点即可。
向后分析,抓取阶段,应该对应函数opifch2。Fch应该就是fetch的缩写。它的开头和结尾也分别调用了gethrtime。根据gethrtime计算的时间是3245316纳秒,3.245毫秒。
还没有完,在opifch2之后,还有一个opifch函数的调用,它的时间是340853纳秒,0.340毫秒。
opiexe+ opifch2+opifch三个函数的时间加在一起:
SQL> select 3245316 +340853+2599565 from dual;
3245316+340853+2599565
----------------------
6185734
对比时间模型中的结果6.185毫秒,两者是一致的。也就是说,时间模型中的sql execute elapsed time,是opiexe、opifch2、opifch三个函数中gethrtime得到的时间之和。
抓取主要的逻辑读都在opifch2函数中完成,opifch我也不知道它是干吗的,看名字也是和抓取相关的。反正它不是向客户端传输结果。向客户端传送结果由另外的代码完成,其时间不记录进sql execute elapsed time。
好,现在将sql execute elapsed time细化为两步:执行和抓取。执行阶段耗时2,599,565纳秒,抓取呢,是3,586,169纳秒。还记得第一天的解析吗,3,434,252纳秒。
原来我认为可有可无的执行,竟然占到27%的时间。解析占36%的时间,而原以为最耗时间的抓取,也不过占了37%。
此处的解析,还是相对十分简单的软软解析,如果是普通的软解析,相信会更加耗时。
也就是说,对于一个普通的通过ROWID进行查询的、列数量不多的Select语句来说,基本上解析、执行、抓取各占三分之一左右的时间。
还有一个问题需要注意,逻辑读在抓取中,会占多少比利呢?抓取最主要的工作就是逻辑读,应该很占很大的比利吧。对比这个很简单,逻辑读的函数Oracle在DSI405中已经有说明:kcbgtcr。我在之前的文章中也有提到过这个函数。
一些资料中介绍,在kcbgtrc中,依次会持有CBC Latch、Buffer Pin,然后用memcpy将用户数据拷贝到PGA的SDU(session Data Unit)中。它应该是抓取工作的主体。
下面,在Dtrace跟踪结果中,查找它:
……………………
-> kcbgtcr i= 754 tm= 14293162334210kcbgtcr enter-------------
->sskgslcas i= 755 tm=14293162363376 sskgslcas enter-------------
<-sskgslcas i= 756 tm= 10789sskgslcas return
->ktrexf i= 757 tm=14293162396052 ktrexf enter-------------
<-ktrexf i= 758 tm= 9685ktrexf return
-> kcbzgs i= 759 tm= 14293162424547kcbzgs enter-------------
->kssadf_numa_intl i= 760 tm=14293162441135 kssadf_numa_intl enter-------------
<-kssadf_numa_intl i= 761 tm= 12653kssadf_numa_intl return
<-kcbzgs i= 762 tm= 42180 kcbzgs return
->sskgslcas i= 763 tm=14293162478047 sskgslcas enter-------------
<-sskgslcas i= 764 tm= 8101sskgslcas return
->sskgsldecr i= 765 tm=14293162508151 sskgsldecr enter-------------
<-sskgsldecr i= 766 tm= 9509sskgsldecr return
<- kcbgtcr i= 767 tm= 199457 kcbgtcrreturn
……………………
在它的前后,我们无法找到gethrtime函数的调用,看来Oracle并没有统计它的时间。Kcbgtcr函数的时间很短,只有199457纳秒,合0.199毫秒。还记得前面统计过的抓取的总时间吗,3,586,169纳秒,3.586毫秒。
逻辑读只这么点时间,好像有点少了。
其实kcbgtcr函数只是逻辑读的开始,memcpy拷贝数据到SDU就不是在这个函数中完成的。Kcbgtcr的主要任务是在Buffer上加Buffer Pin,然后对块做一些检查,确认这是我们要读取的块。
在kcbgtcr调用完成后,才会memcpy拷贝数据到SUD中。然后,Oracle将调用kcbrls,释放Buffer Pin,我认为到kcbrls函数调用完成,一次逻辑读才算结束,也就是从加Buffer Pin到释放Buffer Pin完成。
………………
-> kcbipnns i= 798 tm= 14293163341128kcbipnns enter-------------
<- kcbipnns i= 799 tm= 9755 kcbipnnsreturn
-> kcbrls i= 800 tm= 14293163365737kcbrls enter-------------
->sskgslcas i= 801 tm= 14293163388151 sskgslcasenter-------------
<-sskgslcas i= 802 tm=10357 sskgslcas return
->kcbzar i= 803 tm=14293163416846 kcbzar enter-------------
<-kcbzar i= 804 tm=10863 kcbzar return
->sskgsldecr i= 805 tm=14293163447195 sskgsldecr enter-------------
<-sskgsldecr i= 806 tm= 8598sskgsldecr return
->kcbzfs i= 807 tm=14293163474577 kcbzfs enter-------------
->kssrmf_numa_intl i= 808 tm=14293163488408 kssrmf_numa_intl enter-------------
<-kssrmf_numa_intl i= 809 tm= 10197kssrmf_numa_intl return
<-kcbzfs i= 810 tm=34866 kcbzfs return
<- kcbrls i= 811 tm= 152665 kcbrlsreturn
-> kksumc i= 812 tm= 14293163583633kksumc enter-------------
……………………
Oracle仍没有为这整个逻辑读操作统计时间,没关系,我在每个函数的入口点,显示了Dtrace的内置变量timestamp。我们可以取kcbrls调用结束后下面紧挨着的函数kksumc,它的入口点时间是14293163583633,用这个值减去kcbgtcr函数的入口点时间:14293162334210:
SQL> select 14293163583633-14293162334210 fromdual;
14293163583633-14293162334210
-----------------------------
1249423
这应该是整个逻辑读的时间,1,249,423纳秒,1.249毫秒。
实际上一个逻辑读绝不会这么慢,但我打开了太多探针,速度肯定会受影响。但我们计算出的比利仍是有意义的,因为所有操作都受Dtrace影响了,大家都慢了。下面,我们计算出个比利:
SQL> select 1249423/3586169 from dual;
1249423/3586169
---------------
.34840048
抓取中34.84%的时间是纯逻辑读。
难道抓取的主体不应该是逻辑读吗?为什么逻辑读才占了不到一半的时间,这个我也不太清楚了。虽然抓取中其他的函数是干吗的,我也不全清楚,但有一部分代码的作用是可以确定的。
在opifch2函数或opifct函数的底部,它调用了kxsReleaseRuntimeLock,kxsReleaseRuntimeLock调用了kgxDecrementExamine。kgxDecrementExamine的作用我是可以确定的,释放软软解析时加在SQL子游标堆6 DS上的共享Mutex锁,也就是将共享Mutex锁的引用计数减1。kxsReleaseRuntimeLock函数在本例中用时332,566纳秒,0.333毫秒,将近kcbgtcr的两倍。但释放Mutex函数的时间很短,kgxDecrementExamine只用了51,284纳秒,0.051毫秒。当然,这是因为有Dtrace的影响,我们前面没打开哪么多探针时,它最快用了3303纳秒,0.003毫秒。再次强调一下,由于打开的探针的过多,我们在这里计算的时间没有意义,有用的是比利,百分比。
也就是说,抓取中所有操作并不全是和逻辑读相关,至少有些是和Cursor的Mutex相关的。
总结一下,整个SQL所需时间中,三分之一是抓取,抓取中的三分之一多点,是逻辑读。逻辑读所占时间的比利,比我预想的要大大的少。
但这只是ROWID查询,比如对于一个N层高的索引,通过索引Select一行,逻辑读部分的时间必将大大增加。下面,既然我们已经从代码中找到Oracle时间的秘密,哪么建一个这样的索引试一下吧,亲眼看下每个部分各会将多少比利。
我建了一个三层高的索引,表也不再只有两列,表有5列:
SQL> desc vage2
Name Null? Type
---------------- -------- ------------
ID1 NUMBER(38)
ID2 NUMBER(38)
NAME VARCHAR2(30)
ADDR VARCHAR2(40)
HDATE DATE
表共200多万行,索引在ID1列上,非唯一索引,3层。
非唯一索引和唯一索引是有很大区别的,关于这点,可以参看我以前有关CBC Latch的文章。
这一次,我的测试SQL是:
SQL> select * from vage2where id1=123456;
ID1 ID2 NAME
---------- ----------------------------------------
ADDR HDATE
-------------------------------------------------
123456 7859 DBMS_CDC_PUBLISH
DBMS_CDC_PUBLISH 26-NOV-13
这条SQL将执行一个INDEX RANGE SCAN,在一个三层高的索引中查找ID1=123456的行,然后回表将其余列读取出来。
这次,我们的Dtrace脚本又要修改一下:
bash-3.2# cat all_time.d
#!/usr/sbin/dtrace -s -n
char *memnr;
int tm2,dba;
dtrace:::BEGIN
{
i=1;
}
pid$1::gethrtime:entry
{
printf("i= %d tm= %d %senter------------- ",i,timestamp,probefunc);
i=i+1;
tm1[probefunc]=timestamp;
}
pid$1::gethrtime:return
{
printf("i=%dPID::entry:==%s:%s:%s:%s %x %d %d",i, probeprov, probemod, probefunc,probename,arg0,arg1,arg1-tm2);
tm2=arg1;
i=i+1;
}
pid$1:::entry
/probefunc[0]!=0x6d &probefunc[1]!=0x65 & probefunc[2]!=0x6d /
{
printf("i= %d tm= %d %s enter:%x%x",i,timestamp,probefunc,arg0,arg1);
i=i+1;
tm1[probefunc]=timestamp;
}
pid$1:::return
/probefunc[0]!=0x6d &probefunc[1]!=0x65 & probefunc[2]!=0x6d /
{
printf("i= %d tm= %d %sreturn",i,timestamp-tm1[probefunc],probefunc);
i=i+1;
tm1[probefunc]=0;
}
pid$1::kcbgtcr:entry
{
memnr=copyin(arg0+4,4);
dba=*((int *)memnr);
printf("i=%d %s,file=%3d block=%3d",i, probefunc, dba>>22,dba&0x003fffff);
i=i+1;
}
pid$1::memcpy:entry
{
memnr=copyin(arg1,arg2);
printf("%2x%2x%2x%2x%2x%2x%2x%2x%2x%2x%2x%2x%2x%2x%2x%2x%2x%2x%2x%2x",
memnr[0],memnr[1],memnr[2],memnr[3],memnr[4],memnr[5],memnr[6],memnr[7],memnr[8],memnr[9], memnr[10],memnr[11],memnr[12],memnr[13],memnr[14],memnr[15],memnr[16],memnr[17],memnr[18],memnr[19]);
printf("i=%d %x %x%d(END)",i,arg0,arg1,arg2);
i=i+1;
}
dtrace:::END
{
trace(i);
}
说一下哪些地方修改了。首先看这里“#!/usr/sbin/dtrace -s –n”,我把-F选项去掉了。这样将不再有函数进入、退出的缩进。
另外增加kcbgtcr的处理:
pid$1::kcbgtcr:entry
{
memnr=copyin(arg0+4,4);
dba=*((int *)memnr);
printf("i=%d %s,file=%3d block=%3d",i, probefunc, dba>>22,dba&0x003fffff);
i=i+1;
}
这一块的目的是从kcbgtcr中将逻辑读的文件号、块号找出来,这可以方便我们理解跟踪结果。
其他都和以前一样。
好开始测试吧。
先来看看当前的时间模型:
SQL> /
SID STAT_NAME VALUE/1000
------------------------------------ ----------
18 DB CPU 550
18 DB time 1225.528
18 parse time elapsed 688.332
18 sql execute elapsed time 880.829
运行跟踪结果,执行测试SQL。
再看看时间模型中的值:
SQL> /
SID STAT_NAME VALUE/1000
-------------------------------------------------------------------------- ----------
18 DB CPU 580
18 DB time 1253.353
18 parse time elapsed 691.377
18 sql execute elapsed time 890.581
这次的解析时间为:
SQL> select 691.377-688.332from dual;
691.377-688.332
---------------
3.045
总执行时间:
SQL> select 890.581-880.829from dual;
890.581-880.829
---------------
9.752
在Dtrace跟踪结果中,和之前一样,我将opiexe、opifch2和opifch三个函数中的gethrtime函数时间相加:
SQL> select(24284655937512-24284652861203)+(24284665326316-24284661411315)+(24284672183216-24284669422452)from dual;
(24284655937512-24284652861203)+(24284665326316-24284661411315)+(24284672183216-24284669422452)
-----------------------------------------------------------------------------------------------
9752074
两者结果是一致的。其中三部分的时间分别是:
SQL> select(24284655937512-24284652861203) opiexe,(24284665326316-24284661411315) opifch2,(24284672183216-24284669422452)opifch from dual;
OPIEXE OPIFCH2 OPIFCH
---------- --------------------
3076309 3915001 2760764
OPIFCH2时间最长,3.915毫秒,因为主要逻辑读都在这里。
OPIFCH最短,在这个函数中释放共享Mutex。
下面,我们依次找一下根、枝、叶和表块逻辑读的时间,以根块为例,下面这段跟踪结果是根块的逻辑读:
kcbgtcr:entry i= 850 tm=24284662486828 kcbgtcr enter:fffffd7ffc97c890 1
kcbgtcr:entry i=851kcbgtcr,file= 6 block=8323
sskgslcas:entry i= 852 tm=24284662528175 sskgslcas enter:3aad17978 0
sskgslcas:return i= 853 tm= 13525sskgslcas return
ktrexc:entry i= 854 tm=24284662563366 ktrexc enter:fffffd7fffdfaa10 cee4dc0
ktrEvalBlockForCR:entry i= 855 tm=24284662649127 ktrEvalBlockForCR enter:fffffd7ffc97c004 cee4dc0
ktrEvalBlockForCR:return i= 856tm= 16863 ktrEvalBlockForCR return
ktcckv:entry i= 857 tm=24284662684303 ktcckv enter:fffffd7fffdfaa30 fffffd7ffc97c004
ktcckv:return i= 858 tm= 45968ktcckv return
kdifkc:entry i= 859 tm=24284662762116 kdifkc enter:39ee30014 fffffd7ffc97c7e8
kdxbrs1:entry i= 860 tm=24284662779549 kdxbrs1 enter:39ee3004c fffffd7ffc9266f0
lmebucp:entry i= 861 tm=24284662809430 lmebucp enter:fffffd7ffc926731 4
lmebucp:return i= 862 tm= 11353lmebucp return
kdxbrs1:return i= 863 tm= 52315 kdxbrs1 return
kdifkc:return i= 864 tm= 78622kdifkc return
ktrexc:return i= 865 tm= 287230ktrexc return
sskgsldecr:entry i= 866 tm=24284662875949 sskgsldecr enter:3aad17978 1
sskgsldecr:return i= 867 tm= 10708sskgsldecr return
kcbgtcr:return i= 868 tm= 414563kcbgtcr return
索引的根块不需要Buffer Pin,所有读取操作都在kcbgtcr中完成了,这种读被称做:consistent gets – examination。它们的时间很容易找到了,就是kcbgtcr调用结束中的时间。比如根块的时候是414563纳秒。
枝块的读也是consistent gets – examination,我这次的跟踪结果是182251纳秒。
非唯一索引的叶块和表块,都是正常需要Buffer Pin的,它们都是正常的consistent gets。需要计算从kcbgtcr到kcbrls结束的时间。表块的比较方便,直接“kcbrls结束时间-kcbgtcr开始时间”即可。叶块比较麻烦,因为叶块的Buffer Pin要一直加到表块Buffer Pin释放才释放,所以,叶块的逻辑读时间将会包含表块的,我用总逻辑读减去表块逻辑读时间,做为叶块逻辑读时间吧,得到值如下:
表块:1,105,860
叶块:889,615
最终,我把所有数据汇总成这张比例表格:
抓取 |
执行 |
解析+执行 |
|
根块 |
6.20% |
4.25% |
3.24% |
枝块 |
2.73% |
1.87% |
1.42% |
叶块 |
13.33% |
9.12% |
6.95% |
表块 |
16.57% |
11.34% |
8.64% |
所有块总和 |
38.83% |
26.58% |
20.26% |
简单说下这张表格的意义,以表中第一行“根块”行为例,根块逻辑读时间,占抓取时间的6.20%,占执行时间(时间模型中的sql execute elapsed time)的4.25%,占解析+执行时间(也就是SQL的总时间)的3.24%。
最后一行“所有块总和”,是根、枝、叶、表4个块的总计。比如4个块的逻辑读时间总计,占抓取时间的38.83%,占执行时间的26.58%,占执行加解析总时间的20.26%。
通过以上表格,可以看出来,纯逻辑读所占的时间比例其实并不高。
以上的例子,索引是3层,如果是4层、5层甚至更高层的索引呢?上面的表格中已经有总结,枝块逻辑读在SQL总时间中只占1.42%,就算多加几层枝,影响也并不是十分大。
解析+执行 |
|
解析 |
23.79% |
执行 |
76.21% |
纯执行 |
24.04% |
总逻辑读 |
20.26% |
抓取 |
52.17% |
还有,上面的这张表格,总结了SQL执行各个阶段的时间占用比例。解析+执行,是总的SQL执行时间。纯执行是时间模型sql execute elapsed time中纯粹执行SQL的时间,也就opiexe函数的执行时间。总逻辑读是根、枝、叶、表块所有的逻辑读时间。抓取是时间模型sql execute elapsed time中的抓取时间,也就是opifch2函数和opifch函数的执行时间。
从这张表格中看到,解析,本例中还是软软解析,占了总时间的23.79%,而根、枝、叶、表块总的逻辑读,只占20.26%。还不如软软解析占的比例更高。
我想,是时候为这几天的研究下个结论了。但今天已经太晚了。明天,让我们作个总的总结。
周三 晴 风
我的总结有以下几点:
1、软软解析的时间消耗在整个SQL消耗时间中,占的比例比预想的要高。
2、索引枝块的逻辑读成本非常低。
3、Oracle使用gethrtime函数统计时间模型。
让我们将这几点的影响展开说一说。
首先从第一点开始。当你还在想办法减少SQL逻辑读时候,是否想过,如果可以减少软软解析的次数,SQL总时间消耗也会大大下降。
从我们前面的总结中看,软软解析的时间占SQL执行时间的23.79%,降低它的次数,肯定会对减少CPU消耗有很大帮助。
如何减少解析次数呢?Oracle已经给我们准备好了方法,叫做“一次解析,多次执行”。具体,参看我的这篇文章吧。
除解析外,纯执行阶段也占了不少时间,但这一阶段是必不可少的,这一块无法优化。这是第一点。
下面,第二点,索引枝块逻辑读成本问题,枝块是以consistent gets – examination方式逻辑读,虽然都是逻辑读,但比普通的逻辑读少一次CBC Latch,少了获取、释放Buffer Pin的过程,它消耗CPU很少。因此,查询时访问的索引枝块就算多些,也没关系。或者,换句话说,索引的层高就算高些,影响也不大。对我们来说,可以在创建索引的初期,使用较高的PCTFREE,为索引预留较多的空间。这样虽然在初期会增加索引层高,但,没关系,根据我们前面的总结,枝块逻辑读只占很少CPU。如果层高能换来竞争减少,哪么增加层高就是值得的。
最后一点,Oracle中时间的秘密。
有科学家研究,我们这个世界的时间是静止的,所有时间:过去、现在、未来,已经都存在了,时间像一个长条面包,我们一直处在“现在”这个切片上。
现实世界的时间,还是留给哲学家、物理学理去研究吧。Oracle中时间,则没什么神秘的。调用gethrtime函数获得时间。这个函数,前文已经有过它的反汇编:
> gethrtime::dis
libc.so.1`gethrtime: movl $0x3,%eax
libc.so.1`gethrtime+5: int $0xd2
libc.so.1`gethrtime+7: ret
使用int指令,调用d2号BIOS系统调用中的功能,获得当前的纳秒时间。这就是Oracle中时间的秘密。
这一点和我们的前两点结论不同,它没有实际意义。但它对于我们后面的研究,意义重大。
我一开始查看Dtrace跟踪结果时,就像刘姥姥进了大观园。两眼一模黑,什么都看不懂。
慢慢的这一块是干吗的、哪一块是干吗的,越搞越清楚。这就在于一点点突破。现在,我们又发现了gethrtime。而且,经过我的测试,不单是时间模型,等待事件中的时间也是从这里来的。
等待事件有类似如下的代码:
kslwtbctx:entry i= 670 tm=24284659266674 kslwtbctx enter:fffffd7fffdfb200 1
gethrtime:entry i= 671 tm=24284659330022 gethrtime enter-------------
gethrtime:return i=672PID::entry:==pid1994:libc.so.1:gethrtime:return 7 24284659351731 3414219
kskthbwt:entry i= 673 tm=24284659366736 kskthbwt enter:0 a77a7e97
kskthbwt:return i= 674 tm= 6858kskthbwt return
memcpy:entry 55 1 0 0 0 0 00ffffff7f7ffdffff 0716562i=675 3ac7429b8 fffffd7fffdfb268 48(END)
kslwt_start_snapshot:entry i= 676 tm=24284659402639 kslwt_start_snapshot enter:3ac742950 3ac742950
kslwt_start_snapshot:return i=677 tm= 5815 kslwt_start_snapshot return
kslwtbctx:return i= 678 tm= 147689kslwtbctx return
nioqsn:entry i= 679 tm= 24284659446658nioqsn enter:cedda60 0
memcpy:entry 10 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0i=680 d4aa020 fffffd7fffdfb457 1(END)
nioqsn:return i= 681 tm= 18155nioqsn return
kslwtectx:entry i= 682 tm=24284659472437 kslwtectx enter:fffffd7fffdfb200 fffffd7fffdfb457
gethrtime:entry i= 683 tm=24284659482776 gethrtime enter-------------
gethrtime:return i=684PID::entry:==pid1994:libc.so.1:gethrtime:return 7 24284659485827 134096
kslwt_end_snapshot:entry i= 685 tm=24284659498331 kslwt_end_snapshot enter:3ac742950 3ac742950
kslwt_end_snapshot:return i= 686 tm= 4906kslwt_end_snapshot return
kskthewt:entry i= 687 tm=24284659526087 kskthewt enter:a77a7f1d 0
kskthewt:return i= 688 tm= 4885kskthewt return
kslwtectx:return i= 689 tm= 62748kslwtectx return
等待事件是第一个memcpy中内容,“55 1”,大小端反转是0x0155,十进制是341,它是v$event_name中的event#,如下可以获得Oracle在这段代码中在等待什么:
SQL> select name fromv$event_name where event#=341;
NAME
----------------------------------------------------------------
SQL*Net message to client
而等待的时间呢?很简单,用这段代码下面的gethrtime,减去上面的gethrtime,就是此处SQL*Net message to client的等待时间。
也就是说,我们可以从跟踪结果中获得,Oracle从哪里开始,等待什么时间,一直等到哪里结束。Log file sync、buffer busy waits、……各种各样等待事件的意义,将很容易被我们发掘出来。
有了这些东西,还有一点发现,但这次已经写了很多,留到下次吧。我上面帖出来的哪段代码,主要是登记等待事件SQL*Net message to client。我发现它的等待时间,并不记入时间模型中的DB Time。这一点,留作一个尾巴,到续集中继续研究吧。
好了,今天的内容并不多。但生活并不总有学习、工作,还有,陪老婆逛街。这次的内容就到这里吧。希望对大家有帮助。