尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Oracle 11g升级19c后变慢?执行计划漂移排查与SPM固化实战

Oracle 11g升级19c后变慢?执行计划漂移排查与SPM固化实战 十一月的某个周五傍晚我正等着下班。同事说数据库从11g切换到19c的窗口顺利完成应用侧也做了切流压测数据看着一切正常。结果第二天上午业务一进来情况就完全不同了报表查询转圈核心接口大面积超时应用侧的报警群从九点半开始就没停过。最难受的是数据库的CPU、IO、内存指标看起来都还算健康应用服务器的负载也不高但用户体感就是“慢到没法用”。这套系统从Oracle 11g升到19c之后应用系统运行缓慢、接口超时的问题我前前后后排查了整整一个周末才彻底按住。今天就把整个排查链路和处置方案写出来给后面要踩同一趟水的同学一个参照。这个问题的适用人群很明确正在做或准备做Oracle 11g到19c迁移的DBA、运维工程师、应用架构师。它不是什么高深莫测的底层故障而是一连串“看起来都正常但叠加起来就坏菜”的细节问题。核心结论先放在这儿多数迁移后的“变慢”不是数据库变慢而是执行计划在对齐新版本优化器时发生了漂移再加上一些统计信息和默认参数上的差异把慢SQL放大到了用户可感知的接口超时。下面按我实际的排错过程展开写。1. 切换当天的异常画像数据库“显示正常”接口却截在超时边缘1.1 从压测绿灯到线上血崩中间只隔了一个业务高峰切换当天晚上我们的步骤是这样的先做数据库层的数据校验表数量、行数、序列、同义词逐一比对通过后再让应用网关切流最后用自动化脚本跑了一轮基础压测。压测结果确实不错核心接口平均耗时都在几百毫秒以内。所以我们当时是带着比较轻松的心态离开机房的。第二天上午十点业务侧开始集中操作问题就爆发了。其实不是某一个接口挂掉而是“所有查询类接口集体变慢”。数据库的CPU使用率只有30%左右磁盘IO的avg wait也小于2ms但从应用监控看接口P99响应时间从128ms直接拉到了3到8秒甚至出现了一些超过30秒的读取请求直接触发网关超时。这个现象非常典型数据库侧“没病”应用侧“很痛”。这种情况下千万不要急着去调应用代码、换连接池或者重启数据库。第一次排查这类问题的人最容易犯的错误就是把有限的精力全砸在“看起来最可疑”的监控图上结果转了一大圈也没定位到真正的断点。1.2 用AWR和ASH把“体感慢”翻译成数据库语言既然数据库底层的CPU和IO都不高那就必须回答一个更尖锐的问题DB Time去哪里了Oracle的AWR报告里“DB Time”和“CPU Time”的差距就是判断慢SQL是否存在的最直观窗口。我拉取了切换前后的两组AWR快照对比发现切换前DB Time和CPU Time基本持平说明绝大多数时间在CPU上“干正事”而不是等待切换后DB Time几乎接近CPU Time的两倍这就说明大量会话陷入了等待事件。进一步看了ASHActivity Session History中的TOP事件前几名集中在这三个上db file sequential read、cursor: pin S wait on X、以及部分library cache lock。尤其是db file sequential read的等待飙升往往意味着SQL在执行时做了大量单块读也就是说它在走索引访问但访问方式出了问题比如嵌套循环中反复读取同一个数据块。到这步问题方向已经很清晰了数据库内部一定存在一批执行路径不合常理的SQL而不是网络、存储或应用服务器的容量问题。1.3 先排除掉三片“假嫌疑”省下三分之二的排查时间拿到上面结论以后我仍然花了一些时间把三块噪音排掉因为如果不清掉这些“假嫌疑”后面说再多SQL问题应用和网络团队的同事也不会信服。第一块是监听问题检查监听日志确认没有大量的连接延迟或异常断连监听进程数、连接速率都和切换前一致。第二块是网络链路这次新数据库的IP和端口还过了防火墙转发我专门从应用服务器到数据库主机做了TCP往返时延测试稳定在1.8ms左右属于正常范围。第三块是应用连接池参数确认maxActive、minIdle、最大等待时间都跟迁移前保持一致没有因为新库配置模板覆盖产生隐性的连接数瓶颈。排完这三块问题就可以基本收敛到数据库内部执行层。这里想提醒所有做迁移的人接入层、网络层、连接池的排查要放在SQL层之前做但也要“快进快出”不能等项目组都被拉进来一起开会了SQL层面的分析还没开始。我给自己定的时间盒是一个小时超过这个时间必须往数据库深水区走。2. 执行计划漂移一条SQL从毫秒到十秒的真正转折点2.1 同一张表、同一条SQL优化器为什么敢换路线Oracle的CBO基于成本的优化器并不是一个静止的程序它会根据optimizer_features_enable和一系列内部参数来决定自己“按哪个版本的行为方式”工作。11g到19c中间横跨了12c、18c等多个版本优化器的基数估算算法、直方图使用策略、动态采样阈值都有明显变化。换句话说表结构没变、索引没变、SQL文本没变但优化器用来做决策的“尺子”变了。所以在看似完全一致的环境里执行计划漂移是正常现象不漂才不正常。打个比方同样一张城市地图一个司机习惯走老城区的窄路因为他在老版本导航软件里学到的是“小路上虽然弯多但更快”换成新版导航后算法告诉他“直行大路优先级更高”于是他从第一个路口就走了一条完全不同的路线。路还是那些路但决策逻辑变了。这个类比用在11g到19c的迁移上特别贴切。2.2 统计信息“原样搬家”却给了19c一本过期的账本我们当时的迁移方式是expdp/impdp这个过程中有一个很容易被忽略的细节导出时会默认把统计信息也带走。表面上看这是好事省去了新库重新收集统计信息的等待但实际埋下了大坑。开发环境或预发环境的统计信息被原样导入了生产库里面记录的NUM_ROWS、列基数、直方图数据和生产库切换后真实的数据分布压根对不上。更麻烦的是投产后的前几个小时内业务还在不断写入新数据而自动统计任务虽然在凌晨跑了但19c的DBMS_STATS默认偏好和11g不完全一样。比如incremental、method_opt、granularity这些参数的取值变化会导致统计信息刷新后某些关键列的直方图仍然失真。结果就是优化器的估算严重偏离实际行数走了成本更低的错误计划。这类问题有个特点你不能说统计信息“没更新”因为它确实更新了但更新后的数据也不足以支撑优化器作出正确判断。排查的时候不要只看DBA_TAB_STATISTICS里的LAST_ANALYZED时间更要把关键表的NUM_ROWS和真实表数据量做一次核对。我当时就发现一张核心业务表的统计信息行数是1200万但实际扫出来只有370万整整差了3倍多。2.3 抓现场用SQL Monitor和ASH锁定三条“最慢样本”说回定位手段。我不是靠运气找到那几条慢SQL的靠的是V$ACTIVE_SESSION_HISTORY和V$SQL_MONITOR的组合拳。先通过ASH按会话等待事件聚合找出占用时间最长的几个SQL_ID再通过实时SQL监控视图确认这些SQL在执行计划中的实际行源。这里给出一个我常用来捞慢SQL的参考SQL在新的运维环境里同样适用SELECT sql_id, plan_hash_value, round(elapsed_time/1000000, 2) as elapsed_sec, executions, round(rows_processed/executions) as avg_rows FROM gv$sql WHERE executions 0 AND command_type 3 AND last_active_time sysdate - 1 ORDER BY elapsed_time DESC FETCH FIRST 10 ROWS ONLY;结合V$SQL_MONITOR里输出的执行细节我锁定了三条SQL它们共同出现在一个核心查询链路上主表关联明细表、再过滤最近七天的数据、按批次号排序返回。这个SQL在11g时代执行计划走的是HASH JOIN配合主表的索引扫描耗时稳定在200ms左右在19c上执行计划变成了NESTED LOOPS驱动表还是一个没走索引的全表扫描。毫秒和十秒的差距就这么来的。注意gv$sql里的last_active_time字段在19c里仍然可用但如果库在RAC环境务必用gv$而不是v$否则会漏掉其他实例上的慢SQL样本。3. 19c三个新默认值看着是“优化”实则是OLTP接口的隐形杀手3.1 自适应计划运行时悄悄替换连接方式19c一个很重要的变化是默认开启了自适应计划Adaptive Plan。这个概念本身很漂亮优化器先生成一个默认计划然后在真正执行时探测实际的基数如果发现和预估偏差过大就会自动切换备选方案。比如原来计划里用的是哈希连接运行时发现右边的结果集其实非常小就会改成嵌套循环。问题也出在这个“自动”上。它不是稳定不变的而是在不同执行批次间来回摇摆。那三条慢SQL里其中一条在第一次执行时还被优化器选为哈希连接到了第二次、第三次执行因为绑定变量不同、探测到中间结果集变小就跳成了嵌套循环。对于OLTP高频接口来说这种“计划漂移”就是灾难同样的接口请求一会快、一会慢一旦漂移到次优计划数据库会话就会长时间占用最终表现为接口超时。平时在做迁移方案时很多团队不会专门去关这个特性。但真实的生产环境里业务请求模式是多变的自适应计划的不确定性经常抵消掉它理论上带来的收益。我后面花了很大的力气做执行计划固化其中一个重要原因就是要把这种“运行时随机切换”从业务链路里彻底拿掉。3.2 统计信息自动失效策略旧计划在缓存里“赖着不走”另一个容易被忽略的默认行为变化是统计信息更新后的游标失效策略。在11g时代重新收集统计信息后相关游标通常很快失效下一条SQL会按新的统计信息重新解析。19c的默认行为则温和得多它不会在统计信息更新后立刻把所有旧游标全部失效而是采用了相对保守的自动失效算法。这对我们当时的排查造成了很大的干扰。我第一天下午手动跑了一次DBMS_STATS.GATHER_TABLE_STATS把关键表都收集了一遍期待执行计划快点恢复正常。结果发现AWR里依然能看到旧计划的影子有些连接仍在使用更新前的游标。普通人看到统计信息已经“刷新”了就会觉得SQL层没毛病转头去查其他环节。实际上把DBMS_STATS调用的no_invalidate参数设为FALSE才能强制让旧游标失效、及时让新计划生效。这一点在迁移后头几天的敏感期内特别重要。3.3 连接内部游标复用变差并发接口雪上加霜第三个细节不在优化器层而在连接和游标管理。11g切到19c之后应用服务器和数据库之间的会话连接是全新建立的shared pool里那些被长期缓存的游标在切换后往往不再命中。加上有些老系统写的SQL文本里没有统一大小写、没有使用绑定变量或是在连接池里长时间复用同一个连接却不断执行不同的SQL就会导致parse count显著升高。从AWR的实例统计里可以看到% SQL with executions1这个指标从迁移前的99%以上掉到了85%左右parse count total几乎是切换前的两倍。单独看每一条SQL解析时间不高但在高并发接口场景下所有请求同时间涌进来解析压力就被成倍放大进而演化成cursor: pin S wait on X和library cache lock等待。这也是为什么我在前面说不能只看DB CPU还要看会话在等什么。4. 完整处置流水线从锁定TOP SQL到把计划彻底焊死4.1 先用AWR和DBA_HIST_SQLSTAT把所有证据固定下来排错的第一步不是改参数而是固定现场。我新建了一个专门的临时表空间给SQL Tuning Set用然后把AWR窗口里耗时前20的SQL全部导出来形成了升级后的完整基线。这样做的好处是后面改任何一个参数都可以拿这个基线去对照效果。BEGIN DBMS_SQLTUNE.CREATE_SQLSET( sqlset_name UPGRADE_19C_TOP_SQL, description Capture top sql after upgrade to 19c); DBMS_SQLTUNE.CAPTURE_CURSOR_CACHE_SQLSET( sqlset_name UPGRADE_19C_TOP_SQL, time_limit 3600, repeat_interval 60, top_sql_limit 100); END; /等这个采集跑完就可以从DBA_HIST_SQLSTAT里去验证几组关键指标哪个SQL的ELAPSED_TIME变化最大、PLAN_HASH_VALUE是否发生过切换、EXECUTIONS有没有下降。把证据链钉死接下来的每一步整改都在这个框架里进行避免陷入“东改一下西看一下”的泥潭。4.2 用DBMS_XPLAN对比新旧计划把断点定位到具体行源光知道SQL慢还不够要精确知道它慢在哪一步。我是这样做的先从AWR历史里拿到这条SQL在切换当天和切换前的PLAN_HASH_VALUE再分别用DBMS_XPLAN.DISPLAY_AWR把两份执行计划打出来进行逐行对比。SELECT * FROM TABLE( DBMS_XPLAN.DISPLAY_AWR( sql_id 7h8k2x9f4qjw1, plan_hash_value 3500683184, format ALL));对比结果很清楚旧计划中主表通过索引ROWID回表然后驱动明细表做HASH JOIN新计划里驱动表直接走了TABLE ACCESS FULL明细表也放弃了索引改用全表扫描后再做嵌套循环。旧计划成本大约3400新计划显示成本高达9000多。即便不看成本单从“全表扫描”这几个字就已经能解释为什么接口从200ms退化到5秒。这里我特别想提一个实操细节对比执行计划时别只盯着成本值要看每个操作节点的“实际返回行数”。用DBMS_XPLAN的ALLSTATS LAST格式再配合GATHER_PLAN_STATISTICS提示可以拿到真实的A-Rows和E-Rows。行数偏差超过10倍以上的节点就是计划漂移的裂缝所在。我们锁定的SQL问题节点就在明细表的过滤条件上估算行数只有80行实际返回了24000行差了300倍优化器完全按错误基数做了后续的算子选择。4.3 用SPM基线把经过验证的最优计划固化下来定位到这个程度下一步就不是继续观察等待而是主动把执行计划锁死。Oracle的SQL Plan ManagementSPMSQL计划基线就是干这个的。我先把旧库或者历史AWR中发现的最优计划加载进SQL计划基线然后通过DBMS_SPM把专家执行计划标记为accepted和fixed。我的操作思路是与其直接改SQL文本加提示hint不如先用SPM在数据库侧做一层“兜底约束”。这样可以不改应用代码、不影响其他SQL单独把问题SQL的计划固定住。DECLARE v_plans_loaded PLS_INTEGER; BEGIN v_plans_loaded : DBMS_SPM.LOAD_PLANS_FROM_CURSOR_CACHE( sql_id 7h8k2x9f4qjw1, plan_hash_value 3500683184, fixed NO); DBMS_OUTPUT.PUT_LINE(Loaded plans: || v_plans_loaded); END; /基线生效后我在DBA_SQL_PLAN_BASELINES里确认了该计划的状态是ACCEPTED。接下来连续观察了三个业务高峰接口P99稳定回落到300ms以内超时日志基本消失。SPM的好处是它不是简单粗暴地禁用新的计划而是让优化器知道“这个已接受的计划优先选用”后续即使统计信息再变化也不会突然跳走。这一点对于长期稳定运行非常重要。4.4 统计信息矫正与回归验证改一步、验一步基线固定后我又返回去把统计信息的问题补完。这里有一点经验教训统计信息收集和SPM固化最好不要在同一时间做。一次变更里只改一件事后面出问题时才能准确归因。我当时先把SPM基线设置完成确认计划已经锁死之后才去重新收集统计信息。具体收集时把no_invalidate显式设为FALSE强制旧游标失效让新统计信息马上参与解析BEGIN DBMS_STATS.GATHER_TABLE_STATS( ownname BUSINESS, tabname CORE_ORDER, method_opt FOR ALL COLUMNS SIZE AUTO, cascade TRUE, no_invalidate FALSE); END; /收集完以后我再次对照AWR发现DBA_HIST_SQLSTAT里已经没有旧的PLAN_HASH_VALUE在继续贡献耗时了所有执行都落入SPM基线计划的轨道里。回归验证这一步我特意用了一个包含多个绑定变量取值区间的测试脚本分别用高频值、低频值和空值跑一遍确认接口耗时都在正常范围内。这一步很值得做因为很多“恢复”都是假恢复只是某个特定取值下刚好碰到了好计划。配合回归验证我在监控看板上放了四个指标接口超时率、SQL平均耗时、等待事件Top5、计划基线接受数。前两个偏业务视角后两个偏数据库视角任何一个异常都能快速定位到具体层。5. 这次切换踩坑后我沉淀出的“迁移前性能体检单”5.1 升级前先做“影子库SQL回放”把计划漂移提前暴露这次问题最大的教训就是不要等到生产切完再去看执行计划。11g到19c这么大的版本跳变提前用SQL Tuning Set做一次影子库回放代价远远低于事后救火。具体做法是在11g生产库上用DBMS_SQLTUNE.CAPTURE_CURSOR_CACHE_SQLSET抓出一周内的TOP SQL然后导入到建好的19c测试库用DBMS_SQLTUNE或者压测工具去执行一遍直接对比执行计划和耗时。我建议至少跑满一个业务周期的数据因为很多SQL的问题只会在月底对账、批量跑批这种低频但重型的查询里暴露。我们在这次迁移里就是在回放阶段漏掉了对账类SQL导致第二天晚间跑批时又出现了一轮性能回退。好在有SPM基线托底影响范围被控制住了。5.2 一个参数对照表把19c的“新默认值”逐项过一遍经过这一遭我整理了一份简明的迁移参数对照表每次做Oracle跨版本迁移时都拿它过一遍效果非常好。这里也给出来参数或行为11g常见表现19c默认行为迁移期建议OPTIMIZER_ADAPTIVE_PLANS不适用/未启用默认开启OLTP混合负载建议评估后设为FALSEOPTIMIZER_ADAPTIVE_STATS不适用默认开启确认直方图统计无波动后再决定DBMS_STATS的失效策略更新统计信息后游标快速失效默认AUTO失效保守敏感期收集时显式NO_INVALIDATEFALSE统计信息导入存在跨版本偏移风险自动保留原值迁移后务必核对关键表NUM_ROWS必要时重收集SQL Plan Management常用但不强制仍是可选机制迁移后对TOP SQL主动建立基线这张表不复杂但每一项背后都是一次血泪教训。特别是第一行OPTIMIZER_ADAPTIVE_PLANS要不要改成FALSE建议结合业务负载看如果系统里大部分是简单等值查询、固定报表SQL那收益不大关了反而稳如果本身就有很多复杂分析型SQL可以考虑保留但必须配好SPM基线。5.3 用“接口埋点×等待事件”双层视图消除盲区最后说一个团队协作层面的沉淀。过去DBA看数据库指标应用团队看接口耗时两者经常各看各的出了问题互相扯皮。这次我们建立了一张双层的“关联视图”上层是核心接口的请求量、耗时、超时率下层是数据库的等待事件、TOP SQL、活动会话数。两边按时间轴对齐任何一个接口变慢马上能在数据库侧确认关联SQL。这个小改动让我在处理这次迁移故障时省了非常多沟通成本。切流后的所有决策都有一个“业务口径数据库口径”的共识基础而不是一方说“应用没报错”另一方说“数据库很正常”。说难听点这次能从周五下午扛到周日夜里靠的已经不是运气而是这套能把双方语言互相翻译过来的机制。回到开头那个场景如果让我再经历一次11g到19c的迁移我会把SGA与PGA内存参数的核对、统计信息重收集窗口、SPM预演这三件事全部前置而不是等到接口超时再回来补课。数据库版本升级说到底不是一个“搬数据”的活儿是一个“让旧世界在新引擎里继续稳定跑起来”的活儿。希望这份记录能让后来者少熬一个周末。
返回列表