Oracle在管理共享池内存方面面临着难以置信的挑战。多年来的所有改变、bug、补丁和各种性能问题都说明了这一点。虽然这可能会引起一些同情,但当面对与内存管理相关的棘手问题时,同情很快就会转化为愤怒。在本节中,我将解释如何管理共享池内存、多年来的管理进展、如何分配和释放内存、如何处理可能出现的4031错误,以及最后如何解决共享池锁存器争用。
From Hashing to Subpools
在Oracle 7和Oracle 8i中,共享池管理是在一种有趣的哈希结构帮助下执行的。如果还记得我们关于cache buffer 哈希链与library cache哈希链,那么这将非常有意义,但这里存在一种苦恼。当一个进程需要共享池中的内存时,它产生的哈希和链与所请求的内存大小相关。链也通常被称作heap,它是可用内存块链表。因此,从概念上讲,前几个链与大约1KB的内存块相关,后几个链与大约2KB的内存块相关,以此类推。虽然这确实很巧妙,但是经过一段时间对大小不一致的内存进行分配和释放之后,链实际上可以变成几千个节点长。请记住,哈希缓冲区链的大小平均在0到1之间。所以一个由几千个节点组成的链是巨大的。更糟的是,只有一个共享池latch锁来覆盖所有哈希链!清洗共享池帮助很大,因为链将减少到一个可观的规模。但这无法操作大型生产数据库,因此Oracle不得不进行更改。
Oracle9i引入了子池,这自然会导致多个共享池锁存器。基于哈希的策略被多个子池替换,每个子池包含一个在标准LRU策略上操作的堆。Oracle也开始标准化内存需求大小,这增加了找到可接受内存大小块的可能性。子池、多个共享池锁latch和LRU策略极大地减少了共享池内存管理问题。如果您同时管理过Oracle8i和Oracle9i系统,您可能会经历这种变化,并注意到有很大的不同。
数据库系统中共享池子池的数量可以通过查看实例参数_kghdsidx_count或通过计算x$kghlu视图中的行数来判断。
下面的查询显示了与共享池子池相关的一系列SQL语句。在这个例子中,一个大小为800MB的共享池存在三个子池。x$ksmss查询对于每个子池返回一行记录并且如果存在java pool还会另外加一行记录。设置子池数量的实例参数_kghdsidx_count不能被动态修改。如果你想影响Oracle调用一个子池号发生改变,你必须设置实例参数并回收实例。
SQL> @spspinfo SQL> select sum(bytes/1024/1024) sp_size 2 from v$sgastat 3 where pool='shared pool'; SP Size (MB) ------------ 800 SQL> select count(*) no_sp from x$kghlu; Num of SPs ---------- 4 SQL> select INST_ID, KSMDSIDX, KSMSSLEN 2 from x$ksmss 3 where ksmssnam='free memory'; INST_ID KSMDSIDX KSMSSLEN ---------- ---------- ----------- 1 0 301989888 1 1 18818468 1 2 12659340 1 3 7697300 1 4 20482152 SQL> select i.ksppinm param,v.ksppstvl value 2 from x$ksppi i, x$ksppcv v 3 where v.indx=i.indx 4 and v.inst_id=i.inst_id 5 and i.ksppinm='_kghdsidx_count'; PARAM VALUE -------------------- ----- _kghdsidx_count 4
Oracle对子池的数量设置了严格的限制。在Oracle 11g中,可以使用7个共享池子池来启动实例,但有8个子池,该实例没有启动——实际上,在重新启动之前需要关闭实例。
有趣的是,Oracle不必遵从子池号的意愿。实际上,在一个类似于上面查询结果的Oracle数据库11.1g的示例中,实例参数被设置为2,实例重新启动,但是Oracle创建了三个子池。在Oracle数据库11.2g中,实例参数再次被设置为2,实例重新启动,并且按照指定的Oracle创建了两个子池。在没有手动设置实例参数的情况下运行Oracle数据库11.1g和11.2g, Oracle只创建了一个子池。因此,尽管你可以影响甲骨文,它仍然保留做出改变的权利。
内存分配与回收
内存分配是相当简单的。它遵循标准的LRU算法并与pinning与locking一起使用。当一个Oracle进程(服务器或后台进程)请求内存时,Oracle内核中的一部分称作为heap manager(堆管理器)的会被执行。虽然细节不断变化,但概念算法基本相同。
Oracle进程需要特定数量的内存,这些内存被转换为多个特定大小内存块的请求。堆管理器搜索与每个请求匹配的单个大小的内存块。多个内存块(认为是非连续的)是不行的。如果进程请求4KB内存,堆管理器必须从共享池内存中返回4KB内存块的地址。
在Oracle9i中,Oracle进程获得子池latch,并将在放弃之前搜索子池至多5次。因为内存的情况可能会发生急剧且快速的变化,允许多次传递会增加找到内存的可能性。然而在五次搜索之后,当持有各自共享池latch时,如果合适的大小的内存块没有找到,Oracle将会放弃,并posts错误代码4031,“out of mmemory”信息,并且会话将会停止处理。对于每个Oracle DBA来说,这在生产系统中将是不可接受的。
在Oracle Database 10g中,Oracle进程对内存的要求更加强烈。如果在五次搜索之后在当前共享池中没有找到合适的内存,进程将移动到另一个子池。这一过程将继续进行到所有定义的子池被搜索完为止。如果在这时,没有找到合适的内存,就像以前一样,Oracle将会放弃并posts 4031错误并且停止处理。Oracle在这个版本中所做的是消耗更多CPU和更长时间地持有共享池latch来减少返回错误消息的机会。从数据库操作的角度来看,性能较慢总比没有性能好。在我们解决性能问题时,至少可以执行工作。
当内存不足时,Oracle将回收不被频繁访问的内存块。可能在尝试检索SQL语句的文本时遇到过这种情况,并且它不再缓存在共享池中。幸运地是,Oracle不会回收内存供其它对象使用。例如,如果一个游标被pinned(固定)了,Oracle将不会回收相关的内存,不管该内存是不是被频繁访问。事实上,即使清除共享池也不会删除被固定的游标。如果真的想清空共享池并且想从头开始,可以重启实例。
共享池latch竞争识别与解决
共享池latch被用来序列化共享池内存的管理。这意味着像搜索内存,LRU活动,分配内存与回收内存请求共享池latch这样的操作。因为从Oracle 9i开始存在多个子池,并且每个子池有它自己的共享池latch,只要使用这个版本或之后的版本就可以大大减少共享池lat这个特别的解决方案非常简洁,因为它只需要很少的工作,而且我们不是在玩弄Oracle的共享池LRU算法。但是,请记住,更多的子池可能需要更多的共享池内存,需要重新启动实例才能使实例参数更改生效,Oracle保留不尊重您的建议的权利ch竞争的可能性。但有些时候这些仍然不够。下面有些方案可能减少latch获取时间latch持有时间或者两者都减少。
固定大且频繁使用的对象
此策略用来确保对象成功进入缓存,不用管内存活动或对象大小。任何包第一次被调用时,整个包被加载到内存中。操作中在激活共享池后如果需要触发,将强制执行大量的内存管理活动,这将导致对象不能被加载而触发4031错误。即使如果对象被成功加载,用户可能会注意到应用程序的延迟。
有些时候可能想要固定小对象。例如,假设一个对象有一种高强度活动模式,长时间的暂停导致对象的内存被释放,然后是另一段高强度活动。为了确保没有应用程序延迟且为了减少内存管理,我们可以简单固定对象。
大多数大型Oracle应用程序都提供一个脚本,其中包含要固定在共享池中的对象,并且它们将建议在实例启动后立即运行该脚本。重要的是要知道,即使您的应用程序供应商提供了这样一个列表,您也可以通过了解您的组织实际使用对象的方式来细化这个列表。供应商应用程序开发人员通常会创建固定列表。然而大多数应用程序开发商认为他们的对象是最重要的并且应该总是被固定。但实际上,很多时候,在应用程序在生产环境中运行之前,没有人真正知道您的组织将如何使用它。因此如果出现4031错误,这对于修改固定列表来说是一个好消息。
想要确保对象总是固定在共享池中有四个简单步骤要操作。关键词pin常被使用,dbms_shared_pool包的keep函数被用来确保对象保留在共享池中。缺省情况下当创建数据库时这个包不会被加载,因此第一步就是要加载它。下面的代码就是用来创建这个过程。
[oracle@jytest1 ~]$ sqlplus / as sysdba SQL*Plus: Release 12.2.0.1.0 Production on Mon May 6 14:30:28 2019 Copyright (c) 1982, 2016, Oracle. All rights reserved. Connected to: Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production SQL> @$ORACLE_HOME/rdbms/admin/dbmspool.sql Session altered. Package created. Grant succeeded. Session altered.
下一步骤是找到大的或频繁的对象。Oracle保持对共享池对象使用进行跟踪并且可以通过v$db_object_cache视图来查看这些信息。下面是使用OSM脚本dboc.sql来识别潜在的对象。您可能会看到一组比其他包大得多的包,以及执行得比其他包频繁得多的包。还可能有一些对象,您个人知道它们具有不同寻常的执行配置文件,而您希望缓存它们。
一旦有了要保存的对象列表,下一步就是确定如何将它们放入缓存中。keep函数用于固定对象,或者更好地说,用于将对象保存在共享池中。
SQL> @dboc 10 20 old 9: where a.sharable_mem >= &min_size new 9: where a.sharable_mem >= 20 old 10: and a.executions >= &min_exec new 10: and a.executions >= 10 DB/Inst: jy/jy2 07-May 08:26am Report: dboc.sql OSM by OraPub, Inc. Page 1 Oracle Database Object Cache Summary Obj Exe Size Owner Obj Name Type Loads (k) (KB) Kept? ------------ ----------------------------------- ---- ----- ----- ----- ----- SYS DBMS_STATS_INTERNAL PBDY 0 32 492 NO SYS PLITBLM PKG 0 8 8 NO SYS DBMS_ASSERT PBDY 0 6 16 NO SYS STANDARD PBDY 0 3 32 NO SYS DBMS_STATS_INTERNAL PKG 0 1 122 NO SYS DBMS_SQLDIAG PBDY 0 1 40 NO SYS DBMS_SQLTUNE_UTIL0 PBDY 0 1 16 NO SYS DBMS_AUTO_TASK PBDY 0 0 24 NO SYS DBMS_AUTO_TASK PKG 0 0 28 NO SYS DBMS_STANDARD PKG 0 0 44 NO SYS DBMS_ADVISOR PBDY 0 0 69 NO SYS DBMS_SQLTUNE_UTIL2 PBDY 0 0 20 NO SYS DBMS_UTILITY PKG 0 0 12 NO SYS PRVT_ADVISOR PBDY 0 0 176 NO SYS DBMS_SQLTUNE_UTIL1 PBDY 0 0 57 NO SYS DBMS_STATS_ADVISOR PBDY 0 0 167 NO SYS DBMS_SYS_ERROR PBDY 0 0 8 NO SYS DBMS_OUTPUT PBDY 0 0 12 NO SYS DBMS_UTILITY PBDY 0 0 57 NO SYS DBMS_PDB PBDY 0 0 12 NO SYS DBMS_STATS_ADVISOR PKG 0 0 24 NO SYS DBMS_SQLTUNE_INTERNAL PBDY 0 0 532 NO 22 rows selected. SQL> l 1 select a.owner ownerx, 2 a.name namex, 3 decode(a.type,'PACKAGE','PKG','PACKAGE BODY','PBDY','FUNCTION','FNC','PROCEDURE','PRC') typex, 4 a.loads/1000 loadsx, 5 a.executions/1000 execsx, 6 a.sharable_mem/1024 sizex, 7 a.kept keptx 8 from v$db_object_cache a 9 where a.sharable_mem >= &min_size 10 and a.executions >= &min_exec 11 and a.type in ('PACKAGE','PACKAGE BODY','FUNCTION','PROCEDURE') 12* order by executions desc, sharable_mem desc, name
为了将一个游标保存在共享池中,从v$sql,v$sqlarea或者v$open_cursor中收集它的地址与哈希值。下面的代码显示地址(6877c238)和哈希值(1356456286)在它们之间使用逗号进行连接作为一个参数输入,第二个参数是C,因为我们要保存一个游标。对于保存触发器参数为T,对于序列,使用Q,对于包,过程与函数,参数为P。
SQL> exec dbms_shared_pool.keep('6877C238,1356456286','C'); PL/SQL procedure successfully completed.
上面的代码片段可以用于编程结构,但是大多数人发现下面的选项最容易使用。下面的代码用来保存jy方案中的TuoMi过程。
SQL> exec dbms_shared_pool.keep('jy.TuoMi'); PL/SQL procedure successfully completed.
最后,在发出上述代码片段之后,您可以轻松地进行检查,以确保确实保存了对象。从下面的输出结果可以看到jy.TUOMI过程对象的Kept列被设置为YES。
SQL> @dboc 0 0 old 9: where a.sharable_mem >= &min_size new 9: where a.sharable_mem >= 0 old 10: and a.executions >= &min_exec new 10: and a.executions >= 0 DB/Inst: jy/jy2 07-May 08:48am Report: dboc.sql OSM by OraPub, Inc. Page 1 Oracle Database Object Cache Summary Obj Exe Size Owner Obj Name Type Loads (k) (KB) Kept? ------------ ----------------------------------- ---- ----- ----- ----- ----- SYS DBMS_STATS_INTERNAL PBDY 0 32 492 NO SYS PLITBLM PKG 0 8 8 NO SYS DBMS_ASSERT PBDY 0 6 16 NO .............. SYS DBMS_SQLTUNE_INTERNAL PKG 0 0 71 NO JY TUOMI PRC 0 0 36 YES SYS DBMS_SMB_INTERNAL PBDY 0 0 32 NO SYS DBMS_SQLTUNE PKG 0 0 32 NO .............. 99 rows selected.
经常有人问我,多长时间修改一次固定列表。就我个人而言,除非有很好的理由,否则我不喜欢调用任何数据库更改。改进固定列表的一个很好的理由是,如果系统突然开始出现共享池latch争用,或者遇到4031个错误。这一点非常重要:如果添加了应用程序功能、发生了应用程序升级或应用程序使用发生了显著变化,则从更主动的角度细化固定列表。
清空共享池
虽然不在任何列表的最上面,但是只要刷新共享池就可以立即缓解共享池latch争用。对于oracle9i之前的系统尤其如此,那时还不存在子池。这很明显不是一个最优解决方案,因为每个对象都没有固定在共享池中将被删除并且它们的内存会被回收。初始结果可能会适得其反,因为它可能会导致立即进行大量的硬解析,正如我们所知,这会消耗大量CPU资源,并强制执行非自然数量的锁。然而,这种不幸的情况很快就会平息下来。
有时,共享池大小的组合,数据库版本(Oracle 9i)与应用程序的使用将使DBA别无选择,只能计划定期共享池刷新。这就是现实情况。
如下面的代码片段所示,刷新共享池非常简单,但效果确实显著
SQL> alter system flush shared_pool; System altered.
增加子池数量
最简单、最强大和最合适的共享池latch解决方案之一是简单地添加子池,增加子池也将增加共享池latch。前面的“从哈希到子池”小节详细介绍了这个过程。这个特别的解决方案非常简洁,因为它只需要很少的工作,而且我们不是在玩弄Oracle的共享池LRU算法。但是,请记住,更多的子池可能需要更多的共享池内存,需要重新启动实例才能使实例参数更改生效,Oracle保留不尊重您的建议的权利。
减少共享池大小
这听起来可能很奇怪,在子池存在之前,增加共享池大小最终可能导致共享池latch争用。每一种算法的性能都是有限的,都是针对特定情况而设计的。当情况发生变化时,算法可能无法按预期执行。不要忘记,增加缓存来支持更多的活动几乎总是需要更多的CPU资源来管理。因此,可能会有一个收益递减点。Oracle最初的共享池内存管理算法在大约600MB的共享池中运行得相当好,但是当它达到750MB左右时,dba开始看到大量的共享池latch争用是非常常见的。一旦引入了子池,特别是与我概述的其他解决方案相结合,共享池latch争用就可以成功地解决。
4031错误解决方案
Oracle在决定什么时候放弃,什么时候继续使用CPU和保持latches之间有一个微妙的平衡。多年来,Oracle耗尽共享池内存的可能性已经降低,但是4031错误的几率仍然高度依赖于Oracle共享池内存的数量和应用程序。下面是一个实际的4031错误消息
ORA-04031: unable to allocate 4192 bytes of shared memory ("shared pool","SELECT * F...","sql area (6,0)","kafco : qkacol"):4031:375:2008:ocicon.c
上面的信息显示,4KB内存正尝试在子池6中进行分配,但是由于某些原因,不能完成分配。幸运地是有一些方法来减少收到4031错误的机会。
清空共享池
与解决共享池latch争用一样,4031错误的一个解决方案也是清空共享池。虽然没有DBA愿意承认定期清空共享池,但这仍然有效。根据Oracle版本、分配的共享池内存的数量以及应用程序独特的内存使用模式,这可能是您的最佳选择。对于oracle 9i之前的系统尤其如此。
增加共享池大小
从概念上讲,增加共享池内存为Oracle提供了更大的灵活性来满足内存请求。然而,除了好处之外,在转移计算资源时也总是有成本的。在大多数情况下,收益实际上大于成本,因此,如果操作系统有可用内存,或者可以将内存从其他Oracle缓存转移到共享池,增加共享池内存很可能减少4031个错误。
请记住,每当您要求Oracle管理更多内存时,都需要更多的CPU来管理这些内存。在oracle9i之前的系统中尤其如此,因为可能存在非常长的内存链堆。如果链有数千个块长,而4031个错误可能会消失,那么在试图获取共享池latch和扫描长链时,这种情况可能会表现为严重的共享池latch争用和大量CPU消耗——所以要小心。
如Oracle文档所述,如果您通过自动内存管理获得解放,您可能需要设置最小的共享池大小。在增加缓冲区缓存的过程中,Oracle会自动减少共享池的大小,以至于开始出现4031个错误。
增加共享池保留大小
当一个较大的包最初被加载到一个已经非常活跃的共享池中时,就会出现一个常见的内存分配挑战。共享池越活跃,特别是当它很小并且对象大小非常不同时,就越有可能找不到所需的内存。
假设我们的服务器进程需要内存来存储一个大游标。当Oracle搜索共享池内存时,如果对象大小大于阈值,Oracle首先搜索保留区域。如果在保留区域中没有找到内存,Oracle将到非保留区域搜索。这种策略有助于将较小的对象排除在保留区域之外,从而将其保留给较大的对象。
有三个实例参数可以组合使用:
.shared_pool_reserved_size被用来直接设置共享池保留大小以字节为单位
.隐藏参数_shared_pool_reserved_pct,它的缺省值为5(5%),可以被用来代替shared_pool_reserved_size。
.一个相对大的对象是由实例参数_shared_pool_reserved_min_alloc来定义的,它的缺省值为4400字节。有趣的是缺省值4400字节仅仅比常见的单个块请求4096字节大。因此,在默认情况下,Oracle表示任何大于一个典型大小的内存块请求都被认为是大的,因此应该从保留的大小中获得内存。
前两个参数中的任何一个都可以用来为相对较大的对象设置共享池保留内存大小。如果您设置其中一个参数,Oracle将计算另一个参数。通过仔细调整这些参数,性能分析人员可以增加进程找到大量内存的可能性,同时仍然维护大量内存给相对较小对象使用。虽然这些参数通常不会调整,但如果发生4031错误,它们的小心调整可能会修复问题。
最小化游标固定时间
当执行游标时,游标也被固定。毕竟,您不希望SQL语句在执行期间突然消失!这是好消息。潜在的坏消息是,当执行完成时,固定游标被释放。如果没有其他进程固定游标,Oracle可以随意销毁,即释放关联的内存。现在假设有人想重新执行游标。如果它已被释放,将执行硬解析,因为整个游标将被重建!每个应用程序使用模式都是独特的;因此,当与更小的共享池或许多独特的SQL语句(或者两者都有)结合使用时,内存管理和库缓存活动可能会变得异常紧张。减少硬解析的一种方法是固定游标,使它们不能被释放。
Oracle提供了一个特殊的实例参数,该参数将保持所有会话的所有游标固定到关闭游标为止。但是,这种好处是以增加共享池内存消耗为代价的,因此,增加了接收4031错误的可能性。Oracle非常清楚这一点,所以为了鼓励回收释放内存并降低发生4031个错误的可能性,cursor_space_for_time实例参数默认设置为false。
如果系统正经历4031错误,你应该要检查cursor_space_for_time参数值。如果你的系统在过去某个时间点已经经历了严重的共享池latch急用,那么可以理解有人将cursor_space_for_time设置为true了。虽然你可能不会决定设置cursor_space_for_time参数为false。但这是一个有效选项应该被考虑。
减小保留对象的内存消耗
如果有太多对象通过执行dbms_shared_pool.keep过程被强制保留在共享池中,它们可能会消耗大量的内存Oracle可能无法成功地管理剩下的内存。此外,如果没有将大型对象保存在共享池中,则实例已经运行了一段时间,然后引用该对象,当强制加载该对象时,内存可能不可用。关键是不要随意地将对象保存在共享池中。
升级数据库版本到10gr2
当然,4031个错误不是升级的惟一原因,而是从Oracle数据库10gr2开始将内存标准化为4KB块。虽然我永远不会仅仅因为这个改进就建议升级到这个版本,但是这可能是升级的一部分原因。
就像对段区大小进行标准化一样,拥有标准的内存块大小可以提高快速找到合适内存的可能性。可以找到的内存越快,消耗的CPU周期就越少,必须持有共享池latch的时间就越短,存在大量浪费的小内存块的可能性就越小(增加4031错误的可能性)。