ITPUB论坛-中国最专业的IT技术社区

 找回密码
 注册
查看: 24228|回复: 88

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

[复制链接]
论坛徽章:
70
夏利
日期:2013-09-29 21:02:15天蝎座
日期:2015-12-11 09:52:33马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11兰博基尼
日期:2014-01-02 19:02:10保时捷
日期:2013-12-28 20:27:38
发表于 2012-5-25 10:10 | 显示全部楼层 |阅读模式
本帖最后由 vage 于 2012-5-25 10:23 编辑

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

(前两章地址
揭密Oracle之 七种武器  第一章 搭建测试环境(目前已到第三章)
http://www.itpub.net/thread-1605241-1-1.html

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


        从9iR2开始,Cache Buffers Chain(以下简称CBC)Latch就变成共享Latch了。从那时开始,我想当然的认为,如果我只有读操作,互相之间就不会阻
塞了。于是马上测试:
declare
  myid number;
begin
  for i in 1..10000000 loop
    select id1 into myid from a2_70m where rowid='AAACYJAAEAAAAAUAAA';
  end loop;
end;
/
        这段过程很简单,就是反复的逻辑读某一行。将这段过程在两个会话同时执行,我天真的认为,不会再看到CBC Latch等待。但是,查看等待事件的结果
,令我深深的迷惑。为什么还是有等待呢?无论CBC 链还是数据块,我都没有修改,只是反复读取,为什么共享Latch不共享呢?从此,这个迷团一直困绕着我。
        当然,还有其他一些谜团,比如唯一索引和非唯一索引在读扫描时的区别。最普通的区别,是它们两个逻辑读不一样,唯一索引比非唯一少一个逻辑读
。但其实,它们两个的区别非常大。具体的区别在哪里?这些区别对于我们的选择,会有什么影响?
        这些谜团很长一段时间内没有答案。
        五、六年转眼即过,2011年初,因公司技术转型,我被迫从头学起GreenPlum。翻开几百页的英文文档,我不禁倦意袭来。再看会Oracle的资料,又不禁
精神百倍。于是,我退意蒙生。但是,这段经历,让我有一个意外的收获。阿里的GreenPlum,都是跑在Solaris下。接手GreenPlum运维,必先学会Solaris。在
学习Solaris时,看到有一本书用两页纸介绍了一个工具:DTrace语言,说是可以跟踪Solaris中的任何操作。当时我对Oracle的研究,也陷入了困境。能用的跟
踪事件都用了,很多原理还是无法搞清楚,只能跟着别人,人云亦云一下,自我感觉对Oracle了解甚为深入,已经没什么可以再学的了。但分析一些工作中奇怪
的问题,就总感觉似是,而非。
        这种感觉让我想起来多年前,年青的时候我酷爱神秘文化。什么东西都信,曾在二月底初春时节跳入溥冰覆盖的河水中受洗,随身携带一本荒漠甘泉。
在被女神无情抛弃后,独自站在空旷的教堂祈祷:“仁慈的圣父啊,我知道这是您对我的庇护和煅炼,虽然您的孩子此刻心如刀绞,但我仍然感谢您、爱您。哈
利路亚,阿门。”不久之后,下一位女神出现,却是信佛的。于是我又到家乡著名的大相国寺,皈依佛祖,每逢初一、十五,烧香诵经:“南无西方琉璃药师佛
,南无……”。
        在诸多杂学之中,我最精通的却还是周易。刚刚参加工作哪会,我为我们科室6个人占卦,算他们哪一年结婚、哪一年有小孩,6个人,只有一个算错了
。83%的准确率,很高了。但是,为什么有一次算错了呢?为什么其他的可以算对呢?这些问题我都答不上来,我对周易的理解,始终似是,而非。
        易经这东西,真正的神人传下来的,几千年中,看懂的没几个。但是Oracle呢,我也无法真正的看“懂”它吗。对易经的理解似是而非,这我服气,但
对Oracle,我不想停留在似是而非的境界。        
        当看到这个DTrace后,我眼前顿时一亮,如果用DTrace跟踪Oracle,又会有怎样的效果呢?是否可以打破“似是而非”的僵局呢?于是我马上搜集资料
开始学习,这一下,没想到豁然为我打开一扇大门。于是我再也顾不得什么GreenPlum、什么KPI了。
        好了,言归正传,这一节,从一个重要的提供器开始,PID进程提供器。Solaris在进程调用、退出每个函数时,都设置了Prob,进程提供器的作用就是
打开这些Prob。
        我们可以写如下的脚本,打开PID提供器所有调用函数时的探针:
pid1234:::entry
{
     动作;
}
        这个脚本的作用是打开1234进程所有函数调用处的探针。简单点说,1234进程每调一次函数,都会被触发。这个脚本还可以进一步改成这样:
pid$1:::entry
{
     动作;
}
        用$1代替了1234。$1,这种写法是来自于Shell脚本编程,第一个参数。当然,我们也可以pid$2。
        接下来,我们可以定义什么动作呢?当然还是观察了.
        在我上传的《Solaris 动态跟踪指南》书中,P68页,列出了全部的内置变量,这次,我们使用这几个内置变量:probeprov, probemod, probefunc,
probename,arg0和arg1……
        probeprov:提供器名
        probemod : 模块名
        probefunc:函数名,这是我们要查看的重点。
        probename:探针名,只有两个。entry,return,一个进入、一个是退出。
        arg0,arg1,…… :调用函数时,传递给函数的参数。        
        这些内置变量,无需定义,可以直接使用。内置变量中保存了很多重要的值,在上篇文章已经有用到过。
        好,我们的最终脚本程序,是这个样子:
        这个探针的使用很简单,我们总的脚本如下:
#!/usr/sbin/dtrace -s -n
dtrace:::BEGIN
{
        i=1;
}
pid$1:::entry
{
        printf("i=%d PID::entry:==%s:%s:%s:%s %x %x %x %x %x %x",i, probeprov, probemod, probefunc, probename,arg0,arg1,arg2,arg3,arg4,arg5);
        i=i+1;
}
        参数这块,我们也不知道每个函数都有几个参数,好在多输出参数DTrace并不会报错,所以,我们就多显示几个参数,我显示了前6个:
arg0,arg1,arg2,arg3,arg4,arg5。都以%x,16进程格式显示。
        将此脚本保存为all_func.d,授于执行权限,开始执行。
        对了,别忘了,本章的目的,是观察CBC Latch。更进一步的,观察逻辑读的CBC Latch。
        打开一个会话,查询出它对应的进程号:
SQL> select c.sid,spid,pid,a.SERIAL# from (select sid from v$mystat where rownum<=1) c,v$session a,v$process b where c.sid=a.sid and
a.paddr=b.addr;
       SID SPID                PID    SERIAL#
---------- ------------ ---------- ----------
       863 970                  22          1
        我的进程号是970。另外,在开始观察前,执行几次如下语句,让读是逻辑读:
select * from a2_70m where rowid='AAACYJAAEAAAAAUAAA';
        如下运行脚本,观察970号进程:
# ./all_func.d 970 > logic_read1.log
dtrace: script './all_func.d' matched 124179 probes
        根据显示结果,共有124179个探针被打开。十几万个探针,说明Oracle内部,有十几万个函数。C语言中,程序代码的复用,全靠函数了。C又被称为函
数语言吗。不过,Oracle内部竞然有十几万个函数,还是出乎我的意料。不过,函数分的越细,对我们调试、跟踪越好。在没有源代码的情况下,我们只能跟踪
到函数级别了。
        
        跟踪结果会很多,为了便于观察,我将结果重定向到logic_read1.log文件中。
        另外,由于会打开太多探针,有可能会超出DTrace的限制,报出错误,可以修改/kernel/drv/fasttrap.conf中fastrap-max-probes设置,在我的测试环
境中,我设置为fastrap-max-probes=1000000。
        另外,如果在970进程执行期间,all_func.d脚本报内存不足,可以在脚本开头加上去内存大小或刷新频率的设置:
#!/usr/sbin/dtrace -s -n -x switchrate=10hz -b 16m
        -x switchrate=10hz,设置刷新频率。DTrace会结果发送到输出终端,这个值可以理解为发送频率。在数据没有发送到输出终端前,DTrace会先保存到
自己的缓存中。因此,增加刷新频率,可以减少内存使用。
        -b 16m , 修改缓存大小。
        好了,来看结果吧,在970进程对应的会话中,再执行一次:
select * from a2_70m where rowid='AAACYJAAEAAAAAUAAA';
        回到执行DTrace命令的窗口,按Ctrl+C。然后查看结果,先看一下有多少行输出吧:
# cat logic_read1.log|wc -l
    1211
        1211行,这是运行一次软软解析,再加上对一个块逻辑读取出一行,Oracle所要调用的函数次数。这也是我们最细粒度的跟踪级别了。比10046等任何一
个Event,都要细致的多。除非你去看源码,否则,不可能比这个更细、更深入了。
        下面,让我们来看看结果都是什么吧:
# cat logic_read1.log|more
CPU     ID                    FUNCTION:NAME
  3 172611                     memcpy:entry i=1 PID::entry:==pid970:libc.so.1:memcpy:entry 8047708 c0f2c28 1 c028934 c02a6dc 6
  3  52316              kslwte_resmgr:entry i=2 PID::entry:==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:==pid970:oracle:sskgslcas:entry 8ea1d750 0 20000016 1 fdc3f17c 81e1c064
  3  57740                     kcbzar:entry i=557 PID::entry:==pid970:oracle:kcbzar:entry 8ef9a5b4 8ea1d750 108000 8045368 1 fdc3f17c
  3 101760                   slmxnoop:entry i=558 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
  3 101760                   slmxnoop:entry i=559 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
  3 101760                   slmxnoop:entry i=560 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
  3 101760                   slmxnoop:entry i=561 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
  3 101760                   slmxnoop:entry i=562 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
  3 101760                   slmxnoop:entry i=564 PID::entry:==pid970:oracle:slmxnoop:entry 81ff1de4 fdc3f1ec 8ea1d750 8045338 a9bdd25 c030d18
  3 111578                 sskgsldecr:entry i=566 PID::entry:==pid970:oracle:sskgsldecr:entry 8ea1d750 20000016 1 fdc3f17c 81e1c064 8045510
  3  52568                     kssrmf:entry i=568 PID::entry:==pid970:oracle:kssrmf:entry 8ef9a590 8e94811c 81ff1de4 20000016 8ea1d750 8ef9a5b4
        和这个地址相关的有这十几行。在这里,有一点编程习惯再说一下,要申请某一个地址处的Latch,这个Latch的地址,是这个函数的最重要的参数,因
此,Oracle会把它排在第一位,也就是说,以上这十几行中,第一个参数不是8ea1d750的,基本可以排队掉了。
        所以,我们只剩这些行需要关注:
# cat logic_read1.log|grep "entry 8ea1d750"
  3 111575                  sskgslcas:entry i=517 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 20000016 fdc3f1e4 fdc3f18c fdc3f1e4
  3 111578                 sskgsldecr:entry i=526 PID::entry:==pid970:oracle:sskgsldecr:entry 8ea1d750 20000016 fdc3f1e4 fdc3f18c fdc3f1e4 804544c
  3 111575                  sskgslcas:entry i=552 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 20000016 1 fdc3f17c 81e1c064
  3 111578                 sskgsldecr:entry i=566 PID::entry:==pid970:oracle:sskgsldecr:entry 8ea1d750 20000016 1 fdc3f17c 81e1c064 8045510
        这四行,两个函数调用,sskgslcas、sskgsldecr,第一个参数都是Latch的地址:8ea1d750。我相信这不是巧合,它们肯定是申请、释放Latch的函数。
        i=517这行,Oracle调用sskgslcas持有Latch,在i=526这行,调用sskgsldecr释放,接下来在i=552又一次调用sskgslcas持有Latch,在i=566处调用
sskgsldecr释放。一次逻辑读对应两次Latch调用。
        结果是这样吗,让我们继续验证,Oracle的Oradebug可以调用某个Oracle自身的函数,就有它来验证吧:
SQL> oradebug setmypid
Statement processed.
SQL> oradebug call sskgslcas 0x8ea1d750 0 0x20000016 0xfdc3f1e4
Function returned 1
SQL>
        sskgslcas参数的取值,就是我们上面的跟踪结果。我只用了4个参数,其实应该只有3个参数。但是,用Oradebug时,多传了参数也无所谓。
        Function returned 1,这一行说明我们的调用是成功的。
        回到970进程对应的会话,再次执行如下语句:
SQL> select * from a2_70m where rowid='AAACYJAAEAAAAAUAAA';
        被Hang住了,在另一个会话中查看等待事件(970号进程对应的会话ID是863):
SQL> select sid,event,p1raw,p2 from v$session where sid=863;
       SID EVENT                                                            P1RAW            P2
---------- ---------------------------------------------------------------- -------- ----------
       863 latch: cache buffers chains                                      8EA1D750        122
        863果然在等待CBC Latch,而且根据P1RAW列的值,所等的Latch就是8EA1D750。接着,sskgsldecr是释放Latch,继续验证此点,在刚才Oradebug的会话
中继续执行:
SQL> oradebug call sskgsldecr 0x8ea1d750 0x20000016
Function returned 20000016
        同样,sskgsldecr 0x8ea1d750 0x20000016,这个函数的参数来自于我们的跟踪文件。我们这样手动调用结束,刚才被Hang的会话,已经可以顺利执行
下去了。说明Latch已经被释放。
        看,我们很轻松就已经找到了Oracle申请、释放CBC Latch的函数。一切都是如此简单。
        到这里,可能有人会有不同意见了。如果你看过其他一些牛人的书,包括Oracle的DSI405,都说到Latch的调用、释放,是用kslgetl(独占)、
kslgetsl(共享)和kslfre,怎么我又说申请、释放Latch是另外的函数呢。
        这很容易理解,DSI405是讲9i的。其他牛人说的也没错,kslgetl(独占)、kslgetsl(共享)和kslfre的确也是Latch相关的函数。物理读一个块时,
Oracle也会用这三个函数来加、释放CBC Latch,但逻辑读不是。
        这很容易理解,逻辑读是最繁忙的操作,Oracle专门为它开个小灶、做做优化不是很正常吗。而且,提前说一下,Mutex也是用sskgslcas申请的(释放
不是用sskgsldecr),关于Mutex内幕,我们到后几章再详细说,顺便说一句,要想揭开Mutex内幕,也只有D&G(DTrace+GDB)了。
        我们还要再接着研究。CBC Latch的地址是8ea1d750,在这个地址处,Oracle都放了什么呢。有两种方式可以观察这个,用Oradebug,或者,改写我们的
DTrace脚本。我用后一种方式吧,这种方式早晚要熟练掌握的,而且并不是每个要观察的值,都可以用Oradebug。
        使用DTrace,如果参数是地址的话,将地址的址读出来,这种方法在上一章中已经有描述了,如下修改脚本程序:
#!/usr/sbin/dtrace -s -n
char *memnr;
int latchaddr;
dtrace:::BEGIN
{
        i=1;
        latchaddr=0;
}
pid$1::sskgslcas:entry
{
        memnr=copyin(arg0,12);
        latchaddr=arg0;
        printf("[%2x%2x%2x%2x|%2x%2x%2x%2x|%2x%2x%2x%2x]",memnr[3],memnr[2],memnr[1],memnr[0],memnr[7],memnr[6],memnr[5],memnr[4],memnr
[11],memnr[10],memnr[9],memnr[8]);
        printf("i=%d PID::entry:==%s:%s:%s:%s %x %x %x %x %x %x",i, probeprov, probemod, probefunc, probename,arg0,arg1,arg2,arg3,arg4,arg5);
        i=i+1;
}
pid$1::sskgslcas:return
{
        memnr=copyin(latchaddr,12);
        printf("[%2x%2x%2x%2x|%2x%2x%2x%2x|%2x%2x%2x%2x]",memnr[3],memnr[2],memnr[1],memnr[0],memnr[7],memnr[6],memnr[5],memnr[4],memnr
[11],memnr[10],memnr[9],memnr[8]);
        printf("i=%d PID::entry:==%s:%s:%s:%s %x %x %x",i, probeprov, probemod, probefunc, probename,latchaddr,arg0,arg1);
        i=i+1;
}
        在这个脚本中,我只观察CBC的申请和释放。copyin函数的使用,上一章有,不再重述。需要注意的时,我在pid$1::sskgslcas:entry中,执行了这样一
行:latchaddr=arg0;目的是将Latch的地址保存到全局变量latchaddr中。然后,在sskgslcas申请Latch后,再观察一下此地址中的值。
        看一下观察结果吧:
# cat logic_read2.log|grep "8ea1d750"
  0 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 291| 0 0 07a]i=3 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 20000016 fdc1a2dc fdc1a284 fdc1a2dc
  0 175725                 sskgslcas:return [20 0 016| 0 0 291| 0 0 07a]i=4 PID::entry:==pid970:oracle:sskgslcas:return 8ea1d750 16 1
  0 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 292| 0 0 07a]i=5 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 20000016 1
