暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

[译文] Oracle 序列:RAC

原创 Jonathan Lewis 2021-08-02
1557

在本文中,我将花大部分时间解释当您从单实例 Oracle 迁移到多实例 Oracle (RAC) 时问题如何变得更糟(并得到解决)。我将通过提及在从 12c 到 19c 的升级中出现的序列的新特性来结束这篇文章,强调一项 RAC 可伸缩性特性。

注意:在我将本文的最终草稿通过电子邮件发送给 Simple Talk 编辑器几分钟后,Oracle-L 列表服务器上出现了一个问题,询问为什么 在查询访问它时启用了缩放 和 扩展的序列中出现重复值 并行运行。不久之后答案就出来了:这是由于(未发布的)错误 31423645,该错误已在 Oracle 19.11 中修复,并且可以为早期版本的 Oracle 提供反向端口。”

RAC

每当从单实例转向多实例时,根本问题是如何避免实例之间对“流行”数据块的过度竞争。这个通用问题变成了序列的两个特定问题:

  • 刷新序列“缓存”更新 table 中的特定行sys.seq$。
  • 每个将连续值插入索引的会话都将尝试访问相同的“右手/高值”索引叶块。

但是,在担心可能的热点之前,必须考虑序列和 RAC 的另一个问题。实例如何协调它们对序列值的使用并避免两个实例使用相同值的风险?有两种解决方案:默认noorder 机制,每个实例的行为就好像它不知道其他实例一样。另一种order选择是,实例通过使用全局队列不断协商,以确定在任何时候哪个实例应该负责序列。

默认机制 (noorder)

假设您已经使用基本命令创建了一个序列

create sequence s1 cache 5000;

Oracle 在seq$表中创建了一行,highwater = 1并且缓存= 5000。调用 for 的第一个实例s1.nextval将该行读入其全局内存区域,并将值 1 返回给调用会话,更新表以设置表highwater到 5001。(注意:我已经说过“实例”正在调用一个值,更准确地说,我应该说实例中的一个会话。)

当下一个实例调用时会发生什么s1.nextval?它会做任何实例通常会做的事情;它将从中读取行的当前值seq并说“当前最高值为 5001,缓存大小为 5000;我将更新seq.highwater为 10,001 并将值 5001 返回给会话”。如果第三个实例随后调用s1.nextval,则发生相同的过程 - 它读取行的当前状态,将 更新highwater 为 15,001,并将 10,001 返回给用户会话。

这三个实例中的任何一个都可能是第一个耗尽其缓存的实例——当它耗尽时,它将seq$再次读取该行,将 更新highwater 5,000 并返回曾经是highwater. 如果这是第一个实例,它的调用nextval将从 5,000 跳到 15,001。

这种noorder机制的结果是每个实例都将在不同的数字范围内工作,并且实例之间不会有重叠。如果您的会话每秒登录一次数据库以发出调用nextval(并且它们每次最终都通过不同的实例进行连接),那么返回的值似乎相当随机地分散在“数字”指定的范围内实例数 x 缓存大小。” 将保证唯一性,但不会保证排序。

但是,这对两个热点有什么作用呢?我选择了 5,000 的缓存大小,而不是让它默认为 20,这样实例之间就会有相当大的差距,这有助于解决这两点。就目前而言,seq$序列行的块将在实例之间移动,以相当低的频率进行更新。实例将花费大部分时间将键值插入在索引中间隔几个块的叶块中,每次实例刷新其缓存时都会发生间歇性冲突。在很大程度上,优化 RAC 系统中的序列仅意味着设置足够大的缓存大小——在某些情况下,cache 1e6这将是一个完全合理的设置。

这并不是所有性能问题的完整解决方案,在简要介绍使用非默认order选项创建的序列之后,我将对此有更多要说的。

带有 ORDER 的序列

如果您正在运行单实例 Oracle,那么您的序列值总是按顺序生成。有些值可能会丢失,有些值可能会被获取它们的会话无序使用,但“下一个值”只有一个来源。该值由实例生成为“前一个值”加上“增量”,因此这些值始终按顺序生成。

如上所述,在多实例 Oracle 中,默认情况下,实例将具有单独的非重叠缓存,这些缓存将在与缓存大小相关的数量上彼此不同步。当您从全局角度查看序列时,无法保证值会按顺序生成——这就是特定于 RAC 的顺序选项发挥作用的地方。

