1. 项目概述:一次Oracle数据库Hang进程的排查与移除实战
那天下午,监控告警突然响起,提示某个核心业务数据库的响应时间飙升。登录服务器一看,一个关键的批处理作业已经“卡”在那里超过两个小时,前端应用对应的查询也全部超时。经验告诉我,这很可能不是简单的慢SQL,而是遇到了Oracle数据库中那个让所有DBA都头疼的问题——进程挂起(Hang)。这次经历,就是一次完整的从现象定位、原因分析到最终安全移除Hang进程的实战记录。无论你是刚接触Oracle的运维新人,还是遇到过类似问题但处理过程不够清晰的老手,这篇记录都能给你提供一个可复现的排查框架和操作思路。我们将深入一个模拟的生产环境场景,拆解每一步操作背后的原理和考量。
2. Hang进程的本质与常见诱因分析
在动手之前,我们必须搞清楚,Oracle中的“Hang”到底意味着什么。它不是一个具体的错误代码,而是一种状态描述:一个或多个会话(进程)因为无法获取所需的资源而陷入无限期等待,无法继续执行其工作单元,同时通常还会阻塞其他依赖这些资源的会话。
2.1 理解等待事件与资源争用
Oracle数据库的运行依赖于一套精密的协调机制。每个会话在执行时,如果需要某个暂时不可用的资源(比如一块数据被另一个会话锁定),它不会“忙等”消耗CPU,而是进入一个等待状态,并在V$SESSION视图中记录一个等待事件(Wait Event)。这是分析Hang问题的黄金入口。
常见的导致Hang的等待事件可以归为几大类:
- 并发争用:最常见的是
enq: TX - row lock contention(行锁等待)和enq: TM - contention(表锁等待)。一个会话更新了某行数据未提交,另一个会话要更新同一行,后者就会Hang住。 - 空间管理:
enq: HW - contention(高水位锁争用)发生在大量并发插入时;buffer busy waits(缓冲区忙等待)则是多个进程想同时读写同一个数据块。 - 内部资源争用:
latch free(闩锁释放等待)、library cache lock(库缓存锁)等多与共享池、SQL解析相关,频繁的硬解析或游标失效可能引发此类问题。 - I/O相关:
db file sequential read(数据文件顺序读)等待时间异常长,可能意味着存储性能瓶颈或单点故障。
注意:并非所有长时间等待都是“Hang”。一个运行数小时的大查询,其等待事件可能是
db file scattered read,但它是在正常工作。判断Hang的关键在于进程是否在推进,以及是否形成了阻塞链。
2.2 构建问题分析的心理模型
当接到Hang的告警时,我的思路通常遵循一个分层排查模型:
- 第一层:用户感知层。应用报错、前端超时。此时需要快速确定影响范围:是单个功能、单个用户还是整个实例?
- 第二层:会话与SQL层。进入数据库,找出状态异常(
INACTIVE但等待事件非空,或ACTIVE但长时间不推进)的会话,并定位其正在执行的SQL。 - 第三层:资源与阻塞层。这是核心。找出“谁在等谁”,即绘制出阻塞链(Blocking Chain)。一个会话在等待另一个会话持有的资源,而另一个会话可能又在等待第三个会话,形成链条。链条的源头,就是需要重点关注的“罪魁祸首”。
- 第四层:根本原因层。分析源头会话为什么持有资源不释放?是程序未提交事务、有死循环、还是在等待外部系统响应?
本次案例中,我们直接进入了第三层,因为影响范围已经是整个实例的某些特定操作变慢。
3. 诊断工具链与关键查询实战
工欲善其事,必先利其器。Oracle提供了丰富的动态性能视图(V$视图)来诊断Hang。下面是我在本次排查中依次使用的关键查询,并附上解读。
3.1 快速定位异常会话
首先,获取一个当前会话活动的快照。我习惯使用以下改进版的查询,它信息更全面:
SELECT s.inst_id, s.sid, s.serial#, s.username, s.program, s.machine, s.status, s.sql_id, s.event, s.seconds_in_wait, s.blocking_session, s.blocking_instance, sq.sql_text FROM gv$session s LEFT JOIN gv$sql sq ON (s.sql_id = sq.sql_id AND s.inst_id = sq.inst_id) WHERE s.status = 'ACTIVE' OR (s.status = 'INACTIVE' AND s.event NOT LIKE 'SQL*Net%') AND s.type != 'BACKGROUND' ORDER BY s.seconds_in_wait DESC;查询解读:
gv$session:在RAC环境中使用,单实例可用v$session。这是会话信息的核心视图。s.blocking_session:至关重要。如果该字段不为空(非0),说明此会话正在被另一个会话阻塞。这里就找到了阻塞链的“下一环”。s.event和seconds_in_wait:长时间的非空闲等待事件(排除SQL*Net message from/to client这类通常的网络空闲等待)是重点怀疑对象。- 关联
gv$sql:是为了立刻看到问题会话在执行什么SQL语句,有时问题SQL本身就能说明一切(例如,一个未加索引的全表更新)。
执行这个查询后,我立刻发现了好几个SID,它们的event都是enq: TX - row lock contention,并且blocking_session都指向同一个SID: 134。SID 134自身的event是SQL*Net message from client,看起来是空闲的,但它的blocking_session字段是0。这强烈暗示SID 134是阻塞链的源头,它持有着行锁,但可能因为应用逻辑(如未提交事务)或前端挂起,导致锁没有释放。
3.2 深入剖析阻塞链
找到疑似源头后,需要确认完整的阻塞关系。Oracle有专门的脚本或视图可以展示树形阻塞链。我最常用的是手动追踪结合以下查询:
SELECT LPAD(' ', LEVEL*2) || s.sid AS tree_sid, s.inst_id, s.serial#, s.username, s.program, s.event, s.sql_id, s.row_wait_obj#, o.object_name, o.object_type FROM gv$session s LEFT JOIN dba_objects o ON s.row_wait_obj# = o.object_id START WITH s.sid = &blocked_sid -- 输入一个被阻塞的SID CONNECT BY PRIOR s.blocking_session = s.sid AND PRIOR s.blocking_instance = s.inst_id AND s.blocking_session IS NOT NULL;查询解读:
START WITH ... CONNECT BY:这是Oracle的层次查询语法,用于从某个被阻塞的会话开始,递归向上查找它的所有阻塞者,直到找到根节点(blocking_session为空的会话)。row_wait_obj#和dba_objects:关联这个字段可以知道会话正在等待哪张表上的哪一行(通过row_wait_row#等字段可以进一步定位,但这里我们更关心对象)。这能立刻告诉我们争抢的热点对象是什么。- 运行这个查询,输入一个被阻塞的SID,结果会以缩进格式显示出一条清晰的链条。在我的案例中,链条的根节点正是
SID 134。
3.3 挖掘源头会话的详细信息
现在,焦点集中在SID 134上。我们需要知道它到底在干什么。
SELECT s.sid, s.serial#, s.username, s.program, s.machine, s.osuser, s.logon_time, s.last_call_et, -- 最后一次调用经历的秒数 t.start_time, t.status, t.used_ublk, -- 未提交事务使用的undo块数,>0 通常意味着有未提交事务 t.used_urec -- 未提交事务使用的undo记录数 FROM v$session s JOIN v$transaction t ON s.saddr = t.ses_addr WHERE s.sid = 134;关键发现:
last_call_et值非常大(超过7200秒),这意味着该会话已经超过2小时没有与数据库进行任何有效交互了。used_ublk和used_urec的值都大于0,这确凿地证明了该会话存在一个未提交的事务(Transaction)。- 结合
program和machine字段,我定位到这是一台应用服务器上的一个特定服务进程。
至此,诊断结论已经清晰:应用服务器上的一个服务进程(SID 134)开启了一个事务并修改了数据,但未提交。该事务持有的行锁阻塞了后续所有试图修改相同数据的会话,导致它们挂起。而该应用进程本身可能由于程序bug、网络中断或资源死锁等原因,已经僵死,无法继续执行提交或回滚指令。
4. 制定移除策略与风险评估
找到“病根”后,下一步是“手术”。直接杀死(Kill)一个会话是最后的手段,但必须谨慎评估风险。
4.1 评估会话状态与事务影响
- 会话状态:
SID 134的状态是INACTIVE,event是空闲等待,且last_call_et极长。这表明数据库认为这个连接是空闲的,应用层很可能已经失去了对这个连接的控制。 - 事务影响:
used_ublk的数量可以帮助粗略估计未提交事务的大小。数量不大,说明修改的数据量可能较小。我需要进一步确认它修改了哪些数据:
这个查询可能无法直接看到SELECT s.sid, s.serial#, o.object_name, o.object_type, l.row_wait_file#, l.row_wait_block#, l.row_wait_row# FROM v$session s JOIN v$session_wait l ON s.sid = l.sid LEFT JOIN dba_objects o ON l.row_wait_obj# = o.object_id WHERE s.sid = 134 AND l.event = 'enq: TX - row lock contention';SID 134自己锁定的行,但可以通过被它阻塞的会话来反查。结果指向了几张业务配置表。这让我稍微松了口气——不是核心的交易流水表。 - 回滚开销:杀死一个带有未提交事务的会话,Oracle会自动回滚该事务。回滚所需的时间和I/O资源与事务大小成正比。我需要评估在业务高峰时段,回滚操作对系统性能(特别是Undo表空间和I/O)的潜在冲击。
4.2 沟通与决策
基于以上评估:
- 风险:回滚操作可能导致短暂的I/O压力,但鉴于事务较小,影响可控。被阻塞的业务已经停滞,不处理则损失持续。
- 沟通:我立即联系了该应用的服务负责人,告知他们
SID 134对应的应用进程可能已经僵死,并确认该进程可以重启。这是关键步骤,必须确保应用端有能力处理连接中断后的重启或异常处理,避免杀死会话后引发应用层更复杂的错误。 - 决策:在获得应用侧确认后,决定执行会话移除操作。同时,我通知了业务方,相关配置表的功能会有短暂中断(回滚期间,这些行上的锁依然存在,直到回滚完成)。
5. 安全移除Hang进程的操作实录
移除会话的命令很简单,但细节决定成败。
5.1 获取完整会话标识
在Oracle中,要终止一个会话,需要同时知道SID和SERIAL#。这是为了防止误操作:如果一个会话被终止后迅速有新的会话重用了同一个SID,SERIAL#会增加,仅凭SID就无法操作新会话。我们之前查询的结果中已经包含了这两个值(例如,SID=134, SERIAL#=12345)。
5.2 执行终止命令
在SQL*Plus或具备DBA权限的数据库连接中,执行:
ALTER SYSTEM KILL SESSION '134,12345' IMMEDIATE;参数解读:
'134,12345':单引号内的字符串,格式为'SID,SERIAL#'。IMMEDIATE:这个选项至关重要。它告诉数据库不要等待会话主动释放资源或回滚,而是立即将会话标记为终止,并将会话持有的所有资源(包括锁)释放,然后由后台进程SMON来负责异步回滚事务。如果不加IMMEDIATE,命令会等待会话完成当前操作(可能永远等不到),相当于无效。
5.3 验证操作结果
执行命令后,立刻再次运行诊断查询,检查阻塞链:
- 原先被
SID 134阻塞的那些会话,其event应该从enq: TX - row lock contention迅速变为waiting for smon to disable tx recovery或其他短暂等待,然后很快恢复正常(ACTIVE或变为空闲)。 - 查询
SID 134的状态:
你可能会看到状态变为SELECT sid, serial#, status, last_call_et FROM v$session WHERE sid = 134;KILLED。过一段时间后,这个会话记录会从v$session中消失。 - 监控回滚进度(可选,对于大事务很重要):
或者查看SELECT usn, state, undoblocksdone, undoblockstotal FROM v$fast_start_transactions;v$transaction视图中对应会话的事务是否已消失。
在我的操作中,命令执行后大约3秒内,之前被阻塞的批处理作业和前端查询全部恢复运行。监控系统上的等待事件图表中,enq: TX的峰值迅速下降。
5.4 后续观察与根因追溯
问题暂时解决,但工作还没结束。
- 观察:持续观察系统是否稳定,确认没有新的相同阻塞模式出现。
- 根因分析:我联系应用团队,一起分析
SID 134对应的应用日志。最终发现,是由于一个罕见的边界条件触发了代码中的异常处理分支,该分支在记录错误日志后,没有正确关闭数据库连接和提交/回滚事务,导致连接池中的这个连接一直持有旧事务。这是一个典型的应用层资源泄露问题。 - 预防措施:
- 应用侧:修复代码缺陷,确保异常路径下的事务和连接得到妥善处理。建议使用
try-catch-finally或类似结构,在finally块中执行资源清理。 - 数据库侧:考虑为应用用户会话设置
IDLE_TIME资源限制,强制断开长时间空闲的会话(需谨慎评估,避免影响长事务业务)。 - 监控侧:在监控系统中增加对
enq: TX等待事件的告警阈值,并设置定期扫描长时间存在未提交事务的会话的作业。
- 应用侧:修复代码缺陷,确保异常路径下的事务和连接得到妥善处理。建议使用
6. 常见问题与深度避坑指南
在实际操作中,你可能会遇到比本例更复杂的情况。以下是一些常见场景和应对技巧。
6.1 当ALTER SYSTEM KILL SESSION无效时
有时,执行KILL命令后,会话状态长时间停留在KILLED或MARKED FOR KILL,锁依然存在。这通常发生在操作系统进程级别。
- 原因:会话可能正在执行一个不可中断的操作(如网络I/O),或者数据库进程(
PMON)清理速度较慢。 - 解决方案:
- 在操作系统级杀死进程:
- 首先,从数据库中找到会话对应的操作系统进程ID(SPID):
SELECT s.sid, s.serial#, p.spid, s.program, s.osuser FROM v$session s, v$process p WHERE s.paddr = p.addr AND s.sid = 134; - 登录数据库服务器,使用操作系统命令(Linux下为
kill -9 <SPID>)强制终止该进程。这是非常强硬的手段,应作为最后选择,并务必先确认SPID的正确性。
- 首先,从数据库中找到会话对应的操作系统进程ID(SPID):
- 重启实例(万不得已):如果大量关键会话被一个无法杀死的进程阻塞,且严重影响业务,可能需要计划内重启实例。这需要严格的变更管理和业务窗口。
- 在操作系统级杀死进程:
6.2 处理分布式事务与两阶段提交
如果Hang会话涉及分布式数据库事务(使用DB_LINK),情况会更复杂。它的状态可能是IN TRANSACTION,并且v$session中可能看到LOCKWAIT指向一个远程对象。
- 排查要点:检查
DBA_2PC_PENDING视图,查看是否有挂起的分布式事务。 - 处理建议:优先尝试在应用或协调节点提交/回滚。如果无法解决,可能需要DBA手动强制提交或回滚(
COMMIT FORCE/ROLLBACK FORCE),这需要精确的事务ID,操作风险极高,务必参考Oracle官方文档并在测试环境演练。
6.3 预防优于治疗:建立日常监控
定期检查以下内容,可以将Hang问题扼杀在摇篮里:
- 长时间未提交事务监控:
SELECT s.sid, s.serial#, s.username, s.program, s.last_call_et, t.start_time, t.used_ublk, t.used_urec FROM v$session s, v$transaction t WHERE s.saddr = t.ses_addr AND s.last_call_et > 1800 -- 超过30分钟 ORDER BY t.start_time; - 锁争用监控:定期检查
v$lock和v$session,关注blocking_session不为空的会话。 - 应用设计规范:
- 事务要短小精悍,尽快提交。
- 避免在循环中执行逐条提交,应使用批量操作。
- 更新操作尽量使用主键或高效索引,减少锁定的范围和时长。
- 设置合理的SQL执行超时时间(如通过
ORA-00028会话超时或应用层超时)。
6.4 关于RAC环境的特别考虑
在Oracle RAC环境中,Hang可能涉及多个实例。上述查询大部分需要使用gv$开头的全局视图(如gv$session,gv$lock)。阻塞链也可能跨实例。此时,blocking_instance字段会指示阻塞会话所在的实例号。KILL命令也需要在正确的实例上执行,或者在任意实例使用ALTER SYSTEM KILL SESSION 'sid,serial#,@inst_id' IMMEDIATE;语法,其中inst_id来自gv$session。
移除一个Hang进程,就像做一场精细的外科手术。它考验的不仅是DBA对Oracle内部机制的理解深度,更是对排查流程的严谨性、操作风险的评估能力以及与上下游团队协作沟通的综合把握。每一次成功的处理,都应该成为优化系统和预防下一次故障的宝贵经验。真正的价值不在于“杀掉”进程的命令本身,而在于构建起从快速诊断到根因预防的完整能力闭环。