ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Oracle 12c在线redo日志误删后的故障诊断与恢复实战指南

Oracle 12c在线redo日志误删后的故障诊断与恢复实战指南

redo在线日志被误删,这大概是在Oracle 12c数据库运维里最能让人瞬间清醒的事故之一。我这些年处理过的类似case不算少,最常见的原因是DBA在清理/u01这类目录下的历史日志文件时,把redo01.log、redo02.log当成了普通日志顺手删掉。接下来剧情几乎都一样:alert日志里开始刷ORA-00313、ORA-00312,日志切换卡住,应用侧批量写入全部悬停。这篇文章围绕这个标题做一次完整复盘——从redo被删后的故障表现、诊断方法,到各种状态下怎么安全恢复,把12c多租户和RAC环境里的特殊坑也一并讲清楚。

1. 事故背景:redo日志被删后,数据库到底在慌什么

1.1 redo日志在12c体系里的角色定位

redo log在Oracle数据库里的作用,可以用一个很朴素的类比来解释:数据文件就像是账房的余额登记本,而redo日志就是流水账。你每做一笔操作,会计先往流水账上记一笔,然后才去改余额。这样即使某天余额本被撕了,只要流水账还在,我们就能从最近一次完整备份往后一笔一笔重放,把余额恢复到崩溃前的状态。

在12c里,这个逻辑没有本质变化,但多租户架构让负担变重了:一个CDB容器里挂着多个PDB,所有PDB产生的事务redo全都写到CDB实例的在线redo里。也就是说,redo一旦出问题,影响的不是一个模块,而是容器里所有租户的全部写操作。RAC环境下更麻烦,每个实例有自己的redo thread,一个节点上的redo被删,可能只挂掉那一个节点,但整个集群的健康检查、负载均衡都会陷入混乱。

1.2 redo文件被删后的三种典型现场

redo日志被误删后,数据库并不一定立刻崩溃,表现取决于被删的日志组当时处于什么状态,我归纳出三种最常见现场。

第一种,数据库还在正常运行,只是某个非当前日志组的数据文件丢了。这种最轻,查询、更新暂时不出错,但日志切换一到那组就会开始报错,归档进程也可能异常,整体上是个“慢性病”。

第二种,日志切换过程中卡死,所有写事务全部挂起。通常是因为某一组redo被删后,LGWR尝试切换到该组或归档进程尝试读取该组时失败,事务无法继续。这种属于“急性病”,业务完全等不了,必须马上处理。

第三种,数据库已经因为其他原因关闭,再次启动时发现某个redo成员不存在。这种最危险,尤其是被删的那组是CURRENT状态时,实例恢复可能直接无法完成,数据库处于无法open的状态,处置难度直线上升。

这三种现场,对应的恢复思路完全不同。所以我一直跟团队强调:听到“redo被删”之后的第一反应不应该是慌乱,而是先搞清楚被删的日志组在v$log里面的状态,再看数据库当前是mount还是open状态。

2. 先别急着恢复:用10分钟认清redo丢失的真实状态

2.1 从alert日志和动态视图里锁定线索

拿到事故现场,我习惯按下面顺序摸情况。

先看alert日志,通常路径在$ORACLE_BASE/diag/rdbms/{dbname}/{SID}/trace/alert_{SID}.log,搜索ORA-00312、ORA-00313、ORA-16038、ORA-00318这类关键字。这些错误会直接告诉我们哪个日志组、哪个member文件有问题。

接着登录数据库,执行最重要的两条查询:

SQL> SELECT group#, thread#, sequence#, bytes, status, archived FROM v$log; SQL> SELECT group#, thread#, member, status FROM v$logfile;

v$log的status字段是判断危险等级的关键。CURRENT表示LGWR当前正在写的日志组,被删了就等于账本正在记的那一页被撕掉了,影响最大;ACTIVE表示该日志组里还有未完成checkpoint的数据信息,实例恢复或介质恢复时可能还需要用到它;INACTIVE表示已经被安全checkpoint,不再需要参与恢复,删除后的处置难度相对最低。

