简介:围绕某银行DB2系统临时表空间异常膨胀导致SQL执行变慢的真实案例,这份DOC文档面向DBA、数据库运维与性能诊断人员,系统梳理从ACTIVE SESSION激增、常规资源排查无果,到定位TEMPSPACE1达10GB的完整分析过程。文档结合LATCH分析、STACK收集、db2trc暂停实例等工具方法,说明临时表空间过大如何引发资源消耗、内存压力、LATCH竞争与磁盘排序开销,并给出SQL优化、参数调整和临时对象清理等处置思路,适合作为DB2性能排错与案例复盘参考。资源包仅含1个DOC文件,约607KB,轻量便于查阅;已有5903人学习,说明该案例在数据库运维群体中具有较高关注度。读者可从中获得真实故障的定位路径、监控指标判读方法、常用诊断工具的组合用法及问题解决框架,对日常性能问题分析有较强借鉴价值。
1. 临时表空间不是“临时”的:10G 的 TEMPSPACE1 如何把 OLTP 拖进 4 秒
一次典型的 DB2 性能问题排查,最容易被怀疑的是 SQL 本身、锁、缓冲池、I/O,而真正的元凶常常是那个几乎没人看的系统临时表空间 TEMPSPACE1——它不存业务数据,不参与事务提交,涨到 10GB 之后却让一堆毫不相干的简单查询从毫秒级变成 3~6 秒。更反直觉的是:拖慢查询的不是往临时表空间里写数据,而是读它的元数据。监控代理周期性对表空间做快照,快照要遍历 SMS 容器的数据页,这段代码持有 pool_table_latch,而并发查询在创建内部临时表时必须拿同一把 LATCH,于是二十多个 EDU 排成一队,ACTIVE SESSION 瞬间冲到几十。这条链路里没有一行用户 SQL 写错,搞懂它比记住任何一条调优命令都值钱。
2. 活动会话数飙升的真相:从 db2top 到 MON_CURRENT_SQL 的排查链路
业务侧反馈“系统变慢”这种信号通常是模糊的,凭感觉做出的反馈可能有偏差,也可能压根是应用层的问题。但当监控频繁捕捉到 ACTIVE SESSION 数量突破 50,而历史曲线长期在 10 以内、偶发 30,这就不是感觉问题了——数据库层面确实有东西在堵。这一章要解决的是:怎么在十分钟内确认“是数据库的问题”,并且把范围从“整个实例”收窄到“某几条语句在等某个内部资源”。
2.1 ACTIVE SESSION 到底统计了哪些状态
ACTIVE SESSION 即活动会话数,指当前正在执行的 application 数量。准确地说,正在处理 SQL 语句请求的会话都计入,而一条语句从编译到执行完成会经历多个状态:Compiling、UOW Executing、Lock Waiting、Commit Active(COMMIT 本身也是一条语句),如果启用了 WLM 的并发限制,排队中的会话处于 Queued 状态。这些状态全部计入 ActSess。反过来,一个事务里执行完上一条语句、下一条还没开始的会话处于 UOW Waiting,不计入。
这个口径差异是排查时第一个容易翻车的地方:ACTIVE SESSION 高,不代表 SQL 都在干活,也可能都在等。一般的 OLTP 系统这个数值在 10 以内,繁忙系统二三十个,再往上就要考虑扩容。当一个平时只有几个会话的系统突然频繁冲到几十,只有两种可能——业务访问量真的涨了,或者 SQL 执行时间变长了。跟业务负责人确认访问量没有增长,基本就可以把排查方向锁定在后者。
查看方式有两类。一类是db2top -d <dbname>,进入后按d看数据库整体指标,默认 2 秒刷新;按l(小写 L)进 SESSIONS 页面,能看到每个会话的具体状态。这次问题里活动会话的状态清一色是 UOW Executing,说明它们都在真正执行,不是在等锁。另一类是通过db2 LIST APPLICATIONS SHOW DETAIL自己统计状态分布,适合脚本化采集和留证。
2.2 快速排除法:一屏看完 CPU、内存、I/O、命中率、锁
确认是数据库问题之后,先做一轮成本最低的排除,别一上来就扎进语句分析。db2top 的数据库页面基本能在一屏内给出结论:
- CPU 利用率和操作系统内存使用量在正常区间,排除资源耗尽;
- HitRatio 正常,排除缓冲池不足导致的物理读放大;
- 没有 Lock Waiting,排除锁等待链;
- 右下角四个指标 AvgPRdTime(平均物理读时间)、AvgDRdTime(平均直接读时间)、AvgPWrTime(平均物理写时间)、AvgDWrTime(平均直接写时间)正常,排除 I/O 抖动。
这一屏里还有两个容易被误读的数字。一个是 Deadlocks 218,看着吓人,但它是累计值,只要不在持续增长就不影响当前性能;另一个是 SortOvf(排序溢出)为 10,偏高,但应用本身有大量排序语句,有溢出属于预期。判断一个指标要不要追,标准是“它是否在时间维度上和故障窗口重合”,而不是“它的绝对值是否好看”。这条经验在后面识别 TEMPSPACE1 时还会再用一次。
2.3 抓慢语句的三条路径:db2top、MON_CURRENT_SQL、MON_GET_PKG_CACHE_STMT
排除完外围,开始抓执行时间长的语句。三条路径的粒度和用途不同,配合着用效率最高。
第一条是 db2top 里的包缓存视图。在界面输入D进入动态 SQL 列表,再输入z,上方会提示Column number for descending sort:,输入4回车,即按 Avg ExecTime 降序。这次看到前几条平均执行时间接近 4 秒,但 Num Execution 都只有几次——原因是应用拼凑 SQL 字符串而没有用变量绑定,几乎每条语句对 DB2 来说都是全新的,缓存里统计不到执行次数。看到这种“次数极少但时间很长”的形态,可以直接推断同类语句全都慢,不要因为样本小就放过。
第二条是管理视图,用来抓“此刻正在跑什么”。当 ACTIVE SESSION 高企时,这条 SQL 能按已运行时长倒序列出当前语句:
-- 查看当前正在执行的语句,按已运行秒数倒序 SELECT COORD_MEMBER AS MEMBER, VARCHAR(SESSION_AUTH_ID, 10) AS USER, VARCHAR(CLIENT_APPLNAME, 10) AS APPNAME, ELAPSED_TIME_SEC AS ELAPSEDTIME, VARCHAR(ACTIVITY_STATE, 10) AS STATE, TOTAL_CPU_TIME, ROWS_READ, ROWS_RETURNED, QUERY_COST_ESTIMATE, DIRECT_READS, DIRECT_WRITES, VARCHAR(STMT_TEXT, 2000) AS STMT_TEXT FROM SYSIBMADM.MON_CURRENT_SQL ORDER BY ELAPSED_TIME_SEC DESC;ELAPSED_TIME_SEC是语句从开始执行到查询视图那一刻的累计秒数,所以抓到的值一定比完整执行时间短,这是这个视图的固有偏差,不是数据错误。另一个坑是它只反映“抓的这一刻”,语句跑完就消失,需要多执行几次才能在会话高峰时命中现场。抓到的语句数量接近 30 条,正好和 ACTIVE SESSION 的数量对上。
第三条是表函数,用来做时间分布的归因分析。这是整轮排查里信息密度最高的一个工具:
-- 按语句统计平均执行时间,并计算各类等待占总执行时间的百分比 SELECT MEMBER, NUM_EXECUTIONS, STMT_EXEC_TIME, DECIMAL((TOTAL_ACT_WAIT_TIME / DOUBLE(STMT_EXEC_TIME)) * 100, 5, 2) AS pct_wait, DECIMAL((POOL_READ_TIME / DOUBLE(STMT_EXEC_TIME)) * 100, 5, 2) AS pct_read, DECIMAL((LOCK_WAIT_TIME / DOUBLE(STMT_EXEC_TIME)) * 100, 5, 2) AS pct_lock, DECIMAL((LOG_DISK_WAIT_TIME / DOUBLE(STMT_EXEC_TIME)) * 100, 5, 2) AS pct_log, DECIMAL((TOTAL_SECTION_SORT_TIME / DOUBLE(STMT_EXEC_TIME)) * 100, 5, 2) AS pct_sort, CAST(FLOAT(STMT_EXEC_TIME) / FLOAT(NUM_EXECUTIONS) AS DECIMAL(10,3)) AS AVG_EXEC_TIME, VARCHAR(STMT_TEXT, 500) AS STMT_TEXT FROM TABLE(MON_GET_PKG_CACHE_STMT(NULL, NULL, NULL, -2)) WHERE NUM_EXECUTIONS > 1 AND STMT_EXEC_TIME > 0;三个 NULL 参数分别对应“不限定成员、不限定语句、不限定应用”,最后的-2表示获取所有成员上的全部语句;如果只想看某个成员,把第一个参数换成成员号。NUM_EXECUTIONS > 1是为了过滤掉只执行过一次的噪声语句,STMT_EXEC_TIME > 0是防止除零。
结果里能看到平均执行时间在 2~6 秒不等,其中 5 条以with开头的语句PCT_WAIT在 95% 以上——执行时间几乎全耗在等待上,但 wait 的构成里 READ、LOCK、日志写、排序都不是大头。到这里可以往查询里加TOTAL_EXTENDED_LATCH_WAIT_TIME字段,答案就浮出来了:它们在等 LATCH。另外 5 条慢语句是调用存储过程的,从这条查询看不出时间分布,属于工具的能力边界,得换个思路。
提示:pct_* 这一组百分比是基于总执行时间算的,几条语句加起来可能超过 100%,因为它统计的是“等待类型占比”而不是互斥的耗时切片,别拿它做严格的时间守恒校验。
此时很容易做出一个错误决策:既然抓到了慢 SQL,那就优化 SQL。这次也确实先这么干了——给几个全表扫描的语句建索引,快是快了,但系统整体没恢复。回过头看,那些既长又复杂的 SQL 逻辑并不是最近才上线的,应用没变更、数据库没变更,努力方向从一开始就偏了。及时打住比继续埋头优化更重要:慢语句是症状,LATCH 等待才是可追的线索。
3. LATCH 机制:DB2 内部的轻量锁与 pool_table_latch 为什么会被表空间快照抓住
从“语句慢在 LATCH 等待”到“具体是哪个 LATCH、谁持有、持有者在干什么”,中间隔着一层没有文档覆盖的黑匣子。DB2 的表函数只暴露了TOTAL_EXTENDED_LATCH_WAIT_TIME这样的汇总监控元素,对 LATCH 本身没有任何介绍性材料,这在排查时是实打实的障碍。这一章把 LATCH 的定位、pool_table_latch 的归属,以及怎么把“一瞬间”的等待抓成证据讲清楚。
3.1 LATCH 与 LOCK 的分工:保护内部结构 vs 保护用户数据
可以这样理解 DB2 中的 LATCH:它的目的是保证多线程访问内部共享资源的一致性,本质是一种轻量锁,锁的对象是数据库引擎自己的内存数据结构,不是用户的行、表、页。
举个具体例子。内存里有一个预取队列(PREFETCH QUEUE)结构,处理用户请求的协调 EDU 需要预取时往队列里塞任务,多个预取 EDU 不停地从队列里读任务。为了协调这两类 EDU 对同一个队列的访问,每次访问前都要先获取一个 LATCH,其他 EDU 等它释放才能进。
LATCH 和 LOCK 的区别由此可以推出来:LOCK 的持有期跟事务绑定,可能持续到 COMMIT,持有者是用户事务,等待者能被db2pd -wlocks或事件监控看到;LATCH 的持有期通常只有微秒到毫秒级,跟着一段临界区代码走,持有者是 EDU,不体现在任何锁视图里。这解释了一个常见的困惑:明明没有锁等待,语句却卡住不动。LATCH 是引擎内部的自旋/短等,没有“锁等待时间”这个维度,只能靠TOTAL_EXTENDED_LATCH_WAIT_TIME或直接抓现场。
3.2 pool_table_latch 保护什么:表池结构与 SMS 容器页统计
LATCH 的名字用的是内部命名规则,从字面看不出什么有用信息,pool_table_latch尤其如此。它的完整符号是SQLO_LT_SQLB_PTBL__pool_table_latch,从命名拆解:SQLO是操作系统抽象层,SQLB是缓冲池与表空间管理模块,PTBL是 pool table,最后才是 latch 类型。也就是说它保护的是“表池”这个描述表空间与容器映射关系的内部结构。
关键点在于,这个结构是全局粒度的——一个数据库实例里所有表空间的元信息共享一把 LATCH,而不是每表空间一把。任何要读表空间元信息的操作都得先拿它,包括两个看起来毫不相关的动作:
- 查询执行时创建内部临时表,需要确认系统临时表空间的位置和数据池,走
sqlbGetSysTempDatapool,要拿这把 LATCH; - 监控采集表空间快照,需要遍历 SMS 表空间的容器、统计数据页,走
sqlbSnapshotTablespace → sqlbMonSMSGetContainerPages,也要拿这把 LATCH。
前者的持有时间几乎恒定,取决于结构查找的复杂度;后者的持有时间跟容器数量和页数正相关。当 TEMPSPACE1 只有一个很小的初始尺寸时,这个遍历几微秒就结束了,谁也不会注意到;当它膨胀到 10GB、354661 个页、4 个容器时,同样的遍历要做的活儿多了几个数量级,持有时间被拉长到足以让并发查询排队的程度。这就是“表空间变大”和“查询变慢”之间那条看不见的因果链。
3.3 db2pd -latches -rep 2:把一瞬间的等待抓成证据
理解机制之后,取证是下一步。第一轮快速排除时已经跑过db2pd -latches,没发现异常——因为 LATCH 等待只有几秒就清除了,单次采样大概率撞在空档上。改用带重复参数的命令持续观察:
# 每 2 秒采样一次 LATCH 状态并持续输出到屏幕,用于捕捉瞬时争用 db2pd -latches -rep 2-rep 2的含义是每隔 2 秒重复执行并打印,不指定次数就一直跑,Ctrl+C 结束。输出里能看到 EDU ID 11665 持有类型为pool_table_latch的 LATCH,而 ID 2877、2981 等 23 个 EDU 在等同一把。23 个等待者意味着此刻至少有 24 个 EDU 被这一个临界区堵住——ACTIVE SESSION 冲到几十,源头就在这里。
采样间隔的选择有讲究。间隔太大容易错过,太小则每次输出的采集本身也会消耗资源,而且刷屏太快反而看不清。对于持续几秒量级的争用,2 秒是个能兼顾命中率和可读性的经验值;如果怀疑争用更短促,可以降到 1 秒,但要接受输出量翻倍。
注意:
db2pd -latches只给出 Holder 和 Waiter 的 EDU 编号与 LATCH 类型,不给出“它们当时在执行哪行代码”。真正定位到函数级,得靠下一章的 STACK 取证。
4. STACK 取证:db2pd -stack 与 analyzestack 怎么把 Holder 和 Waiter 的调用链对上
拿到 EDU 编号和 LATCH 类型,等于拿到了嫌疑人的身份证号,但还不知道他在干什么。DB2 的db2pd支持在收集 STACK 的同时把每个 EDU 当时的 LATCH 信息和 LOCK 信息一并抓下来,这就把“谁持有”和“持有者在执行什么代码”绑在了一起。这一章讲采集方法、两个栈的读法,以及那个藏在安装目录里的分析脚本。
4.1 采集命令与生产环境的代价
先把现场抓下来,再慢慢分析。采集脚本大致如下:
# 进入一个容量足够的临时目录,避免 STACK 文件写满根分区 cd /tmp mkdir -p stack0531 # 对实例内所有 EDU 抓栈:每隔 3 秒抓一次,连续抓 10 次 # dumpdir 指定输出目录,默认会落在 DIAGPATH 下 db2pd -stack all dumpdir=/tmp/stack0531 -rep 3 10-stack all表示实例内所有 EDU,也可以只抓指定 EDU 编号;dumpdir决定*.stack.txt落在哪,生产环境务必显式指定到一个容量足够的目录,因为一次all加十轮重复可能产生上百个文件;-rep 3 10是每 3 秒一轮、共 10 轮,这个组合是专门为“间歇性出现”的争用准备的——抓一次碰不上,就连着抓十次。
代价必须说清楚:抓栈会让每个 EDU 在信号处理点短暂停顿,数据库越繁忙,这个停顿对吞吐的影响越明显。所以采集前要评估时间窗口,尽量避开业务高峰,或者把轮次降下来。这是典型的用性能换可观测性的操作,别在问题上头的时候无脑-rep 1 100。
采集完成后先用 grep 快速确认有没有捞到目标:
# 在所有栈文件里搜索目标 LATCH,确认是否捕捉到 Holder / Waiter cd /tmp/stack0531 grep pool_table_latch *.stack.txt命中的行会形如Holding Latch type: (SQLO_LT_SQLB_PTBL__pool_table_latch) - Address: (0x...), Line: 9032, File: sqlmonss.C,或者Waiting on latch type: ... Line: 11384, File: sqlbpool.C。第一轮没抓到就再跑一轮,这种间歇性争用抓不到是常态,不是工具坏了。
4.2 两个栈的读法:从函数名反推操作类型
函数名是内部符号,看着劝退,但其实信息量很大。不用看懂每一行,只需要抓住调用链里那几个“语义明显”的函数,结合 DB2 的模块划分就能定位操作类型。
Holder 的栈(节选,关键调用链自下而上):
sqleProcessSubRequest -- 处理子请求(监控采集走的独立请求通道) sqlmPdbRequestRouter -- 监控请求路由 sqlmonssagnt -- 监控快照代理 sqlbSnapshotTablespace -- 对表空间做快照 sqlbMonSMSGetContainerPages -- 统计 SMS 表空间的容器页 SQLB_FIND_BUF::next -- 遍历文件查找缓冲 sqlofindn2 -- 文件查找 __xstat64 -- 系统调用:取文件状态从sqlbSnapshotTablespace → sqlbMonSMSGetContainerPages这两级就能读出来:这个 EDU 在对表空间做快照,具体动作是统计 SMS 表空间的容器数据页。再往下走到__xstat64,说明它在对容器文件做 stat 系统调用取文件状态。容器文件越多、目录项越复杂,这段就越慢——TDA 文件堆积正好会把这里撑爆。
Waiter 的栈(同样节选):
sqlrita -- 运行时聚合 sqlriFetch -- 取行 sqlsfetc -- 从排序/临时结构取数 sqldEvalDataPred -- 谓词求值 sqldTableCreateTemp -- 创建临时表 sqldInternalCreateTable -- 内部建表 sqlbGetSysTempDatapool -- 获取系统临时数据池 SQLO_SLATCH_CAS64::getConflict -- 争用 LATCH这条链的意思是:一个查询在执行谓词求值时需要建内部临时表,建表过程要拿系统临时表空间对应的数据池,于是去申请pool_table_latch,撞上 Holder 没释放,卡在getConflict里自旋/等待。
两边一对,因果就闭合了:监控代理定期抓表空间快照 → 快照遍历 SMS 容器页 → 持有 pool_table_latch → 并发查询建临时表拿不到锁 → 排队 → ACTIVE SESSION 飙升 → 业务感知变慢。整个过程里没有任何一条用户 SQL 有逻辑错误,也没有任何配置被改坏。
4.3 analyzestack:把上百个栈文件聚合成结论
手工 grep 加肉眼比对,在十来个文件时还行,上百个就撑不住了。安装目录的sqllib/pd下有一个 Perl 脚本analyzestack,能把指定目录里所有*.stack.txt中相同的 STACK 汇总统计,并顺带做 LATCH 分析:
# -d 50 : 对比栈时比较的函数个数,越大越严格 # -i 目录 : 栈文件所在目录,也可以直接列多个 stack.txt # -l : 执行 LATCH 分析 perl ~/sqllib/pd/analyzestack -d 50 -i /tmp/stack -l它会生成LatchAnalysis.out和StackAnalysis.out两个结果文件(文件名可通过参数改)。-d这个参数值得多说一句:它决定“函数调用链前多少个函数相同才算同一个栈”,值调小会把不同的栈合并,调大则可能把本该合并的栈拆开。默认值在多数场景够用,遇到调用链高度相似但归属不同的情况,再上下调。
这个脚本的定位要说清楚:它是随产品一起提供的示例脚本,脚本头部明确声明按现状提供、不做任何保证,官方知识中心也找不到它的使用说明,用法只能靠-help自己摸。这不影响它在排查中的实用价值,但要清楚它的输出是线索而不是结论,最终的判断还得回到调用链语义上。
提示:
analyzestack对栈文件的命名格式有依赖,手工重命名过*.stack.txt之后再跑可能解析失败,采集后保持原始文件名最省事。
5. 避坑清单:临时表空间、TDA 文件与诊断工具的五个常见坑
排查走到这一步,机制已经清楚了,但真正落地修复时踩的坑往往比分析阶段更多。下面五条都是这次或类似场景里真实绊过人的地方,按“现象 → 原因 → 解决”记下来。
5.1 临时表空间相关的三个坑
坑一:重启实例之后临时表空间还是那么大。
现象是执行db2 LIST TABLESPACES SHOW DETAIL依然要好几秒,TEMPSPACE1 的 Total pages 纹丝不动,业务侧的卡顿只是短暂缓解又复发。原因是 SMS 系统临时表空间里的 TDA 文件(形如SQL04313.MEMBER0000.TDA)在特定条件下会遗留在表空间内并占满空间,这是产品的既定行为,不是缺陷;实例重启不会重建这些容器文件,只有数据库级别的 deactivate/activate 才会。解决方式是安排维护窗口执行db2 deactivate db <dbname>再db2 activate db <dbname>,执行完立刻复查 Total pages 和 Used pages 是否归零。这次的 10GB 就是靠这一步一次性释放的,之后LIST TABLESPACES恢复到秒内返回。
坑二:临时表空间暴增之后,只盯着当前负载看,忽略了历史曲线。
现象是问题复现时负载并不高,怎么都解释不了 10GB 是怎么来的。原因是暴涨发生在过去的某个时间点,和当前负载无关。这次翻监控系统的表空间大小历史曲线,才看到某个时刻从 0 直接跳到 10G,对应的是那一瞬间 ACTIVE SESSION 冲到 800 多——大量语句同时使用临时表空间,数据库没崩、语句也都执行成功了,但留下了一堆 TDA 文件,成了后来踩到的雷。解决方法是在排查任何“空间异常”类问题时,第一动作是拉历史曲线找突变点,再拿突变点时间去比对当时的会话数、慢语句和告警记录,而不是在当前时刻的指标里找答案。
坑三:把“临时表空间设大一点”当成优化手段。
现象是有人建议直接扩容 TEMPSPACE1 来“避免溢出”,或者反过来把它改小来“防止膨胀”。原因是对因果关系的方向判断反了:这次的问题不是空间不够,而是空间太大导致元数据遍历变慢。SMS 临时表空间的容器与页数直接决定了快照和建临时表这两条路径的成本,盲目扩容只会让每次遍历更贵。解决方法是先确认是否存在 TDA 文件残留,把残留清掉恢复常态,再根据真实排序溢出去调SORTHEAP之类的内存参数;临时表空间的物理大小不该作为性能调节旋钮。
5.2 诊断工具相关的两个坑
坑四:单次采样看不到 LATCH 争用,就判定“没有 LATCH 问题”。
现象是db2pd -latches跑一次干干净净,于是把 LATCH 这条线索划掉了。原因是这类争用只持续几秒,单点采样的命中率很低。解决方法是改用-rep持续采样,并且把采样窗口拉到能覆盖业务毛刺的时间段;如果还是抓不到,就上db2pd -stack多轮抓栈,用grep在文件堆里找证据。判断“某个瞬时现象不存在”需要的是采样密度,不是采样次数。
坑五:在生产高峰长时间抓栈,把可观测性操作变成了二次故障。
现象是抓完栈之后业务反馈更卡了,监控上 CPU 和响应时间同时恶化。原因是抓栈会让每个 EDU 在信号处理点停顿,-stack all在几百个 EDU 的实例上代价很高。解决方法是把抓栈窗口压到最小(比如-rep 3 10这种三十秒量级的窗口)、避开高峰、抓够证据就立刻停;同时把dumpdir指向容量充足的目录,避免栈文件写满分区引发另一类故障。工具本身没问题,问题在于把它当成了无成本操作。
6. 从 10G 到 0:table pool latch 的设计取舍与用 db2trc 钉死因果链
把 table pool 的 LATCH 做成全局粒度,是一个典型的设计取舍。做细粒度(每表空间一把、甚至每容器一把)能显著降低并发争用,代价是 LATCH 数量随表空间和容器数量线性增长,每次查找要先定位到正确的锁实例,内存开销和查找路径都变长;而表空间元信息变更本身是低频操作,绝大多数系统里这把锁的争用压根不可观测。站在引擎设计者的角度,选全局一把是合理的——它把复杂度留给了极端场景,把简洁和低开销留给了常态。这次踩到的,正是那个极端场景:一个本应很小、却因为 TDA 文件残留膨胀到 10GB 的临时表空间,把低频操作的临界区拉长了几百倍,正好撞上高频的建临时表路径。
所以真正的修复动作不是在数据库里调什么参数,而是把异常状态清掉、恢复到设计假设的前提上。这也是这类问题的通用解法:当机制在设计假设内运行时一切正常,出问题的往往是某个对象偏离了假设,排查的重点应该是“什么让它偏离了”,而不是“怎么绕过这个机制”。
因果链到这里是靠推理闭合的,还能不能实测验证一次?可以,db2trc提供了把实例“暂停”在指定函数入口的能力,让原本一闪而过的现场定格下来:
# 终端一:连接数据库,开启跟踪并在目标函数入口挂起实例 db2 CONNECT TO mydb db2trc on -crash DB2.SQLB.sqlbSMSGetContainerPages.entry -suspend # 此时实例在进入该函数时被挂起,从容抓取现场信息 db2pd -latches db2pd -stack all dumpdir=/tmp/stack_trc # 终端二:抓完现场后恢复执行并关闭跟踪 db2trc off-crash表示只跟踪指定入口点相关的事件,把开销和 trace 文件体积压到最小;-suspend是关键,它让实例在命中该入口时停住,正好是表空间快照即将遍历容器页、即将持有 pool_table_latch 的那一刻。在这个定格状态下抓-latches,能稳定看到 Holder 是谁;抓-stack,能稳定拿到那条sqlbSnapshotTablespace → sqlbMonSMSGetContainerPages的调用链。相比靠-rep撞运气,这是把因果链从“推断”变成“复现”的手段。
这条路径我后来固定成了习惯:凡是遇到“间歇性、无锁等待、语句莫名变慢”的问题,先确认有没有一个高频监控动作在抓表空间或容器级信息,再看相关对象的规模是不是偏离了常态。那次之后,我每次接手一个新库,都会先把 TEMPSPACE1 的 Total pages 和容器数记进基线表,并在监控里给临时表空间大小单独加一条曲线——不是因为它重要,恰恰是因为它太容易被忽略,血泪经验告诉我,安静的对象膨胀起来最没有征兆。希望帮到你。
本文还有配套的精品资源,点击获取