前言
MyDumper 是一款社区开源的 MySQL 逻辑备份工具,该工具由 C 语言编写,支持多线程备份和恢复,导出文件压缩等,比官方的 mysqldump 命令备份效率更高,是 DBA 常用的逻辑备份软件。
1、问题背景
根据审计要求,需要对数据库进行逻辑备份备份工具版本:MyDumper 0.9.5 备份用户:dbbackup 用户权限:lock tables,reload,process,replication client,select,event,trigger,show view,execute on *.*备份命令:mydumper -h rds.aliyun.com -P 3306 -t 8 -W -u dbbackup -p '123456' -s 2000000 --regex '^(?!(information_schema.|performance_schema.|sys.))' -c -o /data/backup
备份时过滤掉系统库,只备份业务库,查看备份日志,发现某个 Aliyun RDS For MySQL 5.7(小版本号:20210430) 实例备份时有如下告警信息,提示 sys 库下边的视图【Broken table detected】。
** (mydumper:19312): WARNING **: Broken table detected, please review: sys.host_summary
** (mydumper:19312): WARNING **: Broken table detected, please review: sys.host_summary_by_file_io
** (mydumper:19312): WARNING **: Broken table detected, please review: sys.host_summary_by_file_io_type
** (mydumper:19312): WARNING **: Broken table detected, please review: sys.host_summary_by_stages
** (mydumper:19312): WARNING **: Broken table detected, please review: sys.host_summary_by_statement_latency
** (mydumper:19312): WARNING **: Broken table detected, please review: sys.host_summary_by_statement_type
复制代码
这些 WARNING 信息并不会影响备份,但有几个问题:一、已经指定不备份 sys 库,为什么去检查 sys 库下边的表或视图。二、为什么提示 sys 库下的视图对象 【Broken table detected】信息。三、其它实例备份都没有 WARNING 信息,只在备份这个 RDS For MySQL 时有 WARNING 信息。
带着这些疑问,打算去源码中寻找答案。
2、下载源码到本地
github地址:https://github.com/mydumper/mydumper
克隆代码到本地:
git clone https://github.com/mydumper/mydumper.git
切换版本
git checkout v0.9.5
复制代码
3、查看源代码
MyDumper 源码不算太难,只看 mydump.c 文件就能了解 mydumper 备份流程。全篇搜索关键字【Broken table detected】,发现在【dump_database】函数的一个 while 循环中,当 is_view 为假 ,并且 row[ecol] 值为空,就输出这个告警信息。
while ((row = mysql_fetch_row(result))) {
.... //省略
/* Check for broken tables, i.e. mrg with missing source tbl */
if ( !is_view && row[ecol] == NULL ) {
g_warning("Broken table detected, please review: %s.%s", database, row[0]);
dump = 0;
}
..... // 省略部分代码
}
复制代码
查看函数调用关系:
main()
->start_dump()
->dump_database() -- WARNING 信息出现在这个函数中
复制代码
main -> start_dump -> dump_database
查看整段代码到dump_database
函数入口处,整个过程并没有过滤备份库表行为,从 start_dump()
代码中可以看到,Mydumper 会先执行 show databases
命令获取所有库名称,将所有库名称做为参数传入到 dump_database()
函数中,dump_database()
函检会测所有数据库对象,其实检测最后一步才判断是否为备份对象,如果是,就把它放到一个消息队列中。
# start_dump 函数部分代码
......
if (db) {
dump_database(conn, db, nufile, &conf);
if(!no_schemas)
dump_create_database(conn, db);
} else if (tables) {
get_tables(conn, &conf);
} else {
MYSQL_RES *databases;
MYSQL_ROW row;
if(mysql_query(conn,"SHOW DATABASES") || !(databases = mysql_store_result(conn))) {
g_critical("Unable to list databases: %s",mysql_error(conn));
exit(EXIT_FAILURE);
}
while ((row=mysql_fetch_row(databases))) {
if (!strcasecmp(row[0],"information_schema") || !strcasecmp(row[0], "performance_schema") || (!strcasecmp(row[0], "data_dictionary")))
continue;
dump_database(conn, row[0], nufile, &conf); //dump_database是一个重点函数,其中逻辑很多
/* Checks PCRE expressions on 'database' string */
if (!no_schemas && (regexstring == NULL || check_regex(row[0],NULL)))
dump_create_database(conn, row[0]);
}
mysql_free_result(databases);
...
复制代码
看到这里,第一个问题有了答案,即使只备份一个表,Mydumper 依然会将所有数据库对象检测一遍,dump_database
函数涉及到变量值比较多,通过阅读代码难以直接分析到变量值,准备使用 GDB 调试工具继续分析问题。
4、使用 GDB 分析问题
4.1 安装 GDB
apt install gdb
4.2 GDB 常用命令
list --查看当前位置代码
b N_LINE --在第N_LINE行上设置断点
r --从头开始运行程序,示例: r -h 127.0.0.1 -P 3357 -t 1 -W -u root -p 'c123456'
n --执行下一行语句, 如语句为函数调用, 不进入函数中
s --执行下一行语句, 如语句为函数调用, 进入函数中
info threads --显示所有线程
thread --显示当前线程ID
print --打印变量值
info break --查看所有断点
del xx --删除断点
continue --从当前位置继续运行程序
display --在每次暂停的时候输出表达式的值
复制代码
4.3 开始调试
4.3.1 进入 GDB 调试模式
# gdb mydumper
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
......
Type "apropos word" to search for commands related to "word"...
Reading symbols from mydumper...done.
(gdb)
复制代码
4.3.2 计划在 1452 行设置断点,因为 dump_database 函数在这个 while 循环中
while 循环源码示例:
......
1443 MYSQL_RES *databases;
1444 MYSQL_ROW row;
1445 if(mysql_query(conn,"SHOW DATABASES") || !(databases = mysql_store_result(conn))) {
1446 g_critical("Unable to list databases: %s",mysql_error(conn));
1447 exit(EXIT_FAILURE);
1448 }
....
1452 while ((row=mysql_fetch_row(databases))) {
1453 if (!strcasecmp(row[0],"information_schema") || !strcasecmp(row[0], "performance_schema") || (!strcasecmp(row[0], "data_dictionary")))
1454 continue;
1455 dump_database(conn, row[0], nufile, &conf);
......
复制代码
4.3.3 运行 GDB 程序,重点关注 (gdb)
行内容
① 在 1452 行设置断点;b 1452
② 开始执行程序;r -h rds.aliyuncs.com -P 3306 -t 1 -W -u -p '' --regex '^(xxxxx\.$)' -c -o /data/backup
③ 程序运行到 1452 行会暂停,输入【n】进入到 if 条件判断中;n
④ print row[0] 显示当前库名称,显示当前是值 information_schema 库;(gdb) print row[0]
$1 = 0x9ff598 "information_schema"
⑤程序继续运行,但循环没有结束,还是会经过 1452 行,会再次暂停,此次当前库是 test 库;c
⑥继续输入 c ,直到显示当前库名称是 sys 库;
调示代码:
(gdb) b 1452
Breakpoint 1 at 0x41dad9: file /tmp/mydumper/mydumper.c, line 1452.
(gdb) r -h rds.aliyuncs.com -P 3306 -t 1 -W -u -p '' --regex '^(test\.$)' -c -o /data/backup
Starting program: /usr/bin/mydumper -h rds.aliyuncs.com -P 3306 -t 1 -W -u -p '' --regex '^(test\.$)' -c -o /data/backup
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff56cc700 (LWP 12599)]
Thread 1 "mydumper" hit Breakpoint 1, start_dump (conn=0x9f16c0) at /tmp/mydumper/mydumper.c:1452
warning: Source file is more recent than executable.
1452 while ((row=mysql_fetch_row(databases))) {
(gdb) n
1453 if (!strcasecmp(row[0],"information_schema") || !strcasecmp(row[0], "performance_schema") || (!strcasecmp(row[0], "data_dictionary")))
(gdb) print row[0]
$1 = 0x9ff598 "information_schema"
(gdb) c
Continuing.
Thread 1 "mydumper" hit Breakpoint 1, start_dump (conn=0x9f16c0) at /tmp/mydumper/mydumper.c:1452
1452 while ((row=mysql_fetch_row(databases))) {
(gdb) n
1453 if (!strcasecmp(row[0],"information_schema") || !strcasecmp(row[0], "performance_schema") || (!strcasecmp(row[0], "data_dictionary")))
(gdb) print row[0]
$2 = 0x9ff5d8 "test"
(gdb)
...... //过程省略,直到遇到sys 库。
复制代码
⑦最终切换到 sys 库,输入 n
让程序运行到 dump_database
函数入口,输入 s
进入函数内部;
Thread 1 "mydumper" hit Breakpoint 1, start_dump (conn=0x9f16c0) at /tmp/mydumper/mydumper.c:1452
1452 while ((row=mysql_fetch_row(databases))) {
(gdb) n
1453 if (!strcasecmp(row[0],"information_schema") || !strcasecmp(row[0], "performance_schema") || (!strcasecmp(row[0], "data_dictionary")))
(gdb) print row[0]
$5 = 0x9ff680 "sys"
(gdb) n
1455 dump_database(conn, row[0], nufile, &conf);
(gdb) s
dump_database (conn=0x9f16c0, database=0x9ff680 "sys", file=0x0, conf=0x7fffffffe3d0) at /tmp/mydumper/mydumper.c:1915
1915 void dump_database(MYSQL * conn, char *database, FILE *file, struct configuration *conf) {
(gdb)
复制代码
⑧ 进入到 dump_database 函数内部后,开始单步运行,代码首先执行 show table status
查看库下所有表或视图,返回一些元数据和统计信息;
+-----------------------------------------------+--------+---------+------------+------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------------+----------+----------------+--------------------------------------------------------------------------------+
| Name | Engine | Version | Row_format | Rows | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Create_time | Update_time | Check_time | Collation | Checksum | Create_options | Comment |
+-----------------------------------------------+--------+---------+------------+------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------------+----------+----------------+--------------------------------------------------------------------------------+
| host_summary | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | execute command denied to user 'dbbackup'@'%' for routine 'sys.format_time' |
| host_summary_by_file_io | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | execute command denied to user 'dbbackup'@'%' for routine 'sys.format_time' |
| host_summary_by_file_io_type | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | execute command denied to user 'dbbackup'@'%' for routine 'sys.format_time' |
复制代码
这段代码主要为了确定两个变量值 ecol
和 ccol
,ecol 记录的是 Engine 列在 show tables status 结果中列的位置,ccol 记录的是 Comment 列在 show tables status 结果中列的位置,变量值 ecol=1 ,ccol=17。
源代码:
int ecol= -1, ccol= -1;
for (i=0; i<mysql_num_fields(result); i++) {
if (!strcasecmp(fields[i].name, "Engine")) ecol= i;
else if (!strcasecmp(fields[i].name, "Comment")) ccol= i;
}
调示代码:
Thread 1 "mydumper" hit Breakpoint 1, start_dump (conn=0x9f16c0) at /tmp/mydumper/mydumper.c:1452
1452 while ((row=mysql_fetch_row(databases))) {
(gdb) n
1453 if (!strcasecmp(row[0],"information_schema") || !strcasecmp(row[0], "performance_schema") || (!strcasecmp(row[0], "data_dictionary")))
(gdb) print row[0]
$5 = 0x9ff680 "sys"
(gdb) n
1455 dump_database(conn, row[0], nufile, &conf);
(gdb) s
dump_database (conn=0x9f16c0, database=0x9ff680 "sys", file=0x0, conf=0x7fffffffe3d0) at /tmp/mydumper/mydumper.c:1915
1915 void dump_database(MYSQL * conn, char *database, FILE *file, struct configuration *conf) {
(gdb) n
1919 mysql_select_db(conn,database);
(gdb) n
1920 if (detected_server == SERVER_TYPE_MYSQL)
(gdb) print detected_server == SERVER_TYPE_MYSQL
$6 = 1
(gdb) n
1921 query= g_strdup("SHOW TABLE STATUS");
(gdb) n
1925 if (mysql_query(conn, (query))) {
(gdb) n
1926 g_critical("Error: DB: %s - Could not execute query: %s", database, mysql_error(conn));
(gdb) n
1925 if (mysql_query(conn, (query))) {
(gdb) n
1932 MYSQL_RES *result = mysql_store_result(conn);
(gdb) n
1935 int ecol= -1, ccol= -1;
(gdb) n
1933 MYSQL_FIELD *fields= mysql_fetch_fields(result);
(gdb) n
1932 MYSQL_RES *result = mysql_store_result(conn);
(gdb) n
1935 int ecol= -1, ccol= -1;
(gdb) n
1936 for (i=0; i<mysql_num_fields(result); i++) {
(gdb) n
1933 MYSQL_FIELD *fields= mysql_fetch_fields(result);
(gdb) n
1936 for (i=0; i<mysql_num_fields(result); i++) {
(gdb) n
1937 if (!strcasecmp(fields[i].name, "Engine")) ecol= i;
(gdb) n
1938 else if (!strcasecmp(fields[i].name, "Comment")) ccol= i;
(gdb) n
1936 for (i=0; i<mysql_num_fields(result); i++) {
(gdb) n
1937 if (!strcasecmp(fields[i].name, "Engine")) ecol= i;
.... 省略
复制代码
⑨继续向下进入 WARNING 信息提示的代码处,首先定义了两个变量 dump
和 is_view
。
向下看第一个if
判断逻辑,detected_server == SERVER_TYPE_MYSQL
这个条件一定为真,暂时忽略掉,row[ccol] 为空 或 row[ccol] 为"VIEW 时,认为这个对象是视图,并设置 is_isview=1。
row[ccol] 字段对应的是 show table status
字段的 Comment
列,但实际结果 Comment
列既不为 NULL,也不是 VIEW,而是"execute command denied to user 'dbbackup'@'%' for routine 'sys.format_time'",所以 is_view 变量值还是 0。
向下看第二个if
判断逻辑,当 is_view=0 和 row[ecol] == NULL 时报 WARNING 信息,目前 is_view=0, show table status
的 Engine 列值为 NULL ,符合条件,输出 WARNING 信息。
梳理整个代码逻辑,Mydumper 用 show table status
结果中的 Comment
列值(为空或为 VIEW),来判断该对象是否为视图,如果不是视图,并且 Eengine
列值内容为空,就认为是一个有问题的视图。
分析到这里,也解开了 sys 库下边的视图提示【Broken table detected】的原因。是这个 RDS For MySQL 实例,在 sys 库执行 show table status 命令,返回的 Comment 列值为"execute command denied to user 'dbbackup'@'%' for routine 'sys.format_time'",并不是 VIEW,Mydumper 的检查逻辑认为这是个有问题的对象。
源代码:
MYSQL_ROW row;
while ((row = mysql_fetch_row(result))) {
int dump=1;
int is_view=0;
/* We now do care about views!
num_fields>1 kicks in only in case of 5.0 SHOW FULL TABLES or SHOW TABLE STATUS
row[1] == NULL if it is a view in 5.0 'SHOW TABLE STATUS'
row[1] == "VIEW" if it is a view in 5.0 'SHOW FULL TABLES'
*/
-- 第一个if
if ((detected_server == SERVER_TYPE_MYSQL) && ( row[ccol] == NULL || !strcmp(row[ccol],"VIEW") ))
is_view = 1;
/* Check for broken tables, i.e. mrg with missing source tbl */
-- 第二个if
if ( !is_view && row[ecol] == NULL ) {
g_warning("Broken table detected, please review: %s.%s", database, row[0]);
dump = 0;
}
调示代码:
(gdb) b 1948
Breakpoint 2 at 0x4193a8: file /tmp/mydumper/mydumper.c, line 1948.
(gdb) c
Continuing.
Thread 1 "mydumper" hit Breakpoint 2, dump_database (conn=0x9f16c0, database=<optimized out>, file=0x0, conf=0x7fffffffe3d0) at /tmp/mydumper/mydumper.c:1948
1948 while ((row = mysql_fetch_row(result))) {
(gdb) n
1958 if ((detected_server == SERVER_TYPE_MYSQL) && ( row[ccol] == NULL || !strcmp(row[ccol],"VIEW") ))
(gdb) print row[0]
$2 = 0xa03930 "host_summary"
(gdb) print row[1]
$3 = 0x0
(gdb) print row[17]
$4 = 0xa0393d "execute command denied to user 'dbbackup'@'%' for routine 'sys.format_time'"
(gdb) print detected_server == SERVER_TYPE_MYSQL
$5 = 1
(gdb) print is_view
$6 = 0
复制代码
4.3.4
继续分析为何只有这个 RDS 备份有问题,在备份正常实例的 sys 库下执行 show table status
命令,Comment
列均为 VIEW,会顺利通过 Mydumper 的判断,认为这是一个正常的视图。对比 RDS 之间的差异,出问题的阿里云 RDS For MySQL 实例购买时间较早,对应的小版本为 2021 版小版本,猜测是这个小版本在权限方面处理的和其它小版本不一样,导致的问题。
5、 总结
分析完问题原因后,又把 MyDumper 的源码整体看了一遍,它采用了生产者/消费者模式,在备份开始创建一个 queue 队列,然后依次检查数据库所有对象,在检查的最后一步来判断这个表是否为需要备份的表,如果是需要备份的库表,把它放到 queue 队列中,所有对象检查完成后, queue 队存中存储的就是需要备份的库表,最后消费线程开始消费 queue 队列内容,调用对应的函数备份库表。
关于领创集团
(Advance Intelligence Group)
领创集团成立于 2016 年,致力于通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈。集团旗下包含企业业务和消费者业务两大板块,企业业务包含 ADVANCE.AI 和 Ginee,分别为银行、金融、金融科技、零售和电商行业客户提供基于 AI 技术的数字身份验证、风险管理产品和全渠道电商服务解决方案;消费者业务 Atome Financial 包括亚洲领先的先享后付平台 Atome 和数字金融服务。2021 年 9 月,领创集团宣布完成超 4 亿美元 D 轮融资,融资完成后领创集团估值已超 20 亿美元,成为新加坡最大的独立科技创业公司之一。
评论