fdc1a274 81e1c064
  0 175725                 sskgslcas:return [20 0 016| 0 0 292| 0 0 07a]i=6 PID::entry:==pid970:oracle:sskgslcas:return 8ea1d750 16 1
        我显示了latch地址处的12个字节,我将结果整理一下:
进入sskgslcas函数时:[ 0 0 0 0| 0 0 291| 0 0 07a]
从sskgslcas返回时  :[20 0 016| 0 0 291| 0 0 07a]
进入sskgslcas函数时:[ 0 0 0 0| 0 0 292| 0 0 07a]
从sskgslcas返回时  :[20 0 016| 0 0 292| 0 0 07a]
        我一共显示了12个字节。后4个节字,7A,10进制是122。这个是Latch编号。中间4个字节,291、292,明显是我访问的次数。这些可以从v
$latch_children视图中得到。后4个字节是LATCH#列,中间4个字节,就是GETS列了。
        最前面4个字节,20000016,正好是sskgslcas的第三个参数。我觉得这个应该是模式。
        看来,sskgslcas的作用,应该就是将第三个参数的值“20000016”交换到Latch 地址所指向的内存中。然后访问次数加1。
        接下来,该如何确定20000016是否是模式呢?这个,从这里就看不出来了,我们要找个索引试试。
        在我的测试表a2_70m,ID1列上有个索引,索引名是A2_70M_ID1。我使用如下测试语句:
