2.4.1    实例分析

1.  9-1分裂分析

下面我们通过一些实例的操作来进一步深入分析9-1分裂的过程吧,这将是一件很美妙的过程,就像看到一棵树生长一样。具体步骤如下:

步骤1  创建一个新表alex_t05及索引,我们主要考察索引idx_alex_t05_id:

SQL> create table alex_t05 (id number, name varchar2(100));
SQL> create index idx_alex_t05_id on alex_t05 (id);

步骤2  向alex_t05表中顺序地添加3000行数据,此过程我们将开启10224事件进行跟踪:

SQL> alter session set events '10224 trace name context
2  forever,level 1';

Session altered

SQL> declare
2  begin
3    for i in 1 .. 3000 loop
4      insert into alex_t05 values (i, 'alex');
5    end loop;
6    commit;
7  end;
8  /

SQL> alter session set events '10224 trace name context off';

Session altered

下面是10224跟踪事件记录的索引分裂过程的日志,可以看到其记录了5次的索引叶节点数据块的分裂,也就是说整个过程发生了5次索引分裂。因为测试表和索引都是新建了,也就是说此时索引树结构应该有6个叶节点数据块。

splitting leaf,dba 0x010267a4,time 14:53:00.149

kdisnew_bseg_srch_cbk reject block -mark full,dba 0x010267a4,time 14:53:00.151

kdisnew_bseg_srch_cbk rejecting block ,dba 0x010267a4,time 14:53:00.151

kdisnew_bseg_srch_cbk using block,dba 0x010267a6,time 14:53:00.152

kdisnew_bseg_srch_cbk reject block -mark full,dba 0x010267a6,time 14:53:00.152

kdisnew_bseg_srch_cbk rejecting block ,dba 0x010267a6,time 14:53:00.152

kdisnew_bseg_srch_cbk using block,dba 0x010267a7,time 14:53:00.152

splitting leaf,dba 0x010267a7,time 14:53:00.420

kdisnew_bseg_srch_cbk reject block -mark full,dba 0x010267a7,time 14:53:00.421

kdisnew_bseg_srch_cbk rejecting block ,dba 0x010267a7,time 14:53:00.421

kdisnew_bseg_srch_cbk using block,dba 0x010267a8,time 14:53:00.421

splitting leaf,dba 0x010267a8,time 14:53:00.652

kdisnew_bseg_srch_cbk reject block -mark full,dba 0x010267a8,time 14:53:00.652

kdisnew_bseg_srch_cbk rejecting block ,dba 0x010267a8,time 14:53:00.652

kdisnew_bseg_srch_cbk using block,dba 0x010267a5,time 14:53:00.652

splitting leaf,dba 0x010267a5,time 14:53:00.879

kdisnew_bseg_srch_cbk reject block -mark full,dba 0x010267a5,time 14:53:00.879

kdisnew_bseg_srch_cbk rejecting block ,dba 0x010267a5,time 14:53:00.879

kdisnew_bseg_srch_cbk using block,dba 0x010267ae,time 14:53:00.881

splitting leaf,dba 0x010267ae,time 14:53:01.128

kdisnew_bseg_srch_cbk reject block -mark full,dba 0x010267ae,time 14:53:01.128

kdisnew_bseg_srch_cbk rejecting block ,dba 0x010267ae,time 14:53:01.128

kdisnew_bseg_srch_cbk using block,dba 0x010267af,time 14:53:01.128

步骤3  再来验证一下发生的分裂是不是发生了9-1分裂。通过数据字典的查询,可以看到该会话(因为是新建的会话)制造了5次9-1分裂,这和10224事件跟踪的结果是不谋而合的。验证结果如下所示:

SQL> select s.sid, n.name, s.value
2    from v$sesstat s, v$statname n
3   where s.statistic# = n.statistic#
4     and sid in (select sid from v$mystat)
5     and value > 0
6     and n.name like '%split%';

