由一次ora-00600 [4000]想到的:
项目测试库出现过一次由于异常断电引起的ora-00600 [4000]造成数据库无法启动的故障,诊断为系统回滚段数据字典(字典而不是回滚段本身)损坏,且该库处于非归档模式,无任何备份。后使用bbed修改 system表空间数据文件,将相关数据块的事物状态手工修改为一致性状态,数据库得以打开。
由 此引发另一种思路:如果数据库无法使用rman、冷备份、dg、ogg、dsg等方式正常恢复打开,那么还能否找回其中数据?考虑到业务数据还存在于存储 设备上(只是完整性有一些缺失),能否在不启动数据库的情况下,由应用从存储设备上直接读出数据库中的业务数据?为解决以上问题,使用dba一些日常工具 结合oracle数据库内部结构的知识,来分析oracle数据库定位数据的原理。这样做的目的是最终能够通过编写代码(java或者c++)实现半自动 或全自动的数据库无法mount状态下的恢复。
定位数据的方式:
Oracle数据库服务进程在定位某行数据时,采用了数据字典+段头extent map的“集中—分散”存储模式,具体为:
1、在数据字典中存放数据段(对应表或分区)头的物理位置
2、在数据段头存放段中所有extent(即连续的block集合)的物理位置
首先从数据字典入手,oracle数据库在创建时会生成一系列的基础数据字典表,这些基础字典记录了一个数据库的物理架构,oracle正是通过这些基 础字典,将逻辑层的表与物理层的存储进行转换和映射。其实建库时运行的catalog.sql等脚本只是在这些基础数据字典上建立了相关的视图、同义词等 (还有一些内存表的映射视图),因此catalog.sql脚本所建立的“字典”存在与否都不会影响数据库的正常运行,但基础数据字典一旦出现异常,数据 库就无法启动。
oracle记录段头物理位置的字典视图是sys.dba_segments,其中记录了段头所在的文件号及在该文件中的块号,其中文件号对应文件物理位置在控制文件中查询。对于sys.dba_segments视图,最值得关心或者说与本文关系最为密切的是:
select a.owner,a.segment_name,a.header_file,a.header_block fromdba_segments a;这四个字段,通过这四个字段就可以定位某用户的某个段的段头在哪个文件的哪个数据块内。通过plsql developer等工具查看sys.dba_segments视图的定义(sqlplus也可,但作为一个成熟的dba,熟练掌握各种工具的特点,完成 不同任务,最大幅度减少达成目标所需的工作量,是一项非常重要的素质),可见其数据来源于sys.sys_dba_segs视图:
create or replace view sys.dba_segments as
select owner, segment_name, partition_name,segment_type, tablespace_name,
header_file, header_block,
decode(bitand(segment_flags, 131072), 131072, blocks,
(decode(bitand(segment_flags,1),1,
dbms_space_admin.segment_number_blocks(tablespace_id, relative_fno,
header_block, segment_type_id, buffer_pool_id, segment_flags,
segment_objd, blocks), blocks)))*blocksize,
segment_objd, blocks), blocks))),
decode(bitand(segment_flags, 131072), 131072, extents,
dbms_space_admin.segment_number_extents(tablespace_id, relative_fno,
header_block, segment_type_id, buffer_pool_id, segment_flags,
segment_objd, extents) , extents))),
initial_extent, next_extent, min_extents, max_extents, pct_increase,
freelists, freelist_groups, relative_fno,
decode(buffer_pool_id, 0, 'DEFAULT', 1, 'KEEP', 2, 'RECYCLE', NULL)
fromsys_dba_segs;
继续观察sys.sys_dba_segs视图的定义,
create or replace view sys.sys_dba_segs
(owner, segment_name, partition_name,segment_type, segment_type_id, tablespace_id, tablespace_name, blocksize,header_file, header_block, bytes, blocks, extents, initial_extent, next_extent,min_extents, max_extents, pct_increase, freelists, freelist_groups, relative_fno,buffer_pool_id, segment_flags, segment_objd)
as
select NVL(u.name, 'SYS'), o.name, o.subname,
so.object_type, s.type#,
ts.ts#, ts.name, ts.blocksize,
f.file#, s.block#,
s.blocks * ts.blocksize, s.blocks, s.extents,
s.iniexts * ts.blocksize,
decode(bitand(ts.flags, 3), 1, to_number(NULL),
s.extsize* ts.blocksize),
s.minexts, s.maxexts,
s.extpct),
decode(bitand(ts.flags, 32), 32, to_number(NULL),
decode(s.lists, 0, 1, s.lists)),
decode(s.groups, 0, 1, s.groups)),
s.file#, s.cachehint, NVL(s.spare1,0), o.dataobj#
from sys.user$ u, sys.obj$ o, sys.ts$ ts,sys.sys_objects so, sys.seg$ s,
sys.file$ f
where s.file# = so.header_file
ands.block# = so.header_block
ands.ts# = so.ts_number
ands.ts# = ts.ts#
ando.obj# = so.object_id
ando.owner# = u.user# (+)
ands.type# = so.segment_type_id
ando.type# = so.object_type_id
ands.ts# = f.ts#
ands.file# = f.relfile#
union all
select NVL(u.name, 'SYS'), un.name, NULL,
decode(s.type#, 1, 'ROLLBACK', 10, 'TYPE2 UNDO'), s.type#,
ts.ts#, ts.name, ts.blocksize, f.file#, s.block#,
s.iniexts * ts.blocksize, s.extsize * ts.blocksize, s.minexts,
s.maxexts, s.extpct,
decode(s.lists, 0, 1, s.lists)),
decode(s.groups, 0, 1, s.groups)),
s.file#, s.cachehint, NVL(s.spare1,0), un.us#
from sys.user$ u, sys.ts$ ts, sys.undo$ un,sys.seg$ s, sys.file$ f
where s.file# = un.file#
ands.block# = un.block#
ands.ts# = un.ts#
ands.user# = u.user# (+)
ands.type# in (1, 10)
andun.status$ != 1
andun.ts# = f.ts#
andun.file# = f.relfile#
select NVL(u.name, 'SYS'), to_char(f.file#) ||'.' || to_char(s.block#), NULL,
decode(s.type#, 2, 'DEFERRED ROLLBACK', 3, 'TEMPORARY',
4, 'CACHE', 9, 'SPACEHEADER', 'UNDEFINED'), s.type#,
s.iniexts * ts.blocksize,
s.extpct),
s.file#, s.cachehint, NVL(s.spare1,0), s.hwmincr
from sys.user$ u, sys.ts$ ts, sys.seg$ s,sys.file$ f
where s.ts# = ts.ts#
ands.type# not in (1, 5, 6, 8, 10)
and s.file# = f.relfile#;
可见其是由三个sql语句块联合而成,而其中2、3语句块明显是回滚段等segment,因此着重观察第一语句块,
and s.file# = f.relfile#
可 见其数据来源为sys.user$ u, sys.obj$ o, sys.ts$ ts, sys.sys_objects so, sys.seg$s,sys.file$ f这几个基础系统字典表(这些已经都是物理存在的表而不是视图了),而其中最重要的owner、segment_name、header_file、 header_block字段,则分别来自sys.user$、sys.obj$、sys.seg$、sys.file$等字典表,也就是说要在不启动数 据库的情况下定位到用户数据,必须先定位以上几个基本数据字典。那么这几个表的物理存储位置又在哪里呢?或者说在数据库不启动的情况下,如何获得这四个表 的物理存储位置?要回答这个问题,先要了解一下oracle启动的过程,oracle在启动时会加载这些基础数据字典表,而这些字典表的create语句 都写在sys.bootstrap$表里,
SQL> select *from sys.bootstrap$ a where a.sql_text like '%SEG$%';
LINE# OBJ# SQL_TEXT
---------- ------------------------------------------------------------------------------------------
14 14 CREATE TABLE SEG$("FILE#"NUMBER NOT NULL,"BLOCK#" NUMBER NOT NULL,"TYPE#" NUMBE
而sys.bootstrap$表的物理位置其实是间接“写死”在oracle代码中的。
在每一个oracle数据库(以10g为例,其他版本相差不大)的system表空间的第一个文件的header block中都按固定结构存放有kcvfh这个结构体,其定义可以通过bbed工具查看。
这 里简单介绍一下bbed工具。Bbed是oracle公司提供的一个文本编辑工具(有些dba把bbed想象的非常神秘高深,其实它就是一个普通的文本编 辑器,用其他文本编辑工具(比如UltraEdit)的宏功能其实可以实现bbed的绝大部分功能),通过它可以方便的查看oracle数据在物理层面的 存储结构。
为安全考虑,先把数据库system表空间的第一个文件拷贝到另外的目录以便观察。在sqlplus中执行select file#||' '||name||' '||bytes from v$datafile ;将结果中的路径和文件名修改为先前数据文件的拷贝,用来生成bbed列表文件,这样用bbed打开的就是这份复制文件,而不是真正的库中的文件。 Bbed安装编译过程不再赘述。
进入bbed后默认偏移量是1,即file header block,
BBED> map
File:/home/oracle/vie.pdf (1)
Block: 1 Dba:0x00400001
------------------------------------------------------------
DataFile Header
struct kcvfh, 676 bytes @0
ub4tailchk @8188
Map命令会自动检测当前数据块的type,并显示该类型数据块内所包含的数据结构信息(这也就是为什么设置不同偏移量,map输出的数据结构信息也会不 同),这里看到,system表空间的file header block中包含一个名为kcvfh的结构体,该结构体占用676 bytes,深入观察:
BBED> p kcvfh
struct kcvfh, 676 bytes @0
struct kcvfhbfh, 20 bytes @0
ub1type_kcbh @0 0x0b
ub1 frmt_kcbh @1 0xa2
ub1 spare1_kcbh @2 0x00
ub1 spare2_kcbh @3 0x00
ub4 rdba_kcbh @4 0x00400001
ub4 bas_kcbh @8 0x00000000
ub2 wrp_kcbh @12 0x0000
ub1 seq_kcbh @14 0x01
ub1 flg_kcbh @15 0x04 (KCBHFCKV)
ub2 chkval_kcbh @16 0x2671
ub2 spare3_kcbh @18 0x0000
struct kcvfhhdr, 76 bytes @20
ub4 kccfhswv @20 0x00000000
ub4 kccfhcvn @24 0x0a200100
ub4 kccfhdbi @28 0xea382478
text kccfhdbn[0] @32 P
text kccfhdbn[1] @33 H
text kccfhdbn[2] @34 O
text kccfhdbn[3] @35 N
text kccfhdbn[4] @36 E
text kccfhdbn[5] @37 D
text kccfhdbn[6] @38 B
text kccfhdbn[7] @39
ub4 kccfhcsq @40 0x00001ae3
ub4 kccfhfsz @44 0x00017c00
s_blkz kccfhbsz @48 0x00
ub2 kccfhfno @52 0x0001
ub2 kccfhtyp @54 0x0003
ub4 kccfhacid @56 0x00000000
ub4 kccfhcks @60 0x00000000
text kccfhtag[0] @64
text kccfhtag[1] @65
text kccfhtag[2] @66
text kccfhtag[3] @67
text kccfhtag[4] @68
text kccfhtag[5] @69
text kccfhtag[6] @70
text kccfhtag[7] @71
text kccfhtag[8] @72
text kccfhtag[9] @73
text kccfhtag[10] @74
text kccfhtag[11] @75
text kccfhtag[12] @76
text kccfhtag[13] @77
text kccfhtag[14] @78
text kccfhtag[15] @79
text kccfhtag[16] @80
text kccfhtag[17] @81
text kccfhtag[18] @82
text kccfhtag[19] @83
text kccfhtag[20] @84
text kccfhtag[21] @85
text kccfhtag[22] @86
text kccfhtag[23] @87
text kccfhtag[24] @88
text kccfhtag[25] @89
text kccfhtag[26] @90
text kccfhtag[27] @91
text kccfhtag[28] @92
text kccfhtag[29] @93
text kccfhtag[30] @94
text kccfhtag[31] @95
ub4kcvfhrdb @96 0x00400179
struct kcvfhcrs, 8 bytes @100
ub4 kscnbas @100 0x00000008
。。。。。。。。。。。。。。。。。
省略后面内容
这里的 ub4 kcvfhrdb @96 0x00400179就是oracle启动过程中非常关键的变量:root dba,root dba中记录了sys.bootstrap$的物理位置,因此可以把root dba理解为指向sys.bootstrap$的指针。这里该变量的值为0x00400179,是按照dba(datablock address)的格式存放,将其转换为二进制后的前10位为文件号,后22位为块号 0x00400179转换为二进制0000 0000 0100 0000 0000 0001 0111 1001,文件号部分:0000 0000 01,块号部分:00 0000 0000 0001 01111001,表示1号文件的377号块,这个数字不是我的库的巧合,而是10g版本数据库的固定位置,在oracle 10g数据库启动时会默认在该偏移量的位置寻找指向sys.bootstrap$表的指针。在11g中sys.bootstrap$的位置改到了 0x00400208。而10g、11g该指针的位置(即rootdba变量自己的位置)是相同的,都是file header block的96偏移量位置。需要注意的是这里的偏移量96,并不是从文件头部开始计算:oracle在建立datafile时,实际生成的文件会比管理 员输入的文件尺寸大一个db_block_size的大小,比如某数据库的db_block_size=8k,建立表空间时指定某文件大小为100M,那 么该文件的实际大小是100M+8K,这8K放在文件的最前面,称作os header block,root dba(也就是指向sys.bootstrap$的指针)的物理偏移量是db_block_size+96。这也就是为什么在一些使用裸设备的库上,在把 裸设备交给oracle的时候,不能把文件尺寸写的和设备的尺寸完全一样,而要稍小一点,否则创建时会出现ORA-27042错误。
这里有一个小技巧,在使用bbed分析数据文件结构时,要观察在不同主机上的不同版本的库,有些库的数据文件很大,复制、查看不便,可以针对需要,使用 dd命令截取重点关注的部分,比如我在观察11g的file header block时使用 ddif=/u01/app/oracle/oradata/mydb/system01.dbf of=/home/oracle/lt140_header2.tracecount=1000 skip=0 bs=8192截取该文件的前8M数据,生成一个文件,然后传输到主要用来做分析工作的主机上,修改bbed配置文件指向该文件就能用bbed对其进行分 析,这么做还有一个原因就是11g是不带bbed工具的,需要从10g复制源文件再进行编译链接。结果导向、尽量简化中间操作过程是dba工作的重要指导 原则。
通过root dba的了解,已经可以在不打开数据库的情况下定位到sys.bootstrap$表的物理位置,下一个问题是,sys.bootstrap$表里存储的 只是基础字典表的创建语句,那么这些表的物理存储位置是如何确定的?进一步观察sys.bootstrap$表的sql_text字段,以file$为 例:SQL> set line 2000
SQL> set headoff
SQL> select *from sys.bootstrap$ a where a.obj# = 17;
17 17
CREATE TABLEFILE$("FILE#" NUMBER NOT NULL,"STATUS$" NUMBER NOT NULL,"BLOCKS"NUMBER NOT NULL,"TS#" NUMBER,"RELFILE#"NUMBER,"MAXEXTEND" NUMBER,"INC" NUMBER,"CRSCNWRP"NUMBER,"CRSCNBAS" NUMBER,"OWNERINSTANCE"VARCHAR2(30),"SPARE1" NUMBER,"SPARE2"NUMBER,"SPARE3" VARCHAR2(1000),"SPARE4" DATE) PCTFREE 10PCTUSED 40 INITRANS 1 MAXTRANS 255 STORAGE ( INITIAL 64K NEXT 1024K MINEXTENTS 1 MAXEXTENTS 2147483645 PCTINCREASE 0OBJNO 17 EXTENTS (FILE 1 BLOCK 113))
这个create语句的storage 子句中有两个不常见的参数OBJNO 17 和EXTENTS (FILE 1 BLOCK 113),前者是指定该segment的对象号,而extents是直接指定segment的segmentheader block存储位置,我多次尝试在客户发起的sql语句中使用这两个参数未能成功,判断为只有在数据库处于特定状态或以特定身份运行时才能使用这些参数。 就是说这两个参数直接指定了段在存储上得物理位置。从该参数已经可以直接定位其所对应的数据字典段头的物理位置,而oracle的段头中的extents map结构体就包涵了该段全部的数据块的物理地址。
在sys.bootstrap$表中记录的数据字典表的create语句,还有一部分是没有直接指定存储位置的,这些表都是某个簇的成员,其实际数据存储在簇中,比如SYS.TAB$:
CREATE TABLE TAB$("OBJ#" NUMBERNOT NULL,"DATAOBJ#" NUMBER,"TS#" NUMBER NOTNULL,"FILE#"
。。。。。。。。。。。。。。。。。。。。省略。。。。。。。。。。。。。。。。。。。。。。。。。。。。
NUMBER,"SPARE2"NUMBER,"SPARE3" NUMBER,"SPARE4"VARCHAR2(1000),"SPARE5" VARCHAR2(1000),"SPARE6" DATE)STORAGE ( OBJNO 4 TABNO 1) CLUSTERC_OBJ#(OBJ#)
红色字体白色背景的就是指定SYS.TAB$表加入到C_OBJ#簇中。
至此,在关闭数据库的情况下定位数据块的大致思路已经清晰,总结如下:
1、在system 表空间的第一个数据文件的特定偏移位置,找到root dba变量
2、root dba变量的值就是指向sys.bootstrap$表的物理位置的指针
3、sys.bootstrap$表中记录了数据库基础字典表的物理位置
4、基础字典表内记录了用户段段头的物理存储位置
下一步就是要通过某个段的segmentheader block从数据文件中直接读出表的数据。
本文转自 hsbxxl 51CTO博客,原文链接:http://blog.51cto.com/hsbxxl/768877,如需转载请自行联系原作者