SQL> select * from a2_70m where id1=1;
       ID1        ID2 CC1
---------- ---------- ------------------------------
         1         10 A-----------------------------
        以上语句,多执行个几次,在另一个会话中,查看索引的块和Latch地址:
SQL> set pagesize 50000
SQL> set linesize 10000
SQL> select file#,dbablk,tch,ba,HLADDR from x$bh a,dba_objects b where a.obj=b.data_object_id and object_name='A2_70M_ID1' order by
FILE#,DBABLK;
     FILE#     DBABLK        TCH BA       HLADDR
---------- ---------- ---------- -------- --------
         5      23449          0 8189E000 8E98DAD4
         5      23450          0 81A74000 8EAF0390
         5      23451          0 8189C000 8EA150C8
         5      23452          3 81A78000 8EB77E00
         5      23453          3 81A76000 8EA9CB38
         5      23454          0 81A72000 8E9C13F4
         5      23455          0 8189A000 8EB2412C
         5      23456          0 81A70000 8EA48E64
         6       5695          3 818A0000 8EACBC98
        多执行几次测试语句,找出TCH值不断在增加的,这些块就是索引扫描时相关的块了。我这里是5号文件23452、23453块,和6号文件5695块。索引的root
块,都是段头的下一个块,我们可以如下确认一下:
SQL> select segment_name,header_file,header_block from dba_segments where segment_name=upper('A2_70M_ID1');
SEGMENT_NAME                   HEADER_FILE HEADER_BLOCK
------------------------------ ----------- ------------
A2_70M_ID1                               5        23451
        段头是23451块,哪么23452就是root块了。提一个注意事项,索引扫描在10.2.0.2后是不用读段头的,真接Root、枝、叶。但在10.2.0.1,有时还是需