SID   NAME                            VALUE
---------- ------------------------- -------
1621  leaf node splits                  5
1621  leaf node 90-10 splits           5

步骤4  再来分析一下索引idx_alex_t05_id的结构吧,同时DUMP出索引树形结构。

此时的分裂就索引空间的使用率来说,是比较高效的,使用率大约在76%左右。从DUMP文件可以看到每个叶节点块大约存储570个条目,简单地做个换算,76%充盈的叶块可以存储570行的记录,那一个100%充盈的块即可以存储571/0.76=750行记录。

同时我们看到索引的PCT_FREE=10%,也就是说索引的叶块的利用率可以达到90%,单块可以存储记录行数即为750*0.9=675。但实际情况单块存储的记录行数在没有达到675行就已经分裂了,PCT_FREE参数的设置被忽视了。当发生9-1分裂的时候,PCT_FREE参数值仅仅可以作为参考,不知道5-5分裂的时候是否也是一样呢?验证如下所示:

SQL> analyze index idx_alex_t05_id validate structure;

Index analyzed

SQL> select height,
2  round((del_lf_rows_len/lf_rows_len)*100,2)||'%' ratio,
3  pct_used from index_stats where name= 'IDX_ALEX_T05_ID';

HEIGHT  RATIO          PCT_USED
---------- ---------- ----------
2  0%                    76

SQL> select pct_free from user_indexes
2  where index_name='IDX_ALEX_T05_ID';

PCT_FREE
----------
10

SQL> alter session set events 'immediate trace name
2  treedump level 311979';

—– begin tree dump

branch: 0x10267a4 16934820 (0: nrow: 6, level: 1)

   leaf: 0x10267a6 16934822 (-1: nrow: 577 rrow: 577)

   leaf: 0x10267a7 16934823 (0: nrow: 570 rrow: 570)

   leaf: 0x10267a8 16934824 (1: nrow: 570 rrow: 570)

   leaf: 0x10267a5 16934821 (2: nrow: 570 rrow: 570)

   leaf: 0x10267ae 16934830 (3: nrow: 570 rrow: 570)

   leaf: 0x10267af 16934831 (4: nrow: 143 rrow: 143)

—– end tree dump

2.  5-5分裂分析

接下来再来看一下5-5分裂的情况,它和9-1分裂的方式和原理是不一样的。我们知道有三种情况会触发5-5分裂,我们来逐一测试一下吧。

场景1  左侧节点发生新值插入的情况,我们可以对上面9-1分裂的例子变换一下,反序插入3000条记录,再通过10224事件和树形结构的DUMP来分析。反序插入记录的代码如下所示:

SQL> declare
2  begin
3    for i in 1 .. 3000 loop
4      insert into alex_t05 values (3001-i, 'alex');
5    end loop;
6    commit;
7  end;
8  /

此时,我们注意到索引同样只发生了叶节点块的分裂,但是同样的操作,分裂次数由5次增加到了9次。为什么呢?因为触发的是5-5分裂,5-5分裂导致索引叶节点的数据块使用率不高,或者说在使用率还不高的时候就发生了分裂。示例如下:

SQL> select s.sid, n.name, s.value
2    from v$sesstat s, v$statname n
3   where s.statistic# = n.statistic#
4     and sid in (select sid from v$mystat)
5     and value > 0
6     and n.name like '%split%';

SID   NAME                            VALUE
---------- ------------------------- -------
1621  leaf node splits                   9
1621  leaf node 90-10 splits            0

所以,此时索引空间总体的使用率也由之前的76%下降到了48%(见下面的统计信息),从索引树形结构的DUMP文件中也可以看到单块存储的记录行数由570行下降到了281行,说明此时索引有点像个“虚胖子”了。如果一个索引上非常频繁发生5-5分裂,则这个“虚胖子”将变得越发“虚弱”,在一次简单的查询或者DML操作的时候,会扫描非常多的索引块,直接导致I/O次数的增加,特别是在并发度很高的表上,这样的变化甚至可能是致命的,可能会让你丧失近10倍的响应速度。当然,这种问题可以通过索引重建来修复,2.5节会具体展开介绍。

