[MYSQL] mysql坏块检查

2024-08-22 15:31:17 浏览数 (1)

导读

当mysql存在坏块的时候, 查询对应的表就会报错,然后数据库就crash了. 比如:

也就是只有我们查询有坏块的表的时候才会发现有坏块,启动的时候并不会做坏块检查, 那么我们要怎么知道数据库有哪些表有坏块了呢? 有坏块后怎么处理呢?

innochecksum

mysql提供了一个工具innochecksum来检查数据块.

正常情况下, 打印页信息, 比如:

代码语言:shell复制
(venv) 14:03:07 [root@ddcw21 mysql-8.0.37]#innochecksum /tmp/t20240612.ibd -S

File::/tmp/t20240612.ibd
================PAGE TYPE SUMMARY==============
#PAGE_COUNT	PAGE_TYPE
===============================================
       1	Index page
       1	SDI Index page
       0	Undo log page
       1	Inode page
       0	Insert buffer free list page
       2	Freshly allocated page
       1	Insert buffer bitmap
       0	System page
       0	Transaction system page
       1	File Space Header
       0	Extent descriptor page
       0	BLOB page
       0	Compressed BLOB page
       0	Subsequent Compressed BLOB page
       0	SDI BLOB page
       0	Compressed SDI BLOB page
       0	Other type of page
===============================================
Additional information:
Undo page type: 0 insert, 0 update, 0 other
Undo page state: 0 active, 0 cached, 0 to_free, 0 to_purge, 0 prepared, 0 other
(venv) 14:03:09 [root@ddcw21 mysql-8.0.37]#

如果是坏块的话, 打印信息则如下:

代码语言:shell复制
(venv) 14:03:56 [root@ddcw21 mysql-8.0.37]#innochecksum /tmp/test_badpage_20240822.ibd -S
Fail: page 4 invalid
Exceeded the maximum allowed checksum mismatch count::0

也就是可以使用innochecksum来检查数据库是否存在坏块, 该工具要求数据库停止运行. 即要停库后再检查.不然会有如下报错:fcntl: Resource temporarily unavailable

为了安全, 也就将就把. 所以本文就结束了. 感谢观看!

坏块校验原理

有时候我们并不能关闭数据库, 但就是想要校验坏块, 总不能去查询所有表吧, 而且如果有坏块的话, 数据库就挂了啊. 这还得了.

那就只能来挖innodbchecksum的源码了. 看下校验原理, 然后我们自己写脚本来校验. 我们还是使用万能的gdb调试来做.

代码语言:shell复制
(echo -e "break mainnrun /data/mysql_dev/data/db1/t20240612.ibd -S -C crc32"; while true;do echo 'step';done) |gdb /root/mysql_source/mysql-8.0.37/bldx86/runtime_output_directory/innochecksum > /tmp/t20240822_innochecksum.gdb.txt 2>&1

然后我们就得到了innochecksum的完整堆栈信息了. 稍加整理就能得到调用过程:

也就是最终还是走的crc32校验. 相关代码如下:

代码语言:c 复制
uint32_t buf_calc_page_crc32(const byte *page,
                             bool use_legacy_big_endian /* = false */) {

  ut_crc32_func_t crc32_func =
      use_legacy_big_endian ? ut_crc32_legacy_big_endian : ut_crc32;

  const uint32_t c1 = crc32_func(page   FIL_PAGE_OFFSET,
                                 FIL_PAGE_FILE_FLUSH_LSN - FIL_PAGE_OFFSET);

  const uint32_t c2 =
      crc32_func(page   FIL_PAGE_DATA,
                 UNIV_PAGE_SIZE - FIL_PAGE_DATA - FIL_PAGE_END_LSN_OLD_CHKSUM);

  return (c1 ^ c2);
}

也就是 对FIL_PAGE_HEADER做crc的结果^FIL_PAGE_DATA 即为我们需要的crc32值.

FIL_PAGE_OFFSET之类的可以查看我之前写的文章: https://www.modb.pro/topic/625137

不好理解, 我们画个图吧.

也就是只校验了一部分header和所有data, 连PAGE_TYPE,SPACE_ID之类的均未校验.

难道就这么简单么. 我们来测试下吧.