要读段头的。
        好,用我们刚才的脚本,开始观察吧。
先执行脚本:
# ./all_func.d 970 > logic_read3.log
dtrace: script './all_func.d' matched 3 probes
        再执行测试SQL,显示logic_read3.log内容,观察结果,先看根块吧:
# cat logic_read3.log|grep 8eb77e00
  1 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 721| 0 0 07a]i=3 PID::entry:==pid970:oracle:sskgslcas:entry 8eb77e00 0 1 fdc1a3bc fdc1a3b4 fdc1a278
  1 175725                 sskgslcas:return [ 0 0 0 1| 0 0 721| 0 0 07a]i=4 PID::entry:==pid970:oracle:sskgslcas:return 8eb77e00 16 1
        根块Latch的地址是8eb77e00,先只看一下根块。注意第三个参数,不是20000016,而是1。我们自己调一下试试:
SQL> oradebug call sskgslcas 0x8eb77e00 0 1
Function returned 1
(释放是:
SQL> oradebug call sskgsldecr 0x8eb77e00 1
Function returned 1

        再到另一个会话执行测试SQL,不会被阻塞。看来这才是共享模式啊。再往下看跟踪文件,8eacbc98是root块后接着申请的一个Latch,它对应6号文件
5695号块。看来它是枝块了。
# cat logic_read3.log|grep 8eacbc98
  2 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 784| 0 0 07a]i=5 PID::entry:==pid970:oracle:sskgslcas:entry 8eacbc98 0 1 fdc3f2c4 fdc3f2bc fdc3f180
  2 175725                 sskgslcas:return [ 0 0 0 1| 0 0 784| 0 0 07a]i=6 PID::entry:==pid970:oracle:sskgslcas:return 8eacbc98 16 1
        枝块获得CBC Latch,也是共享的。
        那么5号文件23453块,它应该是叶块了,查看它的获取Latch情况:
