拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OGG复制延迟背后:数据库锁竞争排查与死锁还原实战

OGG复制延迟背后:数据库锁竞争排查与死锁还原实战

1. 一次离奇“卡死”引发的排查

做数据运维的人都知道,生产环境最怕的不是直接报错的故障,而是一种“一切看起来都正常,但业务就是不动”的诡异状态。那天上午,我盯着监控大屏,OGG(Oracle GoldenGate)的Replicat进程显示Running,没有报错,复制延迟却从几十秒一路飙升到三个小时。数据库的活跃会话里出现了一个“SESSION LOCK”标记,v$session里blocking_session字段指向了一个模模糊糊的SESSION ID,可顺着这个ID查下去,却发现持有锁的那条会话没有任何SQL在执行。更让人头疼的是,alert日志里搜了一圈,连一条ORA-00060都没有。也就是说,Oracle自己根本没识别出这是一个死锁。

这种场景,就是标题里说的“看不见”的死锁——数据库层面没有报出循环等待,但实际系统已经卡成了一锅粥。后来我们引入了DBdoctor做全景诊断,才把这个锁竞争链条从会话到SQL、从持有到等待、从OGG到另一个“隐藏角色”完整还原了出来。这篇博文就把整个排查过程、背后的机制、以及可复用的诊断思路分享出来,适合正在跟OGG打交道、或者被锁等待问题折磨过的DBA和运维朋友。

OGG这个工具,本质上是做数据复制的,它本身不产生数据业务,但它需要以数据库会话的身份去读取事务日志、执行目标端SQL。一旦它和业务SQL、DDL操作、外部定时任务在同一个数据库实例里相遇,就会触发锁竞争。而多数情况下这种竞争并不满足Oracle死锁检测器的“环状等待”条件,于是Oracle不吭声,OGG也不吭声,只有业务在默默变慢。

2. OGG到底在什么环节参与“抢锁”

2.1 三个核心进程各自的锁行为

要搞清楚OGG怎么抢锁,得先看清它的架构。OGG在源端一般有Extract进程,主要读取数据库的redo log或者archive log,解析出事务变化后写入本地trail文件;然后在源端还可能有一个Data Pump进程负责把trail文件传输到目标端;目标端则是Replicat进程,负责把接收到的trail文件解析为SQL并应用到目标库。这三个进程里,真正会在数据库内部产生锁的就是Extract和Replicat。

很多人有一个误区,认为Extract读日志就不碰表,所以没有锁。实际上,如果不做特殊配置,Extract在启动时或需要读取数据字典时,会和DDL操作争夺字典锁;如果用了“Critical Transaction”直接抽取表数据,那它更会像普通会话一样持有表锁、行锁。而在目标端,Replicat执行应用SQL时,它的行为和一个业务应用几乎没有任何区别,每条INSERT/UPDATE/DELETE都会走正常的锁机制。所以,OGG抢锁的本质,不是OGG有什么特殊的锁,而是它作为一个“普通会话”参与了数据库并发控制。

2.2 常见的四类锁竞争场景

第一类是行锁竞争,Replicat执行DML时,和业务事务更新了同一行,互相等待提交或回滚。这类竞争最直观,Oracle的自动死锁检测往往能识别。第二类是表锁和DDL锁竞争,比较隐蔽,比如有人在业务库执行了ALTER TABLE修改字段,即使使用在线DDL(Online DDL),也会有一个短暂的瞬间需要拿表锁,此时如果OGG的Replicat正好要往这张表写入数据,就会形成阻塞。第三类是字典锁竞争,源端Extract要解析一个刚被修改过的表的日志时,需要读取数据字典,而此时某个运维脚本正在执行DDL并持有相关字典锁,Extract就会阻塞。第四类是OGG内部的队列锁与外部门锁的组合,比如Extract写trail文件时的I/O锁,和操作系统层的外部进程锁,如果和数据库锁相互等待,会形成一个Oracle根本感知不到的死锁闭环。

2.3 为什么“死锁”在日志里看不见

Oracle的锁定机制里,有一个后台进程负责死锁检测。但它的检测逻辑依赖资源管理器的等待图,需要形成“A等B,B等C,C等A”的环。而在OGG参与的很多锁竞争里,等待关系往往是链状的:Replicat在等业务会话持有的行锁,业务会话又在等一个外部脚本站住的锁,外部脚本则在等OGG进程结束后释放的部分资源——这个环绕过了数据库资源管理器,或者跨越了数据库边界,因此Oracle不会抛ORA-00060。这就导致一个问题:你查看v$session时能看到等待,看到blocking_session,但往源头追几层,发现最后一个blocker是一个“非数据库”实体,比如一个shell脚本、一个LDAP连接、一按Java进程,这条线就断了。DBdoctor这种专业的诊断工具,恰恰能把断掉的线重新接起来。

