凌晨两点半,值班手机连续振了两波:先是归档目录使用率告警,紧接着弹出一串ORA-00257。我爬起来翻alert log,清掉几个过期归档后顺手查了下v$log_history,发现这套库平均每8分钟就切一次redo日志。真正的问题根本不在归档空间,而是日志组太小、切换太勤。后来把在线日志组从2GB调到8GB,两个月再没出过同类故障。
这篇帖子就把Oracle redo日志从原理、查询、日常巡检、增删改操作,到大小估算和高发故障排查完整梳理一遍。内容面向刚接手Oracle库的运维同学,也适合被日志切换频率折磨过一阵子的老手对照查漏。我的原则是:先讲清楚"为什么",再给操作命令,最后说坑在哪。
1. 先搞懂Redo日志在做的事:崩溃恢复里它才是主角
1.1 Redo是“事后的流水账”,Undo是“反悔用的橡皮擦”
很多人刚接触Oracle时,最容易把redo和undo搞混。简单说,redo记录的是"数据块被改成了什么",undo记录的是"如果想反悔,应该改回什么"。可以这样类比:redo是一本只进不出的流水账本,每一笔交易都按时间顺序记下来;undo是一个撤销栈,执行错了可以从栈顶弹回原来的状态。
数据库每次修改数据块之前,都会先在内存里生成一条redo record。这条记录不是把整块数据快照下来,而是一个变更向量:哪个文件第几号块、哪个偏移量、从什么旧值变成什么新值、对应哪个事务和SCN。现代数据库里"放弃写redo"和"保证崩溃恢复"是矛盾的,所以性能优化可以压缩日志量,但绝不能完全去掉日志。
再往深说一层:事务提交时,Oracle向用户返回COMMIT成功的标志,靠的不是数据文件写成功了,而是redo日志写成功了。只要redo落盘,事务就算"焊死"了;哪怕数据块还在内存缓冲里没写出去,崩溃后也能靠redo重放回来。理解了这一点,后面所有关于日志大小、组数、存储位置的选择就都有了判断依据。
1.2 一条INSERT从提交到落盘,LGWR到底做了什么
一条普通DML语句的执行链路是这样的:
- 会话生成SQL执行计划,在Buffer Cache中定位目标数据块。
- 内存中修改数据块,同时在日志缓冲区(log buffer)生成对应的redo record。
- 用户执行COMMIT时,日志写进程LGWR把log buffer里的事务redo记录写入在线redo日志文件。
- 在线日志写入成功后,COMMIT才返回成功;期间会话会等待
log file sync事件。
这里有个关键认知:数据和日志写盘的时机完全分离。数据块是脏块,由DBWn进程在检查点或空间压力下慢慢写盘;redo则是提交时就立刻同步写盘。所以生产环境里,数据文件放在慢盘上最多是拖慢整体IO,而redo日志一旦放在慢盘上,所有提交都会被拖垮——AWR里如果log file sync排在等待事件前面,十有八九是日志文件所在存储出了问题。
Oracle还做了组提交优化:多个会话同时提交时,LGWR会把一批redo合并成一次磁盘写,提升吞吐。这也是大量小事务并发场景下,日志写入依然能维持较高TPS的原因之一。
1.3 实例崩溃恢复:为什么缺了Redo就恢复不了
实例崩溃后,数据库启动时自动执行实例恢复,分两步:
- 前滚(roll forward):从检查点位置开始,把在线redo日志里记录的变更重放到数据文件,已提交事务的效果全部恢复。
- 回滚(roll back):利用undo,把未提交事务在数据文件上留下的修改撤销掉。
前滚能恢复的范围,取决于检查点推进到了哪个SCN。检查点之后、故障点之前的所有修改,只要还没写进数据文件,就必须靠redo补回来。如果这段redo日志丢了或没归档,补不回来的那部分数据就永久丢失了。
这就是为什么DBA对redo日志的敬畏感应该比对数据文件更强——数据文件坏了,可以用备份加归档加redo恢复;redo坏了,恢复链路直接断掉。后面第5节讲故障时还会反复回到这个认知。
2. 动手之前先会看:状态查询与巡检基本功
2.1 三个视图看透在线日志的家底
操作redo日志之前,先要会查状态。日常最常用的三个动态视图:
v$log:每个日志组一行,包含组号、序列号、大小、成员数、状态、是否已归档等信息。v$logfile:每个日志文件一行,包含所属组号、成员路径、文件状态。v$log_history:历史日志切换记录,按时间排序,用于统计切换频率。
看日志组状态,执行:
SELECT group#, thread#, sequence#, bytes/1024/1024 AS size_mb, members, status, archived, first_time FROM v$log ORDER BY group#;看组成员,执行:
SELECT group#, status, type, member FROM v$logfile ORDER BY group#, member;v$log.status字段的取值含义必须记熟,这是所有后续操作的前提:
| 状态值 | 含义 | 能否被覆盖/删除 |
|---|---|---|
| UNUSED | 新建后从未使用 | 可 |
| CURRENT | 当前正在写入的日志组 | 不可 |
| ACTIVE | 已切换但检查点尚未完成,实例恢复还需要它 | 不可被覆盖 |
| INACTIVE | 检查点已完成,内容不再需要 | 可覆盖,可删除 |
| CLEARING | 正在被clear logfile重建 | 操作期间不可动 |
| CLEARING_CURRENT | 正在clear当前日志组 | 操作期间不可动 |
v$logfile.status更简单:空字符串表示正常,INVALID表示文件不可访问或已损坏,STALE表示文件内容不完整,一般出现就要处理。
2.2 日志切换频率怎么算
日志切换频率是衡量日志组大小是否合理的核心指标。查最近7天每天的切换次数:
SELECT TO_CHAR(first_time, 'YYYY-MM-DD') AS day, COUNT(*) AS switches FROM v$log_history WHERE first_time >= SYSDATE - 7 GROUP BY TO_CHAR(first_time, 'YYYY-MM-DD') ORDER BY 1;再看小时级别的分布,识别峰值时段:
SELECT TO_CHAR(first_time, 'YYYY-MM-DD HH24:00') AS hour_bucket, COUNT(*) AS log_switches FROM v$log_history WHERE first_time >= SYSDATE - 7 GROUP BY TO_CHAR(first_time, 'YYYY-MM-DD HH24:00') ORDER BY 1;一般经验是:平均每20到30分钟切一次,即每小时2到3次,比较健康;峰值小时不超过6次。如果平均每小时超过6次,基本可以断定日志组偏小,需要加大;如果一天都切不了几次,说明日志组偏大,在在线日志平均只用一遍的情况下问题不大,但要注意归档延迟和覆盖风险。
2.3 日常巡检应该盯什么
我通常把redo巡检固定在每天早上执行一遍,检查以下内容:
| 检查项 | 查询方式 | 异常信号 | 处理动作 |
|---|---|---|---|
| 日志组状态 | v$log | 长期没有INACTIVE组 | 检查检查点是否推进缓慢 |
| 成员有效性 | v$logfile | 出现INVALID/STALE | 尽快修复或替换成员 |
| 归档状态 | v$log.archived | CURRENT之外有ARCHIVED='NO' | 等待归档,持续不归档则查归档进程 |
| 归档目的地 | v$archive_dest | status非VALID或error非空 | 清理空间、检查网络/盘IO |
| 切换频率 | v$log_history | 每小时超过6次 | 计划加大日志组 |
| alert log | 文本日志 | 出现checkpoint incomplete | 降低切换压力或优化检查点 |
如果你刚接手一套不熟悉的库,别急着改任何配置,先花10分钟把上面几条跑一遍,形成基线数据。后面再做变更时,有没有效果,拿基线一对比就知道。
3. 在线日志的增删改:常用操作命令与隐藏前提
3.1 新增日志组和成员的正确姿势
新增一组带两个成员的日志组:
ALTER DATABASE ADD LOGFILE GROUP 4 ('/oraredo_a/redo04a.log', '/oraredo_b/redo04b.log') SIZE 4G;几个容易踩的细节:
- 组内成员大小必须一致,否则LGWR无法并行写。
- 组与组之间最好也保持一致大小,避免切换后容量跳变,影响切换频率统计的准确性。
- 不指定SIZE时,默认沿用现有组的大小。换句话讲,如果你要调整日志大小,新增时必须显式写SIZE。
只给已有组增加成员,用:
ALTER DATABASE ADD LOGFILE MEMBER '/oraredo_b/redo02b.log' TO GROUP 2;新增成员后,该组所有成员立即同步生效,不需要重启数据库。我的习惯是加完一条马上查v$logfile确认状态正常,不要留到第二天巡检才发现路径写错了。
3.2 删除日志组与成员:先满足两个前提
删除日志组有两个硬前提:待删组不能是CURRENT;删完后整个库至少保留两组在线日志。如果状态是ACTIVE,先做检查点让它变成INACTIVE。标准操作流程:
ALTER SYSTEM SWITCH LOGFILE; ALTER SYSTEM CHECKPOINT; ALTER DATABASE DROP LOGFILE GROUP 3;第一句把当前写日志切到别的组,第二句强制DBWn推进检查点,第三句删除。删除后确认v$log里已经看不到这个组。
删除单个成员更常见,比如某个成员所在磁盘要下线:
ALTER DATABASE DROP LOGFILE MEMBER '/oraredo_a/redo03a.log';这里必须严肃提醒:不要用操作系统rm命令直接删redo日志文件。你rm了文件,控制文件里的元数据还在,Oracle启动时发现成员缺失会直接报错。无论增删成员还是增删组,都走ALTER语句,让Oracle自己维护元数据。
3.3 多路复用的放置策略:别把鸡蛋放同一块磁盘
在线redo日志组至少要两个成员,这是生产环境安全底线。但"两个成员"只是数量要求,真正重要的是放置策略。
常见错误是把两个成员放到同一块物理盘的不同目录,比如/u01/redo_a/redo01a.log和/u01/redo_b/redo01b.log。看起来路径不同,实际盘坏了两个一起坏,多路复用等于白搭。正确做法是:
- 两个成员放在不同的物理盘、不同的盘柜或不同的存储故障域。
- 虚机环境放在不同的底层数据存储上。
- 一个成员放本机高性能盘,另一个放远一点的灾备存储,至少保证单点存储故障时不丢当前日志。
成员数量也不是越多越好。LGWR写日志时要并行写每一个成员,成员越多写放大越明显。普通生产库两个成员够用,核心库可以放3个,超过3个通常弊大于利。
3.4 调整日志大小的完整流程:没有ALTER SIZE,只有新建+切换+删除
在线日志一旦创建,大小就无法直接修改。想调大或调小,唯一的路径是"新建目标大小的组,切换过去,删掉旧组"。以下是从2GB调到8GB的标准流程:
- 新增一组8GB日志,比如GROUP 4。
- 执行
ALTER SYSTEM SWITCH LOGFILE把当前日志切到GROUP 4。 - 重复切换,把所有旧组都切过一轮。
- 等待旧组状态变为INACTIVE,必要时执行
ALTER SYSTEM CHECKPOINT。 - 逐个
ALTER DATABASE DROP LOGFILE GROUP n删除旧组。
这里最容易翻车的是第4步:切换后立刻删除旧组,常常报类似"log needed for crash recovery"的错。原因很简单——刚切走的组里还有没被检查点覆盖的变更,数据库还要靠它做实例恢复。这时候不要硬删,等检查点推进,或者强制一次checkpoint再删。
4. 大小和组数怎么定:一个能算出来的配置方法
4.1 三步法:按切换频率反推容量
日志大小最朴素的计算思路是:先知道业务每小时生成多少redo,再决定希望每小时切几次,两者一除就是单组大小。公式如下:
单组大小 = 每小时redo生成量 / 期望每小时切换次数 × 1.2末尾的1.2是安全余量。举个例子:某业务峰值小时生成3.6GB redo,希望峰值小时最多切4次,那么单组大小为3.6 / 4 = 0.9GB,加20%余量后约1.08GB,标准配置取2GB即可。
每小时redo生成量有两个获取途径:
- 从AWR报告的Instance Activity统计里找"Redo size"指标,用相邻快照差值除以间隔时间。
- 没有AWR时,用现有日志组大小乘以每小时切换次数反推:
redo每小时 = 单组大小 × 每小时切换次数。这个办法在现场特别实用。
4.2 用历史数据测算峰值时段
生产环境判断日志大小,不能只看平均值,要看峰值。我一般拉最近7天的小时切换分布,找出切换最密集的时段,再看该时段的redo生成量,用峰值数据代入公式重新算一遍。
如果你已经开了AWR,可以直接查DBA_HIST_SYSSTAT里stat_name为redo size的差值:
SELECT s.snap_id, TO_CHAR(s.end_interval_time, 'YYYY-MM-DD HH24:MI') AS end_time, ROUND((h.VALUE - LAG(h.VALUE) OVER (ORDER BY h.snap_id)) / 1024 / 1024, 2) AS redo_mb FROM dba_hist_sysstat h JOIN dba_hist_snapshot s ON h.snap_id = s.snap_id WHERE h.stat_name = 'redo size' AND h.snap_id BETWEEN 100 AND 120 ORDER BY h.snap_id;找出其中最大的redo_mb值,那基本就是峰值小时的redo量。这个值除以你期望的峰值切换次数(建议不超过6),再乘1.2,就能确定一个压得住业务的日志组大小。
4.3 组数、检查点与实例恢复时间的三角关系
日志组数量不能拍脑袋定。至少3组是最低要求,因为我见过太多"两组日志一个坏一组就全库不能切换"的惨状。常规生产库4组比较稳,特殊场景加到6组也不算夸张。
组数太少,最直接的后果是切完一轮后,LGWR想覆盖第一组时发现它还没完成检查点或归档,于是等待,出现log file switch (checkpoint incomplete)或log file switch (archiving needed)等待事件,业务提交跟着被拖慢。
组数一旦够用,影响切换瓶颈的就是检查点推进速度。Oracle有fast_start_mttr_target参数控制检查点目标,值设置得越小,DBWn写盘越勤快,实例恢复时间越短,但IO消耗也越大。这里要明白一个道理:日志组大小和组数间接决定了实例恢复的"回放长度"。崩溃时,实例要从最早那个未完成检查点的日志组的SCN开始回放,日志越大、检查点间隔越长,恢复需要重放的redo就越多,启动时间就越长。所以"日志越大越好"是片面的,平衡点通常落在:峰值小时切换3到4次,检查点推进速度能保证至少两组INACTIVE可用。
5. 生产环境最容易踩的几个坑:ORA错误排查实录
5.1 ORA-00257:归档进程卡住导致全库报错
ORA-00257是最常见的redo相关告警,报错信息一般是"archiver error. Connect internal only, until freed."。它的本质是LGWR要切换日志,但归档进程没法把上一个日志归档出去,于是所有日志切换卡住,最终数据库写操作全部阻塞。
接到这个告警,正确排查链路是:
- 查
v$archive_dest,看每个目的地的status和error。 - 如果是快速恢复区满了,查
v$recovery_file_dest的space_limit和space_used。 - 如果归档目录在文件系统,直接看目录剩余空间。
- 确认是空间问题后,先确认备份策略可靠,再清理过期的归档文件,有RMAN就用RMAN清理,不要手动rm。
- 空间恢复后执行
ALTER SYSTEM SWITCH LOGFILE,让LGWR把卡住的日志切走,数据库通常随即恢复。
但我必须说,清归档只是止血,不是治病。真正要问的是:为什么归档涨这么快?回到开头那个案例——归档空间之所以爆掉,根因是日志组太小导致切换太频繁。如果每次ORA-00257都只清归档不调整日志大小,下个月还会再来一次。
5.2 ORA-00312 / ORA-00313:日志文件读不到
ORA-00312和ORA-00313通常成对出现,比如ORA-00313: open failed for members of log group 2,下面跟着ORA-00312: online log 2 thread 1: '/oraredo_a/redo02a.log'。它们代表日志文件物理上打不开了。
处理这类故障之前,先冷静判断损坏范围:
- 只有组内一个成员损坏,另一个成员还在:数据库多半还能正常运行。可以先把好成员复制到好路径,作为新成员加入,再删掉坏成员。如果组不是CURRENT,还可以用
ALTER DATABASE CLEAR LOGFILE GROUP n重建整个组。 - 整个组都丢失,且不是CURRENT组:如果该组已经归档过,执行
ALTER DATABASE CLEAR LOGFILE GROUP n即可重新初始化;如果该组没有归档,必须用ALTER DATABASE CLEAR UNARCHIVED LOGFILE GROUP n,但这样做会让之前所有全备和归档的恢复链断裂,操作前心里要有数。 - 丢的是CURRENT组,实例已经崩溃:这类场景已经不是一段SQL能解决的,属于备份恢复甚至考虑容灾切换的级别。不要试图用
clear logfile欺骗自己,该找备份找备份,该failover就failover。
说句不好听的,这类故障大多源于多路复用没做好。但凡成员分散在不同物理盘,一个盘坏了也不至于整个组丢光。
5.3 ORA-01624:日志组删不掉的连锁反应
删除日志组时遇到ORA-01623常见些——"log is the current log",说明你忘了切换;ORA-01624则更隐蔽——"log needed for crash recovery",意思是这个组虽然切走了,但它的内容还在为实例恢复服务,检查点还没越过它。
处理思路就一条:让日志组从ACTIVE变成INACTIVE。前面已经在3.2节给过标准流程,这里补充一个实操技巧:执行ALTER SYSTEM CHECKPOINT后,不要立刻反查v$log,给它几秒钟让DBWn把脏块落盘,再查状态。如果反复checkpoint仍是ACTIVE,大概率是DBWn写盘卡住了,这时候要把排查重点从日志切到存储IO上,而不是继续跟日志组较劲。
另外,如果当前只有两组日志,想删其中一组,必须先加一组新的。数据库最少保留两组在线日志,这个约束不是限制,是保护机制——只有一组日志时,切换直接等于覆盖正在用的日志,整个实例会直接崩。
5.4 日志切换卡顿:等待事件帮你定位瓶颈
有时没有报错,但业务提交明显变慢。这时候不要猜,去查等待事件。跟redo强相关的三个事件:
log file sync:会话等待提交的redo写入日志文件。平均值高,说明日志磁盘写入慢,优先查存储链路。log file parallel write:LGWR等待并行写所有成员。平均值高,说明成员之间IO差异大,某个慢盘拖了后腿。log file switch (checkpoint incomplete):切换时下一组还没检查点完成。这指向日志数量不足或检查点推进太慢,按第4节思路去调。
我还见过一种隐藏很深的卡顿:归档目标在慢速网络盘上,归档速度跟不上日志切换速度,日常没有报错,但每次切换都隐式等待归档完成。这种问题光看在线日志查不出来,必须在v$archive_dest里看目的地响应性能,或者直接对比日志切换时刻与归档完成时刻的差值。
6. 从在线到归档:Redo日志与备份恢复的联动
6.1 归档模式决定Redo日志的“保质期”
在线redo日志是循环使用的,写满就切换,切换后如果检查点也完成了,理论上可以被覆盖。那么"被覆盖之前的日志"去了哪里?取决于数据库处于什么日志模式:
- NOARCHIVELOG模式:在线日志覆盖时不归档,介质恢复最多恢复到最近一次备份,备份之后的所有提交全部丢失。
- ARCHIVELOG模式:日志切换后由归档进程写一份归档日志,配合全量备份,可以把数据库恢复到任意时间点。
生产库几乎都必须开ARCHIVELOG。操作不复杂:
shutdown immediate; startup mount; alter database archivelog; alter database open;改完用archive log list确认。归档模式下,在线日志被覆盖前先归档成功是硬性条件,这也是ORA-00257为什么杀伤力这么大——归档通道一堵,在线日志切换全部瘫痪。
6.2 supplemental logging与force logging:给日志加料
标准redo日志记录的是数据块变更,但它未必记录"每条变更对应的主键是什么"。如果有逻辑复制、CDC采集,或者要用LogMiner解析业务变更,开启补充日志是必须的:
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (PRIMARY KEY) COLUMNS;这句的意思是:除了默认redo内容,额外记录主键和所有列的值,让下游能识别出每一变更对应的具体行。代价是日志量明显增大,切换频率会上升——所以开补充日志时要重新评估日志组大小,别等切换爆炸了才想起来。
FORCE LOGGING则是另一类保险:确保即使表上打了NOLOGGING、或者走了direct path insert,也会生成redo日志。物理备库环境普遍建议开启:
ALTER DATABASE FORCE LOGGING;这个设置适合"节点切换后不能容忍任何数据缺失"的核心业务,代价同样是日志量增加。
6.3 实操中那些写进手册也没人信的细节
最后分享几条只有真正值守过才会注意到的经验。
第一,在线redo日志要放全库最"稳且快"的存储路径,优先级高于数据文件。因为数据文件的写入可以延迟、可以合并,redo的写入却是每次提交的必经之路。我亲眼见过生产库把数据文件放SSD、redo放机械盘,结果AWR里log file sync一路飙红。
第二,不要拿redo日志目录"顺手放点别的"。归档文件、临时文件、expdp导出文件统统不要往里塞。redo文件碎片化、目录空间被占满,都会在最不应该出问题的时候给你来一刀。
第三,每次变更日志配置后,至少观察一个完整业务周期。我习惯在调整日志组大小后,连续7天早晨统计切换次数,确认峰值小时切换频率降下来了,才算这次调整闭环。只看一天数据,很容易被业务低峰期或高峰期误导。
第四,新环境接手时,先跑一遍第2节的两条查询,把日志组状态、成员情况和切换频率存档。后面任何告警出现,第一反应不是翻文档,而是跟基线对比——有没有新增INVALID成员?切换频率是不是明显上升?有了基线,一半的故障现场你已经提前知道了答案。
我自己就是从那次ORA-00257之后养成了这个习惯:任何一次归档告警,处理完空间之后必须回头查一遍日志切换频率和redo生成量,确认是不是需要调整日志组本身。清归档、扩空间只是战术动作,把日志大小和组数调整到匹配业务模型,才是让这套机制长治久安的策略。如果你手里的库也在频繁切换日志,不妨今晚先跑一条v$log_history统计,说不定答案已经写在那里了。