刚接手一套多租户环境时我的第一反应是先把备份体系搭起来。可看了半天发现之前的备份方式还是最原始的expdp逻辑导出所有PDB逐个手工执行导出文件散落在各台机器上没人记得清理。结果磁盘被撑爆过两次还有一次因为某个PDB漏备恢复时才发现数据是半个月前的。后来我把这套流程彻底重写做成了一个基于RMAN的Oracle PDB全自动备份批处理脚本核心就三件事一键备份所有PDB、备份完自动压缩、按保留策略清理过期文件。整套脚本跑下来凌晨两点定时执行日志齐全基本做到无人值守。这篇文章就把完整脚本、设计思路、以及我在实测和上线过程中踩过的坑原原本本分享出来给正在折腾多租户备份的同学做个参考。1. 多租户时代的备份困境为什么单库脚本不够用1.1 从expdp到RMAN多租户备份的演变Oracle 12c引入多租户架构之后备份的边界发生了一个很重要的变化一个CDB里可以跑十几个PDB每个PDB对业务方来说都是独立的数据库。以前单实例时代大家习惯用expdp做逻辑备份觉得导出dmp文件简单、恢复也直观。但在多租户场景下expdp的短板特别明显——它只能导出一个PDB而且逻辑备份天生不带归档日志、数据文件状态这些物理信息一旦要做完整恢复重建的时间成本和数据丢失风险都不可控。RMAN在12c之后原生支持backup pluggable database命令可以按PDB粒度做物理备份。物理备份的好处是恢复时直接还原数据文件不需要像impdp那样逐表逐对象重建。而且RMAN备份集本身支持压缩、增量、并行配合恢复目录还能做统一的备份管理。所以从多租户时代往后看各PDB的备份应该统一收敛到RMAN体系而不是继续靠expdp堆机器。这套全自动备份脚本的定位就是在RMAN基础上把多PDB逐个备份、压缩、清理的重复劳动全部脚本化定时执行不再依赖人肉操作。1.2 脚本要解决的四个核心痛点我在设计脚本前先梳理了手工备份和半自动备份常见的问题脚本的每个功能点都是冲着具体痛点去的。第一个痛点是漏。十几个PDB靠人一个个敲命令漏掉一两个太正常了。脚本必须能自动发现当前CDB里所有处于读写状态的PDB动态生成备份列表不用手工维护PDB清单。第二个痛点是乱。备份文件如果永远只跟日期挂钩而不跟PDB挂钩时间一长根本分不清哪个文件属于哪个库。所以脚本里的文件名必须同时带PDB名和时间戳一眼就能看出来。第三个痛点是撑。只备份不清理再大的磁盘也有爆的一天。必须有明确的保留策略7天还是14天逾期自动删除。第四个痛点是断。备份过程如果中途出问题有没有日志可查脚本跑了没有备份文件多大有没有校验这些信息必须完整落到日志文件里。搞清楚这四个痛点脚本的设计骨架就出来了动态发现PDB、RMAN备份并输出独立日志、备份后压缩、按周期清理。1.3 为什么选定Linux Shell RMAN组合这个脚本我选了纯Shell配合RMAN没有引入第三方工具原因很简单Oracle数据库环境里rman target /配合操作系统认证本来就是标准姿势Shell脚本可以直接复用ORACLE_HOME和ORACLE_SID环境变量不需要额外部署Agent也不依赖图形界面。另外Shell在做文件查找、压缩、删除这些操作时有天然优势。find、gzip、date这些命令组合起来几行就能实现按时间戳命名和过期清理。相比之下如果非要用PL/SQL或存储过程去做这些事反而绕了远路。当然如果你已经有企业级备份调度平台脚本也可以作为底层执行器接入逻辑是一样的。2. 脚本整体设计备份、压缩、清理三步拆解2.1 总流程先探测PDB再逐个备份最后压缩清理整个脚本的执行顺序是这样的加载公共环境变量和配置ORACLE_SID、ORACLE_HOME、备份目录、保留天数。用SQL查询当前CDB中所有处于READ WRITE状态的PDB名称过滤掉种子库PDB$SEED。循环遍历PDB列表对每个PDB单独执行RMAN备份输出独立的RMAN日志。全部备份完成后用gzip对当天生成的备份文件做操作系统级压缩。按保留天数清理过期压缩包同时清理过期的RMAN日志。整个脚本用LOCK_FILE做防重入保护避免定时任务还没跑完又被拉起一次。这个顺序里有一个细节值得注意RMAN的delete noprompt obsolete放在备份命令内先执行而不是留到压缩之后。原因是RMAN管理的是备份集原文件一旦我们后续对.bkp文件做了gzip改名RMAN再执行清理时会认为物理文件丢失产生大量冗余报错。所以先让RMAN清理再统一压缩是最优顺序。2.2 为什么选RMAN备份集而不是镜像副本RMAN备份有两种基本形式备份集backupset和镜像副本image copy。备份集的本质是只包含数据块变化的压缩格式文件不能直接当数据文件用镜像副本则是数据文件的一比一拷贝。在多PDB自动备份场景下我坚定选备份集。理由有三点第一备份集默认去重和压缩空间占用通常能降到原始数据的三到五成第二备份集天然支持增量备份和前后端并行以后想做增量策略有扩展空间第三镜像副本对一个十几个PDB的CDB来说磁盘消耗太大除非你有专门的快速恢复区且磁盘特别充裕否则没理由选它。脚本里我用了backup as compressed backupset相当于在RMAN层面先做了一层压缩后面操作系统层面再用gzip补一层。有人会问这不是双重压缩吗实测下来RMAN的压缩和gzip的压缩算法侧重不同RMAN压缩后的备份集再gzip往往还能小10%到20%所以这个组合并不浪费。2.3 每个PDB独立备份的意义可能有人觉得直接对整个CDB执行一次backup database不是更省事表面上是的但它把所有PDB绑定在一个备份集里存在两个隐患一是某个PDB出问题会影响整次备份的成败判定二是在恢复单个PDB时RMAN要跨一个大备份集去定位逻辑上更复杂。脚本按PDB循环备份每个PDB一个备份集、一个日志文件。这样做的好处是故障隔离某个PDB备份失败不影响其他PDB恢复时想恢复哪个就恢复哪个RMAN直接定位对应备份集就行。代价仅仅是多几次RMAN进程启动这个开销完全可以接受。3. 核心脚本完整可落地的实现版本3.1 公共配置区与环境变量脚本开头的配置区是改动频率最高的地方我把所有可能需要调整的参数全部集中到一起#!/bin/bash # Oracle PDB 全自动备份脚本一键备份 压缩 过期清理 # 适用环境: Oracle 12c/19c/21c 单机或RAC多租户环境 # 公共配置区 export ORACLE_SIDORCLCDB export ORACLE_HOME/u01/app/oracle/product/19.3.0/dbhome_1 export PATH${ORACLE_HOME}/bin:$PATH export NLS_DATE_FORMATYYYY-MM-DD HH24:MI:SS BACKUP_BASE/backup/pdb_backup # 备份文件根目录 LOG_DIR${BACKUP_BASE}/logs # RMAN日志与脚本日志目录 RETENTION_DAYS7 # 备份保留天数 DATE_TAG$(date %Y%m%d_%H%M%S) # 统一的时间戳 LOCK_FILE/tmp/pdb_backup.lock # 防重入锁文件ORACLE_SID这里填的是CDB的实例名不是单个PDB名因为RMAN备份PDB需要先连接到CDB。ORACLE_HOME一定要确保准确如果有多套Oracle环境最好在脚本里显式指定避免PATH里混入错误版本的rman和sqlplus。NLS_DATE_FORMAT这个环境变量也建议设成固定的它对RMAN日志里的时间格式有影响。如果不设某些系统默认格式可能和脚本预期不一致排查日志时容易一头雾水。3.2 获取PDB清单动态感知而非手工维护脚本第一步是自动获取当前可备份的PDB列表# 获取PDB清单 PDB_LIST$(sqlplus -S / as sysdba EOF set pagesize 0 feedback off heading off verify off echo off select name from v\$pdbs where name ! PDB\$SEED and open_mode READ WRITE; EOF ) if [ -z ${PDB_LIST} ]; then echo [$(date %F %T)] 没有找到可备份的PDB请检查CDB状态。 exit 1 fi echo [$(date %F %T)] 本次需要备份的PDB列表: echo ${PDB_LIST}这里用了v$pdbs视图open_mode READ WRITE确保只备份正常打开且可读写的PDB。如果某个PDB正处于MOUNTED状态意味着它不允许正常读写跳过它是合理的选择但后续最好人工确认一下原因。PDB$SEED是种子模板它内容固定且不可修改完全不需要备份过滤掉是必须的。整个查询不依赖任何外部配置文件所以新增一个PDB后不需要改脚本非常契合多租户弹性扩展的特性。3.3 逐个PDB执行RMAN备份获取到PDB列表后循环体是核心# 逐个PDB执行RMAN备份 for pdb in ${PDB_LIST}; do echo [$(date %F %T)] 开始备份 PDB: ${pdb} rman target / log ${LOG_DIR}/rman_${pdb}_${DATE_TAG}.log EOF run { backup as compressed backupset pluggable database ${pdb} format ${BACKUP_BASE}/${pdb}_%U.bkp; delete noprompt obsolete; } exit; EOF if [ $? -ne 0 ]; then echo [$(date %F %T)] PDB ${pdb} 备份失败请检查日志: ${LOG_DIR}/rman_${pdb}_${DATE_TAG}.log continue fi echo [$(date %F %T)] PDB ${pdb} 备份完成。 donerman target /借助操作系统认证连接本地CDB不需要在脚本里明文写密码安全性和简洁性都更好。log参数让每个PDB的RMAN输出落到独立日志文件里日志文件名的PDB和时间戳对排查问题非常友好。备份文件格式里${pdb}_%U.bkp中的%U是RMAN自动生成的唯一标识避免同一PDB多次备份时文件名冲突。我在run块里还加了delete noprompt obsolete这一步会把超出保留策略的旧备份集清理掉属于RMAN体系内的清理后面OS级别的清理处理的是gzip后的文件。3.4 压缩与过期清理操作系统级别的收尾备份环节完成后脚本进入压缩和清理阶段# OS级别压缩备份文件 echo [$(date %F %T)] 对当天生成的备份文件执行gzip压缩... find ${BACKUP_BASE} -maxdepth 1 -type f -name *.bkp -mmin -240 -exec gzip -9 {} \; # 清理超过保留天数的压缩备份文件 echo [$(date %F %T)] 清理超过 ${RETENTION_DAYS} 天的备份文件... find ${BACKUP_BASE} -maxdepth 1 -type f -name *.bkp.gz -mtime ${RETENTION_DAYS} -delete # 同时清理过期的RMAN日志 find ${LOG_DIR} -maxdepth 1 -type f -name *.log -mtime ${RETENTION_DAYS} -delete # 清理后统计磁盘占用 echo [$(date %F %T)] 当前备份目录占用情况: du -sh ${BACKUP_BASE} echo [$(date %F %T)] 本轮备份任务全部结束。压缩这步用-mmin -240匹配4小时内生成的.bkp文件既覆盖了本轮备份产生的文件又不会误压缩到历史遗留的未压缩文件。-9参数表示最高压缩比虽然耗时稍长但多PDB场景下磁盘收益更重要。清理用-mtime ${RETENTION_DAYS}7天就写7表示修改时间超过7天整的文件才会删。这里判断的是文件mtime也就是文件修改时间和备份内容中的日期没有关系按过期备份文件这个定义是准确的。3.5 防重入保护定时任务崩溃的兜底机制定时任务最怕遇到的情况是上次备份因为网络慢、磁盘慢还没跑完下一次调度又触发了两个进程同时写同一个目录互相干扰。我给脚本加了一个简单的锁文件机制# 防重入保护 if [ -f ${LOCK_FILE} ]; then echo [$(date %F %T)] 检测到锁文件疑似上次备份未正常结束脚本退出。 exit 1 fi touch ${LOCK_FILE} trap rm -f ${LOCK_FILE} EXIT锁文件在脚本开始时创建结束时自动删除。如果上次脚本异常中断锁文件还残留新脚本会直接退出而不是强行并发执行。trap rm -f ${LOCK_FILE} EXIT保证了即使脚本中途报错退出锁也会被清理。这个机制虽然简单但在生产环境的定时任务里能避免掉大部分并发灾难。3.6 执行权限与最低运行要求脚本文件放到/opt/scripts这样的固定目录后记得赋执行权限chmod x /opt/scripts/pdb_backup.sh运行前提是当前操作系统用户能通过/ as sysdba连上数据库通常是oracle用户或者DBA组用户。如果你用crontab调度注意crontab里的环境变量和交互式Shell不一样建议在crontab里写成0 2 * * * /opt/scripts/pdb_backup.sh /backup/pdb_backup/logs/cron.log 21脚本内部已经显式export了ORACLE_HOME和ORACLE_SID这比依赖crontab环境里残缺的变量可靠得多。4. 压缩策略磁盘空间与时间的权衡4.1 RMAN压缩与gzip压缩的分工这个脚本里实际有两层压缩RMAN的compressed backupset和操作系统的gzip。RMAN压缩工作在整个备份过程中同步完成几乎没有额外的时间开销而且它对Oracle数据块格式有深度理解能识别空闲块和重复模式。但它的压缩率放在业界看并不算激进主要是为了平衡备份耗时和CPU占用。所以备份完成后我再用gzip -9补一刀。这一刀做的是再榨一点空间实测下来20%左右的进一步缩减很正常。有人担心双重压缩会拖慢整体备份流程实际上gzip阶段是文件系统级的纯CPU操作和数据库运行完全解耦放在凌晨低峰期执行几乎无感。4.2 如何估算备份需要的磁盘空间备份前最怕的就是磁盘不够。这里给一个可以快速估算的方法假设某个PDB的总体数据量为SRMAN压缩备份集通常能压到数据量的40%左右也就是0.4*S。经过gzip再压20%最终每个PDB的备份文件大约是0.32*S。保留D天总空间需求大约是0.32 * S * D * PDB数量。举个具体例子某环境3个PDB总数据量300GB压缩后单次备份约96GB保留7天需要670GB左右再加上日志和缓冲余量准备1TB比较稳妥。按这个公式你在上线前就能提前规划磁盘而不是等到备份报错才发现空间不够。4.3 压缩耗时与CPU的实测参考在一次模拟生产环境的实测中总数据量约120GB分成3个PDBRMAN压缩备份耗时42分钟gzip压缩耗时18分钟最终备份目录从120GB降到31GB。整批任务从开始到清理结束大约1小时5分钟。CPU方面gzip阶段单核压力比较大如果是多核服务器gzip默认单进程跑可以考虑把gzip -9换成pigz并行压缩耗时还能再降一半。如果线上对CPU特别敏感也可以把压缩级别从-9降为-6压缩率稍微降一点但时间能省三分之一对业务影响更小。5. 过期清理的边界问题避免误删和磁盘爆炸5.1 保留策略7天只是个起点RETENTION_DAYS7这个值是我在多个环境里用得比较多的默认值但它不代表所有业务都该用7天。决定保留天数时主要看两个因素恢复时间目标RTO和数据变更频率。如果你允许恢复时丢失一天的数据并且有最近的逻辑导出兜底那7天完全够用。如果每次备份完就删那等于没备份。我见过一些公司把备份保留期拉到30天主要是为了应对业务方一个月之后才发现数据有问题的场景。磁盘充足的情况下保留期设长一点没有坏处真正的风险是保留期和磁盘容量不匹配。5.2 RMAN清理与OS清理怎么配合不出错这里要特别强调一个配合边界也是很多人容易踩的坑。RMAN的delete obsolete只能删除RMAN登记过的备份集文件。一旦我们把.bkp文件用gzip改成.bkp.gzRMAN的备份目录里登记的物理路径就指向一个不存在的文件。此时如果再去执行RMAN清理RMAN会认为备份文件丢失报错甚至产生大量异常信息。所以脚本里严格遵守了顺序RMAN的delete noprompt obsolete在备份块内先执行此时文件还没被gzipRMAN能正常删除自己登记的旧备份集当天的备份集gzip改名之后后续的过期清理就完全交给OS的find -mtime来做。两条清理路径互不干扰各管一段。5.3 防止删错的几个安全网清理操作本身是删除行为一旦误删不可恢复。我建议至少加三道安全网第一find命令先用-name精确匹配备份文件后缀。脚本里只匹配.bkp.gz不会动到其他文件。第二清理前打印准备删除的文件列表。第三初次上线时手动执行find ... -delete之前的预览命令比如把-delete换成-print肉眼确认一下要删的文件对不对再放生产。把-delete换成-print的命令大概是find ${BACKUP_BASE} -maxdepth 1 -type f -name *.bkp.gz -mtime ${RETENTION_DAYS} -print这一步花不了两分钟但能避免因为时间戳格式问题把新鲜备份误删的情况。6. 实测数据与运行效果脚本上线后的真实表现6.1 一次完整任务的输出与产物脚本正常运行时的输出大概是这样的[2025-06-11 02:00:01] 本次需要备份的PDB列表: PDB1 PDB2 PDB3 [2025-06-11 02:00:02] 开始备份 PDB: PDB1 [2025-06-11 02:42:19] PDB PDB1 备份完成。 [2025-06-11 02:42:20] 开始备份 PDB: PDB2 [2025-06-11 03:18:47] PDB PDB2 备份完成。 [2025-06-11 03:18:48] 开始备份 PDB: PDB3 [2025-06-11 03:51:22] PDB PDB3 备份完成。 [2025-06-11 03:51:23] 对当天生成的备份文件执行gzip压缩... [2025-06-11 04:09:36] 清理超过 7 天的备份文件... [2025-06-11 04:09:37] 当前备份目录占用情况: 31G /backup/pdb_backup [2025-06-11 04:09:38] 本轮备份任务全部结束。备份完成后目录结构是这样的/backup/pdb_backup/ ├── PDB1_0c1a2b3c_20250611.bkp.gz ├── PDB2_0d4e5f6a_20250611.bkp.gz ├── PDB3_0a7b8c9d_20250611.bkp.gz └── logs/ ├── rman_PDB1_20250611_020001.log ├── rman_PDB2_20250611_020001.log └── rman_PDB3_20250611_020001.log每个PDB的备份文件独立存在带日期和唯一标识一眼就能看出哪天的哪个库。6.2 如何快速判断RMAN备份是否成功只看脚本输出的备份完成还不够严谨因为RMAN命令的退出码有时候不准确。更可靠的方式是查看RMAN日志文件的开头和结尾grep -E RMAN-00571|RMAN-03009|ORA- /backup/pdb_backup/logs/rman_PDB1_*.log正常的RMAN日志结尾会看到类似Finished backup at 2025-06-11 02:42:18 Starting Control File and SPFILE Autobackup Finished Control File and SPFILE Autobackup Recovery Manager complete.一旦看到ORA-、RMAN-03009这类错误串说明这轮备份有问题需要进一步排查。生产环境中建议把这段检查也写进脚本里不满足条件就触发告警这样才能真正做到无人值守。7. 容易踩的坑与排查记录7.1 坑一NLS_DATE_FORMAT导致日志时间混乱早期版本的脚本里我没有设置NLS_DATE_FORMAT结果在某个LANG环境下RMAN日志里时间显示成了05/11/25 02:00:00这种格式和脚本里date命令输出的2025-06-11 02:00:00对不上排查哪个日志对应哪次备份时非常别扭。后来我在脚本开头固定export NLS_DATE_FORMATYYYY-MM-DD HH24:MI:SS无论crontab环境怎么变RMAN日志、脚本输出、文件名三者的时间口径完全一致。这类问题看有点奇怪但确实发生过建议大家一开始就固定这个变量。7.2 坑二gzip过后RMAN报文件丢失这是我第一次调整脚本时亲身踩过的。最初版本我把gzip放在RMAN备份之后、delete obsolete之前结果执行到删除时RMAN报错提示备份集文件无法访问。排查原因后发现RMAN登记的物理文件名是PDB1_xxxx.bkp但gzip之后变成了PDB1_xxxx.bkp.gzRMAN按原路径找不到文件。解决方式就是前文说的顺序调整先让RMAN删除自己管理的过期备份集再统一gzip新文件。这之后每次备份生成的压缩包就脱离了RMAN的登记范围清理逻辑完全由OS的find负责整个流程稳定多了。7.3 坑三磁盘空间不足导致备份半途中断某次预发布环境测试时脚本跑到第二个PDB突然报错日志里有ORA-19504和ORA-27030意思是无法创建备份文件原因就是磁盘满了。当时我意识到脚本里缺少备份前的空间预检。后面我在脚本开头加了一个简单的检查逻辑备份前先统计备份目录当前占用和磁盘可用空间如果剩余空间低于预估需求就提前退出。这里可以用du和df命令简单实现AVAIL_SPACE$(df -BG ${BACKUP_BASE} | awk NR2 {print $4} | sed s/G//) echo [$(date %F %T)] 备份目录当前可用空间: ${AVAIL_SPACE}G实际生产建议直接在脚本里做一次估算比较看到可用空间低于某个阈值就中止避免备份到一半磁盘爆炸把原本能正常服务的库也拖下水。7.4 坑四PDB状态异常被静默跳过脚本只备份READ WRITE状态的PDB。有一次某个PDB因为异常被置于MOUNTED状态脚本自动跳过了它但没有任何日志提示直到业务反馈数据库不对劲才发现备份缺失。这个问题的根因是脚本默认只要不在READ WRITE状态就不备份这个判断本身没错但跳过的动作必须有显式记录。改进方案是在获取PDB列表时把MOUNTED、READ ONLY等其他状态的PDB也查出来并输出提示这样任何异常状态都会在日志里留下痕迹。7.5 坑五归档日志撑爆快速恢复区多租户环境下如果只备份数据文件而不管归档日志快速恢复区会逐渐被归档撑爆导致后续备份直接失败。脚本里我在RMAN块中加入了sql alter system archive log current来触发当前归档切换确保数据文件备份的SCN与归档日志对齐。更完整的做法是在RMAN块里增加归档日志备份和清理backup archivelog all delete input;这样备份完的归档日志会被拉进备份集并从快速恢复区删除恢复区的空间压力大幅降低。如果你生产环境归档特别多这一步几乎是必须的。8. 后续优化方向与运维建议8.1 从全备走向增量备份当前脚本是每周每天都是全量备份简单可靠但数据量持续膨胀后全量备份的耗时和空间消耗都会成为瓶颈。RMAN对PDB也支持增量策略比如周日全备周一到周六增量。增量备份要做的是记录每个PDB的SCN基线脚本里需要在RMAN命令块里加上类似backup incremental level 1 pluggable database ${pdb};配合CONFIGURE BACKUP TYPE TO COMPRESSED BACKUPSET和CONFIGURE ARCHIVELOG DELETION POLICY一起使用。增量备份的恢复依赖最近的基线和各增量的串接对恢复流程要求更高适合在RTO和RPO有明确目标的团队里推进。8.2 监控告警备份系统的最后一公里脚本本身跑得再好如果没人知道它失败了等于没有备份。最简单的告警方式是两个一是检查备份目录下最近24小时内是否生成了新的.bkp.gz文件二是检查RMAN日志里是否有RMAN-或ORA-错误。任何一项异常都说明备份可能出问题需要立刻人工介入。# 检查是否有今天的备份文件 if find ${BACKUP_BASE} -maxdepth 1 -name *.bkp.gz -mmin -1440 | grep -q .; then echo 备份文件存在 else echo 超过24小时未生成备份文件请检查! # 这里可以接邮件/短信/企业微信推送 fi如果公司有监控平台把这个检查逻辑做成一个定时探针更省事。备份系统必须纳入告警体系不能只靠明天看一眼日志。8.3 恢复演练备份脚本合格与否的最终标准最后想提醒一件比脚本本身更重要的事定期做恢复演练。备份脚本跑顺利了不代表恢复就一定能成功。RMAN的备份集是否完整、归档日志是否齐全、控制文件备份是否可用都要通过真实的恢复演练来验证。我个人的习惯是每季度挑一个非核心PDB做一次完整的恢复测试新建一个测试环境的CDB把备份集还原进去然后打开数据库跑一遍关键业务查询。这一步能把脚本、盘空间、归档策略所有环节的真实问题全部暴露出来。自动备份脚本只是备份体系的一部分真正可靠的数据保护是备份能成功、恢复能验证、清理不会误删这三件事同时成立。我在实际使用中最大的体会是写这个脚本本身不算难难的是把边界情况想清楚把清理逻辑和RMAN的删除逻辑理顺再把监控告警接上。这套流程跑顺之后多租户环境的备份工作就从一个需要每天盯着的体力活变成了一个按部就班的自动化任务睡得也踏实多了。如果你正在折腾PDB备份完全可以把上面的脚本拿去做底子根据自己环境的PDB数量和磁盘情况调整保留期但顺序和防重入这些设计建议原样保留。