SQL> select height,
2  round((del_lf_rows_len/lf_rows_len)*100,2)||'%' ratio,
3  pct_used from index_stats where name= 'IDX_ALEX_T05_ID';

HEIGHT RATIO         PCT_USED
---------- ---------- ----------
2  0%                    48

如下为索引树形结果的DUMP日志:

—– begin tree dump

branch: 0x10267a4 16934820 (0: nrow: 10, level: 1)

   leaf: 0x10267a7 16934823 (-1: nrow: 471 rrow: 471)

   leaf: 0x10267b0 16934832 (0: nrow: 281 rrow: 281)

   leaf: 0x10267af 16934831 (1: nrow: 281 rrow: 281)

   leaf: 0x10267ae 16934830 (2: nrow: 281 rrow: 281)

   leaf: 0x10267ad 16934829 (3: nrow: 281 rrow: 281)

   leaf: 0x10267ac 16934828 (4: nrow: 281 rrow: 281)

   leaf: 0x10267ab 16934827 (5: nrow: 281 rrow: 281)

   leaf: 0x10267a6 16934822 (6: nrow: 281 rrow: 281)

   leaf: 0x10267a5 16934821 (7: nrow: 281 rrow: 281)

   leaf: 0x10267a8 16934824 (8: nrow: 281 rrow: 281)

—– end tree dump

如预期的结果一样,在发生5-5分裂的时候,PCT_FREE参数再一次的被忽视。参数PCT_FREE在索引创建时起作用,而在使用时往往被忽略。如下是索引IDX_ALEX_T05_ID的PCT_FREE参数设置情况:

SQL> select pct_free from user_indexes
2  where index_name='IDX_ALEX_T05_ID';

PCT_FREE
----------
10

场景2  DML操作时,索引块上没有足够空间分配新的ITL槽。

这种情况应该和第三种情况(新值待插入的索引块上存在其他未提交的事务)结合起来一起看会更加清晰。两种情况都是应用的并发度高而引发的。同一索引数据块上的事务数超出了实际允许的ITL数,导致MAXTRANS不足。另外,从Oracle 10g开始,MAXTRANS的值被强制设置为255,不能修改,但实际ITL槽数所占空间一般不能超过数据块大小的一半,如默认8KB大小数据块的限制为169。

我们模拟一个高并发的例子来看一下吧,只做一个简单的insert操作,并发度设置为300(大于169),每个并发进程循环执行100次。

SQL> insert into alex_t05 values
2  (trunc(dbms_random.value()*10000), 'alex');

查询并发插入操作前后的系统统计信息,做个差值得到并发操作的等待事件统计和索引分裂状态统计(整个过程数据库无其他会话操作)。因为是无序的插入,索引大部分分裂情况都是5-5分裂。而从等待事件来看,出现了“enq: TX – allocate ITL entry”等待事件,意味着出现了数据块上ITL的争用,当然这其中包括了数据块和索引块的争用。示例如下所示:

SQL> select event, total_waits
2    from v$system_event
3   where event in
4         ('enq: TX - allocate ITL entry', 'enq: TX - index contention');

EVENT                                 TOTAL_WAITS
-------------------------------- -----------
enq: TX - allocate ITL entry                 18
enq: TX - index contention                  134

SQL> select n.name, s.value
2    from v$sysstat s, v$statname n
3   where s.statistic# = n.statistic#
4     and n.name like '%split%';

NAME                                VALUE
------------------------- ----------
leaf node splits                   5522
leaf node 90-10 splits              53
branch node splits                   22
root node splits                      0

在索引块上,有两种情况会触发“enq: TX – allocate ITL entry”等待。

q  达到数据块上最大事务数限制。

q  递归事务ITL争用。

场景3  新值待插入的索引块上存在其他未提交的事务。

