首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >[MYSQL] binlog校验

[MYSQL] binlog校验

原创
作者头像
大大刺猬
发布2025-05-13 15:47:40
发布2025-05-13 15:47:40
19200
代码可运行
举报
文章被收录于专栏:大大刺猬大大刺猬
运行总次数:0
代码可运行

导读

binlog是什么?

binlog是一个非常重要的日志,是mysql server层的日志, 记录用户的各种操作(changes). 默认启用.

binlog有啥用?

  1. 备份恢复: 可以用来恢复数据到任一时间点.(或者异常重启恢复)
  2. 高可用: 也可以用来搭建从库,集群等
  3. 审计: 查看数据的历史变更情况, 但是看不到执行者的IP,只能看到thread-id.

binlog怎么校验是否损坏?

这个是本文重要讨论的, 虽然mysqlbinlog--verify-binlog-checksum选项就能校验binlog是否损坏(其实是写这个工具之前没发现这个参数....).

binlog损坏了怎么办?

凉拌

校验原理

结构

在讨论校验原理之前, 我们先简单看看binlog的结构:

  1. binlog由若干个event构成.
  2. 每个event由19字节的event_header(9字节)和event_body构成
  3. 第一个event为FORMAT_DESCRIPTION_EVENT, 记录的是binlog版本,mysql版本,checksum算法等信息.
  4. relay log和Binlog格式完全一样. 只不过binlog开头多了4字节的magic(b'\xfebin')
  5. event_header部分信息为:

对象

大小

描述

timestamp

4

时间戳

event_type

1

event的类型

server_id

4

serverid

event_size

4

这个event大小(含event_header&checksum)

log_pos

4

结束位置的偏移量

flags

2

一些flag

这部分信息之前都是讲过的, 有兴趣的可以往前面翻翻:https://www.modb.pro/topic/625137

校验原理

在FORMAT_DESCRIPTION_EVENT中, 最后的信息是checksum_alg, 表示这个binlog是否有checksum位. 当然前提是参数binlog_checksum的值为CRC32才行. 如果binlog_checksum未设置校验的话, 是无法校验Binlog的. 我们这里就只讨论存在校验的情况.

有些情况可能会关闭binlog校验, 比如MGR

既然要校验, 那么肯定得每个event都校验,比较不可能文件写完之后才校验,那样就没得校验的意义了. 由于是server层实现的, 校验的算法通常就是crc32 (innodb使用的是crc32c).

那么这个校验值应该存储在哪呢? 最简单的就是存储在event结尾出. 出于兼容性考虑, event_header记录的大小/pos应该包含这4字节. 于是得到如下结构:

我们要校验的时候, 也只需要将event_header+event_body的crc32校验值和记录的crc32校验值比较即可确定event是否损坏.

设计思路

既然知道该校验信息了, 那么就来设计个binlog的校验工具吧.

选语言: 我们使用python编写, 这样开发效率最高, 而校验主要是涉及到文件读取和crc32计算,这2者在编程语言之间差距不大(有时候感觉还挺快的)

兼容性: 使用者可能是python2或者python3环境, 所以我们需要考虑py2和py3的兼容性. 使用者可能是5.7, 8.0, 8.4环境, 所以还需要考虑mysql的兼容性. 还得同时支持binlog和relay log(就4字节的差)

使用: 由于功能单一, 不需要复杂的参数, 所以直接使用位置参数即可. 而使用者可能还想一次性校验多个文件, 所以还得考虑多个文件的情况, 而且文件可能有不存在的情况.

校验: 遇到坏的event之后, 就不应该继续校验了, 毕竟无法确定event的哪部分是损坏的, 也就无法确定下一个event的位置

说得比较简洁, 有些问题只有实际写的时候才会遇到, 比如: py2使用binascii.crc32校验的结果是有符号的(范围-2**31, 2**31-1), 而py3使用binascii.crc32校验的结果是无符号的(范围0, 2**32-1), 这种问题很多情况发现不了, 只有足够多的数据量(或者运气好)才能发现. 当然解决办法比较简单, 直接将结果&0xffffffff即可变成无符号的整数

验证

我们还是来看看实际效果吧:

用法:

代码语言:shell
复制
python3 check_binlog_v2.py /tmp/m3314.000019

正常情况: 无输出

代码语言:shell
复制
15:11:40 [root@ddcw21 ei]#python3 check_binlog_v2.py /tmp/m3314.000019
15:11:43 [root@ddcw21 ei]#

有坏event的情况:

代码语言:shell
复制
15:38:26 [root@ddcw21 ei]#python3 check_binlog_v2.py /tmp/m3314.000019_bad
/tmp/m3314.000019_bad have bad event at: 316

多文件的情况

代码语言:shell
复制
15:12:51 [root@ddcw21 ei]#python3 check_binlog_v2.py /tmp/m3314.000019*
/tmp/m3314.000019_bad have bad event at: 316 
/tmp/m3314.000019_bad2 event_header corruption!!! current offset:0

python2的情况:

代码语言:shell
复制
15:13:25 [root@ddcw21 ei]#python2 check_binlog_v2.py /tmp/m3314.000019*
/tmp/m3314.000019_bad have bad event at: 316 
/tmp/m3314.000019_bad2 event_header corruption!!! current offset:0

无校验值的情况:

代码语言:shell
复制
15:25:19 [root@ddcw21 ei]#python2 check_binlog_v2.py /data/mysql_3314/mysqllog/binlog/m3314.000035
/data/mysql_3314/mysqllog/binlog/m3314.000035 have not binlog_checksum

mysql 5.7的环境

代码语言:shell
复制
15:40:14 [root@ddcw21 ei]#ls -ahl /data/mysql_3308/mysqllog/binlog/m3308.001098
-rw-r----- 1 mysql mysql 77M Apr 30 18:12 /data/mysql_3308/mysqllog/binlog/m3308.001098
15:40:21 [root@ddcw21 ei]#
15:40:22 [root@ddcw21 ei]#time python3 check_binlog_v2.py /data/mysql_3308/mysqllog/binlog/m3308.001098

real	0m0.149s
user	0m0.114s
sys	0m0.035s

mysqlbinlog的校验(得去掉stdout的正常信息, 不然不方便看...)

代码语言:shell
复制
15:43:59 [root@ddcw21 ei]#mysqlbinlog --verify-binlog-checksum /tmp/m3314.000019_bad >/dev/null
ERROR: Could not read entry at offset 316: Error in log format or read error 1.
ERROR: Event crc check failed! Most likely there is event corruption.

总结

  1. 在这个脚本写完之后,准备总结下的时候才发现其实官方也支持binlog的校验(--verify-binlog-checksum), 不过之前没有发现而已, 因为之前解析的时候有问题会直接报错的, 而这次没有报错出来, 就以为没得这个功能. 于是就重新写了一版(所以是v2)
  2. 对于损坏的binlog,如果是从库,可以重建; 如果是主库呢?(只要数据提交了, 数据库没挂, 貌似就没得影响...)

参考:

https://dev.mysql.com/doc/refman/8.0/en/binary-log.html

https://dev.mysql.com/doc/refman/8.0/en/group-replication-limitations.html

https://docs.python.org/zh-cn/2.7/library/binascii.html#module-binascii

附源码:

下载地址:https://github.com/ddcw/ddcw/blob/master/python/check_binlog_v2.py

代码语言:python
代码运行次数:0
运行
复制
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# check binlog/relaylog, like: mysqlbinlog --verify-binlog-checksum

import os
import sys
import struct
import binascii

F_EVENT_HEADER = struct.Struct("<LBLLLH")
F_CRC32 = struct.Struct("<L")
def help():
	sys.stdout.write("usage:\n\tpython "+str(sys.argv[0])+" mysql-bin.00000n\n")
	sys.exit(1)

def get_filename():
	if len(sys.argv) == 1:
		sys.stdout.write("need a filename at least\n")
		help()
	filename = []
	for x in range(1,len(sys.argv)):
		fname = str(sys.argv[x])
		if not os.path.exists(fname):
			sys.stdout.write("filename: "+fname+" is not exists\n")
			help()
		filename.append(fname)
	return filename

def first_event_check(bdata):
	binlog_version,mysql_version,create_timestamp,event_header_length = struct.unpack('<H50sLB',bdata[:57])
	mysql_version = mysql_version.decode()
	offset = 57
	if mysql_version[:1] == "5":
		offset += 38
	elif mysql_version[:4] == "8.4.":
		offset += 43
	elif mysql_version[:1] == "8":
		offset += 41
	else:
		sys.stdout.write("donot support version:"+mysql_version+"\n")
		help() 
	event_post_header_len = bdata[57:offset]
	return True if struct.unpack('<B',bdata[offset:offset+1])[0] else False

def read_event(f,filename):
	start_offset = f.tell()
	event_header = f.read(19)
	if event_header == b'':
		return 0,b'',b''
	if len(event_header) != 19:
		sys.stdout.write(filename+" event_header corruption!!! current offset:"+str(start_offset)+"\n")
		sys.exit(1)
	timestamp,event_type,server_id,event_size,log_pos,flags = F_EVENT_HEADER.unpack(event_header)
	if event_size < 19:
		sys.stdout.write(filename+" event_header corruption!!! current offset:"+str(start_offset)+"\n")
		sys.exit(2)
	event_body = f.read(event_size-19)
	if len(event_body) != event_size-19:
		sys.stdout.write(filename+" event_body corruption!!! current offset:"+str(start_offset)+"\n")
		sys.exit(3)
	return start_offset,event_header,event_body


if __name__ == "__main__":
	for filename in get_filename():
		with open(filename,'rb') as f:
			if f.read(4) != b'\xfebin':
				f.seek(0,0) # relay log
			start_offset,event_header,event_body = read_event(f,filename)
			if not first_event_check(event_body):
				sys.stdout.write(filename+" have not binlog_checksum\n")
				break
			while True:
				start_offset,event_header,event_body = read_event(f,filename)
				if len(event_body) == 4: # STOP_EVENT
					break
				if event_header == b'': # finish (ROTATE_EVENT)
					break
				crc32_v1 = F_CRC32.unpack(event_body[-4:])[0]
				crc32_v2 = binascii.crc32(event_header+event_body[:-4]) & 0xffffffff # for py2
				if crc32_v1 != crc32_v2:
					sys.stdout.write(filename+" have bad event at: "+str(start_offset)+" \n")
					break

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导读
  • 校验原理
    • 结构
    • 校验原理
  • 设计思路
  • 验证
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档