v$logfile里的status则更偏向物理文件层面,如果某个member显示INVALID或空白,而操作系统层面ls也看不到文件,基本可以实锤就是它了。

2.2 判断数据库处于open还是mount状态

第二个关键变量是数据库当前是开着的还是已经关了。

如果数据库还处于open状态,哪怕日志切换已经卡住,我们的操作空间也大很多,至少可以通过添加新日志组、强制切换、清理无效组的方式,把数据库“扶正”,大多数场景能做到不停机恢复。

如果数据库已经处于shutdown状态,尤其是不干净关闭之后,实例启动到open阶段时需要读取redo完成实例恢复,这时候少了关键redo,直接无法open。这时候就要区分:数据库之前是否是正常关闭?如果是正常shutdown,数据文件已经保持一致,我们走重建控制文件并resetlogs的路线;如果是abort或宕机,redo又是必需的,那就只能依靠备份和归档做恢复了。

这一步判断决定了整个恢复策略,我建议把它写进故障预案,不要到现场再临时想。

2.3 查看归档状态和备份可用性

redo被删之后,归档状态同样重要。

在open状态下执行:

SQL> ARCHIVE LOG LIST; SQL> SELECT thread#, sequence#, archived, status FROM v$log;

如果某个日志组对应的归档文件缺失,后续恢复时会出现“断档”。这时需要查一下最近的物理备份情况:

RMAN> LIST BACKUP OF DATABASE; RMAN> CROSSCHECK BACKUP;

知道最后一刻的全备、增量备份、归档备份在哪里,恢复的上限和下限就基本画清楚了。没有备份兜底的话,恢复策略会激进很多,风险和代价都要向业务方讲明白。

3. 数据库还在跑:在线重建与“挪走”损坏日志组

3.1 丢失的是非当前日志组的稳妥处理

如果v$log查询结果里,被删的日志组状态是INACTIVE,而数据库还open,这是最理想的情况。

我的处理顺序是:

  1. 强制做一次全库checkpoint:
    SQL> ALTER SYSTEM CHECKPOINT;
  2. 确认该组已经是INACTIVE:
    SQL> SELECT group#, status FROM v$log;
  3. 直接删掉这个坏日志组:
    SQL> ALTER DATABASE DROP LOGFILE GROUP 1;
  4. 在原来的位置或新的磁盘路径上重建同编号的日志组:
    SQL> ALTER DATABASE ADD LOGFILE GROUP 1 ('/u01/oradata/orcl/redo01a.log', '/u02/oradata/orcl/redo01b.log') SIZE 200M;

这里有个操作细节:日志组编号可以复用,但drop之后要确保所有member文件都物理消失,再重新add,否则可能出现ORA-00362等残留问题。成员文件最好配置两组以上,并且放到不同磁盘,避免再次一锅端。

3.2 丢失的是ACTIVE日志组时,先让它变成INACTIVE

如果v$log里显示被删的组是ACTIVE,不能直接drop。ACTIVE意味着虽然LGWR已经不写它了,但数据文件里还有一部分脏块尚未写入,spfile或控制文件里记录了它需要用于实例恢复。

这时不要慌,先做checkpoint:

SQL> ALTER SYSTEM CHECKPOINT;

checkpoint的作用就是把redo里记录的脏数据全部写到数据文件,完成后LGWR就不再依赖该日志组,状态会从ACTIVE变成INACTIVE。然后按上面的流程,继续走drop和add。

如果checkpoint命令执行时因为日志切换卡住而hang住,可以先手动触发一次日志切换:

SQL> ALTER SYSTEM SWITCH LOGFILE;

但要注意,如果日志文件在操作系统层面已经被删掉,LGWR可能根本切换不了。这种情况我一般先添加一组全新的日志组,让LGWR有可用的新“草稿本”,然后连续手工切换,把业务流量尽快从坏组上挪开:

SQL> ALTER DATABASE ADD LOGFILE GROUP 4 ('/u01/oradata/orcl/redo04a.log', '/u02/oradata/orcl/redo04b.log') SIZE 200M; SQL> ALTER SYSTEM SWITCH LOGFILE;

多次switch logfile之后,原有坏组通常会被轮换到INACTIVE状态,再执行checkpoint和drop就顺利很多。

3.3 丢失的是CURRENT日志组但实例还活着,怎么争分夺秒

CURRENT状态的日志被删是最棘手的在线场景。LGWR手里正在写的那本“流水账”被撕了,数据库表现常常是大量写事务堆积、日志切换报错、实例接近假死。

在Linux这类文件系统上,有一种常见但微妙的局面:如果删除命令发生时,LGWR进程还持有这个文件的句柄,文件虽然从目录里消失了,但进程还在往这个“幽灵文件”里写数据,数据库短时间内还能继续运行。遇到这种情况,我的建议是千万不要重启实例,先以最快速度做一次日志切换,让这个损坏组脱离CURRENT状态:

SQL> ALTER SYSTEM SWITCH LOGFILE; SQL> ALTER SYSTEM CHECKPOINT;

然后立刻查询v$log,如果CURRENT已经落到其他正常的组上,原有的坏组变成ACTIVE或INACTIVE,就按前面的方式drop掉再重建。

如果文件句柄已经失效,LGWR报错无法继续写,切换也做不动,那就不能犹豫了。我的处理是:尝试添加新日志组,并强制切换;这条也无效,就只能考虑受控的shutdown abort,然后走下一章的实例恢复流程。这里要特别提醒,shutdown abort是有代价的,redo一旦缺失,实例恢复可能无法完成,必须在心里有备份方案兜底的前提下做。

3.4 只是丢了一个成员文件,补回来就行

有时候被删的不是整组redo,而是一组中的某一个member。比如redo01.log组本来有两个member,被误删了其中一个。这种损伤很小,v$logfile里会显示某个member状态为INVALID,或者直接消失。

处理办法很简单,在mount或open状态下,给该日志组补一个成员:

SQL> ALTER DATABASE ADD LOGFILE MEMBER '/u02/oradata/orcl/redo01b.log' TO GROUP 1;

补充完成后,LGWR会自动重新初始化该文件,随便观察几轮日志切换,确认成员状态恢复正常即可。需要留意的是,如果原路径目录因为磁盘故障不可写,就把新成员加到其他目录,多路复用本来就是防单点故障的。

4. 数据库已经“趴窝”:从启动失败到实例恢复

4.1 冷启动报错怎么读:ORA-00313与ORA-00312的组合

数据库关闭后再次启动,如果要恢复无法进行,常见报错是这个样子:

ORA-00313: requested failed operation to open file for redo log thread 1 ORA-00312: online log 1 thread 1: '/u01/oradata/orcl/redo01.log'

这种情况说明控制文件里记录的redo路径找不到了。再往下翻,可能还有ORA-27037、ORA-00314、ORA-00316等,细节不同但本质都是:实例open阶段需要加载在线日志,而相应文件已经不存在或严重损坏。

看到组合报错后,不要急着反复startup,而是先执行startup mount,确认数据库能不能进入mount状态。mount只需要控制文件和数据文件头部信息可读,不涉及redo,所以通常是可以挂上去的。

挂上去之后立刻查v$log,看看各个日志组的状态。如果之前是正常关闭的,数据文件本身是一致的,日志组的status可能是UNKNOWN或CLEARING之类的不健康状态。

4.2 正常关闭后丢失redo的恢复:重建控制文件加resetlogs

如果事故前数据库是正常shutdown的,那么数据文件已经完成同步,此时我们其实并不需要旧的redo内容来做实例恢复。问题是控制文件仍然指向那些已经不存在的redo文件,导致open阶段过不去。

我的做法是重建控制文件,让数据库“忘记”旧日志组,用新的登录日志组重新开门。

第一步,先把mount状态下能拿到的元数据备份下来:

SQL> ALTER DATABASE BACKUP CONTROLFILE TO TRACE AS '/tmp/controlfile_trace.sql'; SQL> ALTER DATABASE BACKUP CONTROLFILE TO '/backup/control01.ctl';

第二步,查看trace文件,修改其中的LOGFILE段。trace文件里默认会列出原有日志组路径,我们把路径改成新规划的路径,比如从/u01/oradata/orcl/redo01.log改为/u02/oradata/orcl/redo01.log,并确保新路径对应的目录存在且可写。

第三步,关闭实例后,以nomount方式启动,执行重建控制文件的脚本:

STARTUP NOMOUNT; CREATE CONTROLFILE REUSE DATABASE "ORCL" NORESETLOGS NOARCHIVELOG MAXLOGFILES 32 MAXLOGMEMBERS 4 MAXDATAFILES 1024 MAXINSTANCES 16 MAXLOGHISTORY 2048 LOGFILE GROUP 1 ( '/u02/oradata/orcl/redo01a.log', '/u02/oradata/orcl/redo01b.log' ) SIZE 200M, GROUP 2 ( '/u02/oradata/orcl/redo02a.log', '/u02/oradata/orcl/redo02b.log' ) SIZE 200M DATAFILE '/u01/oradata/orcl/system01.dbf', '/u01/oradata/orcl/sysaux01.dbf', '/u01/oradata/orcl/undotbs01.dbf', '/u01/oradata/orcl/users01.dbf' CHARACTER SET AL32UTF8;

然后open数据库:

SQL> ALTER DATABASE OPEN RESETLOGS;

这里要特别说明,NORESETLOGS还是RESETLOGS,取决于数据文件是否完全一致。正常关闭后数据文件一致,用NORESETLOGS重建控制文件后,理论上可以直接open,但实际中重新定义在线日志路径后,使用RESETLOGS更省事,可以重置日志序列号并强制让数据库接受新的日志状态。执行RESETLOGS之后,所有历史日志序列会被清零重来,之前所有归档立即失效,务必马上做全备。

4.3 非正常关闭且redo缺失,只能走备份恢复路线

如果数据库不是正常关闭,而是宕机、停电、abort之后发现redo被删,事情就严重了。因为数据文件本身是不一致的,系统要完成实例恢复必须读取旧的redo,而redo已经被删,这等于撕掉了恢复的关键线索。

这种情况下,除非有物理备用库或DG可以切换,否则恢复思路基本只有一条:从最近一次完整备份开始恢复。

标准流程用RMAN来做:

RMAN> STARTUP MOUNT; RMAN> RESTORE DATABASE; RMAN> RECOVER DATABASE UNTIL CANCEL; RMAN> ALTER DATABASE OPEN RESETLOGS;

RECOVER DATABASE UNTIL CANCEL会在缺失的redo阶段停下来,我们手动输入CANCEL结束恢复。这样做本质上属于不完全恢复,会丢失从备份点到故障点之间不能通过归档补全的部分数据,业务侧必须确认可接受的数据丢失窗口。

执行之前,先把所有可用的归档备份和增量备份收集齐,用crosscheck确认它们没有被刷掉。恢复完成后同样要立即做全备。如果遇到RMAN恢复时找不到某个归档文件,可以在不全的sequence位置cancel,损失的只是那个时间点到故障点之间的事务。

4.4 恢复之后的验证流程

不管用哪种方式把数据库拉起来,后续验证都马虎不得。

我习惯按下面顺序检查:

  1. 确认实例状态:
    SQL> SELECT instance_name, status FROM v$instance; SQL> SELECT name, open_mode FROM v$database;
  2. 查看所有PDB是否正常打开,12c里很容易出现CDB正常但某个PDB处于MOUNTED或RECOVER状态:
    SQL> SELECT con_id, name, open_mode FROM v$pdbs; SQL> ALTER PLUGGABLE DATABASE ALL OPEN;
  3. 检查日志组是否全部健康:
    SQL> SELECT group#, status, archived FROM v$log; SQL> SELECT group#, member, status FROM v$logfile;
  4. 做几次日志切换,观察是否能正常归档:
    SQL> ALTER SYSTEM SWITCH LOGFILE; SQL> SELECT sequence#, archived, status FROM v$log;
  5. 业务侧抽几个关键事务,确认数据读写的正确性。