如果使用order选项声明一个序列,Oracle 会采用一种策略,为值使用单个“缓存”,并引入一种机制来确保一次只有一个实例可以访问和修改该缓存。Oracle 通过利用其Global Enqueue服务来做到这一点。每当会话发出对 的调用时nextval,实例都会获取序列缓存上的独占 SV 锁(全局入队),有效地表示“谁获得了有关此序列的最新信息——我想要控制”。以独占模式持有 SV 锁的一个实例是唯一可以增加缓存值的实例,并在必要时seq$通过增加高水位来更新表. 这意味着将再次按顺序生成序列号。

调用order选项的直接惩罚是序列化值的生成。您可以生成序列号的速率取决于 Global Enqueue Server 进程 (LCK0/LMD) 可以设法在实例之间移动序列的 SV 锁的速率。除非您的序列只应该以相当低的速率提供值,否则您可能不想使用此选项 - 它不会缩放。

这是一个奇怪的细节,而序列信息通过实例之间传递SV排队,入列统计(venqueue_stat)将不会显示任何得到的SV排队。(系统等待事件 ( vsystem_event) 将报告等待enq: SV - contention,但各个会话 ( v$session_event) 将仅将这些等待显示为events in waitclass other。)

性能影响

出现在单实例 Oracle 中的所有性能问题都会在多实例 Oracle 中再次出现,但往往会变得更糟,因为实例需要通过全局缓存服务或全局排队服务(或两者)来协调它们的活动。

特别是,当一个实例需要更新时seq$,它可能需要调用全局缓存服务来获得gc current get对它需要更新的块的独占访问()。同样,如果序列被用来生成唯一键,那么实例可能也需要调用全局缓存服务来获得对相关索引叶子块的独占访问,以及“右手/高值”问题出现在单实例Oracle 可以成为多实例Oracle 的灾区。

但是,通过在将序列声明为order或之间进行选择,可能的威胁变得更加微妙noorder。有 4 种组合需要考虑:

Noorder / cache N
Order / cache N
Noorder / nocache
Order / nocache

为了让您了解不同选项的效果,我设置了一个 3 节点 RAC 系统并在每个节点上运行一个简单的 PL/SQL 块来执行 1,000 次单行插入,sequence.nextval插入之间有 1/100 秒的睡眠. 然后我测试了上面 4 个选项中的每一个(缓存测试的N = 5000)。为插入所做的实际工作很小(不到 1 秒 CPU);由于与 RAC 相关的等待事件而导致的超额等待时间如下(按超额时间损失的顺序):

image.png

显然,cache优于nocache,并且noorder优于order。大缓存noorder是迄今为止最有效的选择。全局队列管理的排序开销很大,维护seq$表的开销也很大。

当然,这个特定的测试存在缺陷。首先,它运行虚拟机,因此虚拟互连速度比大型专用硬件慢;其次,我没有在这个测试中包含任何索引,并且索引争用的不同模式可能很重要;最后,我每个实例只使用一个会话来进行插入,而生产系统可能会看到在同一实例上运行的会话之间存在不同的争用模式。

不同应用程序可能具有的活动模式有太多可能的变化,因此我为所有这些创建和报告测试是不明智的。请记住,您需要考虑您的应用程序将如何工作,然后设计几个模型来为您的情况制定最合适的策略。然而,我将根据我关于两个关键点的初步陈述,就不同选项如何影响生产系统的性能做一些一般性评论:刷新序列缓存和插入基于序列的唯一索引。

无序/缓存 N

刷新缓存:达到高水位需要刷新缓存的实例将不得不以seq$独占模式获取相关块。对于低缓存值(例如默认值 20),这可能会非常频繁地发生并引入对“gc”(全局缓存)事件的大量等待。对于较大的值(例如我测试中的 5,000),这可能非常罕见,以至于与其他工作负载相比,任何“gc”等待都无关紧要。

插入索引:想象两个实例和一个缓存大小为 5,000 的序列,其中节点 1 比节点 2 更忙。节点 1 当前正在插入 25,001 到 30,000 范围内的值,刚刚达到 28,567。节点 2 正在插入 5,001 到 10,000 范围内的值,并且刚刚达到 9,999。这两个实例插入到不同的索引叶块中,因此没有争用。再插入一次后,节点 2 需要刷新其缓存,因此它现在开始插入 30,001 到 35,000 范围内的值,但节点 1 当前插入的值仅略大于 28,567。这两个节点将插入同一个高值叶块一段时间,直到叶块分裂并可能使节点 1 插入一个叶块,而节点 2 插入其正上方的叶块。几秒钟,“gc cr 块忙”、“gc 缓冲区忙释放”和“gc 缓冲区忙获取”。我将在前往 12c 增强部分的途中评论此行为的解决方法或损坏限制机制。