crc32算法我们还是参考之前解析checksum table命令时的算法, 比较都是mysql的嘛. (关于ibd的结构, 请查看: https://www.modb.pro/topic/625137)

代码语言:python代码运行次数:0复制
import struct,binascii
filename = '/tmp/t20240612.ibd'
f = open('/tmp/t20240612.ibd','rb')
data = f.read(16384)
checksum_field1 = struct.unpack('>L',data[:4])[0]
checksum_field2 = struct.unpack('>L',data[-8:-4])[0]
c1 = binascii.crc32(data[4:26])
c2 = binascii.crc32(data[38:16384-8])
print(checksum_field1,checksum_field2,(c1^c2)&(2**32-1))

啊咧咧, 咋个不一样呢.

CRC32C

看来和那个ut_crc32有关, 应该不是普通的crc32. 不然就直接调zlib的crc32了. 干嘛还写这么麻烦呢. 当我们细看storage/innobase/ut/crc32.cc的实现的时候, 发现实际上是CRC32-C(Cyclic Redundancy Check 32-bit Castagnoli), 与普通的crc32相比, 使用了不同的生成多项式. 找到篇相关博文, 讲得很细, 但看起来比较费劲: https://blogs.oracle.com/mysql/post/faster-crc32-c-computation-in-mysql-8027

看这篇文章时我的感受是:

好在计算机也是个笨蛋, 程序员要能让计算机看懂指令, 就得写简单的代码. 从代码来看基本上激素查个表而已. 我们直接使用python重写

代码语言:python代码运行次数:0复制
import struct
def create_crc32c_table():
    poly = 0x82f63b78
    table = []
    for i in range(256):
        crc = i
        for _ in range(8):
            if crc & 1:
                crc = (crc >> 1) ^ poly
            else:
                crc >>= 1
        table.append(crc)
    return table

			
def calculate_crc32c(data):
	crc = 0xFFFFFFFF
	for byte in data:
		crc = crc32_slice_table[(crc ^ byte) & 0xFF] ^ (crc >> 8)
	return crc ^ 0xFFFFFFFF


crc32_slice_table = create_crc32c_table()

虽然我们看不到代码, 但是能使用python重写, 这就是python的魅力吧.

那我们再次验证下呢.

测试

把我们整理成脚本,来测试下吧.

首先构造一个有坏块的文件. 如果你有的话, 就不用这一步了.

代码语言:python代码运行次数:0复制
f1 = open('/data/mysql_dev/data/db1/t20240612.ibd','rb')
f2 = open('/tmp/test_badpage_20240822.ibd','wb')
alldata = f1.read()
baddata = alldata[:16384*4 100]   100*b'ddcw'   alldata[16384*4 100 100*4:]
f2.write(baddata)
f2.close()
f1.close()

然后校验正常的文件:

然后校验异常的文件:

坏的块确实校验出来了, 也是我们故意损坏的位置. 说明我们的校验工具没问题(棒棒哒!)

总结

  1. mysql ibd文件的坏块校验 就是FIL_HEADER的crc32c值^FIL_DATA的crc32c值. 然后和文件头/尾保存的crc32值比较即可.
  2. CRC32-C 其实有现成的库, 可以使用pip install crc32c去安装.
  3. 遇到坏块的话, 可以使用ibd2sql工具去解析还正常的页的数据. 用法讲过很多次了. 就不再介绍了.

参考:

https://dev.mysql.com/doc/refman/8.0/en/innochecksum.html

https://blogs.oracle.com/mysql/post/faster-crc32-c-computation-in-mysql-8027

附源码

github地址: https://github.com/ddcw/ddcw/tree/master/python/check_innodb_file

这次写得比较简单, 都没做选项解析之类的.

代码语言:python代码运行次数:0复制
import struct
import sys,os
def create_crc32c_table():
    poly = 0x82f63b78
    table = []
    for i in range(256):
        crc = i
        for _ in range(8):
            if crc & 1:
                crc = (crc >> 1) ^ poly
            else:
                crc >>= 1
        table.append(crc)
    return table

			
def calculate_crc32c(data):
	crc = 0xFFFFFFFF
	for byte in data:
		crc = crc32_slice_table[(crc ^ byte) & 0xFF] ^ (crc >> 8)
	return crc ^ 0xFFFFFFFF


crc32_slice_table = create_crc32c_table()
filename = sys.argv[1]
if not os.path.exists(filename):
	print(f'USAGE: python sys.argv[0] xxx.ibd')
	sys.exit(1)
	
f = open(filename,'rb')
PAGENO = -1
while True:
	data = f.read(16384)
	PAGENO  = 1
	if data == b'':
		break
	if data[:4] == b'x00x00x00x00'  and data[26:28] == b'x00x00':
		continue # 未使用的页
	checksum_field1 = struct.unpack('>L',data[:4])[0]
	checksum_field2 = struct.unpack('>L',data[-8:-4])[0]
	c1 = calculate_crc32c(data[4:26])
	c2 = calculate_crc32c(data[38:16384-8])
	#print('PAGENO:',PAGENO,"CHECKSUM:",checksum_field1,checksum_field2,(c1^c2)&(2**32-1))
	if checksum_field1 == checksum_field2 == (c1^c2)&(2**32-1):
		pass # 正常就不打印了, 不然太多
	else:
		print("BAD PAGE:",PAGENO)

0 人点赞