不管现阶段美国和中国对峙到何种程度,不管桀傲不驯拉里.埃里森如何不看好中国,Oracle仍是数据库中的一枝独秀。然而,他山之石可以攻玉,多个国产数据库在关键技术攻关方面的整体水平也已达到国际先进。
国内越来越多的Oracle数据库开始下线,迁移到开源或者国产数据库上,o2k支持实时增量的将Oracle数据库增量变化抽取出来,助力国产化数据库无缝接管Oracle。
笔者作为数据库内核的负责人参与实现了o2k来解析Oracle日志,并于2022年2月15号把它开放出来给社区免费使用,期间经历了各种迷茫,获得了众多大佬的指导,最终总算是交出了一份还算不错的试卷。
作为一个DBA兼开发人员,在整个o2k的实现过程中,对于Oracle数据库的数据库理论和工程能力学习了很多,也进一步对数据库原理有了进一步了解。
借助Oracle的设计理念和实现,我们也看看是否能对新一代的数据库从业者有一定的帮助和借鉴作用。
国内软件由于美国的打压,数据库作为基础软件赢来了一个难得的蓬勃发展期。
曾几何时,数据库从业人员只是公司IT团队->运维团队里的一个小小部门,而今一个能指导开发正确使用数据库,选择数据库来适配业务来适应业务的数据架构师供不应求。
如果能掌握着数据库原理,甚至能主动改造数据库适配相关应用场景的人才年薪至少百万。
作为计算机皇冠上的其中一粒明珠,数据库上承业务在线离线之责,下接硬件内核之妙,看到越来越多的人才加入数据库及数据库内核队伍,不胜欣慰!
本系列文章中我们将由浅入深以Oracle日志解析遇到的重重阻塞为例,来介绍在数据库中常见,而又关键的概念,了解数据库设计思路及工程实现中需要注意的事项。
本文以浅为主,我们先简单介绍一下数据库的背景和Oracle日志解析的基础知识
类似于银行账户系统一样,张三存入100块钱会'先'被记录为'张三增加100'的流水账,然后再把张三的账户从1000块修改为1100块。
数据库为了保证原子性和持久化,也会'先'在redo日志中记录一笔或者多笔redo record,然后再修改数据库实际的行记录
注意,这里的“修改账户/修改行记录”都是在“写流水账/写日志”之后完成的。也就是说redo 先于“数据写入”,这也就是著名的数据库write-ahead logging (WAL) 。
Oracle写数据库日志采用的是物理日志方式,记录的是内部数据块的变化;MySQL在Server层的binlog是逻辑日志,记录的是逻辑行数据的变化。所以对Oracle的日志解析不仅需要理解日志本身filespace、redo record,change vector的变化,还需要理解Oracle内部数据存储的格式。
Oracle中有专门的命令,支持将指定的二进制redo日志解析为逻辑的文本文件,类似于MySQL提供的mysqlbinlog工具,方便用户查看和诊断数据库问题。
ALTER SYSTEM DUMP LOGFILE '/opt/oracle/oradata/master1/redo01.log';
当然,这个解析出来的文本文件也并不是那么容易理解,下文中我们会对关键信息进行解读。
另外,这个逻辑的文本文件也并不是把所有的二进制redo日志中的信息都解析并输出出来,例如supplement log这种,就没有输出
Oracle默认只记录变化的信息,类似于MySQL中binlog_row_image=minimal的情况。
也就是说,你执行了update t1 set c2=3 where id=2,它只会记录c2=3的后镜像数据和c2=2修改之前的前镜像数据。
对于主备物理块同步,这些信息已经足够了,但是对于数据仓库或者大数据平台,不知道到底更新的是哪一行(id=2)的数据,是无法保持跟Oracle的数据一致的。
通过alter database add supplemental log data (primary key) columns;可以让Oracle在记录日志的时候顺便把primary key主键也记录下来,方便同步工具将变化对应到具体哪一行,这些额外增加的日志称为supplemental log。
OGG,O2K等同步工具都会要求源端Oracle开启supplemental log。
WAL日志是逻辑的日志表现形式,一个update的事务更新了5行数据,会产生多个redo record(begin,5行记录的前镜像和后镜像,commit),而这些日志记录到日志文件中是以一个block一个block(默认为512字节)写到文件中的
逻辑上,数据库日志是由一个一个的redo record组成的;
物理上,数据库日志文件中每个block都有Header和Tail,逻辑的redo record记录到物理文件中对应关系如下:
说了这么多理论的、务虚的东西,我们来点干货。
首先,我们看看我们做一个update一行数据到底会生成哪些日志信息,为了排除其他的干扰,我们在做update之前和之后都switch logfile,保证我们查询的redo log中只有update一个变更
在Oracle上执行的语句如下
## 这里新建一个表用于测试
create table test1 (id number primary key, name varchar2(15) not null, hiredate date default(sysdate) );
insert into test1 (id, name) values (1, 'o2k1');
insert into test1 (id, name, hiredate) values (2, 'o2k2', sysdate);
commit
## 为了排除其他的干扰,我们在做update之前和之后都switch logfile,保证我们查询的redo log中只有update一个变更
ALTER SYSTEM SWITCH LOGFILE;
update test1 set name='o2k3' where id=2;
commit;
ALTER SYSTEM SWITCH LOGFILE;
## 查询到底应该从那个归档日志中获取update的变更
select * from v$archived_log;
## 将二进制日志反解析出来
ALTER SYSTEM DUMP LOGFILE '+SSDDG1/chenmm/archivelog/2022_05_12/thread_2_seq_114.1017.1104513037';
## 查看日志被导入到哪个trace文件了
select value from v$diag_info where name='Default Trace File'; # 返回ora_6130.trc
这里我们得到两个文件:
二进制的redo日志文件"thread_2_seq_114.1017.1104513037"
根据这个二进制日志解析出来的文本文件ora_6130.trc
相关的内容我都放到了文末的附录中了,大家可以自行查看。
我们先从文本文件ora_6130.trc入手查看一下redo文件的逻辑形式。参考redo逻辑格式(见附录),可以看到
FILE HEADER:日志文件有日志文件的头,记录了Db Id,Db Name的数据库信息,也记录了文件大小、文件类型以及Blksiz=512(也就是说,redo物理块大小为512字节),对比redo物理格式(见附录)FILE HEADER是放到第一个512字节的BLOCK中。这里的文件号对应的就是日志序号Seq#
FILE HEADER: File Number=3, Blksiz=512, File Type=2 LOG
REDO HEADER:另外这里还记录了这是RAC的哪个节点,是那个序号的redo日志,scn的范围,"Thread 0002, Seq# 0000000114, SCN 0x0000004f1aa1-0x0000004f1aa8"。对比redo物理格式REDO HEADER是放到第二个512字节的BLOCK中
descrip:"Thread 0002, Seq# 0000000114, SCN 0x0000004f1aa1-0x0000004f1aa8" thread: 2 nab: 0x4 seq: 0x00000072 hws: 0x2 eot: 0 dis: 0
REDO Record:redo record是redo逻辑日志的最重要的组成部分,数据库的更新都会记录到一个一个的Redo Record中,我们执行的update和commit就被记录成了两个Redo Record。一个长度为0x0244,一个长度0x00a4
REDO RECORD - Thread:2 RBA: 0x000072.00000002.0010 LEN: 0x0244 VLD: 0x05 ... REDO RECORD - Thread:2 RBA: 0x000072.00000003.0064 LEN: 0x00a4 VLD: 0x01
进一步分解redo record,可以看到redo record又是由一个redo header和多个change vector('CHANGE #?')组成
Redo Header:记录了Redo的长度LEN: 0x0244,redo record的地址RBA: 0x000072.00000002.0010,SCN:0x0000.004f1aa1等信息
REDO RECORD - Thread:2 RBA: 0x000072.00000002.0010 LEN: 0x0244 VLD: 0x05 SCN: 0x0000.004f1aa1 SUBSCN: 1 05/12/2022 17:10:35 (LWN RBA: 0x000072.00000002.0010 LEN: 0002 NST: 0001 SCN: 0x0000.004f1aa1)
Change Vector:它是数据变化的原子结构,观察第一个Redo Record,我们可以看到CHANGE #1记录了事务开始;CHANGE #2记录了update的undo前镜像;CHANGE #3记录了update的redo后镜像;CHANGE #4记录了session信息;而第二个Redo Record的CHANGE #1记录了事务提交的信息
CHANGE #1 TYP:0 CLS:17 AFN:3 DBA:0x00c00080 OBJ:4294967295 SCN:0x0000.004f1121 SEQ:1 OP:5.2 ENC:0 RBL:0 ktudh redo: slt: 0x0013 sqn: 0x00000648 flg: 0x0012 siz: 200 fbi: 0 uba: 0x00c00e4c.0209.23 pxid: 0x0000.000.00000000
在进一步分解change vector,它也是有Change Vector的header和可变数据组成,在Lewis《oracle core》中被称为“唯一最重要的特性”。header和可变数据的具体信息我们在以后的文章中再详细介绍。
这里仅介绍几个最关键的信息:
opcode:OP:5.2记录的是事务开始,OP:5.4记录的是事务结束,OP:5.1记录的是前镜像,OP:11.5记录的是update的后镜像,有兴趣的同学可以参考lewis记录的opcode(https://jonathanlewis.wordpress.com/2017/07/25/redo-op-codes/)详细列表。
xid:事务号,Oracle事务管理起始于undo段,并依此为中心。这也是你看到为什么事务开始5.2、事务结束5.4和undo记录5.1都是对Layer 5 : Transaction Undo的操作的原因。xid记录的信息从某种程度上来说记录的就是在undo上占据了哪个slot。阿里巴巴的GalaxyEngine使用的lizard事务优化跟Oracle的事务异曲同工
CHANGE #1 TYP:0 CLS:17 AFN:3 DBA:0x00c00080 OBJ:4294967295 SCN:0x0000.004f1121 SEQ:1 OP:5.2 ENC:0 RBL:0 CHANGE #2 TYP:0 CLS:18 AFN:3 DBA:0x00c00e4c OBJ:4294967295 SCN:0x0000.004f1120 SEQ:1 OP:5.1 ENC:0 RBL:0 xid: 0x0001.013.00000648 CHANGE #3 TYP:2 CLS:1 AFN:4 DBA:0x010000ad OBJ:98733 SCN:0x0000.004f1a8c SEQ:1 OP:11.5 ENC:0 RBL:0 CHANGE #1 TYP:0 CLS:17 AFN:3 DBA:0x00c00080 OBJ:4294967295 SCN:0x0000.004f1aa1 SEQ:1 OP:5.4 ENC:0 RBL:0
这里要专门说一下undo和redo,由于MVCC多版本的设计:
对所有数据的修改,都需要记录这个数据修改前的值,即在undo里面记录前镜像。对于我们的update就是要记录name='o2k2'
同时,对所有数据的修改又必须先以Redo的方式记录到WAL日志中,也出现了在redo中记录undo信息的情况,即第一个Redo Record的CHANGE #2记录了update的undo前镜像。
如果在数据库做恢复前滚的时候,undo跟表数据一样也需要恢复
也就是说,张三存了100块钱,流水账里面记录的不是"张三 +100",而是“ 前镜像:张三 1000;后镜像:张三 1100”,修改一行数据,可能只是几个字节的变化,但是数据库为了保证数据恢复、读一致性多写了这么多日志,多做了这么多事情。而如果你深入做数据库内核,你会发现这个是一个天才的设计。由于文章篇幅问题,这里不细讲。
进一步细看的话,你可以看到前镜像和后镜像里面具体改的数据,例如,前镜像数据如下:
ncol: 3 nnew: 1 size: 0 col 1: [ 4] 6f 32 6b 32
这里表示这个表有3列,修改了一个列,大小变化为0,修改的是col1,对应的四个字节为6f 32 6b 32,也就是o2k2 6f 32 6b 32是Oracle的数据存储格式,跟redo 的存储结构关系不大。
从上面的逻辑日志可以看出来,Oracle想要对表更新:
首先要在程序中构建一个Redo Record
然后构建几个Change Vector,包括事务开始、修改数据的前镜像、修改事务的后镜像等等
将Redo Record 和Change Vector序列化到Redo Buffer (Oracle有专门的LGWR来刷新Redo Buffer到日志文件中)
最后才是将Change Vector应用到数据块上去,这里来说,应用的是表还是undo,并没有太大区别
上面主要介绍的是redo的逻辑结构,是Oracle帮我们解析出来的,一般的疑难问题排查到这一步应该够了,但是如果你要做日志解析或者想要进一步深入排查,你可能会关注到底他在二进制层面是怎么落地的。
这个章节仅面向1%的读者,如果你对这块不感兴趣,可以直接跳过这个章节:)
参考附录redo物理格式,这里是将redo文件拷贝出来以后用Hexdump以十六进制格式输出的Oracle redo日志
首先看第一个block,是file header的block 第一行,是block header,每个block的开头16个字节记录的都是这个block的header。
对比下面的普通block,可以看到0x0022是logfile header, 0x0122是logfile block 第二行开始是redo文件头file header,这里记录了几个比较关键的信息:“00 02 00 00”表示0x00 00 02 00 = 512即这个redo log一个block到底有多大(在oracle 11.2以后BLOCKSIZE可以设置为512,1024或者4096), “03 00 00 00”表示0x00 00 00 03 = 3表示一共有3个block。
00000000 00 22 00 00 00 00 c0 ff 00 00 00 00 00 00 00 00 |."....��........| 00000010 65 58 00 00 00 02 00 00 03 00 00 00 7d 7c 7b 7a |eX..........}|{z|
注意:由于我们的Oracle是安装在little endian小端x86的linux服务器上的,所以“00 02 00 00”表示0x00 00 02 00 = 512需要倒过来一下,如果你的Oracle跑在Big Endian大端的IBM AIX上的时候,“00 02 00 00”表示0x00 02 00 00 = 131072就不用倒过来了
既然知道了一个block的大小为0x00000200,那第一个真正的redo block的起始位置开始的block就是00000000+00000200=00000200,第二个就是00000400,第三个就是00000600
00000200 01 22 00 00 01 00 00 00 72 00 00 00 00 80 25 cd |."......r.....%�| 00000400 01 22 00 00 02 00 00 00 72 00 00 00 10 80 66 66 |."......r.....ff| 00000600 01 22 00 00 03 00 00 00 72 00 00 00 64 80 1b 9a |."......r...d...|
这里“01 00 00 00”, “02 00 00 00” 和“03 00 00 00”就是block序号,表示这是第几个块;"72 00 00 00"=0x00000072=114是日志序号,对应的就是redo日志空间中的Seq#号;而"00 80", "10 80", "64 80"对应的是该block中第一个redo record相对block起始地址的偏移量。
00000400起始的redo block的redo record是从“10 80”=0x8010-0x8000=0x10=16即从00000410"44 02 00 00..."开始就是redo record的字节信息了。
00000400 01 22 00 00 02 00 00 00 72 00 00 00 10 80 66 66 |."......r.....ff| 00000410 44 02 00 00 05 6e 00 00 a1 1a 4f 00 01 00 00 23 |D....n..�.O....#|
00000600起始的redo block的redo record是从“64 80”=0x8064-0x8000=0x64=100即从00000664"a4 00 00 00..."开始就是redo record的字节信息了。
00000600 01 22 00 00 03 00 00 00 72 00 00 00 64 80 1b 9a |."......r...d...| 00000610 01 00 03 01 00 00 00 00 00 1a 4f 00 01 00 70 72 |..........O...pr| 00000620 6f 32 6b 33 05 14 00 00 00 00 00 00 00 00 00 00 |o2k3............| 00000630 00 00 00 00 00 00 00 00 00 06 00 00 12 00 04 00 |................| 00000640 00 00 02 00 04 00 04 00 00 00 00 00 03 00 4f f0 |..............O�| 00000650 2a 00 37 00 00 00 00 00 00 04 20 0b ff ff ff ff |*.7....... .����| 00000660 53 59 53 00 a4 00 00 00 01 06 00 00 a2 1a 4f 00 |SYS.�.......�.O.|
我们可以明显看到一个redo record是可以跨两个block的。也就是前面我们介绍的逻辑的redo record是可能包含在一个物理redo block中的,也有可能跨多个物理redo block。
第一个redo block比较特殊,"00 80"的起始redo record为0。起始这个redo block中并没有redo record。而是包含了这个redo file的所属实例信息,起止SCN等信息,甚至部分信息是以纯文本来记录的“Thread 0002, Seq# 0000000114, SCN 0x0000004f1aa1-0x0000004f1aa8”
综上所述,本文简略的介绍了Oracle redo的物理和逻辑格式。
一条update语句实际执行时,在Oracle上经历的写undo、记录WAL,修改数据块的过程;介绍了Oracle在二进制redo日志中把逻辑的redo record对应记录到redo block中的。
文末一张图,简要总结一下他们的关系:
o2k是沃趣科技自主研发,基于Oracle redo日志的二进制解析工具,解析的结果以canal的protobuf的形式直接写入到kafka,为数据仓库、人工智能和大数据不可或缺的数据来源管道。
技术合作和生态合作请联系:o2k@woqutech.com
服务电话: 400-678-1800 (周⼀⾄周五 09:00-18:00)
商务合作: 0571-87770835
市场反馈: marketing@woqutech.com
地址: 杭州市滨江区滨安路1190号智汇中⼼A座1101室