# cat logic_read3.log|grep 8ea9cb38
  2 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 783| 0 0 07a]i=7 PID::entry:==pid970:oracle:sskgslcas:entry 8ea9cb38 0 1 fdc3f2c4 fdc3f2bc fdc3f180
  2 175725                 sskgslcas:return [ 0 0 0 1| 0 0 783| 0 0 07a]i=8 PID::entry:==pid970:oracle:sskgslcas:return 8ea9cb38 16 1
  2 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 784| 0 0 07a]i=13 PID::entry:==pid970:oracle:sskgslcas:entry 8ea9cb38 0 20000016 ffffffff fdc3f2c4 fdc3f17c
  2 175725                 sskgslcas:return [20 0 016| 0 0 784| 0 0 07a]i=14 PID::entry:==pid970:oracle:sskgslcas:return 8ea9cb38 16 1
  2 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 785| 0 0 07a]i=15 PID::entry:==pid970:oracle:sskgslcas:entry 8ea9cb38 0 20000016 c030e14 fdc3f180 fdc3f2bc
  2 175725                 sskgslcas:return [20 0 016| 0 0 785| 0 0 07a]i=16 PID::entry:==pid970:oracle:sskgslcas:return 8ea9cb38 16 1
  2 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 786| 0 0 07a]i=17 PID::entry:==pid970:oracle:sskgslcas:entry 8ea9cb38 0 20000016 0 fdc3f2c4 fdc3f2b8
  2 175725                 sskgslcas:return [20 0 016| 0 0 786| 0 0 07a]i=18 PID::entry:==pid970:oracle:sskgslcas:return 8ea9cb38 16 1
        它一共获取了4次,第一次是共享的,后面三次,是独占的。最后还可以再看一下表块,表块要获得两次,都是独占的。这样看来,索引叶块的CBC
Latch的争用,要比表块多啊。建议索引的PCTFREE可以调的比表高些,既能减少中间块分裂的总次数。块中行更少,又能分散争用。
        但这样做会使索引树层数升高,增加索引访问时的逻辑读。对于解决索引块上的CBC Latch争用,这样做还是非常值得的。因为同样是逻辑读,消耗的资
源可是不以同日而语的。索引枝块只需要一次CBC Latch,而且是共享的,并且,不需要把数据拷贝到PGA中,只在Buffer Cache中比较一下Key值,取出下一层块
的位置。这种逻辑读,不会造成争用,因为从头到尾,所有资源都是共享的,所耗资源比表块逻辑读也少的多。而且大的PCTFree,还可以减少索引块分裂次数。
因此,使用这种方式,减少索引叶块的CBC Latch争用,是可行的。
        好,经过上面的测试,本章开头提到第一个问题,已经有了答案。为什么共享的CBC Latch会有争用,答案是因为Oracle以独占的方式持有了它。
        
        在文章开头,我还提到过一个问题,就是唯一索引和非唯一索引读扫描时的区别,刚才我的测试索引,不是非唯一的,我把它重建为唯一索引试试,我
们可以比较下,区别还是非常大的:
SQL> drop index a2_70m_id1;
Index dropped.
SQL> CREATE unique INDEX a2_70m_id1 on a2_70m(id1);
Index created.
        我们的测试语句和刚才相同,只不过这次它的访问路径是唯一索引扫描。
        唯一索引的测试结果,和非唯一有很大不同:
# cat logic_read3.log
CPU     ID                    FUNCTION:NAME
  1 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 01d| 0 0 0 0]i=1 PID::entry:==pid970:oracle:sskgslcas:entry 87d88194 0 35f0001 8886a9c8 87d88194 888f7c48
  1 175725                 sskgslcas:return [ 35f 0 1| 0 0 01d| 0 0 0 0]i=2 PID::entry:==pid970:oracle:sskgslcas:return 87d88194 16 1
  1 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 c67| 0 0 07a]i=3 PID::entry:==pid970:oracle:sskgslcas:entry 8eb77e00 0 1 804520c 8045204 fda522f8
  1 175725                 sskgslcas:return [ 0 0 0 1| 0 0 c67| 0 0 07a]i=4 PID::entry:==pid970:oracle:sskgslcas:return 8eb77e00 16 1
  1 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 bc3| 0 0 07a]i=5 PID::entry:==pid970:oracle:sskgslcas:entry 8eafa97c 0 1 804520c 8045204 fda522f8
  1 175725                 sskgslcas:return [ 0 0 0 1| 0 0 bc3| 0 0 07a]i=6 PID::entry:==pid970:oracle:sskgslcas:return 8eafa97c 16 1
  1 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 c38| 0 0 07a]i=7 PID::entry:==pid970:oracle:sskgslcas:entry 8ea9cb38 0 1 804520c 8045204 fda522f8
  1 175725                 sskgslcas:return [ 0 0 0 1| 0 0 c38| 0 0 07a]i=8 PID::entry:==pid970:oracle:sskgslcas:return 8ea9cb38 16 1
  1 111575                  sskgslcas:entry [ 0 0 0 0| 0 0 bdc| 0 0 07a]i=9 PID::entry:==pid970:oracle:sskgslcas:entry 8ea1d750 0 1 fda52660 fda52658 fda52600
  1 175725                 sskgslcas:return [ 0 0 0 1| 0 0 bdc| 0 0 07a]i=10 PID::entry:==pid970:oracle:sskgslcas:return 8ea1d750 16 1
  1 111575                  sskgslcas:entry [ 0 0 0 1| 0 0 01e| 0 0 0 0]i=11 PID::entry:==pid970:oracle:sskgslcas:entry 87d88194 1 35f0000 c030d18 87d88194 888f7c48
  1 175725                 sskgslcas:return [ 35f 0 0| 0 0 01e| 0 0 0 0]i=12 PID::entry:==pid970:oracle:sskgslcas:return 87d88194 16 1
        索引还是占了同样的数据块,所以对应的Latch不变。可以看到,从根块到叶块,再到数据块,竞然都不是独占的,全是共享的,而且都只需要申请一次
