前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >MYSQL REDO LOG文件解析

MYSQL REDO LOG文件解析

原创
作者头像
大大刺猬
发布2023-04-14 15:37:54
3K0
发布2023-04-14 15:37:54
举报
文章被收录于专栏:大大刺猬大大刺猬

mysql最重要的两个日志 binlog 和 redo(innodb log)

一般备份恢复都是用的binlog, redo log好像从来没去管过, 就跟不会坏似的...(这跟redo设计有关).

基础知识

redo log 是innodb 引擎的日志, 每个事务都由若干个 迷你事务(mtr) 构成, 每个mtr都将写入到N个redo log block.

mtr也分为prepare和commit. 也就是每个事务都有若干个mtr_commit. 感兴趣的可以使用gdb调试(mtr_t::commit)

undo也会产生redo...

redo log在内存中的大小取决于 innodb_log_buffer_size (默认64MB)

redo log在磁盘上的大小取决于innodb_log_file_size, 每组的数量取决于参数innodb_log_files_in_group

: mysql只有1组redo log

REDO LOG 文件格式

Mysql一共有1组redo log, 这一组redo里面有innodb_log_files_in_group个文件, 每个大小和格式一样. 但只有第一个日志ib_logfile0 会记录chkpoint. 所以第一个日志会被频繁写, 磁盘就容易坏, 所以就整了个checkpoint2 -_-

所以我们着重看第一个redo文件的格式. 源码文件:storage/innobase/include/log0log.h

环境

redo参数如下

代码语言:javascript
复制
innodb_log_files_in_group = 4
innodb_log_file_size = 1073741824 #1GB
innodb_log_buffer_size = 67108864 #64MB

格式

每个redo文件都由N个 512 字节的块组成. (扇区也是512字节)

有4个位于头部的块(一共2048字节)记录redo相关信息. 之后的普通块记录数据

每个块 都由HEDAER和BODY和TRAILER(校验值)组成, (不同类型的块可能不一样, 但大致都是这样的)

redo 文件格式
redo 文件格式

下面来看看具体的块

LOG_HEADER

名字

大小(字节)

描述

LOG_HEADER_FORMAT

4

redo格式版本,5.7.38是1

LOG_HEADER_PAD1

4

LOG_HEADER_START_LSN

8

这个文件的起始LSN

LOG_HEADER_CREATOR

32

创建者之类的信息, 一般就是mysql版本信息

trailer

4(结尾4字节)

校验

LOG_CHECKPOINT

LOG_CHECKPOINT1和LOG_CHECKPOINT2是一样的 (轮换着写)

名字

大小(字节)

描述

LOG_CHECKPOINT_NO

8

检查点号

LOG_CHECKPOINT_LSN

8

检查点LSN

LOG_CHECKPOINT_OFFSET

8

检查点偏移量

LOG_CHECKPOINT_LOG_BUF_SIZE

8

innodb_log_buffer_size

trailer

4(结尾4字节)

校验

数据块LOG_BLOCK

又开头固定的12字节头部信息和结尾固定4字节的校验和组成

名字

大小(byte)

描述

LOG_BLOCK_HDR_NO

4

块号,唯一递增

LOG_BLOCK_HDR_DATA_LEN

2

这个块的数据大小(含header和trailer)

LOG_BLOCK_FIRST_REC_GROUP

2

LOG_BLOCK_CHECKPOINT_NO

4

检查点号

data

数据(由N个mtr组成)

trailer

4

校验

MTR

mtr比较多, 格式大致如下

名字

大小

描述

type

mtr类型

space_id

4

表空间ID

page_no

4

页号

page_offset

4

页偏移量

mtr类型可以看官网的: https://dev.mysql.com/doc/dev/mysql-server/latest/mtr0types_8h.html

用PYTHON解析redo

由于redo是循环着写的, ib_logfile0的数据块写完了, 就写ib_logfile1的, ib_logfile1的写完了就写ib_logfile2的, 最后一个写完了就写ib_logfile0的. 但是chk信息是记录在第一个文件里面的, 所以LOG_CHECKPOINT_OFFSET是整个日志组的, 也就是得先计算在组内的哪个文件里面 (LOG_CHECKPOINT_OFFSET/innodb_log_file_size)

其实我都封装好了的....

代码语言:javascript
复制
import mysql_redo_parse
aa = mysql_redo_parse.mysql_redo('/data/mysql_3308/mysqllog/redolog') #传递的是目录
print(aa)
data = aa.blocks(3052158-10) #取的是最后10个block
for x in data:
    print(x)

mysql_redo.blocks() 第一个参数是起始block信息, 第二个参数是取的blocks数量(默认10), 可以跨文件取值

block_id是递增的
block_id是递增的

没有解析数据详情哈, block_type太多了, 懒得去解析了....

总结

mysql 由一个redo log组, 一个组里面有4个文件, 是循环写的.

每个事务由N个迷你事务(mtr组成), 每N个mini事务写入N个redo block(512)