3. 用DBdoctor还原“看不见”的死锁现场

3.1 核心思路:从“等待关系”反推“持有关系”

我们常说,锁问题的本质不是某一个会话的错,而是整个等待关系的拓扑结构出了问题。Oracle原生的v$session、v$lock可以给出单点的会话等待信息,但要手工把这些信息拼成一张完整的等待图,尤其在多节点RAC环境、在OGG和业务并发的情况下,几乎是不现实的。DBdoctor的做法是从数据库内部采集等待事件、会话状态、锁持有者和等待者的对应关系,把这些信息汇总成一张锁等待依赖图。图上每个节点是一个会话,每条连线表示“谁在等谁”。死锁就不再是一个抽象概念,而是图上的一个环。只要这个环出现,哪怕Oracle没有识别出来,你也可以人工判断这就是死锁。

这个思路强调“时间线”而非“瞬时快照”。普通查询只能看到当前这一秒的状态,但一个死锁的形成往往经历了几分钟甚至几小时的演变。例如OGG Replicat可能在10:00就开始等待TL锁,10:05业务会话才进来抢同一行锁,10:10外部脚本才启动并占用一个关键资源,这时候环才闭合。单看10:10的快照,你看到的是业务会话在等外部脚本,却看不到OGG其实才是那个最初被卡住、后续又卡住别人的角色。DBdoctor能按时间线回放锁等待关系的形成过程中,这就能完整还原现场。

3.2 一个可复现的“死锁”模拟环境

为了说清楚这个还原过程,我搭了一个简单的模拟环境。一台Oracle 19c测试库,开了一张业务表T_ORDER,主键ORDER_ID。OGG目标端配置了一个Replicat,映射关系是T_ORDER整表同步。业务侧用一个PL/SQL事务同时更新T_ORDER里三行数据;与此同时,用一个外部shell脚本模拟运维操作,脚本里执行了一条LOCK TABLE T_ORDER IN EXCLUSIVE MODE等待特定条件;最后让OGG的Replicat也执行更新。三者同时启动,就会出现一种“看不见”的死锁:Oracle的等待图里能看到A等B、B等C,但C的会话被识别为background或application类型,等来等去没有形成闭环。

需要强调一点:OGG Replicat默认会在一个事务里批量应用多条更改。如果其中一个批次里的第一条语句被阻塞,整个批次都会挂起,表现成一个长时间运行的会话,但实际上它只是拿着一个未提交事务的锁在等待。这种情况下,你从v$session看到的OGG会话状态非常像“正常执行中”,很多人会忽略它。

3.3 还原死锁的四步法(DBdoctor视角)

第一步,打开DBdoctor的锁等待全景视图。先看是否存在“环形”拓扑,即任意两个会话节点之间是否首尾相接。我先截取了一段锁等待图,发现Replicat会话(SQL*Plus /dgc? 不,程序名是“OGG Replicat”)在等待一个TX锁,而这个TX锁的持有者是来自JDBC连接的业务会话;业务会话又在等待一个程序名为“DataCollector”的外部连接;DataCollector连接正阻塞在一条LOCK TABLE语句上,进一步查看,它等待的对象是一张被OGG写了几万条记录的缓冲表。

第二步,拉取时间线。在特定时间窗口内,DBdoctor会按照会话的第一次等待时间、持有锁的时间、持续时间来排序。这一步能定位到“谁是第一个被锁住的”,以及“最后一个加入的”。在我们的模拟环境里,时间线显示OGG才是第一个被锁住的人,它在业务会话启动之前就已经进入了等待状态,只是因为持续等待不明显,一度被排除在外。

第三步,关联SQL和程序名。DBdoctor的会话详单里能看到每个锁会话最近执行的SQL。OGG Replicat的SQL可能显示为“insert into T_ORDER(...) values(...)”,业务会话是普通的UPDATE,DataCollector则是LOCK TABLE。程序名和SQL一对应,锁竞争的参与方立刻浮出水面。

第四步,回溯历史操作。通过DBdoctor的ASH历史(活动会话历史)快照,可以看到几分钟前是否有人执行过DDL、是否出现过较长时间的“enq: TM - contention”等待。这些历史数据能把“看不见”的因素补全,比如一个DBA曾在测试端跑过ALTER TABLE,虽然SQL已经结束,但它留下的锁元数据和资源状态仍然影响着后续会话。经过这四步,一次数据库层面检测不到的死锁,就从模糊的“卡顿”变成了一张清晰的关系图。

3.4 诊断过程中需要注意的参数和配置