订单/缓存 N

刷新缓存:“ noorder/cache N”中关于“gc”等待的注释仍然适用,但由于seq$任何时刻只有一个实例需要更新表,因此影响将大大降低。但是这种节省是以连续等待SV 入队为代价的。

插入索引:如果你不做一些事情来避免这个问题,索引上右侧/高值叶块的争用将是巨大的。每个实例都会在当前模式下不断地请求块,并且您会看到很多等待索引的“gc”事件;当当前的 high_value 块分裂时,您可能还会看到多次等待“enq: TX – index contention”。当然,如果您采取措施避免对高值索引叶块的争用,则您必须采取措施阻止属于同一索引叶块的连续值,这意味着它们不能再连续了。这表明您并不真正需要“订购”选项。

无序/无缓存

刷新缓存:如果没有“缓存”,则seq$每次调用sequence.nextval. 每个实例都会请求对相关块的独占访问,并显示在各种“gc”等待上浪费了大量时间。但是,由于更新 highwater 也会导致字典缓存(rowcache)被更新,因此等待“行缓存锁定”事件也会花费大量时间。

插入索引:即使您指定了 noorder,nocache 的效果也意味着所提供的值将被很好地排序,实例之间没有“缓存大小”间隙。所有实例将同时插入到同一个高值叶块中。将有“gc”等待索引以及seq. 考虑到所有等待seq和行缓存锁定,由于这些等待索引叶块而损失的额外时间可能并不极端。

订单/Nocache

刷新缓存:至于noorder/nocache,没有缓存,因此每次调用sequence.nextval都是更新seq$表并使所有实例上的序列的行缓存条目无效的请求。事实上,我看不出order/nocache和noorder/ nocache之间的区别。在我的测试中,这两种情况之间的时间差异可能纯粹是运气之一。

插入索引:同样,noorder/的参数nocache适用。

性能 - 进一步的想法

从 4 种组合的列表中可以看出,noorder/cache N大缓存是最具可扩展性的选项,因为它分离了实例的(索引)活动并最大限度地减少了更新seq$表的争用。但是,它仍然具有可能需要进一步考虑的影响。

首先是简单的观察,您可以预期索引大约是“打包”大小的两倍。如果您对单个实例进行测试,将一个会话中 1 到 1,000,000 的值和另一个会话中 1,000,001 到 2,000,000 的值插入到索引列中,您会发现值的较低范围将导致 50/50 叶块分裂。相反,值的上限将导致 90/10 的拆分。如果您有 N 个实例从具有大缓存的序列中插入,除使用最顶部值范围的实例外,所有实例将始终进行 50/50 叶块拆分。我对此的唯一评论是它不是很好,但它可能不是非常重要。