做完这些,才能算故障处理真正结束。恢复后当天不要立刻撤掉现场备件,至少保留全备和日志一段时间。

5. 容易踩坑的诱因场景:归档卡死、RAC线程与PDB范围

5.1 日志切换hang住,被redo缺失连带拖垮

redo文件被删后,最直接的连带反应就是日志切换卡死。LGWR想要切换到下一个日志组时,发现下一个组里可能有active事务或归档失败,于是整个写入链路被堵死。数据库的表现为:等待事件出现log file switch、log file sync,大量会话堆积在insert/update上。

此时即便我们清楚坏组是哪一个,也不能直接drop,因为drop不能影响正在被使用或者尚未归档的日志组。我的处理顺序是:先查v$log,把所有组的状态拉出来,按照“添加新日志组-手工切换-强制checkpoint-清理坏组”的顺序来解开死结。

如果某个组因为不能归档而卡住,比如归档日志目标目录满了,还要先处理归档空间,否则清完redo组也没用。

SQL> SELECT destination, status, error FROM v$archive_dest; SQL> SELECT name, space_limit, space_used FROM v$recovery_file_dest;

如果是闪回恢复区满了,扩容或者调整归档策略,把可用的日志组轮换出来,事情很快就解开。

5.2 RAC环境里,redo thread被删是另一套逻辑

RAC环境比单实例复杂的地方在于,每个实例有自己独立的redo thread。v$log查询结果里会出现多个thread编号,而被误删的日志组可能只属于某一个节点。

这时候如果一个实例的CURRENT redo丢失,该实例可能直接宕掉,而另一个实例还活着。处理要分两步:

第一步,先让活着的实例接管业务,必要时设置service在当前可用节点运行。

第二步,在故障节点上恢复redo。RAC下添加日志组要指定thread:

SQL> ALTER DATABASE ADD LOGFILE THREAD 2 GROUP 10 ('+DATA/orcl/redo10a.log', '+RECO/orcl/redo10b.log') SIZE 200M;

删日志组时也要注意thread归属,不能用一个实例的会话去删另一个实例的redo thread,通常需要连接到对应线程名称的实例,或者使用类似以下的写法:

SQL> ALTER DATABASE DROP LOGFILE GROUP 10;

这里有一个容易忽略的地方:ASM磁盘组里删除文件不能用传统的rm命令,很多人是在ASMCMD或RMAN里把redo文件误删的。操作方向不一样,但处理逻辑通用——查控制文件、查当前状态、按状态推进恢复。

如果故障节点的redo缺失严重到无法自己启动,可以把该实例的redo thread重建出来,再做实例级恢复。RAC场景下我强烈建议平时就把所有实例的redo文件用OMF或ASM来管理,路径统一,排查时能省一半时间。

5.3 12c多租户架构之下,PDB视角的连带麻烦

12c的redo日志是CDB级的,PDB无法单独配置自己的在线日志。这就带来一个很微妙的问题:某个PDB的活跃事务可能正在使用redo,而redo被删会波及所有PDB,某几个PDB可能瞬间变得不可用。

恢复方案上一律在CDB层操作,不需要去改任何PDB的redo配置。但因为一个PDB可能有大量业务数据,恢复后要重点检查PDB的打开状态和数据一致性。

SQL> SELECT con_id, name, open_mode, total_size FROM v$pdbs; SQL> ALTER PLUGGABLE DATABASE pdb1 OPEN;

如果PDB无法打开,多半还需要执行PDB级别的恢复操作。这时DBA通常会在CDB里用以下命令:

SQL> ALTER PLUGGABLE DATABASE pdb1 CLOSE IMMEDIATE; SQL> ALTER PLUGGABLE DATABASE pdb1 OPEN RECOVER;