如果准备在真实环境里复制这个诊断流程,有几个配置要特别注意。对OGG Replicat来说,如果开了BATCHSQL,它会把一批操作打包成一个批处理事务,这会让锁等待变得更为集中,但也更容易隐藏“实际只有一行脏数据”的真相。在诊断时,建议临时关闭BATCHSQL,或者通过HANDLECOLLISIONS参数控制冲突处理,让问题更容易暴露。另一个关键是数据库端的DDL_LOCK_TIMEOUT参数,默认值是0,即DDL拿不到锁会立即报ORA-00054,但在某些版本和场景下,等待行为可能被掩盖。我建议所有运行OGG的库都把DDL_LOCK_TIMEOUT设置一个较小的值,比如5秒,这样一旦出现潜在的锁竞争,DDL会主动超时报错,而不是傻傻等待,并且会在alert日志里留下痕迹。

4. 一次真实排查经历:从延迟告警到完整还原

4.1 现场采集的关键信息

回到开头那次的故障。我记得当时v$session查出来的关键信息大概长这样:

select sid, serial#, program, blocking_session, event, seconds_in_wait from v$session where status='ACTIVE' and wait_class <> 'Idle';

结果里有一行非常刺眼:program列为“OGG Replicat”,event是“enq: TX - row lock contention”,blocking_session指向了一个SID=256的会话。继续查SID=256的program时,发现它是“JDBC Thin Client”,但进一步查询v$lock,它手里攥着一把TM锁,锁的对象是T_ORDER表。到这里,直觉判断是OGG Replicat在等业务会话释放行锁,这一判断看起来没毛病。于是我通过DBdoctor的会话管理功能把JDBC会话断开,果然OGG Replicat立刻恢复了运行,延迟也在几分钟内追平。我以为问题解决了。

4.2 第一次“假解决”带来的麻烦

但那只是表面。两个小时后,同样的告警再次出现,这次延迟涨得更快。我再查,发现又是同一个组合:OGG Replicat在等某个JDBC连接。当时我心想,难道是业务侧写了一个定时任务,持续霸占锁?我再次分析了那个JDBC会话的SQL,发现它执行的是一条普通的INSERT语句,持续不到毫秒就结束了,但在v$session里它的blocking_session却指向另一个奇怪的Program“oracle@host (J000)”——一个调度器作业(Scheduler Job)。顺着J000查下去,发现它是一个数据库内部作业,正在执行“DBMS_STATS.GATHER_TABLE_STATS”的统计信息收集作业,而那个统计作业正在等待另一个资源:为了采样,它要在T_ORDER表上获取一个非常短暂的SN锁(或SH锁),但表上恰好有OGG的一个未提交事务,锁互不兼容。

这就构成了一个链:OGG Replicat等业务INSERT的行锁,业务INSERT等统计作业的SN锁,统计作业又等OGG Replicat未提交事务释放锁。看到这里,我终于意识到,Oracle的自动死锁检测为什么没抛出ORA-00060?因为统计信息收集作业并不是一个普通事务,它的锁等待属于“统计锁”层面,MySQL我不知道怎么做,但在Oracle里这种锁等待图通常不会触发死锁检测。我们第一轮直接杀掉业务连接,等于拆断了这根链条,但根本矛盾没有解决,过一会儿同样的闭环又会从另一个节点重新接上。

4.3 DBdoctor画出的那张完整“环形等待图”

这个案例里,DBdoctor的锁等待拓扑图把每一个参与方都展示了出来:OGG Replicat指向一个普通INSERT会话,这个INSERT会话又指向自动统计信息作业,自动统计信息作业反向指回OGG Replicat,形成了一根封闭的环。图上每个节点还标注了等待起始时间和当前持有的锁类型。一眼看过去,整个死锁链就清清楚楚。随后我点击环上的某个节点,可以查看它在等待过程中切换过的所有等待事件——从TX行锁竞争切换到ENQ: TM – contention,再到statistics lock,这些事件切换记录是普通AWR报表里根本看不到的。

DBdoctor还提供了一个“时间线回放”模式:选择故障时间段,它能按分钟展示所有涉及的会话状态和锁等待关系。回放后发现,链条最早的一环其实是OGG的延迟,而不是业务。OGG Replicat因为一条目标端慢SQL连续重试,始终没有commit,所以它持有的那行锁迟迟不放。这个初始问题导致后续所有会话都积聚在它的后面。等空闲统计作业一进来,死锁链就闭环了。

4.4 修复方案的落地

最终我们做了三件事:第一,将OGG针对目标端慢SQL的“MAXTRANLOOP”或“GROUPTRANSOPS”参数适当调低,避免单个事务积累过多操作而长时间不释放锁;第二,把自动统计信息收集的窗口避开OGG复制高峰,并且给相关作业设置了LOCK_TIMEOUT,让它拿不到锁就主动退出而不是SLEEP等待;第三,在DBdoctor里配置一个锁等待环检测告警,一旦拓扑图中出现环状结构,不管Oracle是否报错,都自动通知值班人员。这个配置上线之后跟踪两周,再也没有出现过同样性质的故障。

