暂无图片
暂无图片
2
暂无图片
暂无图片
暂无图片

[MYSQL] binlog校验

原创 大大刺猬 2025-05-13
324

导读

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
image.png

校验原理

在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字节. 于是得到如下结构:
image.png

我们要校验的时候, 也只需要将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即可变成无符号的整数
image.png

验证

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

python3 check_binlog_v2.py /tmp/m3314.000019

正常情况: 无输出

15:11:40 [root@ddcw21 ei]#python3 check_binlog_v2.py /tmp/m3314.000019 15:11:43 [root@ddcw21 ei]#

有坏event的情况:

15:38:26 [root@ddcw21 ei]#python3 check_binlog_v2.py /tmp/m3314.000019_bad /tmp/m3314.000019_bad have bad event at: 316

多文件的情况

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的情况:

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

无校验值的情况:

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的环境

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的正常信息, 不然不方便看…)

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

#!/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
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论