
简介这份文档面向DB2数据库运维人员与DBA记录某银行生产系统的一次真实性能故障业务响应变慢、活动会话数激增常规排查CPU、内存、I/O、锁等待与缓冲池命中率均无异常最终锁定系统临时表空间TEMPSPACE1膨胀至10GB这一根本原因。文档围绕问题初步确认、LATCH竞争分析、STACK堆栈收集、db2trc暂停实例取证等环节层层展开并给出SQL优化、参数调整与临时对象清理等处置办法属于中高级实战排错参考。资源包共1个文件为doc格式技术文章约607KB可离线阅读与团队内部分享。目前已有5903人学习适合希望积累性能诊断路径、掌握监控工具取证技巧的数据库从业者对照研读。1. 10G 的临时表空间为什么“空间大”本身不是病因DB2 数据库的临时表空间膨胀到 10G大多数人的第一反应是磁盘吃紧、排序溢出、该扩容量了。这次的情况偏偏相反拖慢查询的不是 I/O不是排序而是每一条需要创建内部临时表的 SQL都被一把叫pool_table_latch的内部 latch 卡在门口。更反直觉的是这 10G 里绝大部分是对业务毫无用处的残留——某次瞬时并发冲高留下的 TDA 文件语句早就执行完了文件还躺在容器目录里。理解这个案例的价值在于它演示了一条“监控指标全部正常但系统确实慢”的排查路径而这条路径上的绝大多数工具平时几乎没人碰。2. ACTIVE SESSION 破 50常规排除法为什么全部失效业务侧反馈“系统变慢”这种话单独听是靠不住的可能来自应用层、网络也可能只是用户手感。但监控里 ACTIVE SESSION 频繁冲到 50 以上性质就变了——这是数据库自己给出的证据。历史曲线显示平时活跃会话在 10 个以内偶尔到 30这几天则频繁破 30形状上就是典型的“语句变慢导致会话堆积”而不是“业务量上涨”。2.1 ActSess 的口径哪些状态算活跃UOW Waiting 为什么不算先把口径对齐否则后面所有判断都会歪。在db2top -d dbname里按d进入的界面ActSess 统计的是“当前正在处理 SQL 请求”的会话。语句从编译到执行完成中间会经历 Compiling、UOW Executing、Lock Waiting 等状态COMMIT 本身也是一条语句执行期间会看到 Commit Active如果启用了 WLM 的并发限制排队中的会话处于 Queued。以上全部计入 ActSess。而一个事务里执行完一条语句、下一条还没发出来的间隙应用状态是 UOW Waiting这个不计入。这个区分很关键。如果 ActSess 高是大量 Queued 撑起来的那是并发控制配置问题如果是 Lock Waiting 撑起来的那是应用事务边界问题本次按l进 SESSIONS 页面看到的几乎全是 UOW Executing说明会话是真的在跑只是跑得慢。同时确认了业务访问量没有增长应用和数据库近期也没有变更——这两条排除了“容量型”解释把问题锁定在“单条语句执行时间变长”。2.2 MON_GET_PKG_CACHE_STMT 的时间切片把 4 秒拆成等待和读取排除法跑得很快CPU 利用率正常操作系统内存正常缓冲池命中率正常没有 LOCK WAITdb2top右下角四个指标 AvgPRdTime、AvgDRdTime、AvgPWrTime、AvgDWrTime 都正常说明 I/O 没问题。Deadlocks 显示 218但这是累计值且不直接影响当前吞吐SortOvf 显示 10 略微偏高考虑到应用里确实有大量排序语句暂时不认定为主因。接下来抓慢语句。db2top里按D看 package cache 中的动态 SQL再按z提示Column number for descending sort:输入 4 回车即按 Avg ExecTime 降序。排在前面的语句平均执行时间接近 4 秒但 Num Execution 都只有几次——这不是语句真的只跑了几次而是应用直接拼字符串、不走变量绑定DB2 眼里每条都是全新语句package cache 里自然各出现一两次。这个细节反过来暗示这类语句的全集都在慢。想看“此刻正在跑什么”用 MON_CURRENT_SQL 管理视图最直接-- 列出当前正在执行的 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是抓取瞬间该语句已经跑了多久不是最终耗时所以它天然偏小而且这条查询需要多抓几次才可能撞上 ActSess 高峰。抓回来的结果接近 30 条和监控里的活跃会话数量对得上说明确实是“一堆语句同时在跑、每条都慢”。真正的分水岭是 MON_GET_PKG_CACHE_STMT。它返回的字段极多但价值不在数量在于能把一条语句的总执行时间拆成等待、读、锁、日志、排序几块看占比而不是看绝对值-- 按语句统计各项等待时间占总执行时间的百分比 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)) AS T WHERE NUM_EXECUTIONS 1 AND STMT_EXEC_TIME 0;-2这个成员参数表示取当前成员上的全部语句NUM_EXECUTIONS 1过滤掉只执行一次的噪声STMT_EXEC_TIME 0避免除零。结果里平均执行时间 2 到 6 秒不等其中 5 条以with开头的语句PCT_WAIT超过 95%——也就是说这几条的时间几乎全花在等待上而等待既不是读、不是锁、不是日志、也不是排序。在查询里补上 TOTAL_EXTENDED_LATCH_WAIT_TIME 字段答案就浮出来了它们在等 latch。2.3 命中率正常却慢瓶颈不在数据访问路径上到这里出现了一个容易被忽略的分岔。既然发现了慢 SQL本能反应是去优化给全表扫描的语句建索引把逻辑复杂的长 SQL 拆开重写。这些动作本身没错也确实解决了几条语句但方向是错的——那些长 SQL 不是最近才上线的系统跑了很久都没事最近也没做应用或数据库变更凭什么突然变慢这个反问是整条排查链上最值钱的一步。命中率、I/O、锁等待全部正常说明数据访问路径是健康的而时间分布显示大量时间落在“非读非锁非排序”的等待上说明瓶颈在数据访问之前的一层——DB2 内部的共享内存结构。这时候再回头看 SQL 优化就只是在处理症状。3. LATCH 与 LOCK 的分工为什么 db2pd -latches 第一次什么都没抓到DB2 里 LOCK 和 LATCH 经常被混为一谈其实分工很清晰。LOCK 锁的是业务数据——行、表有 SQL 接口LOCK TABLE、隔离级别、有监控视图MON_LOCKWAITS、有事件监控、有LOCK_WAIT_TIME这样的监控元素出问题一查就有。LATCH 锁的是 DB2 内部的内存数据结构没有 SQL 接口没有事件监控甚至在产品文档里找不到一篇系统性介绍。它是给内部线程用的用户只能从db2pd这类底层工具侧面观察。3.1 从 PREFETCH QUEUE 理解 LATCH 到底在保护什么拿预取队列举例最容易讲明白。DB2 内存里有一个 PREFETCH QUEUE 结构处理用户请求的 EDU 需要预取时往队列里塞一个任务若干个 PREFETCHER EDU 不停从队列里取任务。两边并发访问同一个队列就必须有互斥每次访问前先拿一个 LATCH别的 EDU 等它放开。所以 LATCH 可以粗略理解为“DB2 内部的轻量锁”特点是持有时间极短、粒度极细、数量极多、名字全是内部符号。正因为持有时间短LATCH 争用往往是“脉冲式”的一阵子集中出现几秒后自己消散。这也解释了为什么第一次执行db2pd -latches什么都没看出来——采样窗口恰好落在脉冲之外。改成db2pd -latches -rep 2每隔 2 秒输出一次并持续刷新问题就现形了。3.2 pool_table_latch 的争用现场1 个 Holder 挡住 23 个 Waiter持续观察下抓到的一幕是EDU ID 11665 持有类型为pool_table_latch的 LATCH而 ID 2877、2981 等 23 个 EDU 排在它后面等待。这意味着此刻至少有 24 个操作被串行化ACTIVE SESSION 数量下限就是 24——和监控看到的数字完全吻合。这就是“一个操作卡住二十几个会话跟着卡”的现场。问题是名字看不懂。pool_table_latch从字面只能猜到和“表空间池的表”有关产品文档里查不到它的含义、持有场景、影响范围。麻烦还不止于此LATCH 等待几秒就清除了等你想去抓 Holder 和 Waiter 当前在干什么现场已经没了。3.3 LATCH 的生命周期太短-rep 2 持续采样才抓得到这就逼出一个方法论上的转变既然抓不到“正在发生的瞬间”就去抓“瞬间留下的痕迹”。DB2 的 EDU 在等待 LATCH 时是阻塞在某个函数调用上的而这个调用链就写在它的栈里。所以只要能在 LATCH 等待发生的同一时刻把栈 dump 下来Holder 和 Waiter 各自走到哪一行代码就都留在了文件里。这条路不依赖文档只依赖对函数名的阅读能力和对 DB2 模块划分的基本认知反而是最可靠的。4. STACK 里的证据链从 sqlmonss.C 到 sqlbpool.C 的调用栈对读db2pd的-stack选项正是干这个的它在收集每个 EDU 栈的同时把该 EDU 当时持有的 LATCH 和 LOCK 信息一并写进文件。所以一份 stack 文件里既有“我在等什么”也有“我走到哪了”。4.1 db2pd -stack 收集与文件命名规则cd /tmp mkdir stack0531 # all 表示所有 EDUdumpdir 指定输出目录默认落在 DIAGPATH # -rep 3 10 表示每 3 秒收集一次连续 10 轮 db2pd -stack all dumpdir/tmp/stack0531 -rep 3 10之所以要-rep是因为 LATCH 等待不是常驻现象单次采样大概率落空。10 轮只是起步抓不到就加轮次继续。收集完成后用 grep 在结果里搜 latch 类型名确认有没有命中cd /tmp/stack0531 grep pool_table_latch *stack.txt命中结果长这样一部分文件写着Holding Latch type: (SQLO_LT_SQLB_PTBL__pool_table_latch)位置在sqlmonss.C第 9032 行HoldCount 为 1另一部分写着Waiting on latch type:同样的地址位置在sqlbpool.C第 11384 行。文件名形如20160.10783.003.stack.txt中间一段是 EDU ID最后一段003是第几轮采样——这一点很重要因为同一个 EDU 在不同轮次里的栈是可以对比的能看出它是一直卡着还是在推进。4.2 Holder 栈快照表空间时为什么要遍历容器的每一个数据页Holder 的调用链从下往上读大致是这样一条路径sqleProcessSubRequest→sqlmPdbRequestRouter→sqlmonssagnt→sqlbSnapshotTablespace→sqlbMonSMSGetContainerPages→SQLB_FIND_BUF::next→sqlofindn2→__xstat64。函数名的字面意思已经给了足够信息这是在响应一个监控请求sqlm*前缀是监控模块对表空间做快照具体动作是取 SMS 表空间的容器页信息而最底层的__xstat64是操作系统级的文件状态查询调用。换句话说这个 EDU 正在文件系统里一个一个地取文件的元数据。我一般会这么理解这段逻辑SMS 表空间的“容器”本质上是一个目录目录里堆着一批 TDA 文件要做表空间级别的页统计就得把目录里的文件挨个问一遍大小和状态。容器只有 4 个但每个容器目录里的文件数量可能极其可观这个遍历的耗时就和文件数量成正比而不是和表空间的逻辑大小成正比。4.3 Waiter 栈创建内部临时表为什么也要抢同一把 latchWaiter 的调用链是另一条完全不同的路sqlriFetch→sqlsfetc→sqldEvalDataPred→sqldTableCreateTemp→sqldInternalCreateTable→sqlbGetSysTempDatapool→SQLO_SLATCH_CAS64::getConflict。往下读sqld*前缀是数据定义/数据操作执行层sqldTableCreateTemp明确是创建临时表sqlbGetSysTempDatapool是去取系统临时数据池最后落到SQLO_SLATCH_CAS64的getConflict——这就是 latch 冲突路径说明它确实卡在抢锁上。再结合sqldEvalDataPred谓词求值和sqlsfetc取数基本可以确认一条查询在执行过程中需要物化内部临时表于是去系统临时表空间申请空间而申请这个动作要先拿pool_table_latch。矛盾就在这儿一边是监控代理周期性对表空间做快照遍历容器里的文件另一边是业务查询高频创建临时表需要访问同一把 latch。前者持有时间长文件多后者调用频次高并发查询多两边一撞就形成“1 个 Holder 挡住 23 个 Waiter”。到这里因果链闭合了临时表空间里堆了大量 TDA 文件 → 表空间快照变慢 → 长时间持有 pool_table_latch → 并发查询创建临时表时排队 → ACTIVE SESSION 堆积 → 业务感知为系统变慢。4.4 analyzestack把上千份 stack.txt 收敛成两份报告手工翻 stack 文件在样本少的时候可行几十上百份就不现实了。sqllib/pd目录下有个 Perl 脚本analyzestack可以做汇总把目录里所有*.stack.txt中相同的栈归并计数并单独做 latch 分析。# -d 50 对比栈时比较的函数层数 # -i 指定 stack 文件所在目录也可直接给多个文件 # -l 执行 latch 分析 perl ~/sqllib/pd/analyzestack -d 50 -i /tmp/stack0531 -l运行后会生成LatchAnalysis.out和StackAnalysis.out两个文件前者列出 Holder 与 Waiter 的配对关系后者按出现次数排序给出高频栈。需要提醒的是这个脚本在产品里属于“附赠品”脚本开头的声明写得很直白按现状提供、作为功能示例、不做担保也不提供支持。所以它的输出要当作线索而不是定论最终还得回到具体的栈文件里核对函数和行号。5. 避坑清单TDA 文件、deactivate 边界与采样代价5.1 踩坑一restart instance 释放不了 TDA 文件现象确认是临时表空间残留 TDA 文件后重启数据库实例LIST TABLESPACES SHOW DETAIL依然要跑好几秒ActSess 依然偏高。原因TDA 文件的生命周期绑在数据库激活期上实例重启并不触发数据库级的 deactivate/activate残留文件不会被回收。这一点极容易想当然——重启大法在数据库这里失效了。解决必须走db2 deactivate db dbname再db2 activate db dbname。维护窗口做完之后临时表空间被释放LIST TABLESPACES恢复正常ActSess 回到个位数。5.2 踩坑二把 db2 LIST TABLESPACES SHOW DETAIL 当轻量命令现象问题期间这条本来随手就敲的命令要执行好几秒而且越敲越慢。原因它走的就是快照路径要拿pool_table_latch去遍历容器。容器里文件越多这条命令越慢而它一慢又进一步延长 latch 持有时间形成正反馈。解决问题期间别把它当巡检命令高频调用尤其是监控代理已经定时在采集表空间信息的情况下。排查时优先用db2pd -latches、MON_GET_PKG_CACHE_STMT这类不激化争用的手段。5.3 踩坑三采样窗口错过 LATCH 等待现象第一次db2pd -latches输出干干净净差点得出没有 latch 问题的结论。原因LATCH 等待是秒级脉冲单次采样命中率极低。解决用-rep 2或-rep 3 10做持续采样宁可多跑几轮。判断依据不是有没有看到而是多轮里出现过几次、每次多少人排队。5.4 踩坑四生产库全量 dump stack 的代价现象在繁忙的生产库上执行db2pd -stack all业务侧立刻反馈更慢了。原因dump 所有 EDU 的栈本身要挂起线程、读内存、写文件在高并发下开销明显。解决控制轮次和间隔避开业务高峰dumpdir指到本地盘而不是共享存储采集完立刻停。这一步没有无侵入的选项只能权衡。5.5 踩坑五看到 SortOvf 就去调 sortheap现象db2top上 SortOvf 显示 10第一反应是排序溢出准备去调大排序堆。原因SortOvf 是症状指标应用里确实有大量排序语句少量溢出是常态。把它当主因就会去调一个和真正瓶颈无关的参数。解决先用 MON_GET_PKG_CACHE_STMT 看pct_sort的占比。如果排序等待只占几个百分点而pct_wait占 95% 以上且四项等待都解释不了就该往 latch 方向查而不是动排序参数。6. 设计取舍为什么“设计如此”的 TDA 残留值得被当成一等公民TDA 文件不被删除这件事查到最后会撞上一个很典型的结论这是设计如此。SMS 临时表空间里的 TDA 文件可能遗留并占满文件系统属于已知行为而非缺陷。站在产品设计的角度这个取舍不难理解——临时表空间的首要目标是绝不因为空间不足让语句失败所以在瞬时并发冲高时倾向于激进分配、延后回收把空间回收的时机交给数据库激活周期而不是每次语句结束就同步清理。代价就是一次瞬时高峰当时 ActSess 一度冲到 800 多SQL 全部执行成功、数据库也没崩会在表空间里留下后遗症等到某个监控代理开始周期性做表空间快照时这些无害的垃圾才以 latch 争用的形式反噬。想验证这条因果链还有一个更硬的手段用db2trc把实例按在某个函数入口上从容抓栈。思路是打开 trace 并指定在进入sqlbSMSGetContainerPages时挂起此时整个实例停住再从容 dump# 终端一连上目标库开 trace 并设置挂起条件 db2 connect to mydb db2trc on -crash DB2.SQLB.sqlbSMSGetContainerPages.entry -suspendTrace # 终端二实例被挂起期间慢慢收集所有 EDU 的栈 db2pd -stack all dumpdir/tmp/stack_suspend # 终端一收集完关闭 trace实例恢复 db2trc off具体的挂起参数要以db2trc -h的实际输出为准不同版本的行为有差异。这个实验的意义不在于复现问题而在于证明一件事你怀疑的那条代码路径确实会在你怀疑的时刻被走到。把猜测变成观测是性能排查里唯一可靠的收敛方式。我自己的教训是凡是遇到各项监控指标都正常但系统就是慢的情况不再从 SQL 优化开始而是先确认两件事——有没有哪个内部结构的持有时间被异常拉长以及有没有哪个本该很小的东西悄悄变大了。从那以后我每次接手一个陌生库第一件事都是把临时表空间的当前大小和容器文件数记进基线出问题时对比一下往往比翻一小时执行计划更快。希望帮到你。本文还有配套的精品资源点击获取