附源码

未解析redo data

代码语言:javascript
复制
#解析mysql redo log 
#字节序为大端
import struct
import os

class LOG_HEADER(object):
	def __init__(self,bdata):
		"""
		LOG_HEADER_FORMAT       0 (LOG_HEADER_FORMAT_CURRENT 1)
		LOG_HEADER_PAD1         4
		LOG_HEADER_START_LSN    8
		LOG_HEADER_CREATOR      16
		LOG_HEADER_CREATOR_END  (LOG_HEADER_CREATOR + 32)
		LOG_HEADER_CREATOR_CURRENT      "MySQL " INNODB_VERSION_STR
		"""
		self.LOG_HEADER_FORMAT = struct.unpack('>L',bdata[:4])[0]
		self.LOG_HEADER_PAD1 = struct.unpack('>L',bdata[4:8])[0]
		self.LOG_HEADER_START_LSN = struct.unpack('>Q',bdata[8:16])[0]
		self.LOG_HEADER_CREATOR = bdata[16:16+32].split(b'\x00')[0].decode()
		#懒得校验结尾的trailer了.....

	def __str__(self):
		return f'format:{self.LOG_HEADER_FORMAT} creator:{self.LOG_HEADER_CREATOR} start_lsn:{self.LOG_HEADER_START_LSN}'


class LOG_CHECKPOINT(object):
	def __init__(self,bdata):
		"""
		LOG_CHECKPOINT_NO               0
		LOG_CHECKPOINT_LSN              8
		LOG_CHECKPOINT_OFFSET           16
		LOG_CHECKPOINT_LOG_BUF_SIZE     24
		"""
		#其实是uint32_t  但是 是大端, 所以用Q也行....
		self.LOG_CHECKPOINT_NO = struct.unpack('>Q',bdata[0:8])[0]  #Checkpoint number
		self.LOG_CHECKPOINT_LSN = struct.unpack('>Q',bdata[8:16])[0]
		self.LOG_CHECKPOINT_OFFSET = struct.unpack('>Q',bdata[16:24])[0]
		self.LOG_CHECKPOINT_LOG_BUF_SIZE = struct.unpack('>Q',bdata[24:32])[0] 

	def __str__(self):
		return f'chk no:{self.LOG_CHECKPOINT_NO}   chk_lsn:{self.LOG_CHECKPOINT_LSN}  chk_offset:{self.LOG_CHECKPOINT_OFFSET}  chk_buff:{self.LOG_CHECKPOINT_LOG_BUF_SIZE/1024/1024} MB'

#Bytes used by headers of log files are NOT included in lsn sequence
class LOG_BLOCK(object):
	def __init__(self,bdata):
		#LOG_BLOCK_HDR_SIZE 12
		"""
		LOG_BLOCK_HDR_NO        0
		LOG_BLOCK_HDR_DATA_LEN  4
		LOG_BLOCK_FIRST_REC_GROUP 6
		LOG_BLOCK_CHECKPOINT_NO 8
		"""
		self.LOG_BLOCK_HDR_NO = struct.unpack('>L',bdata[0:4])[0] #block id
		self.LOG_BLOCK_HDR_DATA_LEN = struct.unpack('>H',bdata[4:6])[0] #data length
		self.LOG_BLOCK_FIRST_REC_GROUP = struct.unpack('>H',bdata[6:8])[0] # 0 or LOG_BLOCK_HDR_SIZE (12)
		self.LOG_BLOCK_CHECKPOINT_NO = struct.unpack('>L',bdata[8:12])[0] #chk
		self.data = bdata[12:12+self.LOG_BLOCK_HDR_DATA_LEN-12-4] #减去block_hdr和trailer
		self.empty = True if bdata[:12] == int(0).to_bytes(12,'big') else False
		#self.leader = True if self.LOG_BLOCK_HDR_NO >> 31 > 0 else False
		#The highest bit is set to 1, if this is the first block in a call to fil_io (for possibly many consecutive blocks).
		if self.LOG_BLOCK_HDR_NO >> 31 > 0:
			self.leader = True
			self.LOG_BLOCK_HDR_NO -= 1<<31
		else:
			self.leader = False

	def __str__(self,):
		return f'block_id:{self.LOG_BLOCK_HDR_NO}{" L" if self.leader else "  "}  data_length:{self.LOG_BLOCK_HDR_DATA_LEN}  chk_no:{self.LOG_BLOCK_CHECKPOINT_NO}'


#For 1 - 8 bytes, the flag value must give the length also
def mtr_log(bdata):
	"""
	type         1  mlog_id_t
	space_id     4  space_id_t 
	page_no      4  page_no_t
	page_offset  4  ulint 
	len
	data
	"""
	pass
			


