1. 一次备份成功却恢复失败让我决定在OBET里实现DBV功能事情发生在去年某个周五下午。业务侧反馈一张核心报表查询报错错误码是常见的ORA-01578: ORACLE data block corrupted (file # 8, block # 3521)当时我第一反应是赶紧用RMAN做块恢复。结果坏块确实恢复了但事后复盘时发现一个更让我后背发凉的问题我们日常做的备份居然一直没被发现里面早就含着坏块。那次事故的根因不是硬件也不是存储节点切换而是源端数据文件里已经存在物理坏块。由于坏块所在的区块并不是热区块业务查询平时根本没走到那里所以应用层和监控层都没报警。而备份呢备份脚本用的是 Oracle 自带的RMAN它默认会做物理一致性检查吗并不会。如果你没有显式配置CHECK LOGICAL或者让 RMAN 在备份期间做坏块检测纯物理备份完全可能把一个带病的数据块原样拷到备份集里。等真正要做恢复或容灾切换时才发现备份文件本身就是坏的这就陷入“备份成功恢复失败”的尴尬境地。从那天开始我就在思考一个问题我们内部维护的 OBET 工具原本是用来做数据文件块级抽取、解析和归档的日常也承担了一部分数据文件体检的工作但它一直没有实现像 OracleDBVERIFY也就是大家常说的DBV那样的坏块检测能力。既然 OBET 本来就是直接读写数据文件底层的工具那为什么不能让它顺带把“坏块识别”这件事做掉与其每次都要手工登到库里去跑dbv不如把这个能力直接内建到 OBET 里让日常巡检、备份前预检、恢复前验证都能自动化完成。这篇文章就是我对这次改造的完整复盘为什么做、怎么做、怎么用 Oracle DBV 做对照测试以及这过程中踩过的坑。如果有朋友也想给自己维护的数据库工具加上“坏块检测”能力或者正在纠结“到底该不该信任备份”这个问题可以参考一下我的思路。2. 动手前必须搞懂的 Oracle 数据块坏块原理OBET 之前一直没有实现坏块检测并不是因为“懒”而是因为这件事比表面看起来要复杂得多。直接把数据文件从头到尾读一遍、发现某个块读不出来就叫“坏块”那是最初级的做法误报率会高到没法用。真正可靠的坏块检测需要你理解 Oracle 数据块本身的组织方式。2.1 一个 Oracle 数据块的基本结构Oracle 数据文件的最小 I/O 单位就是块block默认块大小一般是8KB也可以配置成4KB、16KB、32KB取决于建库时的DB_BLOCK_SIZE。每个块内部从逻辑上可以拆成几个区域区域说明块头Block Header20字节的标准头包含块类型、块地址、SCN、校验和等关键元数据表目录/行目录记录表和行的入口位置索引类型块会有不同的组织方式空闲区块内尚未使用的空间数据区实际存储表数据、索引条目、Undo记录等内容的区域在 Oracle 内部标准块头也叫kcbh它是一段固定长度的结构普通数据块、索引块、Undo块、回滚段头块都会有这个头。OBET 做块级解析的时候本质上就是先读kcbh再根据其中的类型字段决定后续怎么解析块体。这里有个很关键的点Oracle 的块头里是带**校验和checksum**的。块在写入磁盘时Oracle 会根据块内容计算校验和并写入块头的固定位置等你再把这块读出来时就可以重新计算一次并与块头里存的值比对如果两者不一致说明这个块在磁盘上已经发生了物理变化可以认定为物理坏块。2.2 坏块的类型物理损坏、逻辑损坏和“软损坏”我经常看到文档把所有坏块混为一谈其实它们差别很大检测手段也不一样。物理损坏是最常见的一种指块本身的内容在磁盘上发生了变化比如字节被改写、块头信息错乱、校验和不匹配。常见诱因是存储设备故障、磁盘控制器异常、意外断电导致写入不完整。这种损坏是 OBET 和 DBV 这类工具最容易识别的因为只需要做结构校验和校验和比对就能发现。逻辑损坏就比较难搞了。块的物理结构可能是完好的校验和也能对上但块内部的数据在逻辑上是矛盾的。比如一个数据块里记录了100行但行目录只有98条或者索引块指向的数据块编号根本不存在或者事务槽状态不一致。这种损坏通常不是存储层造成的很可能是 Oracle 自身 Bug、异常恢复、或者NOLOGGING操作误用导致的。还有一个概念叫“软损坏”soft corruption指的是 Oracle 在访问某个块时发现它当前的状态不合理比如块类型与预期不符、块地址不匹配。DBV里报的Bad header found during verification很多时候就属于这一类。2.3 一个靠谱的坏块检测至少要做四件事既然要检测坏块就不能只靠“读取成功失败”来判断。我设计 OBET 坏块检测功能时把判定规则拆成了四层文件级识别确认数据文件的块大小、文件号、表空间信息这是后续解析的前提。块头合法性检查每个块的kcbh前20字节是否有完整结构块类型是否合法块号是否和物理位置匹配。校验和比对重新计算校验和和块头里存的校验和做对比判断物理层是否被篡改。逻辑一致性检查对块类型做深度解析比如表数据块要检查行目录和空闲区是否合理索引块要检查条目指向是否合法。只有同时走完这四步坏块检测才是有实际价值的。只做第2步和第3步可以应付大部分物理坏块但想要在备份前提前发现潜在风险必须把逻辑检查也纳入进来这正是 DBV 里DBVERIFY命令会同时输出物理和逻辑校验结果的原因。3. OBET 实现 DBV 功能的核心逻辑与代码思路这次改造我的目标很明确在 OBET 已有的块级读取能力之上嵌入一个corruption_scan模块让它可以独立扫描数据文件并输出坏块清单。下面把主要的实现环节展开来说。3.1 第一步读取文件头并自适应块大小OBET 在扫描之前要先知道文件是哪个表空间的、块大小是多少。最靠谱的方式是先读文件的首块也就是第0号块它在 Oracle 里通常是一个文件头块记录了文件大小、块大小、表空间号和文件号。用 Python 做演示的话核心逻辑大概是这样import struct import os def parse_file_header(file_path): with open(file_path, rb, buffering0) as f: first_block f.read(8192) # 第一个字节是块类型文件头块通常对应类型3 block_type first_block[0] # 根据 kcbh 的结构校验和位置在偏移量16占4字节 checksum struct.unpack(I, first_block[16:20])[0] # 文件头里还可以解析出表空间号、相对文件号 tsn struct.unpack(I, first_block[176:180])[0] rfn struct.unpack(I, first_block[188:192])[0] return { block_type: block_type, checksum: checksum, tablespace_id: tsn, relative_file_no: rfn }当然真实场景里不能默认文件里第一块读出来的就是8KB因为 Oracle 可能存在非默认块大小。更稳妥的做法是先读取操作系统文件的实际大小再根据db_block_size配置尝试用不同块大小去解析第一位如果第一位出来的块类型落在合法范围内就认为当前块大小是合理的。OBET 本身支持通过命令行传入blocksize参数这一点和 Oracle DBV 的用法保持一致。3.2 第二步块头合法性检查和校验和比对拿到块大小后就可以按块遍历整个文件了。每读到一个块先做块头合法性检查再算校验和。Oracle 的块头kcbh里几个关键字段我会特别关注偏移量字段含义0kcbh.type块类型比如数据块、索引块、Undo块4kcbh.bh块地址即这个块在数据文件里的相对块号16kcbh.ckc校验和假设我们读取了块大小为BLOCK_SIZE那么校验和比对的核心思路是读整个块将存储校验和的那4个字节先视为0然后对块内容做累加最后把计算结果和块头里存的校验和比较。伪代码如下BLOCK_SIZE 8192 CKSUM_OFFSET 16 def compute_checksum(block): total 0 for i in range(BLOCK_SIZE): if CKSUM_OFFSET i CKSUM_OFFSET 4: continue total block[i] return total 0xFFFFFFFF def check_block(block): stored_checksum struct.unpack(I, block[CKSUM_OFFSET:CKSUM_OFFSET4])[0] actual_checksum compute_checksum(block) return stored_checksum actual_checksum需要注意这只是演示用的简化算法Oracle 实际的校验和计算是按16位字为单位累加的而且对于普通校验和、增量校验和会有不同的分支处理。OBET 在实现时我是从 Oracle 导出块数据后用已知样本做校准逐个字节验证自己算法的准确性。如果你打算照着实现一定要拿真实的正常数据文件和真实坏块样本做校准不要只拿一个合成文件拍脑袋。3.3 第三步逻辑一致性检查校验和通过不代表这个块没问题。OBET 的corruption_scan模块里我把逻辑检查分成几个维度块类型合法性Oracle 常见块类型是有枚举范围的非法值说明块头已经损坏。比如9通常是表/簇数据块10是索引块12是 LOB 数据块如果读出来一个99这种根本不存在的类型直接报错。块号连续性每个块的kcbh.bh字段应该等于该块在文件中的物理块号。比如你在文件第100个块位置读到的块它的块号也应该是100如果不是说明这个块的地址已经错乱。行目录逻辑对表数据块块内部有一个行目录行目录里的槽位数量应该与块内实际行数一致。如果行目录指向的偏移量超出了块的范围说明逻辑损坏。SCN/事务槽检查对 Undo 块和事务相关的块要检查事务槽是否处于合理状态活动事务数是否超额。这一层做下去之后OBET 的输出就会比单纯“校验和不匹配”要丰富得多。比如它会告诉你“块类型非法”还是“行目录引用越界”这让 DBA 在收到告警时能更快判断问题属于硬件层还是数据库层。3.4 第四步并行扫描与结果输出单线程遍历一个100GB的数据文件太慢了。OBET 在实现坏块扫描时我采用了分块并行 分段游标的方式把文件按“段”切成多份每个线程独立扫描一段最后汇总结果。obet corruption_scan --file/u01/oradata/users01.dbf --block8192 --threads8 --formatjson输出结果我设计成两种格式终端友好型和 JSON 型。终端型输出可以直接看到哪些文件、哪些块出问题JSON 型则方便接入到运维平台的告警系统。每发现一个坏块至少输出以下信息字段含义file_no数据文件编号block_no坏块所在块号block_type块类型error_type物理坏块/逻辑坏块/校验和不匹配detail具体描述4. 用 Oracle DBV 做基准测试我的方法和结果任何工具说“能检测坏块”都不能只靠嘴说。我这次最看重的一步就是拿 Oracle 自带的DBV做对照实验验证 OBET 的检测能力到底跟不跟得上原版。4.1 构造测试样本我当时是在一套测试环境里操作的步骤大概是找一个业务不繁忙的表空间数据文件先做一次全量备份。用dd在备份副本上随机改写某个块模拟物理坏块。再用一个没有改写的正常文件作为对照。分别用 Oracle DBV 和 OBET 扫描所有文件对比结果。造物理坏块的命令类似这样dd if/dev/urandom of/backup/users01.dbf.bak bs8192 count1 seek1024 convnotrunc这里故意用了notrunc确保只是覆盖第1024块所在位置的8KB内容而不是把整个文件截断。如果想模拟逻辑坏块就不能用随机数据直接覆盖一般是通过修改块内行目录偏移量来实现这需要更细粒度的块解析能力OBET 内部正好可以用已有的解析函数来完成。4.2 两者的输出对拍用 DBV 扫坏文件时输出大概是这样的DBVERIFY - 开始验证: file /backup/users01.dbf.bak ... 页 1024 出现坏块 坏块相对 dba: 0x02000420 (file 8, block 1024) 在验证期间发现坏头OBET 的输出我设计成类似这样[FILTERED] file_no8, block_no1024, block_type9 error_typePHYSICAL, detailblock header checksum mismatch对比结果两者的检测块号完全一致。这个结果让我放心了不少因为说明 OBET 的校验和算法至少在物理坏块检测上和 Oracle 原生工具处于同一水平。当然我并不是说 OBET 已经全面替代DBV至少在逻辑坏块的细节报告上Oracle DBV 会输出更多内部状态信息OBET 目前还做不到完全对齐。4.3 边界情况NOLOGGING、临时表空间、块0测试过程中有三类边界情况特别值得注意。第一类是NOLOGGING操作产生的块。这类块因为写操作没有生成日志块头里的校验和可能是0或者处于“不保证校验”的状态。如果工具一味按校验和比对很容易误报。OBET 在处理时增加了一个规则若块头中的校验和字段为0且块类型合法就跳过校验和比对只做结构检查。第二类是临时表空间的数据文件。临时文件里的块内容经常是临时排序或者临时表的数据有些块处于未初始化状态。OBET 默认可以直接跳过临时表空间文件因为这类文件的坏块并不会直接影响业务数据告警价值不大。如果你的场景需要扫描可以通过--include-temp参数强制开启。第三类是每个数据文件的第0号块。这个块是文件头块号和普通数据块不同类型也可能跨越多个段。刚开始实现时OBET 把这个块当普通数据块校验导致每个文件都误报一次。后来我特意加了一个file_header分支单独处理第0块才算把误报降下来。5. 实战部署后的坑、调优和后续打算功能开发完只是第一步真正把它用起来还会碰到各种现实问题。5.1 误报最大的来源半写块和回收站上线试运行第一周OBET 就连着报警了几次“校验和不匹配”。我第一反应是存储有问题赶紧让存储团队检查磁盘。结果查了一圈发现报警块分布在某个活动频繁的撤销表空间里这些块其实是正在被写入的撤销块。扫描进程把块读到内存的那一刻块可能正好处于写入一半的状态校验和自然对不上。这个问题在 Oracle 自己的DBV上通常没那么突出是因为它作为 Oracle 官方工具一定程度上会结合数据库内部状态做判定。而 OBET 是文件级别的工具它看不到数据库的锁和事务状态。解决方式有两种一是避开业务高峰期扫描二是在 OBET 的告警输出里增加一个block_typeUNDO的标记提示这类误报需要二次确认。最稳妥的还是用备份文件或者数据文件副本去扫描不要直接扫生产库在线文件除非你明确知道当前写入不活跃。5.2 资源占用太大怎么办全文件扫描是典型的 IO 密集操作。我在一个 2TB 的数据文件上做测试时8线程全速跑能把磁盘阵列的读 IO 直接拉满生产业务延迟明显上升。后来做了几个调整加了 IO 限速参数通过--iops-limit控制每秒读取次数比如限制在5000次/秒以内。增加了时间窗口参数设置成只能在工作日晚上10点后执行。支持只扫描指定块范围比如--start-block0 --end-block100000方便快速定位上次报损的区域。优先扫描备份文件而不是在线文件。备份文件扫描不会影响生产 IO坏了还能及时反馈到备份有效性上。另外一个很多人容易忽略的点扫描大文件时要关闭操作系统的缓存预读机制。OBET 在读文件时使用了posix_fadvise设置POSIX_FADV_DONTNEED避免把大量数据文件内容挤占操作系统 Page Cache从而影响其他关键进程。5.3 把 OBET 的坏块检测融入日常巡检现在 OBET 的坏块检测已经跑在我们的日常备份巡检流程里了。每周备份完成后自动任务会对备份数据文件做一轮坏块扫描一旦发现异常直接把 JSON 报错推送到值班群。这个动作看着简单但解决了一个很实际的痛点以前我们只能靠恢复演练去验证备份频率低、成本高现在每次备份之后都能快速知道“这份备份到底能不能用”。如果你们也想把这类能力接入自己的运维体系我建议输出格式第一时间定成 JSON字段尽可能全后面接告警和报表都会方便很多。不要只输出一个“OK”或“FAIL”一定要带文件的编号、块号、错误类型否则告警到了人手里还是要二次登录服务器去确认效率很低。最后说一点个人体会。坏块检测这件事真正难的不是写一个扫描循环而是搞清楚“什么样的状态才算坏”。很多工具号称能做检测但用的是很粗暴的读异常判定结果要么漏报要么误报到没人愿意看。我这次最大收获是重新理解了 Oracle 数据块内部的组织方式也明白了为什么 Oracle 官方工具要考虑那么多边界条件。如果你也要做类似 OBET 的底层层工具改造建议一开始就把物理校验和逻辑校验分开设计把边界情况想清楚这样后面接入真实生产环境才不会被层出不穷的误报淹没。
阅读完成 · 觉得有帮助?