。可以用个匿名块验证一下:
declare
  myid number;
begin
  for i in 1..10000000 loop
    select id1 into myid from a2_70m where id1=1;
  end loop;
end;
/
        和最开头的存储过程不同的是,select id1 into myid from a2_70m where id1=1 ,这条语句不再直接用ROWID访问,换成唯一索引。在两个会话中分
别执行此段过程,最终查看了一下:
SQL> select event from v$session_event where sid=862;
EVENT
---------------------------------------------
db file sequential read
cursor: pin S wait on X
SQL*Net message to client
SQL*Net message from client
SQL*Net break/reset to client
events in waitclass Other
6 rows selected.
        果然没有CBC Latch的竞争。看到没,区别可是非常之大啊。如果不用DTrace分析,恐怕很难准确的发现这点。看来INDEX UNIQUE SCAN和INDEX RANGE
SCAN,不同的访问路径,Oracle实现起来的方法大相庭径啊。而且,由不由的访问路径起始,上层的操作也会不一样。
        比如同样是TABLE ACCESS BY INDEX ROWID,下层是INDEX UNIQUE SCAN的话,表块将只有共享Latch。下层是INDEX RANGE SCAN的话,表块上将有独占
Latch。
        比较一下唯一索引和非唯一索引的区别:
         唯一         非唯一
------  --------  ----------------
  根    1次共享   1次共享
  枝    1次共享   1次共享
  叶    1次共享   1次共享 3次独占
表块   1次共享           2次独占
        非唯一索引共需8次CBC Latch,其中5次是独占。看来,在读远高于写的环境,想解决CBC Latch竞争问题吗,那就如果可能的话,使用唯一索引吧。
        (当然,出现CBC Latch争用,一般都是SQL惹的祸,调SQL即可。这个结论,是说如何从宏观上减少CBC Latch争用)
        顺便测一下DML,唯一索引时,即使修改索引列,索引的访问不变,都是共享Latch。但表块是独占Latch。其他UNDO块、DUNO段头了等等Latch的持有访
问,我就不再演示了,有兴趣自己测吧。
        其实还有一个问题,就是为什么用Rowid访问一个表块,或者非唯一索引的叶块、表块,Oracle不会以共享的方式获得Latch呢?要解答这个问题,先要
搞清楚一点,为什么用ROWID的形式,访问表块的时候,要申请2次CBC Latch。而根块、枝块只要一次,唯一索引以INDEX UNIQUE SCAN形式访问,所有块都只需
要一次共享的CBC Latch。
        这个问题又可以写一篇很长的文章分析了。不知道放在这里是否合适,因为这篇文章已经有点长了。但我觉得,如果你掌握了今天我们所用的方法,继
续这样的分析难度不大。我先简单描述一下,后面再另起一章详细解剖。可以使用我们第一个脚本:
#!/usr/sbin/dtrace -s -n
dtrace:::BEGIN
{
        i=1;
}
pid$1:::entry
{
        printf("i=%d PID::entry:==%s:%s:%s:%s %x %x %x %x %x %x",i, probeprov, probemod, probefunc, probename,arg0,arg1,arg2,arg3,arg4,arg5);
        i=i+1;
}
        拦截所有操作,你可以发现通过ROWID访问,形式如下:
1、调用sskgslcas获得Latch
2、进行一些未知操作
3、调用sskgsldecr释放Latch
4、未知操作
5、memcpy拷贝内存,从SGA向PGA
6、未知操作
7、调用sskgslcas获得Latch
8、进行一些未知操作
9、调用sskgsldecr释放Latch
        第5步拷贝内存,其实就是真正的逻辑读过程,把数据从SGA中的Buffer Cache,拷贝到PGA,我跟踪出的Memcpy函数形式如下:
  2 172791                     memcpy:entry i=663 PID::entry:==pid972:libc.so.1:memcpy:entry fdad1b10 82c61fde 1e fdad2f94 886f2bf8 8045478
        
        第二个参数82c61fde , 是Buffer Cache中行的位置,我们可以如下确定:
SQL> select file#,dbablk,tch,lower(HLADDR),ba from x$bh where file#=4 and dbablk=20;
     FILE#     DBABLK        TCH LOWER(HL BA
