ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Oracle表空间无法回收?高水位线与SHRINK实战排查指南

Oracle表空间无法回收?高水位线与SHRINK实战排查指南 上个月收到一套Oracle生产库的磁盘告警oradata目录使用率达到了98%。登录服务器简单查了一下一个应用表空间分配了800GB实际数据只有120GB左右。按正常思路这种情况直接收缩表空间、把空闲空间还给操作系统就行。结果我连续执行了三条常规回收SQL全部失败报了一堆让人摸不着头脑的错误码。这篇就把这次空间无法回收的排查过程拆开讲清楚从原因到诊断再到最终落地都是一线实操的经验同行应该用得上。先说结论性的一件事Oracle的“空间回收”和大多数人理解的“删了数据就释放空间”完全是两码事。很多DBA在运维中都会遇到类似情况——表空间明明有很大一部分“空着”却无论如何都收不回来。这里面既有数据库原理层面的限制也有段对象自身的结构依赖和锁问题。我会用实际的操作过程和报错案例把“无法回收”这个结果背后的真正原因一层层剥开。1. 空间无法回收的核心原因剖析遇到空间无法回收的时候先别急着怀疑Oracle出了bug或者表空间损坏绝大多数情况都逃不开下面这几类原因。弄清楚原理后面的诊断和操作才有方向。1.1 高水位线删了不等于还了空间这是所有空间回收问题里最经典、最容易被忽略的原因。Oracle表空间中的数据文件逻辑上被切成一个一个小块段对象在分配给它的这些块里不断插入数据。高水位线High Water Mark)指的就是段中曾经使用过的最高块号。你DELETE掉一千万行Oracle只是把块标记为空闲但段的高水位线不会自动降下来。下一次INSERT的时候Oracle还是会优先使用高水位线以上的新块而不是高水位线以下那些空出来的块。我用一个生活类比来解释一下泳池放了半池水但池壁上曾经浸湿的水痕一直都在。Oracle判断这个段有多少数据量看的不是当前水面上还有多少水而是水痕的位置。所以就算你把数据删光段占用的物理空间还是那么大。表现在表空间层面就是dba_free_space里明明有大量空闲空间数据文件本身却一点没变小。SHRINK操作之所以能释放空间原理就是移动行的物理位置把高水位线以下的那些空块压缩并释放出来。但正因为行要发生物理移动ROWID会变Oracle默认不允许。这也是后面很多报错的根源所在。1.2 段管理和存储参数的限制不是所有的表空间都支持在线收缩。Oracle从9i开始引入自动段空间管理ASSM用位图来管理段内的空间状态10g之后普通表空间默认都是ASSM。SHRINK这种在线压缩操作要求表空间必须采用ASSM。如果你遇到的是早期迁移过来的手动段空间管理MSSM表空间SHRINK直接就会报ORA-10635根本没有商量余地。存储参数方面PCTFREE和MAXEXTENTS同样会影响收缩结果。PCTFREE如果设置过大块内预留空间太多收缩时移动行的空间估算就会出问题MAXEXTENTS过小收缩过程中Oracle需要为段分配临时扩展块来整理数据扩展不动也会直接失败。很多人只盯着高水位线忽略了这两个参数结果脚本在测试库跑得顺顺当当生产库一执行就报ORA-01653或ORA-01654这类报错往往不是空间不够而是MAXEXTENTS触顶了。1.3 对象依赖与锁一个索引就能锁死全局SHRINK不是只移动表的行索引行也要同步维护所以可级联收缩CASCADE会同时处理该表上的索引。麻烦就麻烦在这个“同时处理”上一旦表上有函数索引、全局分区索引或者物化视图日志收缩过程中要做的事情会成倍增加任何一个依赖对象状态异常收缩事务就会整体回滚。生产环境里最常见的其实是锁冲突。SHRINK本质上要改数据行的物理位置为避免DML操作把行地址搞乱Oracle在收缩期间必须拿住表级别的DML锁甚至DDL锁。这时候如果有一个长达数小时的报表事务正在读取这张表的数据SHRINK操作就会一直卡在等待队列里直到超时或被监控脚本直接Kill掉。我见过不少“无法回收”案例排查到最后不是技术问题而是业务高峰期撞上了一张长期未提交的事务。2. 动手之前用诊断思路快速定位问题遇到无法回收的情况最忌讳的就是上来就执行一条SHRINK期望一步到位。我总结了一套从宏观到微观、从参数到对象的排查路径照着走基本十分钟内就能把根因锁定。2.1 看清家底一条SQL理清表空间使用情况第一步是确认表空间在Oracle内部到底是个什么状态。执行下面这条SQL可以同时看到总大小、当前空闲空间和使用率SELECT df.tablespace_name, ROUND(SUM(df.bytes)/1024/1024/1024, 2) AS total_gb, ROUND(NVL(SUM(fs.free), 0)/1024/1024/1024, 2) AS free_gb, ROUND((1 - NVL(SUM(fs.free), 0)/SUM(df.bytes)) * 100, 1) AS used_pct FROM dba_data_files df LEFT JOIN ( SELECT tablespace_name, file_id, SUM(bytes) AS free FROM dba_free_space GROUP BY tablespace_name, file_id ) fs ON df.file_id fs.file_id AND df.tablespace_name fs.tablespace_name GROUP BY df.tablespace_name ORDER BY total_gb DESC;这里有一个非常关键的概念需要区分dba_free_space统计的是表空间内部所有空闲区间的总和而操作系统上数据文件的大小才是真正占磁盘的东西。如果表空间内部空闲空间很大但数据文件大小纹丝不动说明空闲空间集中在高水位线以下属于“够用但是拿不出来”的状态这就是典型的HWM问题。如果表空间内部本身没有多少空闲空间但数据文件极大则说明段对象分配了大量空间但没人用需要进入第二步定位段对象。2.2 层层下钻从表空间到段对象确认了目标表空间之后按段大小排序找出T0P20的对象SELECT * FROM ( SELECT owner, segment_name, segment_type, ROUND(bytes/1024/1024, 2) AS size_mb, extents, blocks FROM dba_segments WHERE tablespace_name TBS_APP ORDER BY bytes DESC ) WHERE ROWNUM 20;通过这条SQL你能快速锁定那些真正占用空间的表、索引或者回滚段。常见情况是TOP3的对象占掉了整个表空间八成的空间而这些对象的数据行数却不多。这时候用DBMS_SPACE.SPACE_USAGE这个包看段内部的实际分配情况会更直观SET SERVEROUTPUT ON DECLARE v_unformatted_blocks NUMBER; v_unformatted_bytes NUMBER; v_fs1_blocks NUMBER; v_fs1_bytes NUMBER; v_fs2_blocks NUMBER; v_fs2_bytes NUMBER; v_fs3_blocks NUMBER; v_fs3_bytes NUMBER; v_fs4_blocks NUMBER; v_fs4_bytes NUMBER; v_full_blocks NUMBER; v_full_bytes NUMBER; BEGIN DBMS_SPACE.SPACE_USAGE(APP, T_ORDER_HIS, TABLE, v_unformatted_blocks, v_unformatted_bytes, v_fs1_blocks, v_fs1_bytes, v_fs2_blocks, v_fs2_bytes, v_fs3_blocks, v_fs3_bytes, v_fs4_blocks, v_fs4_bytes, v_full_blocks, v_full_bytes); DBMS_OUTPUT.PUT_LINE(unformatted || v_unformatted_blocks || fs1 || v_fs1_blocks || fs2 || v_fs2_blocks || fs3 || v_fs3_blocks || fs4 || v_fs4_blocks || full || v_full_blocks); END; /这个包会输出段空间中不同状态的比例fs1到fs4表示块内空闲空间从少到多的块数full是满块数unformatted是已经分配但还没格式化使用的块数。如果看到fs2、fs3、fs4占了绝大多数而这个段的总行数又不多那基本就能确认是碎片化和高水位线问题SHRINK这个方向是对的。2.3 判断可收缩性哪些情况能收哪些不能收在真正执行任何SHRINK命令之前先从系统表里确认三个条件-- 表空间是否使用ASSM SELECT tablespace_name, segment_space_management FROM dba_tablespaces WHERE tablespace_name TBS_APP; -- 表是否已开启ROW MOVEMENT SELECT owner, table_name, row_movement FROM dba_tables WHERE owner APP AND table_name T_ORDER_HIS; -- 是否存在大量回收站对象占空间 SELECT owner, object_name, type, space FROM dba_recyclebin WHERE space 100;这三个检查缺一不可。表空间不是ASSMSHRINK直接免谈表没开启ROW MOVEMENT单独对表做SHRINK会报错回收站里面有大量DROP掉的对象这些空间也不是普通SHRINK能直接释放的需要先PURGE。这里顺便提醒一下平时养成定期清回收站的习惯别让那些被误删对象的尸体占用宝贵空间。3. 实操案例Oracle空间回收从报错到落地讲完原理和诊断思路接下来用一个完整的生产案例复盘展示从第一次报错到最终把空间收回来的整个过程。这套流程不是我凭空生成的是把三类频率最高的生产故障汇总成的一个代表性场景你对照自己的环境稍作调整就能用。3.1 现场还原一个真实的表空间收缩故障环境是Oracle 19c应用表空间TBS_DATA分配了800GB业务高峰期磁盘使用率98%。通过2.1节的SQL查询发现dba_free_space里的空闲空间只有10GB左右说明这800GB空间的绝大部分都已经被段对象占用了可实际业务数据只有120GB左右。也就是说段对象吞掉了大量空间但对业务毫无贡献。第一次尝试按文档执行在线表空间收缩ALTER TABLESPACE TBS_DATA SHRINK SPACE KEEP 300G;结果Oracle直接抛了ORA-10635Invalid segment or tablespace。这个错误码的含义是目标段对象或表空间不支持在线收缩。遇到这个报错很多人第一反应是检查表空间参数但实际表空间确实是ASSM参数没问题。真正的问题出在后面那两个大表上它们的ROW MOVEMENT都是DISABLED状态表空间级SHRINK一旦需要移动这些段对象Oracle会立即拒绝。3.2 诊断过程不要被报错牵着走这个时候如果只盯着ORA-10635的语义去翻MOS大概率会绕很久。正确的做法是回到对象层面把问题拆到最小粒度再执行。我先通过2.2节的段大小查询锁定了三个核心对象APP.T_ORDER_HIS历史订单表段大小390GB实际数据行数约1800万行APP.T_FLOW_LOG流程日志表段大小280GB实际数据行数约2200万行APP.IDX_T_ORDER_TIME前一张表上的复合索引段大小120GB。这三张段对象加起来就有790GB左右基本解释了为什么现在找不到可见的空闲空间。下一步用DBMS_SPACE.SPACE_USAGE查看T_ORDER_HIS内部状态结果fs2加fs3加fs4占了85%以上full块不到5%。这说明该表的大量块都是半满甚至几乎全空的数据分布严重碎片化。在跑任何SHRINK之前我还查了一下V$LOCKED_OBJECT是否有长事务锁着这些表结果一到下班时间点就有一张报表正在扫T_FLOW_LOG的数据为时3小时。这里多说一句日常运维中空间回收前先摸清楚对象上的活动会话非常重要否则收缩动作会被锁直接拖死白等半小时后TIMEOUT回到“无法回收”的谜题里。3.3 解决方案落地一步一步把空间收回来确认业务低峰时段里没有任何长事务会话后我按下面的顺序执行了完整的回收方案第一步给目标表开启ROW MOVEMENTALTER TABLE APP.T_ORDER_HIS ENABLE ROW MOVEMENT; ALTER TABLE APP.T_FLOW_LOG ENABLE ROW MOVEMENT; ALTER TABLE APP.T_INDEX_TEST ENABLE ROW MOVEMENT;这里不用慌张ROW MOVEMENT只是允许行在物理块之间搬迁并不会立刻动数据。它本质上是给SHRINK操作一个许可操作完成后可以再DISABLE掉。第二步对目标表执行级联收缩一次处理一张表ALTER TABLE APP.T_ORDER_HIS SHRINK SPACE CASCADE;这一步会同时压缩T_ORDER_HIS表的段、索引以及LOB段如果存在。执行过程中监控一下生成的归档日志量和会话等待事件。如果等待事件长时间停留在“enq: TX - row lock contention”说明还是有隐蔽的并发事务在占用这张表宁可停掉收缩保业务也不要强行执行下去以免触发大面积的行迁移锁。T_ORDER_HIS收缩完成后段大小从390GB降到了约95GB效果立竿见影。紧接着用同样的命令处理T_FLOW_LOG这个表稍微特殊一些它上面有物化视图日志CASCADE模式直接把物化视图日志一起处理掉了并没有报错。不过如果你的环境里有复杂的物化视图刷新场景建议先检查JOBS是否正在刷新否则收缩和刷新同时跑会产生资源竞争。第三步再次执行表空间级的在线收缩ALTER TABLESPACE TBS_DATA SHRINK SPACE KEEP 300G;这次没有再报ORA-10635Oracle开始真正移动高水位线以上的空闲扩容块。收缩过程耗时约二十分钟期间表空间短暂处于只读状态因为必须保证数据一致。完成后表空间的大小降到350GB但KEEP参数指定的300G只是一个期望值实际文件大小不能小于段实际占用的空间这个参数更多是一个下限保护。最后用df -h查看磁盘使用率从98%降到了73%空间回收闭环完成。3.4 备选方案脏路径上的数据泵导出导入如果你的情况比上面这个更极端比如尝试SHRINK时发现表结构里有太多函数索引、全局分区索引或者表空间本身就是MSSM时代的老古董SHRINK这条路根本走不通不用硬扛。可以用数据泵Data Pump导入导出来清理空间思路是把数据搬到一个全新的表空间-- 原库导出 expdp APP/APP_DIR dumpfileexp_tbs.dmp logfileexp_tbs.log tablesT_ORDER_HIS -- 新库导入到TBS_DATA_NEW impdp APP/APP_DIR dumpfileexp_tbs.dmp logfileimp_tbs.log table_exists_actionreplace remap_tablespaceTBS_DATA:TBS_DATA_NEW这条路唯一的硬伤是时间窗口比较长而且需要额外准备一块临时磁盘和一套导入导出目录。不过它有SHRINK无法替代的优势重建后的表物理结构干净高水位线完全归零索引重建也更紧凑。我一般在老系统升级或者大版本迁移时用这套方案平时生产环境的小规模收缩还是优先SHRINK。4. 常见问题与避坑指南4.1 高频报错速查表这里整理一份我在各类空间回收场景中遇到的报错及处理方法直接对照查就行。报错代码含义处理思路ORA-10635无效段或表空间不支持收缩检查表空间是否ASSM、段是否启用ROW MOVEMENTORA-03297数据文件中包含超范围的数据无法收缩数据文件尾部有段对象先移动段或者对象级SHRINK后再试ORA-01653无法扩展表段表空间剩余空间不足或MAXEXTENTS触顶调整存储参数或扩大表空间ORA-01654无法扩展索引段先查索引所在表空间剩余空间回收站清理后再收缩ORA-14438分区键更新操作不允许SHRINK过程触碰分区约束检查分区表结构和触发器ORA-00060死锁收缩操作与其他DML形成了循环等待定位锁持有者并协调业务ORA-30036UNDO空间不足收缩期间生成的UNDO量过大增大UNDO表空间或者分批收缩这个表格里的坑我都踩过重点提示一下ORA-03297这类报错容易被人误解成“表空间没有足够的剩余空间”其实真正含义是你的数据文件末尾还有数据块文件缩不进去。遇到这个错误不要试图直接改数据文件大小而是要先对文件尾部的段做一次对象级收缩把数据块挪到文件头部再回头来缩文件。4.2 最容易踩的五个坑第一个坑是直接在业务高峰执行SHRINK。SHRINK期间会在表上拿锁时间长短取决于数据量和碎片程度最长可能持续数小时。我见过有人凌晨两点执行收缩操作结果恰好有跨时区的业务在跑批量任务锁冲突导致三天后业务方投诉性能下降。操作前一定要结合业务规律确认低峰窗口不能只看本地时间。第二个坑是不检查回收站就直接收缩。回收站里DROP掉的表虽然不占业务空间但当你尝试扩大或收缩表空间时Oracle会优先考虑回收站对象导致SHRINK过程中突然冒出ORA-01653。处理办法就是先执行PURGE DBA_RECYCLEBIN把垃圾清干净再动手。第三个坑是忽略STATS统计信息。SHRINK会大量移动行直接导致表和索引的统计信息失效。如果收缩之后没有马上重做统计信息执行计划很可能会乱跳跑出几条全表扫描的SQL业务会明显变慢。这是我吃过的大亏后来养成了“收缩后必须重统计”的铁律。第四个坑是大表不应该一次SHRINK到底。一个60GB的大表一次性收缩到20GB期间会生成大量UNDO和REDO对IO和归档日志的压力非常大。正确做法是先确认业务可接受的窗口必要时分批处理比如按分区收缩。第五个坑是忘了处理MAXEXTENTS参数。现在很多系统还是老配置文件MAXEXTENTS只有几十个SHRINK过程中段要临时扩展新的块来存放被挪动的行扩展不了就报错。执行前花一分钟查一下dba_segments的max_extents值偏小就直接用ALTER TABLE STORAGEMAXEXTENTS UNLIMITED预抬一下避免中途翻车。4.3 让空间保持健康的日常习惯把空间回收变成日常运维的一部分比每次等到磁盘告警再救火要靠谱得多。我自己的习惯是每月固定做一次表空间使用率巡检重点看段对象增长率和碎片比例。对这个月增长异常的表提前安排下个月的SHRINK窗口。监控上可以钉住几个关键指标表空间总大小、空闲率、TOP段对象大小和回收站占用任何一项超过阈值就自动发告警。对于业务上只增不减的历史表比如订单历史表、审计日志表我更推荐直接建立按月或按季度的分区配合数据归档策略。分区表在空间管理上有一个天然优势可以单独TRUNCATE或者DROP一个分区瞬间把整个分区的空间释放给操作系统不需要经历SHRINK那种逐行移动的过程。这和表级别的DELETE方式完全不是一个量级。另外提醒一句如果生产环境用的是Oracle EBS这类业务系统WIP工单、历史流水这类表往往数据量极大且更新频繁收缩频率不宜过高。只要空间能维持在两到三个月内有富余尽量不要频繁去动大表。频繁的物理行移动只会不断加剧索引碎片化最后反而让业务查询性能变差。这次案例之后我个人的体会是Oracle空间回收的难点从来不在命令本身而在于操作前有没有把对象的物理结构、锁竞争和存储参数都摸透。遇到无法回收的报错先停一下回到原理层面对照检查一遍原因再走SHRINK这条路你会比我当时少走很多弯路。最后分享一个存在脚本里的习惯动作所有空间回收操作前先SELECT COUNT(*) FROM V$LOCKED_OBJECT确认锁状态再SELECT SUM(BYTES) FROM DBA_RECYCLEBIN确认回收站占用这两步加起来不过半分钟却能挡掉绝大多数收缩失败。
返回列表