有一类场景特别容易忽略:redo被删后,数据库被还原到某个时间点,PDB的恢复点可能晚于CDB的,导致PDB无法正常打开。遇到这种情况,不要反复强制open,先检查PDB的undo和状态,必要时通过快照或备份单独恢复目标PDB。

6. 常见错误速查与DBA的几条保命经验

6.1 错误代码速查表

我把redo丢失场景中出现频率最高的错误整理成一张表,方便现场对照。

错误代码含义常见处理方向
ORA-00313无法打开指定redo日志文件确认文件路径、检查OS层文件是否存在
ORA-00312在线日志组及路径信息,常伴随ORA-00313出现结合v$logfile定位丢失的member
ORA-00314日志文件头部信息损坏通常需要clear或重建该日志组
ORA-00316日志文件类型不对多为控制文件信息与redo不匹配,重建控制文件
ORA-00321日志无法更新检查文件权限、磁盘空间
ORA-00327日志状态与文件实际内容不符大概率需要执行clear unarchived
ORA-16038日志无法归档检查归档目标、闪回恢复区空间
ORA-19809恢复区空间超限清理过期备份或扩大DB_RECOVERY_FILE_DEST_SIZE
ORA-27037物理文件不存在OS层面确认文件是否丢失
ORA-19588RESETLOGS与备份不匹配执行全备或将备份时间点对齐

看到任何一个错误码,第一时间先不要急着操作,回到v$log和v$logfile去定位具体是哪个thread、哪个group、哪个member坏了,再对照本文章的处置方法。

6.2 现场自救的几条铁律

第一条铁律:没有做完整备份评估之前,不要做任何破坏性操作。包括drop logfile、clear logfile、resetlogs,每一步都要想好“如果这一步失败,下一步怎么办”。

第二条铁律:优先保住控制文件。控制文件是数据库的“目录总索引”,redo丢了还有救,控制文件也丢了,恢复难度直接指数级上升。平时定期把控制文件备份到trace,绝对是成本最低的保险。

SQL> ALTER DATABASE BACKUP CONTROLFILE TO TRACE AS '/backup/control_trace_YYYYMMDD.sql';

第三条铁律:执行CLEAR LOGFILE之前想清楚归档后果。下面这条命令会把日志组标记为不再归档:

SQL> ALTER DATABASE CLEAR UNARCHIVED LOGFILE GROUP 2;

这意味着这一段redo对应的数据差异将无法通过备份来恢复。如果业务对数据完整性要求很高,这一步必须经过业务方确认。

第四条铁律:操作完成后马上做全备。任何一次用resetlogs打开数据库,都会让此前的日志序列失效,旧备份直接不可用,不赶紧做全备,相当于把自己再次置于无备份的裸奔状态。

6.3 我踩过一次最深的坑

有一次,我按网上资料对一个ACTIVE状态的日志组执行clear logfile,结果直接报错ORA-01624: log X needed for crash recovery。这条错误的意思是:日志组虽然状态不是CURRENT,但实例恢复还需要它,不能clear。

后来才真正理解,决断一个日志组能不能drop或clear,不能只看status字段,还得看它是否涉及crash recovery。稳妥做法是先ALTER SYSTEM CHECKPOINT,把恢复位置前移,减少对旧日志组的依赖。如果还是不能clear,继续添加日志组并切换,直到它彻底脱离关键路径。

那次之后,我给自己定了一条死规矩:处理redo故障时,所有影响动作的顺序永远是——先加新组、再切换、再checkpoint、再drop或clear,绝不允许跳到第一步。这个顺序看起来笨,但每次都能把风险压到最低。

现在回顾redo被删这类事故,其实最考验DBA的不是SQL命令背得有多熟,而是能否冷静判断当前状态、按顺序推进操作。误删文件这种坑,谁都不敢保证一辈子不踩,但只要我们能把日志组冗余做好、控制文件备份做到位、恢复流程写进预案,真遇到时就能把一场灾难变成一个普通的运维日。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进