1. RAC 归档日志的“本地化”特性:它为什么比单实例更容易“丢”
上周末处理一个两节点 RAC 环境,我在节点二上做例行维护,顺手执行了 RMAN crosscheck archivelog all。结果屏幕刷出一大批节点一的归档日志状态变成 EXPIRED,要不是同事多看了一眼,我差点就敲下 delete expired archivelog all。那批 thread 1 的归档文件在节点一的本地磁盘上明明好端端躺着,只是节点二的 RMAN 进程看不到而已。
这就是 RAC 环境归档日志恢复里的经典问题:很多 DBA 习惯于用单实例的思维去理解归档日志,认为只要 RMAN 连接上数据库,跨节点的归档日志都该被自动识别、自动恢复。但 RAC 底下每个实例有自己独立的 redo thread,归档日志落盘的位置又决定了一个节点能否访问另一个节点的日志文件。所谓“RMAN 跨节点归档日志的识别与恢复”,本质上要先回答三个问题:这张归档日志属于哪个 thread?它物理上放在哪个节点的哪个路径?执行恢复的 RMAN 进程到底能不能看到这个路径?
如果这三件事没理清,恢复 RAC 归档日志时就会碰到两类典型故障:一类是真丢,节点硬盘物理损坏、没有备份,那无论如何都补不回来;另一类是假丢,文件还在另一个节点上,但因为本地路径不可见,RMAN 在控制文件里把它们标成了 EXPIRED 甚至直接把元数据清理掉,最后变成真正的孤儿文件。这篇文章我会把 RAC 归档日志为什么会出现“跨节点识别”困难、如何用 RMAN 和 SQL 精确区分线程与序列、以及从备份中恢复缺失归档的完整操作讲透。无论你是刚接手 RAC 的 DBA,还是正在处理一次归档日志告警,按文中的排查链路走一遍,基本不会跑偏。
1.1 每个实例一条 redo thread,sequence 是“线程私有”的
先从一个最容易被忽略的概念讲起:RAC 的 redo thread 是实例私有的。集群里第一个实例启用 thread 1,第二个实例默认启用 thread 2。每个 thread 内部独立维护 sequence#,因此 thread 1 的 sequence 100 和 thread 2 的 sequence 100 是两个完全无关的日志文件。归档文件名里如果没有带上 thread 标识,或者你在查视图时只看了 sequence 而忽略 thread,那么对两个节点而言,日志连续性判断会彻底错乱。
生产环境里,log_archive_format 通常会设成类似 %t_%s_%r.dbf 的格式,%t 就是线程号,%s 是对应线程内的序列号。例如 1_15234_1109321234.arc 和 2_15234_1109321235.arc,虽然 sequence 都是 15234,但一个是 thread 1 的,一个是 thread 2 的,它们的作用是并行补全整个数据库的不同重做流,不能互相替代。恢复时如果要前滚到某个时间点,thread 1 的最后一个归档和 thread 2 的最后一个归档都必须补齐到一致状态,缺少任何一个线程上的一段连续日志,数据库都无法完整恢复到最新。
RAC 里执行 alter system switch logfile 通常只切当前连接实例的日志线程。若想观察两个线程的归档生产情况,我习惯直接用一条 SQL 看 v$thread:
sql复制select thread#, status, enabled, instance
from v$thread;
结合 v$archived_log 能看到每个线程实际归档到的路径和最大序列号:
sql复制select thread#,
max(sequence#) as max_seq,
count(*) as archived_count
from v$archived_log
where name is not null
group by thread#
order by thread#;
发现某个 thread 的 max_seq 明显落后于另一个 thread,就要警惕是不是那个实例归档中断了。很多“RAC 归档日志恢复”需求,最初就是从这里发现缺口开始的。
1.2 归档落盘方式决定了跨节点可见性
理解线程之后,第二个核心变量是归档日志物理写入到哪里。RAC 环境里常见的归档落盘方式有下面几种:
| 配置方式 | 示例 | 跨节点可见性 | 恢复友好度 |
|---|---|---|---|
| 共享存储/ASM | log_archive_dest_1='location=+FRA' |
所有节点都能直接访问同一份归档 | 最高,推荐 |
| 各节点本地路径 | log_archive_dest_1='location=/u01/app/oracle/arch' |
每个节点只能看到自己写的那部分归档 | 低,恢复时极易被误判 |
| 远程日志传输 | log_archive_dest_2='service=standby' |
通过传输协议复制到的节点可见 | 取决于目标节点配置 |
最推荐的方式是把归档直接写到 ASM 里的 FRA(Fast Recovery Area)。比如设置 db_recovery_file_dest=+FRA,归档目的地使用 location=USE_DB_RECOVERY_FILE_DEST。这样无论哪个实例产生归档,文件都是落在一个所有节点共享的磁盘组里,任何节点通过 sqlplus 或 RMAN 发起查询,都能看到完整的两路线程日志。
反观各节点本地路径,情况就麻烦了。RAC 控制文件是共享的,它记录了所有实例产生的归档日志记录,包括 thread 1 和 thread 2 的日志文件名和路径。但记录归记录,控制文件不会替你判断文件在别的节点物理上是否存在。当节点二的 RMAN 进程尝试访问 /u01/app/oracle/arch 下节点一产生的 1_xxxx.arc 时,因为路径对应的是节点一的本地盘,节点二操作系统的目录里根本没有这个文件,RMAN 就会把该条归档记录标记为 EXPIRED。
这种错误在单实例中几乎不会出现,因为单实例的归档路径一定是本机能访问的路径;但 RAC 一旦把日志落盘在各自节点的本地磁盘上,跨节点的归档识别就会立刻变得不可靠。如果你在节点二上做 crosscheck 并且天真地执行了 delete expired archivelog all,结果不是删除节点一本地文件——因为节点二删不到它——而是把控制文件里那批归档日志的元数据记录清理掉。节点一本地磁盘上的文件成了“孤儿”,下次备份和 recover 时数据库根本就不认识它。
1.3 一个真实场景:节点 A 的 RMAN 看不到节点 B 的本地归档
这种“假丢”的场景我见过不止一次。某个客户两节点 RAC,归档配置为:
- 节点一:
log_archive_dest_1='location=/oradata/arch1' - 节点二:
log_archive_dest_1='location=/oradata/arch2'
节点二的 CRON 里配了一个 RMAN 备份脚本,每天凌晨执行:
bash复制rman target / <<EOF
crosscheck archivelog all;
delete noprompt expired archivelog all;
backup archivelog all delete input;
EOF
我只看这一段就知道会在某个节点上出问题:crosscheck 执行在节点二上,节点二只能看到 /oradata/arch2 下的日志,看不到 /oradata/arch1。控制文件里所有关于线程 1 的归档记录都会被打上 EXPIRED,随后 delete expired 就会清掉这些日志记录。表面上节点一磁盘上的文件没有丢,但控制文件认为它们已经不存在。下一次做 recover database,哪怕 /oradata/arch1 里文件齐全,RMAN 也可能提示找不到归档日志,因为数据库的归档字典里已经删掉这些路径了。
遇到这种情况的修复还算简单:如果归档文件确实还在节点一本地,可以在节点一上对已经“孤儿化”的路径重新执行 catalog start with '/oradata/arch1' 注册回去,再执行 crosscheck archivelog all。但如果之前已经因为删除控制文件记录而让下一次介质恢复失败,那么在恢复过程中发现缺档,损失的时间和精力就不止一次命令了。
所以我在处理 RAC 跨节点归档时,给自己定了一条铁律:凡是 RMAN 报 EXPIRED,先分清楚是“节点不可见”还是“所有节点都不可见”。如果是前者,就不要在本节点上执行删除,而是该去能访问到那个路径的节点执行 crosscheck,或者干脆先把路径改成共享目录。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 用 RMAN 跨节点识别归档日志:从 metadata 到磁盘状态
既然理解了归档日志是怎么“藏”起来的,接下来就要解决识别问题。RMAN 识别归档日志,信息主要来自控制文件或恢复目录。识别分两层:第一层是元数据层,即控制文件里记录的 thread#、sequence#、name、creation_time 等;第二层是物理状态层,即实际文件到底能不能被 RMAN 所在节点的操作系统访问到。
2.1 从 gv$archived_log 摸清全局归档状态
跨节点识别归档日志,第一步一定是拿到整个数据库的全局归档视图,而不是某个节点的局部视图。虽然控制文件是共享的,但 RAC 中每个实例的 v$archived_log 是从自身 SGA 里缓存的控制文件视图读取的,为了保险,我习惯直接查 gv$archived_log,这样能带上 inst_id,一眼看出当前连接的到底是哪个实例视角:
sql复制select inst_id,
thread#,
sequence#,
name,
dest_id,
status,
completion_time
from gv$archived_log
where name is not null
and completion_time > sysdate - 7
order by thread#, sequence#;
这条 SQL 的输出会包含所有线程最近一周的归档文件路径。注意 name 字段就是 RMAN crosscheck 时尝试访问的实际路径。如果同一批 thread# = 1 的记录在节点一上执行能看到 name,而在节点二上执行同样 SQL 也能看到 name 字段,但随后 crosscheck 时节点二说 EXPIRED,那基本可以断定路径没有共享。
除了归档文件本身,还要看每个节点的归档目的地配置。RAC 环境下 gv$archive_dest 可以展示所有实例的归档目的配置:
sql复制select inst_id,
dest_name,
destination,
status,
target,
archiver,
error
from gv$archive_dest
where status != 'INACTIVE'
order by inst_id, dest_name;
error 列尤其值得关注。如果某个节点最近发生过归档失败,这里通常会残留 ORA- 开头的错误信息。跨节点归档文件出现缺口,经常能从这一步提前观察到苗头。
2.2 list/crosscheck 的底层逻辑和“误报过期”
RMAN 的 list archivelog all 和 crosscheck archivelog all 是识别归档日志状态最重要的两个命令,但很多人没搞懂它们的区别。
list archivelog all:只读取控制文件/恢复目录中的元数据,不检查物理文件是否存在。它告诉你数据库认为有哪些归档日志。crosscheck archivelog all:拿元数据和物理文件做核对。RMAN 会尝试按name字段去操作系统或 ASM 里找文件,找到的标记为AVAILABLE,找不到的标记为EXPIRED。
RAC 的坑就在 crosscheck 这一步:RMAN 进程是在某个具体节点上运行的,它去查看 /u01/arch/1_100.arc 这个路径时,实际访问的是当前节点自己的 /u01/arch。如果当前节点不是产生该文件的节点,而且没有共享该目录,文件就会找不到。所以跨节点场景下,EXPIRED 不代表文件已经物理丢失,只是“在当前这个 RMAN session 所能看到的文件系统里不存在”。
处理方式要分场景:
- 如果归档路径本来就是 ASM 或共享 NFS,任何节点执行 crosscheck 都一致,EXPIRED 大概率是真丢。
- 如果归档路径是各节点本地盘,那就要在每个节点分别执行
crosscheck archivelog all,然后在那个节点上处理相应 thread 的 EXPIRED 记录。不要图省事只在其中一个节点上做全量清理。
提示:如果已经误执行了
delete expired archivelog all,也不要慌。先确认物理文件是否还在对应节点的本地磁盘上;如果还在,可以通过catalog start with '/oradata/arch1' noprompt;把目录下的归档重新注册到控制文件。之后再执行一次 crosscheck 验证状态即可。
2.3 恢复目录能否解决跨节点识别问题
不少 DBA 认为只要使用了 RMAN 恢复目录(catalog schema),跨节点识别就不是问题。这个理解只对了一半。恢复目录确实能保存更久的历史归档记录,而且即便控制文件里某些归档记录被覆盖了,也能从恢复目录里找回来。但恢复目录无法替 RMAN 访问另一个节点上的本地磁盘。
举个例子:数据库控制文件中记录了一条归档 thread=1 sequence=123 name=/u01/app/oracle/arch/1_123.arc。恢复目录只是把这个记录又复制了一份,RMAN 要从备份或物理文件读取该归档时,仍然会根据 name 去当前执行节点上寻找。如果 RMAN 连的是节点二,而 /u01/app/oracle/arch 是节点一的本地目录,那结果依然是找不到。
因此可以这样理解:恢复目录解决了“元数据还在不在”的问题,但没有解决“物理文件能不能被当前节点访问”的问题。要实现真正可靠的跨节点识别,要么让归档目录共享,要么在做恢复备份任务时连接到能访问对应路径的节点执行。RMAN 连接到不同实例并不是难事:
bash复制rman target sys/******@racdb1 catalog rman_cat/******@rcat
在节点一上执行,能准确识别它本地盘上的 thread 1 归档;在节点二上执行,则能识别 thread 2 的本地归档。最好的备份方案还是“把共享目录作为唯一归档目标”,这样无论 RMAN 连哪个节点,看到的文件和元数据都是一致的。
3. 实战恢复:归档日志缺失后从备份还原的完整命令族
识别清楚之后,才是真正的手动恢复环节。场景假设如下:两个节点 RAC,节点一本地磁盘发生了部分文件丢失,其中 thread 1 从 sequence 100 到 150 这一段归档日志无法访问;但好在这些归档日志之前已经通过 RMAN 备份到了共享备份目录。我的任务是先把这批缺失的归档日志恢复到节点一原来的归档目录中,然后执行数据库介质恢复。
3.1 恢复前必须确认的三件事
在我用 restore archivelog 之前,一定会让脚本先做三件事,否则容易白折腾。
第一,确认备份里到底有没有需要的归档。查命令:
bash复制RMAN> list backup of archivelog thread 1;
重点看输出里 Start Seq、End Seq 和 Thread 列。如果 backup 里的 Start Seq=90 End Seq=160,那缺失的 100~150 大概率在里面;如果备份只覆盖到 99,那你恢复不了 100 之后的内容,物理上缺档,只能想办法找别的节点或别的备份。
第二,确认缺失的日志具体属于哪个线程、哪个序列区间。可以用 SQL 里读不到的对账方式:
sql复制select thread#,
sequence# + 1 as missing_start_seq
from (
select thread#,
sequence#,
lag(sequence#) over (partition by thread# order by sequence#) as prev_seq
from v$archived_log
where name is not null
)
where prev_seq is not null
and sequence# - prev_seq > 1;
不过这种 gap 检测往往要结合外部备份信息,我通常直接对比 list archivelog all 和文件系统的实际内容,找出具体断档序号。
第三,确认目标目录是否可写、原路径是否还存在。如果原归档目录 log_archive_dest_1='LOCATION=/u01/app/oracle/arch' 被误删或重建,恢复前要先把目录创建好,并检查当前节点是否有写权限。
3.2 RESTORE ARCHIVELOG 的常见写法和节点选择
确认备份和缺失范围后,进入 RMAN 执行恢复。核心命令是 restore archivelog。如果需要恢复的缺失是 thread 1 的 sequence 100 到 150,命令如下:
bash复制RMAN> restore archivelog from logseq 100 thread 1 until logseq 150 thread 1;
这段命令的意思是:从备份中把 thread 1 的 100 到 150 序列归档全部还原出来。注意 thread 1 不能省略,否则 RMAN 可能同时把 thread 2 中同 sequence 的归档也拉出来,造成不必要的空间占用。
如果不知道精确的起始序列,只大概知道是某天晚上缺失的,可以用时间范围:
bash复制RMAN> restore archivelog from time "to_date('2025-06-01 00:00:00','yyyy-mm-dd hh24:mi:ss')" until time "to_date('2025-06-02 00:00:00','yyyy-mm-dd hh24:mi:ss')";
这个写法会还原所有满足时间范围的归档,不区分线程,对 thread 数量少的环境比较实用。若归档原本存放在原节点本地路径,且恢复时
