写点什么

MyDumper 工具介绍

  • 2023-03-02
    北京
  • 本文字数:8278 字

    阅读完需:约 27 分钟

MyDumper工具介绍

前言

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 --显示当前线程IDprint --打印变量值info break --查看所有断点del xx --删除断点continue --从当前位置继续运行程序display --在每次暂停的时候输出表达式的值
复制代码

4.3 开始调试

4.3.1 进入 GDB 调试模式

# gdb mydumperGNU 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:1452warning: 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:14521452 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:14521452                    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:19151915 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'    |
复制代码


这段代码主要为了确定两个变量值 ecolccol ,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:14521452 while ((row=mysql_fetch_row(databases))) {(gdb) n1453 if (!strcasecmp(row[0],"information_schema") || !strcasecmp(row[0], "performance_schema") || (!strcasecmp(row[0], "data_dictionary")))(gdb) print row[0]$5 = 0x9ff680 "sys"(gdb) n1455 dump_database(conn, row[0], nufile, &conf);(gdb) sdump_database (conn=0x9f16c0, database=0x9ff680 "sys", file=0x0, conf=0x7fffffffe3d0) at /tmp/mydumper/mydumper.c:19151915 void dump_database(MYSQL * conn, char *database, FILE *file, struct configuration *conf) {(gdb) n1919 mysql_select_db(conn,database);(gdb) n1920 if (detected_server == SERVER_TYPE_MYSQL)(gdb) print detected_server == SERVER_TYPE_MYSQL$6 = 1(gdb) n1921 query= g_strdup("SHOW TABLE STATUS");(gdb) n1925 if (mysql_query(conn, (query))) {(gdb) n1926 g_critical("Error: DB: %s - Could not execute query: %s", database, mysql_error(conn));(gdb) n1925 if (mysql_query(conn, (query))) {(gdb) n1932 MYSQL_RES *result = mysql_store_result(conn);(gdb) n1935 int ecol= -1, ccol= -1;(gdb) n1933 MYSQL_FIELD *fields= mysql_fetch_fields(result);(gdb) n1932 MYSQL_RES *result = mysql_store_result(conn);(gdb) n1935 int ecol= -1, ccol= -1;(gdb) n1936 for (i=0; i<mysql_num_fields(result); i++) {(gdb) n1933 MYSQL_FIELD *fields= mysql_fetch_fields(result);(gdb) n1936 for (i=0; i<mysql_num_fields(result); i++) {(gdb) n1937 if (!strcasecmp(fields[i].name, "Engine")) ecol= i;(gdb) n1938 else if (!strcasecmp(fields[i].name, "Comment")) ccol= i;(gdb) n1936 for (i=0; i<mysql_num_fields(result); i++) {(gdb) n1937 if (!strcasecmp(fields[i].name, "Engine")) ecol= i;
.... 省略
复制代码


⑨继续向下进入 WARNING 信息提示的代码处,首先定义了两个变量 dumpis_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 1948Breakpoint 2 at 0x4193a8: file /tmp/mydumper/mydumper.c, line 1948.(gdb) cContinuing.
Thread 1 "mydumper" hit Breakpoint 2, dump_database (conn=0x9f16c0, database=<optimized out>, file=0x0, conf=0x7fffffffe3d0) at /tmp/mydumper/mydumper.c:19481948 while ((row = mysql_fetch_row(result))) {(gdb) n1958 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 亿美元,成为新加坡最大的独立科技创业公司之一。

发布于: 刚刚阅读数: 8
用户头像

智慧领创美好生活 2021-08-12 加入

AI技术驱动的科技集团,致力于以技术赋能为核心,通过科技创新的本地化应用,改造和重塑金融和零售行业,以多元化的业务布局打造一个服务于消费者、企业和商户的生态圈,带来个性化、陪伴式的产品服务和优质体验。

评论

发布
暂无评论
MyDumper工具介绍_MySQL_领创集团Advance Intelligence Group_InfoQ写作社区