#storage/innobase/include/log0log.h
#lsn是不包含log header的, 所以计算的时候要减去2048 LSN = (OFFSET - 2048 + LOG_HEADER_START_LSN)
class mysql_redo(object):
	def __init__(self,filedir):
		"""
		LOG_HEADER       512*0 -- 512*0+512
		LOG_CHECKPOINT_1 512*1 -- 512*1+512 /*only defined in the first log file*/
		LOG_CHECKPOINT_2 512*3 -- 512*3+512
		LOG_BLOCK 512*n
		"""
		#LOG_FILE_HDR_SIZE 一共就是 2048

		filedir = os.path.abspath(filedir)
		self.filedir = filedir
		file_count = 0 #innodb_log_files_in_group
		try:
			filename_list = os.listdir(filedir)
			for x in filename_list:
				 file_count += 1 if len(x.split('ib_logfile')) == 2 else 0
		except Exception as e:
			return e
		self.file_count = file_count
			
		self.filename = f'{filedir}/ib_logfile0' #第一个redo文件
		self.filesize = os.path.getsize(self.filename) #redo文件大小
		self.max_blocks = int((self.filesize-2048)/512) #每个redo文件所能存储的block数量
		with open(self.filename,'rb') as f:
			self.log_header = LOG_HEADER(f.read(512))
			self.log_chk1 = LOG_CHECKPOINT(f.read(512))
			f.read(512)#留空
			self.log_chk2 = LOG_CHECKPOINT(f.read(512))
			self.first = True if self.log_chk1.LOG_CHECKPOINT_NO != 0 else False
			self.LOG_CHECKPOINT_LSN = max(self.log_chk2.LOG_CHECKPOINT_LSN,self.log_chk1.LOG_CHECKPOINT_LSN)
			self.LOG_CHECKPOINT_NO = max(self.log_chk2.LOG_CHECKPOINT_NO,self.log_chk1.LOG_CHECKPOINT_NO)
			self.LOG_CHECKPOINT_LOG_BUF_SIZE = self.log_chk2.LOG_CHECKPOINT_LOG_BUF_SIZE if self.log_chk2.LOG_CHECKPOINT_NO > self.log_chk1.LOG_CHECKPOINT_NO else self.log_chk1.LOG_CHECKPOINT_LOG_BUF_SIZE
			self.LOG_CHECKPOINT_OFFSET = self.log_chk2.LOG_CHECKPOINT_OFFSET if self.log_chk2.LOG_CHECKPOINT_NO > self.log_chk1.LOG_CHECKPOINT_NO else self.log_chk1.LOG_CHECKPOINT_OFFSET
			self.avg = 0 if self.LOG_CHECKPOINT_NO == 0 else round(self.LOG_CHECKPOINT_OFFSET/self.LOG_CHECKPOINT_NO/1024/1024,2) #平均每次刷redo数据量MB

	def __str__(self,):
		return f'LAST_CHK:{self.LOG_CHECKPOINT_LSN}  CHK_NO:{self.LOG_CHECKPOINT_NO}  REDO_BUFFER_SIZE:{self.LOG_CHECKPOINT_LOG_BUF_SIZE/1024/1024}MB  OFFSET:{self.LOG_CHECKPOINT_OFFSET}  AVG_WRITE:{self.avg}MB  BLOCKS:{int((self.LOG_CHECKPOINT_OFFSET-2048*(1+int(self.LOG_CHECKPOINT_OFFSET/self.filesize))+511)/512)}'

	def blocks(self,start_block=0,count=10):
		"""
		start_block : 从第N个block开始读 (默认0)
		count : 读多少个block (默认10)
		"""

		def _getblocks(filename,s_off,e_off):
			_t = []
			with open(filename,'rb') as f:
				f.seek(s_off)
				while s_off < e_off:
					_block = LOG_BLOCK(f.read(512))
					s_off += 512
					if _block.empty:
						break
					_block.lsn = f.tell() - 2048 + self.log_header.LOG_HEADER_START_LSN - 512 + _block.LOG_BLOCK_HDR_DATA_LEN #Log flushed up to
					_t.append(_block)
			return _t
				

		start_offset = 2048+2048*int(start_block/self.max_blocks)+start_block*512
		end_offset = start_offset + 512*count
		log_block = []

		while start_offset < end_offset:
			fileno = int(start_offset/self.filesize)
			filename = f'{self.filedir}/ib_logfile{fileno}'
			if int(end_offset/self.filesize) > int(start_offset/self.filesize): #超过一个文件
				log_block += _getblocks(filename,start_offset%self.filesize,self.filesize)
				start_offset += self.filesize - start_offset%self.filesize + 2048
				end_offset += 2048 #跳过header
			else:#最后一次
				log_block += _getblocks(filename,start_offset%self.filesize,end_offset%self.filesize)
				start_offset = end_offset
		return log_block

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基础知识
  • REDO LOG 文件格式
    • 环境
      • 格式
        • LOG_HEADER
          • LOG_CHECKPOINT
            • 数据块LOG_BLOCK
              • MTR
              • 用PYTHON解析redo
              • 总结
              • 附源码
              相关产品与服务
              云数据库 MySQL
              腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档