Oracle 数据库中enq:TX-index contention等待事件浅析

enq:TX-index contention:

enq:TX-index contention是一个非常常见的等待事件,其专指由于索引分裂产生的竞争等待。最常见的索引竞争一般发生在主键索引上,主键值从序列(sequence)中获取,每个事务都会生成一条新的记录,每条记录都要获得一个新的序列号,因为从sequence中取出的值是单向递增的,当索引中插入数据,并且维护索引结构的时候,不得不一直走向索引的最右侧的分支,对于每一个操作,都会想要维护索引中最右边的叶节点,那么所有的操作都会关注同一个内存块,希望能够维护这块内存,这就是一种典型的竞争形式。但在同一时间,只有一个人能够修改这块内存,因此当有一个人在修改的时候,其他所有想修改的人只能处于等待状态。

在一个并发量很大的OLTP系统中,开发者希望记录每个用户的登录和操作情况,因此需要创建基于主键的索引。接下来我们将通过实验说明如何减少这类索引竞争。

首先来看在没有索引的情况下系统的运行状况,系统运行平稳并且性能良好。如下图所示:

 

enq:TX-index contention等待事件_等待事件

接下来我们尝试最简单的B树索引,索引的的值是通过sequence生成的。在做插入操作的时候发现对性能产生严重的影响,响应时间变长,吞吐量直线下降,CPU利用率也从60%下降到20%。查看等待事件,发现大量的enq:TX-index contention,由此说明B树索引并不合适。

为解决热点块的争用,反向索引或许是不错的选择。例如表的某一列:.....1234,1235,1236,1237......,建立正向索引:......1234,1235,1236,1237.......,这四行放在同一个leaf block中,建立反向引,...4321,5321,6321,7321...,这四行放在四个不同的leaf block,这有效的让对一个块的热点操作分散开来,从而避免索引竞争问题。接下来我们引入反向索引,观察一下系统的运行情况:

 

enq:TX-index contention等待事件_主键_02

由上图可见,性能问题立即得到改善,系统的相应时间立即减小,TPS得到回升,CPU使用率提高。运行一个礼拜后,继续观察。

 

enq:TX-index contention等待事件_数据_03

在运行一段时间后又发生了性能问题,系统中出现了大量跟I/O相关的等待事件,反向索引有一个弊端就在于索引的CF会变的越来越大,导致我们要维护索引时必须将很多个索引块放入到buffer cache中,最后SGA装不下的时候,就需要通过磁盘I/O来维护索引,与此同时还会将其他的索引和表都挤出内存,当其他应用获取不到内存时就需要从磁盘上读取数据,而磁盘I/O的时间是毫秒级别,也就是说从磁盘读取数据所花的时间是从内存读取的一千倍。我们实际上所做的只是把导致系统变慢的等待事件替换成另一种等待事件而已。

B树索引会导致内存的竞争和等待,而反向键索引则会导致太多的I/O,接下来尝试第三种方案:索引分区,通过hash将索引分成一个一个小块,这样竞争就不会聚集在最右边的节点上,也就是说,我们用一些小的竞争代替右边节点上的集中的热点竞争,看看能不能减少I/O,同时让系统的响应时间和CPU使用率回归到正常的水平。

在单实例上,通过hash分区索引在一定程度上缓解了插入数据的竞争问题,但我们在进行性能优化的过程中,总是很谨慎使用hash 分区索引,原因有很多。首先我们在对原来没有分区的表进行分区,这是有风险的,比如可能会引起执行计划改变,一些非插入的操作的性能下降等等。除了这个问题之外,还为整个系统带来了一些扩展性问题。

接下来将之前的单实例增加到两个节点,连接数和处理的事务数都增加到原来的两倍,硬件配置增加到两倍,同样使用hash分区索引,观察性能和吞吐量会如何变化。

 

enq:TX-index contention等待事件_等待事件_04

当我们看到第二个节点起来之后,观察到第一个节点的CPU使用率有所降低,但单纯从吞吐量来看基本上是看不出性能的变化的。接下来我们看到响应时间变长了,出现了一些新的等待事件,都是一些GC buffer的事件。

通过hash分区索引我们使得负载分散到了索引的所有叶节点上了,如果想修改的叶节点所在的数据块正好在当前节点的Buffer Cache中,那么修改的时间就是微秒级的,但如果要修改的数据在其他节点上,那就需要通过网络将该数据读取到当前内存中,随着节点的扩展,这些数据块存在于当前实例buffer Cache的可能性就会降低,两个节点,50%,三个节点33%,依次类推。

到这里,总结一下我们所面临的挑战:一是实例间的竞争或者说扩展性问题。二是单节点间的竞争。有没有一种方法。既能解决单节点的竞争问题,又能在扩展中不带来新的问题,这就需要保证缓存的相关性,让数据所在的实例恰好是会被访问的实例。

如果能控制生成代理主键,我们就能把这些特征放入到生成的主键中,这样不仅能够保证得到较好的缓存相关度,从而使RAC可扩展,而且可以把主键分散开,这样在单实例上也不会出现竞争。

首先要考虑的是可以使用实例号作为主键号的开头,这样插入数据的时候就会保存在树节点的一边,也正是这些数据应该被保存到的实例上,这样就可以建立与插入操作相关的缓存相关性。当我们在访问的时候能够准确定位数据所在的实例之后,第二个要考虑的问题就是,访问同一个实例上数据的时候不会竞争同一块内存,如果说智能主键的中间部分如果是对进程号某种方式取余,这样就把对索引的维护分散到同一实例的多个内存块上去。而智能主键的最后一部分是sequence的本身,这样可以保证引用和完整性,确保每一行都是唯一的。因此最终智能主键的组成是:实例ID-进程号取余-序列号

在使用智能主键后,观察系统的运行情况:

 

enq:TX-index contention等待事件_等待事件_05

发现系统的响应时间减少,其他等待事件消失,CPU利用率提高,并且只有CPU在占用时间。跟最初系统没有产生竞争的情况下的性能一样。

Oracle 18c Scalable Sequence新特性:

值得注意的是,oracle在最新发布的数据库版本中引入了可扩展序列的概念。如下所示:

可扩展序列 - Scalable Sequence:通过在CREATE SEQUENCE或ALTER SEQUENCE语句中指定SCALE子句,可以使序列获得健壮的扩展性。

我们来看一下 18c 中的可扩展序列的定义:

通过以下语法定义 scalable sequence:

CREATE | ALTER SEQUENCE sequence_name

...

SCALE [EXTEND | NOEXTEND] | NOSCALE

当 SCALE 语句被指定时, 一个 6 位数的数字被指定作为序列的前缀,末尾、是正常的序列数字,两者联合成为新的序列:

scalable sequence number =6 digit scalable sequence offset number||normal sequence number

在这里,6位数字正是由 实例号||会话号 生成的:

6 digit scalable sequence offset number = 3 digit instance offset number || 3 digit session offset number.

The 3 digit instance offset number is generated as [(instance id % 100) + 100]. The 3

digit session offset number is generated as [session id % 1000].

现在通过这种序列方式,能够真正将来自不同实例的数据分散开来,索引竞争大大降低,从而提升了性能,使得序列变得可扩展。

 

 

如有侵权,请联系 删除谢谢;

日积月累