更重要的是,我使用插入序列忽略了每个实例中多个会话的影响。在(多实例 RAC 的)单个实例中,您可能有许多会话插入非常相似的值,虽然这通常不会在实例之间产生争用,但它可能会在同一实例(即缓冲区)中的会话之间产生争用忙等待、索引 ITL 等待等)。我在上一篇文章中提到的“单实例”解决方法是添加一个因素,例如(1 + mod(sid, 16)) * 1e10在这些情况下,序列提供的值是一个积极的危险。例如,实例 1 的会话 99 生成的条目可能与实例 2 的会话 99 生成的行在同一索引叶块中结束。对于 RAC,您需要更进一步。除了添加一个因子来跨多个叶块传播会话之外,您还需要添加一个因子,通过添加一个“实例因子”来确保不同的实例保持分离(1 + mod(instance,999) * 1e14。这让我想到了 12c 增强功能。

12c 中的序列

Oracle 12c (12cR2) 为序列带来了多项增强。您可以使用标识机制将序列与表列相关联。或者,如果您不想声明身份,则可以使用“ sequence.nextval”作为列的默认值。您可以创建可以“本地”使用的序列——即它们是会话专用的。您可以重新启动序列。最后,12c 通过引入“scale”选项(尽管它没有记录在 SQL 参考手册中并且可能不应该在 18c 之前使用)来自动化最小化 RAC 上的索引争用的业务。

这是一个在 12.2.0.1 和 19c 上产生相同结果模式的小脚本,演示了新的自动可扩展性选项:

create sequence s1; create sequence s2 scale; create sequence s3 scale extend; create sequence s4 maxvalue 1e8 scale; create sequence s5 maxvalue 1e8 scale extend; create sequence s6 maxvalue 1e5 scale; set linesize 48 column nextval format 999,999,999,999,999,999,999,999,999,999,999,999 select s1.nextval, s2.nextval, s3.nextval, s4.nextval, s5.nextval from dual /

image.png

我已经声明前三个序列没有 a maxvalue(这意味着隐式maxvalue是 1e28 – 1),接下来的两个显式maxvalue是 1e8。我已经给出了maxvalue1e5的最终序列 a ,但出于显而易见的原因,我没有尝试从dual 中选择它。

所有六个序列都提供了值 1(经过一些调整)作为它们的第一个值。除了简单的声明 之外s1,结果都被扩展为包括一个前导的六位数字:101720。前三位是(100 + mod(instance_number,100)),接下来的三位是mod(sid,1000)。这与我在上面和上一篇文章中描述的策略完全匹配,以最大限度地减少对索引叶块的争用(代价是几乎到处都看到 50/50 叶块分裂。)

的的意义extend,当你比较选项可见s2用s3或s4用s5。在没有extend选项的情况下,由六位前缀创建的值必须在定义的maxvalue. 这意味着最大可能的“原始”序列号将比您预期的低 6 个数量级。(这就是为什么我没有尝试s6.nextval从中进行选择dual- 它会引发 Oracle 错误“ORA-64603:NEXTVAL 无法为 S6 实例化。将序列加宽 1 位数字或使用 SCALE EXTEND 更改序列”)。如果包含该extend选项,则前缀将乘以足够的 10 次幂,maxvalue从而可以达到您的实际指定值而不会引发错误。

您的代码不再需要对序列的使用强加可伸缩性,这是一个很好的想法,但是在利用此选项之前您仍然需要做一些设计工作。如果您过去已经实现了类似这种新的“双前缀”以最大化序列的可扩展性,您将已经熟悉这个问题:maxvalue在创建序列之前,您需要为序列确定尽可能小的安全性。

Imagine you actually needed “raw” values up to 1e8 (ca. 6 bytes), then a suitably selected maxvaluewith scale and extend would give you a 15 digit number (ca. 9 bytes). 如果您没有指定 a maxvalue,您的序列将变成 28(或 32,带扩展)数字。这意味着典型的 15(或 17)个字节会让您每行、主键中的每个索引条目、任何子表中的每行以及任何“外键”中的每个索引条目存储额外的 6(或 8)个字节指数”。如果它避免了性能威胁,这可能无关紧要,但这是一个需要慎重选择的细节——您应该始终尝试意识到后果,即使您决定忽略它们。

概括

本文根据任何多实例 (RAC) 系统中始终存在的基本争用威胁回顾了序列,并确定了两个热点:更新seq$表作为序列缓存刷新,以及高价值索引叶块,如果没有做任何事情来将不同实例使用的序列值传播到不同的叶块。

结果表明,序列的默认 RAC 行为——每个实例都有自己的值范围——有助于避免索引争用,尽管需要较大的缓存大小才能获得最佳效果。同时,它证明了大的缓存大小可以最大限度地减少更新频率,从而减少对seq$表的竞争。

该order选项降低了可扩展性,因为它使每个实例都竞争当前的高价值索引叶块。当序列信息通过 Global Enqueue 服务在实例中传递时,它还增加了开销。

即使您可以通过设置较大的缓存大小来减少跨实例争用,但每次实例刷新其缓存时,一对实例之间仍会存在一些争用。每个实例内的会话之间仍然存在争用,因为它们竞争“实例本地”高价值索引叶块。这使我们想到了使用由实例编号和会话编号构造的两部分前缀来预先固定序列值的想法。在 12c 中(尽管仅在 18c 中记录),Oracle 允许创建一个序列来处理这项工作,而无需额外的编码。然而,必要的静态声明确实需要决定是否使用一些额外的空间来避免用完序列值的任何风险。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论