5. 常见问题与排查技巧速查

5.1 怎么快速判断“疑似OGG死锁”

如果OGG Replicat进程正常,业务也无明显报错,但复制延迟持续增长,第一步不是看OGG日志,而是查数据库锁视图。重点关注三类信息:程序名包含OGG的会话是否长时间处于非Idle等待;blocking_session不为空且block_chain涉及多个Program;v$lock里的LMODE为6(排他)或为3(行排他)持有者的实际SQL是否长时间没有提交。如果同时满足这三条,基本可以确定有OGG参与的大型等待链。

还有一个容易忽略的细节:OGG Replicat在“LOGREADER”阶段时,会话状态可能会显示为“SQL*Net message from client”,很多人会把它误判为空闲会话。实际上,那可能是OGG进程主循环在等待下一批日志提交的控制信号,并不代表它真正空闲。要区分这个状态,最好结合OGG自身的进程报告(GGSCI里的stats)来看,如果行数没有变化,说明它在等锁或在等更底层的队列,而不是在工作。

5.2 常用锁诊断工具对比

下面这个表是我在实际工作中常用的排查工具和它们的适用场景:

工具/手段能看到的局限性适合场景
v$session + v$lock 手工SQL当前瞬时锁关系、blocking会话只能看单点快照,很难自动发现环突发问题初步定位
Oracle死锁trace(ORA-00060)死锁涉及的两个事务和SQL对“看不见”的闭环无能为力数据库识别到的死锁
AWR/ASH报告会话等待事件分布、历史活跃会话没有精确的锁持有链路关系事后分析性能瓶颈
EM / Grid Control锁等待图、会话阻塞树多节点可视化较弱,有时操作笨重单库或者小规模环境
DBdoctor全局锁等待拓扑图、时间线回放、跨会话程序关联需要能采集到数据库诊断日志,需要部署agent复杂环境中快速识别“看不见”的锁环

这里想多说一句,很多OCP培训资料里会让你直接查all_objects查看object_id,再关联v$lock找阻塞,但实际上遇到真正的多跳等待,手工SQL的效率非常低。花几分钟把工具部署好,比在命令行里反复join十几个视图效率高得多。

5.3 与OGG共存时的三个保命习惯

第一个习惯,永远不要忽略OGG的未提交事务。OGG应用端默认采用“隐式事务”,每批次结束才提交。任何业务查询如果在OGG的批次中访问同一行数据,都会直接堵死在那。所以尽量给OGG的Replicat配置合理的GROUPTRANSOPS(例如100),避免一个批次攒太久。

第二个习惯,执行DDL之前先查OGG延迟。在线DDL虽然破坏性小,但在目标库上执行时仍然会产生语言锁等待。如果OGG延迟很高,先暂停Replicat或把DDL放到维护窗口,否则极容易形成“DDL等OGG、OGG等DDL提交”这种闭环。别小看这个闭环,Oracle极大概率不会报错。

第三个习惯,给所有外部脚本、定时任务、监控程序都加上“锁超时”。不要用默认的无限等待。比如在PL/SQL里使用DBMS_LOCK.SLEEP等于永久等待的写法,或者Java里的setQueryTimeout设置太大,都会成为死锁闭环里的隐藏节点。让每个参与方都有主动退出的时间,整个系统反而更健壮。

6. 写在最后:共享资源场景下的锁管理思路

我后来回看这次故障,最大的教训不是“OGG抢锁”,而是“谁也没有恶意抢锁”,只是几个正常的数据库操作在并发条件下碰在了一起。OGG、业务SQL、统计作业、外部脚本,每一个单独拿出来都无害,连在一起就变成了数据库看不见的死锁环。过去我会习惯性地去杀阻塞会话,以为断开一条链路就能解决。但现在我更倾向于先完整还原一张锁等待图,看清楚谁是源头,谁是被动等待,哪个会话其实是“假阻塞”——它正因为锁超时即将失败,只是还没走到失败那一步。

如果你也想在生产环境里复制一下这个思路,我建议先从最小的闭环开始练习。搭建一套OGG测试环境,配置好一个持续复制的Replicat,再写一个外部脚本去和它抢同一行数据,最后用DBdoctor画出等待关系图。这个过程不需要太多理论知识,只要你亲手看到一次“Oracle不报错但进程卡死”的样子,以后遇到真问题时就不会慌。工具方面,DBdoctor确实给排查提供了很大帮助,你也能在官方文档里找到更细的配置说明;而更重要的,是保持那种“不到最后一个会话就不下结论”的耐心。

返回列表