---------- ---------- ---------- -------- --------
         4         20          7 8ea1d750 82C60000
        BA列,82C60000开始的8K,也就是从82C60000到82C62000,都是4号文件20号块的Buffer。memcpy第二个参数82c61fde,正是在这个范围之间。证明是从
4号文件20号块中拷贝数据。第一个参数地址fdad1b10,它不在任何内存池地址空间范围之内,它是进程自身的内存,可以认为是PGA。第三个参数1e,十进制是
30,是拷贝数据的长度。查看表的定义:
SQL> desc a2_70m;
Name                                      Null?    Type
----------------------------------------- -------- ----------------------------
ID1                                                NUMBER(38)
ID2                                                NUMBER(38)
CC1                                                VARCHAR2(30)
        拷贝30个字节,其实就是将CC1列的数据读到PGA中。
        另外,还有一点,先说明一下,到下一章再详细讲。上面步骤1至3中间的未知操作,和7至9中的未知操作,其实是加Buffer Pin和释放Buffer Pin。其
实,上面那9个步骤,我们可以简化一下:
        
1、调用sskgslcas获得独占Latch
2、加Buffer Pin
3、调用sskgsldecr释放Latch
5、memcpy拷贝内存,从SGA向PGA
7、调用sskgslcas获得独占Latch
8、释放Buffer Pin
9、调用sskgsldecr释放Latch
        但在唯一索引访问时,形式是这样的:
1、调用sskgslcas获得共享Latch
2、memcpy拷贝内存,从SGA向PGA
3、调用sskgsldecr释放Latch
        和ROWID访问的不同之处,没有了Buffer Pin。一个CBC Latch,从逻辑读开始到逻辑读结尾。
        为什么索引Root块、枝块的访问,只需要一次共享CBC Latch,叶块、表块需要多次独占。这个问题,现在可以回答了。Oracle认为根块、枝块不会经常
修改,因为,用一个共享CBC Latch,保护逻辑读所有操作。虽然Latch持有时间长,但由于是共享的,不会有争用。而对于叶块和表块,Oracle认为有可能会频
繁修改,所以,用独占Latch保护,获得Buffer Pin,在Pin的保护下,读取、修改Buffer数据。
        而至于唯一索引,INDEX UNIQUE SCAN的访问路径,Oracle单独做了处理,也依照根块、枝块的方式访问。这说明如果是唯一索引,对表有大量读写混合
的操作,那么CBC Latch竞争会激烈些,因为没有了Buffer Pin,读持有CBC Latch的时间会较长。但对于读远远多于写的环境,由于读都是共享Latch,反而可以
大大减少CBC Latch的争用。
        好了,先到这里吧。已经有点长了。
        本章内容,难度稍高,有兴趣的兄弟还是要好好测测。这章内容是后面的基础,如果这一章没问题,那后理解Mutex等等内容就方便了。
        由于本章长度有限,有些问题,比如Buffer Pin的问题。我们交到以后解决,这里先提出来,有兴趣可以自己动手分析、测试下。
        好,今天就到这里为止了,后续更精彩,敬请期待。


论坛徽章:
13
生肖徽章2007版:兔
日期:2009-03-24 09:50:192014年新春福章
日期:2014-02-18 16:43:09大众
日期:2013-09-02 09:13:19蜘蛛蛋
日期:2013-05-20 09:44:372013年新春福章
日期:2013-02-25 14:51:24蛋疼蛋
日期:2012-11-27 15:47:59ITPUB 11周年纪念徽章
日期:2012-10-09 18:09:19奥运会纪念徽章:足球
日期:2012-10-08 08:54:372011新春纪念徽章
日期:2011-03-23 16:25:422011新春纪念徽章
日期:2011-02-18 11:43:34
发表于 2012-5-25 10:25 | 显示全部楼层
说个题外话,老吕如果是在乱世,肯定能成为独霸一方的诸侯。 干什么事情都能干到底。

使用道具 举报

回复
认证徽章
论坛徽章:
86
沸羊羊
日期:2015-03-06 17:18:41三菱
日期:2013-12-29 21:17:56优秀写手
日期:2013-12-18 09:29:09阿斯顿马丁
日期:2013-12-04 10:14:322009架构师大会纪念徽章
日期:2014-08-04 09:33:532010系统架构师大会纪念
日期:2014-08-04 09:33:532011系统架构师大会纪念章
日期:2014-08-04 09:33:532012系统架构师大会纪念章
日期:2014-08-04 09:33:532013系统架构师大会纪念章
日期:2014-08-04 09:33:532014系统架构师大会纪念章
日期:2014-08-04 09:33:53
发表于 2012-5-25 10:27 | 显示全部楼层
精彩啊

使用道具 举报

回复
论坛徽章:
70
夏利
日期:2013-09-29 21:02:15天蝎座
日期:2015-12-11 09:52:33马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11兰博基尼
日期:2014-01-02 19:02:10保时捷
日期:2013-12-28 20:27:38
 楼主| 发表于 2012-5-25 10:29 | 显示全部楼层
buptdream 发表于 2012-5-25 10:27
精彩啊

后续更精彩,

使用道具 举报

回复
论坛徽章:
70
夏利
日期:2013-09-29 21:02:15天蝎座
日期:2015-12-11 09:52:33马上有对象
日期:2014-02-19 11:55:14马上有钱
日期:2014-02-19 11:55:14马上有房
日期:2014-02-19 11:55:14马上有车
日期:2014-02-19 11:55:14马上有车
日期:2014-02-18 16:41:112014年新春福章
日期:2014-02-18 16:41:11兰博基尼
日期:2014-01-02 19:02:10保时捷
日期:2013-12-28 20:27:38
 楼主| 发表于 2012-5-25 10:31 | 显示全部楼层