前面说到这种情况也是由于高并发造成的,需要和第二种情况结合起来考虑。可以说高并发对第三种情况的影响更为明显。

我们再模拟一个高并发的例子,做一个简单的顺序的insert操作,并发度仍设置为300,每个并发进程循环执行仍为100次。因为是顺序插入的,并发的争用更为集中到最右侧的索引节点块上。Insert语句如下:

SQL> insert into alex_t05 values (seq_alex_t05_id.nextval, 'alex');

从如下查询结果可以看到,索引的分裂次数减少了,但是等待事件的次数增加了,也可以说由于等待时间加长了导致发生索引块的分裂的次数减少了。“enq: TX – index contention”是一个与索引分裂直接相关的等待事件,也仅由索引分裂才会触发的等待事件。当一个索引块上发生DML操作,而这个索引块正在被另外一个事务分裂,则需要等待分裂完成后才能修改上面的数据,此时就会发生“enq: TX – index contention”等待事件。因为争用的索引块更集中了,该等待事件的发生的次数也就增加了。示例如下所示:

SQL> select event, total_waits
2    from v$system_event
3   where event in
4         ('enq: TX - allocate ITL entry', 'enq: TX - index contention');

EVENT                                 TOTAL_WAITS
-------------------------------- -----------
enq: TX - allocate ITL entry               285
enq: TX - index contention               15492

SQL> select n.name, s.value
2    from v$sysstat s, v$statname n
3   where s.statistic# = n.statistic#
4     and n.name like '%split%';

NAME                             VALUE
------------------------- ----------
leaf node splits                   1304
leaf node 90-10 splits              19
branch node splits                    4
root node splits                      0

综上所述,发生5-5分裂时,如果是第一种情况,新值小于索引中的最大值,这在索引的应用中是不可避免的,除非是完全理想状态(索引聚簇因子无限接近于数据块数)下。换而言之,其对性能的影响可以不用太多关注,因为我们做不了什么。反之,更多应该关注的是第二种和第三种情况,特别是第三种情况,因为其发生的概率和影响程度都较大。

3        高并发优化

了解了问题的原理和发生过程,再来说一说解决的办法吧。前面说到索引的争用源自于索引的分裂,因为这是一个代价高昂的操作,而触发索引分裂的契机就是索引上的高并发事务操作。问题现在就集中在“如何去解决高并发导致索引热块分裂的争用”上面。

也许有人要说了,索引块上有分裂和争用就增加一些ITL槽来增加并发处理能力嘛。这样的做法会有效吗?我们不妨修改一下索引的initrans参数来试试看,重复做一下300路并发100次循环的简单顺序插入操作:

SQL> alter index idx_alex_t05_id rebuild initrans 10 pctfree 10;

优化后的效果对比如表2-4所示,从分裂情况来看,索引块的分裂总次数明显下降了,而且更倾向于走9-1分裂,说明增加ITL槽后索引块处理并发事务的能力加强了。但不幸的是,等待事件的等待次数反而增加了,特别是ITL槽的争用的等待更加明显了,所以说修改initrans参数并没有真正解决问题。随着并发度的不断提升,ITL槽的争用也越发激烈,就好像千军万马买火车票,原来2个窗口,现在增加到10个窗口,其实意义并不大,还是需要一些根本的解决手段。

表2-4  INITRANS优化效果

  初始状态 INITRANS优化
等待

事件

enq: TX – allocate ITL entry 285 2472
enq: TX – index contention 15492 25380
分裂

情况

leaf node splits 1304 391
leaf node 90-10 splits 19 84
branch node splits 4 2
root node splits 0 0

在进行下一步的优化之前,我们先来思考一下为什么会形成这个问题呢?是的,是高并发没错。再根本一些的原因是高并发操作过于集中在最右侧的索引块上,如果我们能把这种“集中”打散了,那问题是不是就解决了呢?接下来,我们可以引进反向键的B树索引(REVERSE KEY INDEX)来打散这种“集中”,试着解决这个问题看看。

