凌晨两点四十分,手机在床头柜上震了三下。摸过来一看,运维群里已经刷了十几条消息:应用侧的监控在报"数据库连接失败",业务同事说订单页面全部打不开,而值班的同学远程登上数据库服务器敲了sqlplus,发现实例还在、进程还在、监听也还在,就是谁也连不进去。最后在 alert 日志里翻出一行ORA-00257: archiver error. Connect internal only, until freed.,问题才算有了名字。
如果你正在读这篇东西,大概率是三种人之一:第一种是凌晨被叫起来救火的 DBA,需要马上止血;第二种是写了几年 PHP 或者 Java 业务代码,第一次看到这个报错,想知道"这玩意儿到底是不是我程序的锅";第三种是把这套流程沉淀下来,准备下次不再手忙脚乱的运维同学。这篇内容就是按"止血—定位—根治—应用侧兜底"的顺序来组织的,所有命令我都按能直接复制粘贴的形式给出来,参数和容量计算也会把推导过程写清楚,方便你对着自己环境里的数字套一遍。先把结论放前面:ORA-00257 从来不是一个"等等就好"的错误,它不会自己恢复,必须人工介入,而且越晚处理,影响面越大。
1. 先把 ORA-00257 读透:它到底在说什么
1.1 错误文本里藏着三个关键信息
完整的报错一般是这样的:
ORA-00257: archiver error. Connect internal only, until freed.很多人看到这句话第一反应是"连不上数据库",然后开始怀疑网络、监听、防火墙、连接池配置,方向就跑偏了。实际上这三个短语各自承载了不同的信息量,拆开看就清楚多了。
archiver error告诉我们出错的主体是归档进程(ARCn),不是 SQL 解析器,不是权限校验,也不是网络层。归档进程干的事情很单一:把在线重做日志整块复制成一个归档日志文件。它出错的常见原因就那么几类——目标路径写不进去、目标空间满了、目标路径权限不对。所以排查范围一下子从"整个数据库"收缩到了"归档目的地"。
Connect internal only是 Oracle 的一种自我保护机制。归档区写不进去以后,数据库无法保证崩溃恢复能力,继续对外提供写服务就等于在裸奔,所以它直接把普通会话的口子关掉了,只留给 SYSDBA / SYSPOPER 这类"内部连接"一条缝,让你登进去处理。这也解释了一个现象:为什么应用是"整片"挂掉,而不是"部分接口报错"——因为普通业务账号连会话都建立不了。
until freed是最容易被忽略的一句,也是最重要的定调。它的意思是"在空间被释放出来之前,这个状态不会改变"。也就是说,ORA-00257 是自愈不了的那一类错误,跟 ORA-00054(资源忙)那种"等几秒就过去了"完全是两种性质。
1.2 归档日志、闪回恢复区、控制文件之间的三方关系
要讲清楚为什么会满,得先把三个角色的关系理顺。
数据库处于归档模式(ARCHIVELOG)时,在线重做日志是循环使用的。假设你有三组重做日志,日志组 1 写满后切换到日志组 2,日志组 3,然后再回到日志组 1。回到日志组 1 之前,它里面的内容必须先被"归档"出来,否则这段重做数据一旦被覆盖,就没法做时间点恢复了。
负责这件事的 ARCn 进程会把日志组的内容整块复制成一个文件,默认命名类似1_4521_987654321.arc。这个文件写到哪里?由LOG_ARCHIVE_DEST_1决定。绝大多数环境里,这个参数的值是USE_DB_RECOVERY_FILE_DEST,意思是"写到我配置的那个闪回恢复区里"。闪回恢复区(Fast Recovery Area,FRA)的物理位置由DB_RECOVERY_FILE_DEST指定,容量上限由DB_RECOVERY_FILE_DEST_SIZE指定。
同时,控制文件里会记录每一条归档日志的路径、sequence、线程号、状态。这就带来一个很关键的副作用:控制文件是"账本",磁盘上的文件是"实物"。账本和实物不一致的时候,就会出现各种各样的怪问题,后面第 6 章会专门讲。
把这条链路串起来看:归档写不进去 → 在线日志无法被覆盖 → 日志切换卡死 → 检查点(checkpoint)无法推进 → 数据库的写操作逐步挂起。也就是说,如果放着不管,影响会从"应用连不上"升级成"数据库整体停摆"。
1.3 为什么应用侧只看到"连不上数据库"
从应用的角度看,这个错误的呈现方式往往带有欺骗性。
已经建立的连接池连接,短时间内可能还能用,因为 TCP 层是好的、会话已经建立。但只要业务执行到需要提交事务、或者触发日志切换相关的操作,就可能失败。更常见的是连接池需要扩容新连接,oci_connect直接返回失败。用 PHP + OCI8 的同学会拿到ORA-00257;用 JDBC 的同学可能拿到ORA-12537(TNS 连接关闭)或者被连接池包装成"获取连接超时";还有些中间件会把原始错误吃掉,统一报一个"系统繁忙"。
这是最坑的地方——错误信息被层层包装之后,真正的根因被埋掉了。所以我的习惯是:不管上层怎么包装,应用的错误日志里必须保留原始的 Oracle 错误码和错误文本,否则每次排查都要多花半小时去确认"到底是数据库的问题还是我网络的问题"。
2. 十分钟定位:把根因锁死在具体文件类型上
2.1 第一步永远是看恢复区余量
处理这类问题,顺序比命令更重要。第一件事不是删文件,而是看清楚恢复区现在到底是什么状态。
sqlplus / as sysdba SQL> SET LINESIZE 220 SQL> COL NAME FOR A42 SQL> SELECT NAME, 2 ROUND(SPACE_LIMIT/1024/1024/1024,2) AS LIMIT_GB, 3 ROUND(SPACE_USED/1024/1024/1024,2) AS USED_GB, 4 ROUND(SPACE_RECLAIMABLE/1024/1024/1024,2) AS RECLAIM_GB, 5 ROUND(SPACE_USED/SPACE_LIMIT*100,2) AS USED_PCT, 6 NUMBER_OF_FILES 7 FROM V$RECOVERY_FILE_DEST;典型输出长这样:
NAME LIMIT_GB USED_GB RECLAIM_GB USED_PCT NUMBER_OF_FILES ---------------------------------------- --------- --------- ----------- --------- --------------- /u01/app/oracle/fast_recovery_area 100 100.00 91.37 100.00 1521这里有三个数字要重点看,不是只看USED_PCT。
LIMIT_GB是你给恢复区设的软上限。注意"软"这个字——它跟物理磁盘容量是两回事。有可能磁盘还剩 500G,但你把上限设成了 100G,那照样会满。
USED_GB是当前已用。它满了只是说明"到顶了",但不代表"没救"。
RECLAIM_GB才是决定你走哪条路的关键。它表示"当前占用中,有多少是可以被回收的"——比如已经超过保留期、或者已经备份过的归档。上面这个例子里 91.37GB 可回收,意味着你完全不需要扩容,删一轮归档就回去了。反过来,如果RECLAIM_GB很小(比如 2GB),那说明空间是真不够用,删也删不出多少,必须走扩容或者调整归档目的地这条路。
提示:判断"删一删能不能解决"和"必须扩容"的分界线,就是
SPACE_RECLAIMABLE这个字段。先看它,再决定动作,能省掉很多无用功。
2.2 第二步区分是归档占满还是备份/闪回日志占满
光知道"满了"还不够,得知道是谁把它吃掉的,因为针对不同文件类型的处理方式完全不一样。
SQL> COL FILE_TYPE FOR A22 SQL> SELECT FILE_TYPE, 2 PERCENT_SPACE_USED, 3 PERCENT_SPACE_RECLAIMABLE, 4 NUMBER_OF_FILES 5 FROM V$RECOVERY_AREA_USAGE 6 ORDER BY PERCENT_SPACE_USED DESC;这个视图会把恢复区里的文件按用途分类列出,常见的几类及含义如下:
| FILE_TYPE | 含义 | 满了以后的典型处理 |
|---|---|---|
| ARCHIVED LOG | 归档日志文件 | 最最常见的元凶,清理或转移目的地 |
| BACKUP PIECE | RMAN 备份片 | 检查保留策略,或把备份改写到独立盘/带库 |
| IMAGE COPY | 数据文件镜像副本 | 说明有人做过BACKUP AS COPY且没清理 |
| FLASHBACK LOG | 闪回日志 | 检查是否开了闪回或存在保证还原点 |
| FOREIGN ARCHIVED LOG | 从其他库传过来的归档 | 常见于备库或跨库传输场景 |
| CONTROL FILE / REDO LOG | 控制文件与在线日志副本 | 占用很小,一般可以忽略 |
对照着看,基本一眼就能定位:
ARCHIVED LOG高、PERCENT_SPACE_RECLAIMABLE也高:恭喜,删归档就完事了。ARCHIVED LOG高、PERCENT_SPACE_RECLAIMABLE低:要么保留窗口设得太长,要么最近归档量暴涨(大批量作业、异常日志切换),需要动策略。FLASHBACK LOG高:闪回数据库开着,或者存在带GUARANTEE FLASHBACK DATABASE的还原点,这种最隐蔽。BACKUP PIECE高:备份片和归档在抢同一块空间,架构上就该拆开了。
2.3 第三步回看 alert 日志的时间线
上面两个查询告诉你"现在",alert 日志告诉你"什么时候开始的、怎么恶化的"。
cd $ORACLE_BASE/diag/rdbms/$(echo $ORACLE_SID | tr 'A-Z' 'a-z')/$ORACLE_SID/trace grep -nE "ORA-19815|ORA-19809|ORA-19804|ORA-16038|ORA-00257" \ alert_${ORACLE_SID}.log | tail -60我特别建议你按这个顺序看这几个错误码,因为它们是一条清晰的恶化曲线:
ORA-19815: WARNING: db_recovery_file_dest_size of 107374182400 bytes is 97.31% used ... ORA-19815: WARNING: db_recovery_file_dest_size of 107374182400 bytes is 100.00% used, and has 0 remaining bytes available. ORA-19809: limit exceeded for recovery files ORA-19804: cannot reclaim 52428800 bytes disk space from 107374182400 limit ARCH: Archival stopped, error occurred. Will continue retrying ORA-16038: log 3 sequence# 4521 cannot be archived ORA-19809: limit exceeded for recovery files ORA-00312: online log 3 thread 1: '/u01/app/oracle/oradata/ORCL/redo03.log' ORA-00257: archiver error. Connect internal only, until freed.ORA-19815是预警,从 90% 左右就开始刷,这时候处理成本最低。ORA-16038是恶化信号——某个在线日志组归档失败,意味着它不能被覆盖了。等到ORA-00257出现,其实已经过了好几道防线,只是前面没人看。
所以我在自己环境里做监控的时候,抓的第一个关键字不是ORA-00257,而是ORA-19815。抢在前面动手,整个故障根本不会发生。
2.4 归档生成速率实测:判断"多大才算够"的地基数据
很多人扩容的时候是拍脑袋的:满了 100G 就改成 200G,下次再满再改 400G。这样做事倍功半。真正该做的是先量一下归档的生成速率。
按天统计最近两周的归档量:
SQL> SELECT TRUNC(COMPLETION_TIME) AS DAY, 2 COUNT(*) AS ARCH_FILES, 3 ROUND(SUM(BLOCKS*BLOCK_SIZE)/1024/1024/1024,2) AS ARCH_GB 4 FROM V$ARCHIVED_LOG 5 WHERE COMPLETION_TIME > SYSDATE - 14 6 GROUP BY TRUNC(COMPLETION_TIME) 7 ORDER BY 1;再按小时看日志切换频率:
SQL> SELECT TO_CHAR(FIRST_TIME,'MM-DD HH24') AS HOUR, 2 COUNT(*) AS SWITCHES 3 FROM V$LOG_HISTORY 4 WHERE FIRST_TIME > SYSDATE - 2 5 GROUP BY TO_CHAR(FIRST_TIME,'MM-DD HH24') 6 ORDER BY 1;这两个查询出来以后,容量规划就有据可依了。
这里有一个特别容易搞错的概念,值得单独说:调整在线重做日志的大小,并不会减少归档的总量。
举个例子,你现在重做日志每组 200MB,每 5 分钟切换一次,那么归档速率是:
200MB / 5min = 40MB/min 40MB/min × 60 = 2400MB/h = 2.4GB/h 2.4GB/h × 24 = 57.6GB/day如果我把重做日志改成 1GB 一组,切换间隔变成 25 分钟,速率变成:
1024MB / 25min = 40.96MB/min ≈ 2.46GB/h ≈ 59GB/day几乎一样。因为归档总量本质上等于重做数据的生成速率,而重做生成速率由业务写入量决定,跟重做日志文件多大没关系。调整重做日志大小的真正收益是:减少切换次数、减少 checkpoint 频率、减少归档文件个数、降低 ARCn 的调度压力。它不会帮你省空间。
想真正降低归档量,得从这几个方向入手:大批量导入导出和数据维护操作尽量用 NOLOGGING 或者直接路径加载;减少不必要的索引维护和全表更新;把日报、月结这类集中写入作业错峰,避免短时间内重做暴增。
2.5 常见触发场景对照表
把上面几步的信息拼起来,基本就能对号入座了:
| 场景 | 典型特征 | 处理方向 |
|---|---|---|
| 归档保留窗口过长 | RECLAIM低,归档文件序列号跨度大 | 缩短保留策略或加删除策略 |
| 突发的批量作业 | 某一天归档 GB 数明显高于均值 3 倍以上 | 排查作业、错峰、改用 NOLOGGING |
| 恢复区上限设小了 | 磁盘实际还剩很多,LIMIT却很小 | 直接扩容上限 |
| 备份片堆积 | BACKUP PIECE占比高 | 调整保留策略或把备份写到独立盘 |
| 保证还原点未清理 | FLASHBACK LOG占比持续增长 | 删还原点或关闭闪回 |
| 备库延迟 | 归档删不掉,报 RMAN-08137 | 先修备库链路,再谈清理 |
| 归档目的地权限异常 | 每次都报 ORA-19502/ORA-16014 | 检查目录属主与权限 |
3. 应急止血:让业务几分钟内恢复连接
3.1 最优先动作:先扩容再清理
故障现场的第一原则是:先让库活过来,再慢慢分析。
扩容是一条 SQL 的事:
SQL> ALTER SYSTEM SET DB_RECOVERY_FILE_DEST_SIZE = 300G SCOPE=BOTH;为什么把它放在第一位?因为这是一个元数据层面的操作,不涉及实际的磁盘写入分配,执行是秒级的,立刻就能解除"写不进去"的限制,把数据库从悬崖边拉回来。业务能连上了,你才有从容的时间做后面的清理。
但它有两个前提条件,必须先在操作系统层面确认:
df -h /u01/app/oracle/fast_recovery_area第一,新设的上限不能超过底层文件系统实际可用空间。设一个 300G 的上限,但盘上只剩 50G,那是自欺欺人,过一会儿照样满,而且这次会连带影响操作系统层面的其他进程。第二,如果是 RAC 环境,注意作用域:
-- 所有实例一起改 ALTER SYSTEM SET DB_RECOVERY_FILE_DEST_SIZE = 300G SCOPE=BOTH SID='*';注意:扩容只是一个缓冲垫,不是解决方案。如果归档生成速率是 60G/天,你把上限从 100G 提到 300G,也就是多撑三四天而已。扩完必须接着做清理和策略调整。
3.2 RMAN 交叉校验与过期归档清理
扩容之后紧接着就是清理。这一步用 RMAN,不要用操作系统的rm。
rman target /RMAN> CROSSCHECK ARCHIVELOG ALL; RMAN> DELETE EXPIRED ARCHIVELOG ALL; RMAN> DELETE NOPROMPT ARCHIVELOG ALL COMPLETED BEFORE 'SYSDATE-3'; RMAN> DELETE NOPROMPT OBSOLETE;这四条命令的顺序是有讲究的,不能乱。
CROSSCHECK ARCHIVELOG ALL做的事情是:拿控制文件里的记录,去磁盘上逐个比对。文件在,标记为 AVAILABLE;文件不在,标记为 EXPIRED。为什么这一步必须放在最前面?因为实际运维中,"有人手工删过归档文件"这件事发生的概率远比想象中高。一旦控制文件里记录了某个文件、磁盘上却找不到,后面的 DELETE 会直接报错中断,清理流程就走不下去。
DELETE EXPIRED ARCHIVELOG ALL把刚刚标记为 EXPIRED 的记录从控制文件里清掉,让账本和实物对上。
DELETE ARCHIVELOG ALL COMPLETED BEFORE 'SYSDATE-3'是真正的删除动作,删掉三天前完成的所有归档,同时从磁盘和控制文件里一起移除。
DELETE NOPROMPT OBSOLETE交给 RMAN 按保留策略自己判断哪些过期了,这是日常最省心的一条。
删的过程中如果想知道还有多少没删、哪些是因为什么原因删不掉:
RMAN> LIST ARCHIVELOG ALL; RMAN> REPORT OBSOLETE;有时候会碰到删不掉的情况,RMAN 会给你一句RMAN-08137: WARNING: archived log not deleted, needed for standby or upstream capture process。这个后面 6.2 会专门讲。
还有一个命令要提一下但强烈建议慎用:
RMAN> DELETE FORCE ARCHIVELOG ALL COMPLETED BEFORE 'SYSDATE-1';FORCE的意思是"不管有没有备份、不管备库需不需要,都给我删"。它是核武器,能解决眼前的燃眉之急,但代价是那段窗口期失去了时间点恢复能力。我的态度是:除非数据库已经彻底写不进去、业务停摆、而且你确认过备份是完好的,否则不要用。用之前一定要记录下当前的最早可用 SCN。
3.3 把归档迁到独立目录或独立磁盘
如果这已经是今年第三次遇到,那说明方案本身有问题——归档和备份片挤在同一个恢复区里,两个都容易膨胀的东西互相抢空间,迟早要出事。这时候可以考虑把归档单独挪出去。
SQL> ALTER SYSTEM SET LOG_ARCHIVE_DEST_1='LOCATION=/arch/orcl' SCOPE=BOTH;动手之前,先在操作系统层面把目录建好、权限给对:
mkdir -p /arch/orcl chown oracle:oinstall /arch/orcl chmod 750 /arch/orcl改完之后,主动触发一次日志切换来验证:
SQL> ALTER SYSTEM SWITCH LOGFILE;然后去新目录里看有没有生成.arc文件,同时确认归档目的地状态正常:
SQL> SELECT DEST_ID, STATUS, DESTINATION, ERROR 2 FROM V$ARCHIVE_DEST_STATUS 3 WHERE DEST_ID = 1;STATUS应该是VALID,ERROR应该是空的。
这么改的好处很直接:恢复区里只剩备份片和闪回日志,容量压力小一大截;归档盘可以单独挂一块大盘,扩容的时候互不影响。
注意:改归档目的地属于生产变更,改之前务必确认新路径的可用空间和权限。如果新路径不可写,数据库会立刻抛出 ORA-16014 / ORA-19502,比原来的问题更麻烦——归档连地方都写不了。变更前建议先在测试环境走一遍,或者至少准备好回滚命令。
3.4 应急阶段的四个禁忌
救火的时候,越着急越容易做错事。这几条是我见过最多的翻车点。
第一,不要在操作系统层面直接rm归档文件。这是最典型的错误。文件是没了,但控制文件完全不知情,V$RECOVERY_FILE_DEST的 USED 也不会马上反映。后续做增量备份、做恢复、做 Data Guard 同步的时候,会报出一堆莫名其妙的错误。要删就通过 RMAN 删。
第二,不要在没确认备库应用进度之前把归档删干净。有备库的环境里,主库的归档在备库应用完成之前是有用的。删早了,备库就断了,重建备库的成本远高于加一块硬盘。
第三,不要直接把DB_RECOVERY_FILE_DEST改成一个新目录来"换地方"。这不是换个路径那么简单,Oracle 在新位置会重建目录结构,旧内容需要另外处理,而且这个参数在实例运行期间通常不建议随意改动。想换地方,改LOG_ARCHIVE_DEST_1更安全。
第四,不要在恢复区接近满了的时候去跑大备份。备份片写不进恢复区,也会失败,报 ORA-19809,而且失败前的写入还会进一步挤压剩余空间。
3.5 验证闭环:怎么确认真的好了
清理完不能拍拍屁股就走,得有验证动作。我一般固定做四件事。
先看恢复区水位降下来没有:
SQL> SELECT ROUND(SPACE_USED/SPACE_LIMIT*100,2) AS USED_PCT, 2 ROUND(SPACE_RECLAIMABLE/1024/1024/1024,2) AS RECLAIM_GB 3 FROM V$RECOVERY_FILE_DEST;再主动切一次日志,然后立刻去看 alert 日志尾部有没有新报错:
SQL> ALTER SYSTEM SWITCH LOGFILE;tail -n 50 $ORACLE_BASE/diag/rdbms/*/${ORACLE_SID}/trace/alert_${ORACLE_SID}.log然后用一个普通业务账号(不是 SYSDBA)试着连一次,这一步很关键,因为只有普通账号能连上,才说明 ORA-00257 的状态真正解除了。
最后确认归档进程状态:
SQL> SELECT PROCESS, STATUS, THREAD#, SEQUENCE#, BLOCK# 2 FROM V$ARCHIVE_PROCESSES;STATUS 应该是IDLE或者BUSY,如果是STOPPED或者ERROR,说明还有问题没解决。
4. 长期治理:容量规划与保留策略
4.1 DB_RECOVERY_FILE_DEST_SIZE 该怎么算
扩容不能拍脑袋,按下面这个公式走:
所需恢复区容量 ≈ 每日归档量 × 需要保留的天数 × 安全系数 1.5 + 备份片峰值占用 + 闪回日志预留拿前面 2.4 节测出来的数据套一遍:每日归档 57.6GB,保留 7 天,安全系数 1.5:
57.6 × 7 = 403.2 GB 403.2 × 1.5 ≈ 605 GB也就是说,光归档就需要 600GB 左右的空间。如果备份片还放在同一个恢复区里,每天一份全备 300GB、保留两份,那就再加 600GB,总共 1.2TB。
到这一步你应该会发现一个不对劲的地方:这个数字大得离谱。这时候正确的做法不是去申请一块 1.2TB 的盘,而是反思架构——备份片根本不该和归档挤在同一个恢复区。备份应该直接写到独立磁盘、磁带库或者对象存储上。恢复区里只保留归档和必要的控制文件自动备份,容量需求会立刻下降到可控范围。
安全系数为什么取 1.5?因为归档量是波动的。月末结账、季度报表、批量数据同步这些场景,当天的归档量可能是均值的两三倍。留 50% 的余量,是为了让这些峰值不至于直接顶到上限。
保留天数怎么定?这取决于你的备份周期。如果是每日全备,归档保留 2 到 3 天通常就足够恢复用了。如果是每周全备加每日增量,那归档至少要覆盖"上一个完整全备到当前"的整个区间,比如 8 天。这里的原则是:保留期内必须能够完成一次完整的恢复到任意时间点,多一天都是浪费空间。
4.2 保留策略与归档删除策略的搭配
前面DELETE OBSOLETE能不能正确判断"哪些该删",完全取决于 RMAN 的策略配置。
rman target /RMAN> CONFIGURE RETENTION POLICY TO RECOVERY WINDOW OF 7 DAYS; RMAN> CONFIGURE ARCHIVELOG DELETION POLICY TO BACKED UP 1 TIMES TO DEVICE TYPE DISK;第一条是保留策略:保证最近 7 天内的任意时间点都能恢复。RMAN 会据此判断哪些备份和归档算"过期"。
第二条是归档删除策略,专门管归档,规则是"至少被备份过一次,才允许从磁盘删除"。这条件的意义在于避免"归档还没备份就被删了,结果需要恢复时找不到"。
如果备份是直接写到磁带库的,可以配成:
RMAN> CONFIGURE ARCHIVELOG DELETION POLICY TO BACKED UP 2 TIMES TO DEVICE TYPE SBT;意思是归档必须成功备份到磁带两份才允许删本地。这个数字看你的容灾要求,两份是为了防止单份介质损坏。
有备库的环境还有一条:
RMAN> CONFIGURE ARCHIVELOG DELETION POLICY TO APPLIED ON ALL STANDBY;这条的意思是:归档必须在所有备库上都应用完成了,才允许删除。这是有备库场景下最安全的配置,但也是最容易导致主库归档堆积的配置——因为只要有一个备库卡住,主库的归档就一份都删不掉。
提示:保留策略和归档删除策略同时配置时,RMAN 会取两者中更严格的那个来执行。所以不要把保留窗口设成 30 天、同时又期望归档能被快速清掉,这两个目标是互相冲突的。
4.3 闪回日志这个隐形大户
这是我认为最值得单独讲的一类原因,因为它最隐蔽——归档删得干干净净,容量却还在涨。
先看几个查询:
SQL> SHOW PARAMETER DB_FLASHBACK_RETENTION_TARGET; SQL> SELECT * FROM V$FLASHBACK_DATABASE_LOG; SQL> SELECT NAME, SCN, TIME, GUARANTEE_FLASHBACK_DATABASE, STORAGE_SIZE 2 FROM V$RESTORE_POINT;DB_FLASHBACK_RETENTION_TARGET的单位是分钟,1440 就代表希望保留 1 天的闪回能力。这个参数本身不强制占用空间,它只是一个"目标",闪回日志会根据实际写入情况动态调整。
真正会导致空间被吃光的是V$RESTORE_POINT里GUARANTEE_FLASHBACK_DATABASE = YES的那种保证还原点。一旦建了这种还原点,Oracle 就不能删除还原点之前产生的任何闪回日志,因为删了就达不到"保证能闪回到那个点"的承诺。结果就是闪回日志只增不减,慢慢把恢复区填满,最后报 ORA-00257。
我遇到过的一个典型案例:某次大版本升级前,有人为了"万一升级失败能闪回去",建了个保证还原点。升级顺利完成,但还原点没人清理,两个月后恢复区满了,报 ORA-00257,排查了半天才想到去查V$RESTORE_POINT。
处理方式很直接:
-- 确认不再需要后,删除还原点 SQL> DROP RESTORE POINT BEFORE_UPGRADE_20240315;如果确认环境根本不需要闪回能力:
SQL> ALTER DATABASE FLASHBACK OFF;关掉之前一定要确认没有正在进行的闪回相关操作,并且业务上确实不需要这个能力。
4.4 把清理和监控做成定时任务
靠人记得每天手工清,一定会忘。这两件事必须自动化。
先写一个 RMAN 清理脚本,比如放在/home/oracle/scripts/purge_arch.rman:
CROSSCHECK ARCHIVELOG ALL; DELETE EXPIRED ARCHIVELOG ALL; DELETE NOPROMPT ARCHIVELOG ALL COMPLETED BEFORE 'SYSDATE-3'; DELETE NOPROMPT OBSOLETE; EXIT;再写一个调用脚本/home/oracle/scripts/purge_arch.sh:
#!/bin/bash export ORACLE_SID=orcl export ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1 export PATH=$ORACLE_HOME/bin:$PATH export NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS' mkdir -p /home/oracle/logs rman target / cmdfile=/home/oracle/scripts/purge_arch.rman \ log=/home/oracle/logs/purge_arch_$(date +%Y%m%d).log然后挂到 cron 上,每天凌晨三点跑:
0 3 * * * /home/oracle/scripts/purge_arch.sh监控脚本同样简单,抓一个百分比出来就能用:
#!/bin/bash export ORACLE_SID=orcl export ORACLE_HOME=/u01/app/oracle/product/19.0.0/dbhome_1 export PATH=$ORACLE_HOME/bin:$PATH PCT=$(sqlplus -s "/ as sysdba" <<'EOF' set pages 0 lines 200 feed off head off trimspool on select round(space_used/space_limit*100,2) from v$recovery_file_dest; EOF ) echo "FRA used: ${PCT}%" if [ "$(echo "$PCT > 90" | bc)" -eq 1 ]; then echo "CRITICAL: FRA usage ${PCT}%" exit 2 elif [ "$(echo "$PCT > 80" | bc)" -eq 1 ]; then echo "WARNING: FRA usage ${PCT}%" exit 1 fi exit 0告警阈值我建议这样分档,别等 100% 才报警:
| 使用率 | 状态 | 建议动作 |
|---|---|---|
| < 70% | 正常 | 无需干预 |
| 70% - 85% | 关注 | 检查清理任务是否正常执行 |
| 85% - 95% | 预警 | 人工介入清理,评估是否需要扩容 |
| > 95% | 紧急 | 立即处理,准备扩容方案 |
4.5 备库与双活环境下的额外约束
有 Data Guard 或者类似主备架构的环境,归档管理会多一层约束,因为主库的归档在备库应用完成前不能删。
查看备库的接收和应用进度:
SQL> SELECT DEST_ID, STATUS, RECOVERY_MODE, APPLIED_SEQ, 2 ERROR 3 FROM V$ARCHIVE_DEST_STATUS 4 WHERE DEST_ID = 2;APPLIED_SEQ是备库已经应用到的最大序列号。跟主库当前的日志序列号一对比,就能算出延迟了多少个日志:
SQL> SELECT THREAD#, SEQUENCE# FROM V$LOG WHERE STATUS = 'CURRENT';如果发现ARCHIVED_SEQ - APPLIED_SEQ在持续扩大,说明备库跟不上或者链路有问题,主库的归档会一直堆积,怎么清都清不掉。这种情况下扩容只是拖延时间,真正要修的是同步链路本身——可能是带宽不够、可能是备库的磁盘 I/O 跟不上、也可能是某个批次应用卡住了。
我在这种场景里的做法是:先在主库确认延迟趋势(是稳定还是持续扩大),再上备库看归档应用速率和等待事件。如果延迟只是暂时的(比如主库刚跑完一个大批量作业),那就临时扩大恢复区扛过去;如果是持续扩大,那清理动作完全没有意义,必须先把链路问题解决。
5. 应用侧怎么办:从报错到可用的错误处理写法
5.1 为什么 PHP 层不能简单地"重试到成功"
用 PHP + OCI8 的同学看到 ORA-00257,第一反应往往是加个重试循环,超时就重连。这个思路在应对网络抖动时是对的,但对付 ORA-00257 是会出大问题的。
原因在于:ORA-00257 是实例级故障,不是单个连接的偶发问题。当你加上"重试 N 次,每次等几秒"的逻辑,而高峰期可能有几百个 PHP-FPM 工作进程同时在跑的时候,会发生这么一件事——所有进程都在sleep,进程池被占满,新的请求排不上队,前端从"部分接口报错"变成"整站 502"。原本只是数据库的故障,被应用层放大成了全站故障。
正确的姿势是三层:短重试(1 到 2 次,带随机抖动)+ 快速失败 + 熔断。重试只是为了应对极短时间的瞬态,熔断是为了防止雪崩。
5.2 一个带退避与熔断的连接封装
下面这个类是我在实际项目里用的简化版本,思路是从"报错就重试"改成"识别到 257 就立刻熔断并快速失败"。
<?php final class OraConnection { private const CACHE_KEY = 'ora257_circuit'; private const OPEN_SECONDS = 15; // 熔断持续时间 private const MAX_ATTEMPTS = 2; public static function connect(string $user, string $pass, string $dsn) { // 熔断器打开状态:直接快速失败,不再尝试连接数据库 if (function_exists('apcu_fetch') && apcu_fetch(self::CACHE_KEY)) { throw new RuntimeException('数据库繁忙,请稍后重试', 257); } for ($i = 1; $i <= self::MAX_ATTEMPTS; $i++) { $conn = @oci_connect($user, $pass, $dsn, 'AL32UTF8'); if ($conn !== false) { return $conn; } $err = oci_error(); $code = isset($err['code']) ? (int) $err['code'] : 0; $message = isset($err['message']) ? $err['message'] : ''; $isArchiverError = ($code === 257) || (stripos($message, 'ORA-00257') !== false); if ($isArchiverError) { if (function_exists('apcu_store')) { apcu_store(self::CACHE_KEY, 1, self::OPEN_SECONDS); } error_log(sprintf( '[ORA] ORA-00257 归档区写满,dsn=%s,已触发熔断 %d 秒', $dsn, self::OPEN_SECONDS )); throw new RuntimeException('数据库繁忙,请稍后重试', 257); } if ($i >= self::MAX_ATTEMPTS) { error_log(sprintf('[ORA] 连接失败,code=%d,message=%s', $code, $message)); throw new RuntimeException('数据库暂时不可用', $code); } // 随机抖动,避免所有进程同一时刻集中重试 usleep(random_int(80, 200) * 1000); } throw new RuntimeException('数据库暂时不可用', 0); } }几个细节值得说明。
code === 257和字符串匹配同时判断,是因为不同版本的 OCI 驱动在连接失败时返回的错误码行为不完全一致,有的场景下code取不到 257,但message里一定包含ORA-00257这个文本。双保险,成本几乎为零。
熔断窗口设成 15 秒是一个经验值。太短了没意义(数据库不会那么快好),太长了会导致数据库已经恢复了你还在拒绝流量。15 秒意味着最多每 15 秒有一个请求去试探一次,其它请求全部快速失败,不会打满进程池。
随机抖动(80 到 200 毫秒)是为了躲开"惊群"——如果所有请求都固定等 1 秒重试,它们会在同一时刻全部冲回数据库,等于制造了一次小型压测。
5.3 更重要的:让运维比用户先知道
应用侧的错误捕获,最大的价值不是"让页面好看一点",而是给运维争取时间。
在捕获到 257 的地方,除了error_log,一定要往监控系统打一个独立的事件标记,带上实例名和 DSN。这样运维的告警通道能在用户投诉之前就响起来。很多团队的问题是监控只看"接口成功率"和"响应时间",而这两个指标在数据库刚满的时候可能还没明显变化——因为连接池里还有存量连接撑着。等到成功率真的掉下去,已经晚了二十分钟。
配合第 4.4 节数据库侧的 80% 阈值告警,就形成了双保险:数据库侧提前预警,应用侧兜底确认。
6. 常见问题与排查技巧实录
6.1 删了文件空间却不释放
这是问得最多的一个问题:明明用rm删了几十个 G 的归档,为什么V$RECOVERY_FILE_DEST的USED_GB一点没变?
原因在于控制文件是账本,rm只动了实物,没动账本。Oracle 依然认为那些文件存在,依然把它们算在占用里。
正确的处理顺序:
rman target /RMAN> CROSSCHECK ARCHIVELOG ALL; RMAN> DELETE EXPIRED ARCHIVELOG ALL;CROSSCHECK负责把"账上有、实物没了"的记录标记为 EXPIRED,DELETE EXPIRED负责把这些记录从账本上划掉。两步做完再查V$RECOVERY_FILE_DEST,数字就对了。
如果做完这两步数字还是不对,按下面的顺序继续查:
- 是不是归档本身被备库或者闪回需要,RMAN 拒绝删除?看 RMAN 的输出里有没有
RMAN-08137。 - 是不是删除的只是恢复区里的一部分类型?查
V$RECOVERY_AREA_USAGE,看看是不是BACKUP PIECE或者FLASHBACK LOG占着大头。 - 是不是监控查的是缓存值?
V$RECOVERY_FILE_DEST的数据一般会很快刷新,但如果是在同一个会话里反复查,可以新开一个会话确认。
6.2 RMAN-08137:归档明明删了却说不能用
完整报错大概是这个形式:
RMAN-08137: WARNING: archived log not deleted, needed for standby or upstream capture process archived log file name=/u01/app/oracle/fast_recovery_area/ORCL/archivelog/2024_03_15/o1_mf_1_4521_abc123.arc thread=1 sequence=4521从字面意思就能看出来:RMAN 认为这条归档还被需要,所以拒绝删。
有三种可能的原因,处理方式完全不同。
第一种,配置了ARCHIVELOG DELETION POLICY TO APPLIED ON ALL STANDBY,而备库确实还没应用到这个序列号。这是最正常的情况,也说明你的策略配得是安全的。这时候不应该想着怎么绕过它,而应该去看备库为什么落后:
SQL> SELECT DEST_ID, STATUS, RECOVERY_MODE, APPLIED_SEQ 2 FROM V$ARCHIVE_DEST_STATUS WHERE DEST_ID = 2;第二种,备库已经不用了,但主库的LOG_ARCHIVE_DEST_2还配着,状态是ERROR或者DEFERRED。这种情况下,归档永远等不到"被应用完成",自然永远删不掉。处理方式是把失效的目的地清掉:
SQL> ALTER SYSTEM SET LOG_ARCHIVE_DEST_STATE_2 = DEFER; SQL> ALTER SYSTEM SET LOG_ARCHIVE_DEST_2 = '';第三种,环境里有下游的日志捕获进程(比如做数据同步用的),它在等这条归档。这种情况要先去处理那个进程。
如果确认以上三种都不适用,而且情况紧急,可以临时用:
RMAN> DELETE FORCE ARCHIVELOG ALL COMPLETED BEFORE 'SYSDATE-2';用之前务必记下当前的最早可恢复 SCN:
SQL> SELECT MIN(SEQUENCE#) FROM V$ARCHIVED_LOG WHERE STATUS = 'A';6.3 ORA-19809 / ORA-19804:备份也一起挂了
恢复区满了以后,不只是归档写不进去,RMAN 备份同样会失败:
ORA-19809: limit exceeded for recovery files ORA-19804: cannot reclaim 52428800 bytes disk space from 107374182400 limitORA-19809说的是"恢复区超限了",ORA-19804说的是"想回收 50MB 空间但回收不出来"。后者的出现说明 Oracle 已经尝试过自动清理最老的归档,但清理不掉——通常是因为那些归档还在保留期内或者还被备库需要。
处理顺序还是老一套:先扩容上限解压,再清理。但这里有个额外的注意点:如果备份是写到恢复区的,最好把备份输出改成独立路径,别让备份和归档继续抢空间。
RMAN> BACKUP DATABASE PLUS ARCHIVELOG FORMAT '/backup/rman/%U';指定明确的FORMAT路径,把备份写到独立的备份盘上。
6.4 归档进程卡住与 ARCn 状态异常
有时候空间明明够了,归档还是不出。这时候要看归档进程本身的状态:
SQL> SELECT PROCESS, STATUS, THREAD#, SEQUENCE#, BLOCK# 2 FROM V$ARCHIVE_PROCESSES; SQL> SELECT GROUP#, SEQUENCE#, STATUS, ARCHIVED, MEMBERS 2 FROM V$LOG;正常情况下V$ARCHIVE_PROCESSES.STATUS应该是IDLE(空闲待命)或者BUSY(正在归档)。如果出现STOPPED,说明归档进程已经停了。V$LOG里ARCHIVED列如果是NO,说明这个日志组还没归档成功,也就不能被覆盖。
常见的原因有两个。一个是归档目的地权限或者空间问题,这个在 alert 日志里会有明确的 ORA-19502 之类的报错。另一个是LOG_ARCHIVE_MAX_PROCESSES设得太小,高并发写入时归档跟不上日志切换的速度。默认是 4,如果日志切换非常频繁,可以适当调大:
SQL> SHOW PARAMETER LOG_ARCHIVE_MAX_PROCESSES; SQL> ALTER SYSTEM SET LOG_ARCHIVE_MAX_PROCESSES = 8 SCOPE=BOTH;但要注意,加归档进程只能提高归档的吞吐,如果瓶颈在磁盘写入速度上,加进程反而会加剧 I/O 竞争。所以调这个参数之前,最好先看看那块盘的实际写入性能。
6.5 问题速查表
把上面这些整理成一张表,应急的时候可以按症状直接查。
| 现象 | 最可能的原因 | 第一步动作 |
|---|---|---|
| 应用全部连不上,只有 SYSDBA 能连 | 恢复区满 | 查V$RECOVERY_FILE_DEST,扩容 |
USED_PCT100% 但RECLAIM_GB很大 | 归档过期未清理 | RMAN crosscheck + delete obsolete |
USED_PCT100% 且RECLAIM_GB很小 | 容量真不足或保留期太长 | 扩容 + 调整保留策略 |
| 删了文件空间不降 | 控制文件账本未更新 | CROSSCHECK+DELETE EXPIRED |
| 报 RMAN-08137 | 归档被备库或闪回需要 | 查V$ARCHIVE_DEST_STATUS |
只有FLASHBACK LOG在涨 | 保证还原点未清理 | 查V$RESTORE_POINT |
ARCn状态 STOPPED | 目的地不可写或进程数不足 | 看 alert 日志 + 调LOG_ARCHIVE_MAX_PROCESSES |
| 备份也失败报 ORA-19809 | 备份片和归档抢空间 | 把备份输出改到独立路径 |
| 归档量突然翻几倍 | 批量作业集中写入 | 排查作业 + 考虑 NOLOGGING |
7. 我在几次真实故障里总结的几条经验
7.1 我踩过的三个真实的坑
第一个坑:有一年我接手一个新环境,看到恢复区里有大量归档,第一反应是手工rm掉了一批释放空间。当时确实立刻见效了,应用连上了。但三个月后做恢复演练的时候,RMAN 报了一堆RMAN-06059(找不到归档日志),整个恢复流程走不下去,最后不得不重新做一次全备才恢复演练能力。那次我记住了一件事:归档的删除权只交给 RMAN,手工删能救急,但一定要在事后补一次 crosscheck。
第二个坑:有个环境每次都是BACKUP PIECE占满了恢复区,我一直以为是保留策略太宽松。查了半天发现是有人写了个备份脚本,每天跑一次全备,但备份输出路径默认指向恢复区,而保留策略里没配删除。结果就是每天加 300G,一周就满了。这个问题的根因不在归档,在备份架构。
第三个坑:某次升级之前建了保证还原点,升级完成以后没人清理。两个月后 ORA-00257 出现,我按常规套路删归档、扩容量,折腾了两个小时都没解决——因为删掉的那点空间立刻被闪回日志补回去了。最后是查V$RESTORE_POINT才找到真凶。从此我的巡检清单里多了一项:检查保证还原点是否存在。
7.2 几条不太写在文档里的建议
第一条,把ORA-19815当成主告警,而不是ORA-00257。前者是预警,后者是已经出事了。这两个错误码之间的时间差,通常有十几分钟到几个小时不等,这段时间就是你从容处理的机会窗口。
第二条,不要在恢复区接近满的时候去做一些"顺手"的事,比如临时跑个全备、导出一份数据。这些操作会进一步挤压空间,把"还能抢救"变成"只能扩容"。
第三条,扩容这个动作本身没有成本,所以不要舍不得。但扩容之后一定要记录:什么时候扩的、当时是什么原因、扩到了多少。如果不记录,下一次遇到同样的问题,你会重新走一遍完整的排查流程。我自己的习惯是在变更记录里写清楚三项——触发原因、扩容前后的上限、以及"是否调整了保留策略",这三项决定了下次还会不会重复遇到。
第四条,也是我觉得最重要的一条:ORA-00257 的根因排查,最终一定会落到"是容量问题还是策略问题"。容量问题用钱解决,策略问题用配置解决,而搞错方向的代价是——你以为自己解决了,实际上只是把下一次故障推迟了几天。