gaolu1234 发表于 2012-5-25 10:25
说个题外话,老吕如果是在乱世,肯定能成为独霸一方的诸侯。 干什么事情都能干到底。

搞C的程序员,大多都是这样。我不喜欢糊糊途途使用一个东西。使用的东西,要么自己写,如果写不了,哪就把它搞明白。

使用道具 举报

回复
论坛徽章:
177
秀才
日期:2016-02-18 09:39:10摩羯座
日期:2016-01-20 16:48:10火眼金睛
日期:2016-01-31 22:00:00巨蟹座
日期:2016-01-30 22:10:33目光如炬
日期:2016-01-03 22:00:00秀才
日期:2015-12-21 09:53:46目光如炬
日期:2015-12-20 22:00:00秀才
日期:2015-12-25 15:31:10秀才
日期:2015-12-14 15:02:13秀才
日期:2016-01-21 13:37:04
发表于 2012-5-25 10:31 | 显示全部楼层
NB

使用道具 举报

回复
论坛徽章:
13
生肖徽章2007版:兔
日期:2009-03-24 09:50:192014年新春福章
日期:2014-02-18 16:43:09大众
日期:2013-09-02 09:13:19蜘蛛蛋
日期:2013-05-20 09:44:372013年新春福章
日期:2013-02-25 14:51:24蛋疼蛋
日期:2012-11-27 15:47:59ITPUB 11周年纪念徽章
日期:2012-10-09 18:09:19奥运会纪念徽章:足球
日期:2012-10-08 08:54:372011新春纪念徽章
日期:2011-03-23 16:25:422011新春纪念徽章
日期:2011-02-18 11:43:34
发表于 2012-5-25 10:33 | 显示全部楼层
哈哈。 再水一下,啥时候 老吕给分析下 经济情况,经济走势。

俗话说: 乱世黄金,盛世古董。

现在这个社会,黄金与古董 双飞啊 。

使用道具 举报

回复
认证徽章
论坛徽章:
291
蓝色妖姬
日期:2012-05-19 11:02:10蓝色妖姬
日期:2012-06-12 11:21:48蓝色妖姬
日期:2012-06-12 11:21:48玉兔
日期:2012-08-05 10:00:09玉兔
日期:2012-07-27 11:00:12马上有钱
日期:2014-06-16 15:59:19季节之章:春
日期:2012-06-20 17:38:14季节之章:夏
日期:2012-06-12 10:49:25季节之章:秋
日期:2012-06-12 10:49:25季节之章:冬
日期:2012-06-12 10:49:25
发表于 2012-5-25 10:41 | 显示全部楼层
V哥坚持写,太好了。
一直希望能抽时间跟着做,希望V哥多指教。

使用道具 举报

回复
求职 : 数据库管理员
论坛徽章:
10
咸鸭蛋
日期:2012-02-02 15:23:012014年新春福章
日期:2014-02-18 16:44:08福特
日期:2013-12-04 11:24:26ITPUB社区千里马徽章
日期:2013-06-09 10:15:342013年新春福章
日期:2013-02-25 14:51:24ITPUB社区OCM联盟徽章
日期:2013-03-21 15:35:43ITPUB 11周年纪念徽章
日期:2012-10-09 18:16:00蛋疼蛋
日期:2012-04-09 13:30:04咸鸭蛋
日期:2012-04-10 16:49:36马上有对象
日期:2014-02-18 16:44:08
发表于 2012-5-25 10:46 | 显示全部楼层
首页留名啊,大神贴必须顶下慢慢研究

使用道具 举报

回复
论坛徽章:
59
狮子座
日期:2016-03-26 13:35:402013年新春福章
日期:2013-02-25 14:51:24双黄蛋
日期:2013-02-25 11:06:15ITPUB 11周年纪念徽章
日期:2012-10-09 18:06:20灰彻蛋
日期:2012-04-25 13:19:33最佳人气徽章
日期:2012-03-13 17:39:18玉石琵琶
日期:2012-02-21 15:04:38紫蛋头
日期:2012-03-14 11:16:09ITPUB十周年纪念徽章
日期:2011-11-01 16:21:15鲜花蛋
日期:2011-11-30 14:13:01
发表于 2012-5-25 10:51 | 显示全部楼层
前排

使用道具 举报

回复

您需要登录后才可以回帖 登录 | 注册

本版积分规则

TOP技术积分榜 社区积分榜 徽章 电子杂志 团队 统计 虎吧 老博客 知识索引树 读书频道 积分竞拍 文本模式 帮助
  ITPUB首页 | ITPUB论坛 | 数据库技术 | 企业信息化 | 开发技术 | 微软技术 | 软件工程与项目管理 | IBM技术园地 | 行业纵向讨论 | IT招聘 | IT文档 | IT博客
  ChinaUnix | ChinaUnix博客 | ChinaUnix论坛 | SAP ERP系统
CopyRight 1999-2011 itpub.net All Right Reserved. 北京盛拓优讯信息技术有限公司版权所有 联系我们 网站律师 隐私政策 知识产权声明
京ICP备16024965号 北京市公安局海淀分局网监中心备案编号:11010802021510 广播电视节目制作经营许可证:编号(京)字第1149号
  
快速回复 返回顶部 返回列表