删除掉之前的索引,并重建一个反键索引,如下所示:

SQL> create index idx_alex_t05_id on alex_t05 (id) reverse;

此时的优化效果如表2-5所示,不论从等待事件,还是索引分裂情况来看,优化效果都是比较明显的,这证明我们成功打散了高并发过程中的“集中”。

表2-5 反键索引优化效果

  初始状态 INITRANS优化 反键索引优化
等待

事件

enq: TX – allocate ITL entry 285 2472 860
enq: TX – index contention 15492 25380 1298
分裂

情况

leaf node splits 1304 391 181
leaf node 90-10 splits 19 84 2
branch node splits 4 2 0
root node splits 0 0 0

为什么反键索引会带来这样的效果呢?主要是由反键索引的存储结构决定的,或者说反键索引就是为了解决此类问题而衍生出来的B树索引类型。

对于普通B树索引来说,要存储数据“1234”、“1235”、“1239”,它可能就存储在同一个叶节点块中,如图2-13的左图所示。但是反键B树索引则先会把待存储的数据做一个顺序反转,成为“4321”、“5321”、“9321”后再进行存储,如图2-13的右图所示,则本来连续的数据有可能存储在不同叶节点块中,这样就避免了热点索引块的争用。

book_ch02_13

图2-13  普通B树索引和反键B树索引结构对比

但是,我们说反键索引也是有一利必有一弊,这一弊则同样是由反键索引的存储结构带来的。我们在数据入库的时候得到了优势,那数据读取呢?查询“1234”、“1235”、“1239”记录,本来访问一个索引块即可,现在需要访问三个了,增加了额外的I/O开销。所以,在反键索引的应用中特别要注意的是,其索引列上的查询,尽可能地使用等值查询,避免范围查询,这样可以避免额外的I/O开销。下面是一个强制走反键索引的范围查询,可以看到COST开销是大得可怕的。

SQL> select * from alex_t05 where id between 106001 and 106010;
------------------------------------------------------------------------------------
| Id  | Operation                   | Name            | Rows  | Bytes | Cost (%CPU)|
------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |                 |    11 |    99 |  1146   (1)|
|   1 |  TABLE ACCESS BY INDEX ROWID| ALEX_T05        |    11 |    99 |  1146   (1)|
|*  2 |   INDEX FULL SCAN           | IDX_ALEX_T05_ID |   975 |       |   178   (1)|
------------------------------------------------------------------------------------

当然,我们大可不必因为有弊端而不用它,这样将是非常可惜的。普通B树索引和反键B树索引处理高并发事务的对比情况,如图2-14所示。横坐标轴是递增的高并发数,主纵坐标轴对应柱状图是“enq: TX – index contention”等待事件的平均等待时间,次纵坐标轴对应的是TPS变化曲线。普通B树索引对应的是较高的柱状图和较低TPS曲线,反键B树索引对应的则是较低的柱状图和较高的TPS曲线。

 book_ch02_14

图2-14  普通B树索引和反键B树索引处理高并发事务的对比

从等待事件来看,反键索引几乎完全消除了“enq: TX – index contention”等待;而TPS能力也有4~5倍的性能提升。相信这完全有理由让大家接受反键索引了。

索引的高效使用不仅在设计阶段需要多多关注,在后期维护阶段也是不容忽视的。高并发的事务处理往往可以使一些潜在的问题浮现出来或更为突出,对于索引分裂和生长的问题大致可以总结以下几点:

q  避免一切不必要的索引块分裂发生;

q  尽可能简化索引结构,移除不必要的索引字段;

q  使选择性更强的字段在前,减小分支节点的大小;

q  有可能的话,可以创建压缩索引;

q  对于更新较少的OLAP应用,考虑使用较大数据块的表空间;

q  定期进行索引重建,再好的设计和使用都无法完全避免索引分裂问题。

Trackback

no comment untill now

Add your comment now

切换到手机版