问题背景:
在使用 mydumepr 0.9.5 版本备份云上数据库时,偶尔会有告警信息,几乎都是 sys 库的,其实也没有备份这个库,不知道为什么会去检测 sys 库,并且还有告警信息。
** (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
这个问题在备份某个RDS FOR MYSQL5.7 实例时,可以稳定复现,想尝试从源码中寻找一些答案。
1、下载源码到本地,并切换版本
github地址:https://github.com/mydumper/mydumper
在本地克隆代码:
git clone https://github.com/mydumper/mydumper.git
切换版本
git checkout v0.9.5
参考官方文档编译安装
2、查看源代码
mydumper 整体上看应该不算太难,几乎只看 mydump.c 文件即可。
全篇搜索关键字【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() //备注:会执行 show table status 获取所有表名
start_dump() 涉及备份数据库的代码:
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);
/* Checks PCRE expressions on 'database' string */
if (!no_schemas && (regexstring == NULL || check_regex(row[0],NULL)))
dump_create_database(conn, row[0]);
}
小结:从函数调用代码看,即使我在mydumper时指定了备份的库表,但在正式备份数据前并没有做过滤,对所有表都会做一些预检。
3、开始 GDB 调试源代码
3.1 安装GDB命令
yum install gdb
3.2 常用命令
list 行号 --查看指定位置的行号
b 行号 --设置到断点的行号
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 在每次暂停的时候输出表达式的值
3.3 开始调试
3.3.1
进入GDB 模式
root@:~/czg/mydumper# 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)
3.3.2
打一个断点,在 1452 行处,因为 dump_database 在循环中
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);
3.3.3
运行程序,重点关注 【(gdb)】行内容,这些是我输入的命令,
进入到循环之内,第一个库并不是sys库,输入c 进入下一次循环,因为我们在 1452 行号有断点,所以还是会停下,继续查看是否是 sys 库,直接到遇到 sys 库。
(gdb) b 1452
Breakpoint 1 at 0x41dad9: file /tmp/mydumper/mydumper.c, line 1452.
(gdb) r -h rr-xxaliyuncs.com -P 3306 -t 1 -W -u -p '' -s 2000000 --regex '^(test\.$)' -c -o /root/test
Starting program: /usr/bin/mydumper -h rr-xxxaliyuncs.com -P 3306 -t 1 -W -u -p '' -s 2000000 --regex '^(test\.$)' -c -o /root/test
[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 库,进入到 dump_database 函数中
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 函数,开始单步运行,这段代码主要为了确定两个变量值
ecol 结果中,列名为 Engine 为的下标位置,这个值后面会用到来确定内容。
ccol 结果中,列名为 Comment 为的下标位置,这个值后面会用到来确定内容。
最后ecol =1 ccol=17
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;
(gdb) n
1936 for (i=0; i<mysql_num_fields(result); i++) {
(gdb) n
1937 if (!strcasecmp(fields[i].name, "Engine")) ecol= i;
继续向下,开始进入到 while 中,这个循环中(这是个截图)
再设置一个断点 b 【b 1948】,让程序进入到这个 while 循环中,这个循环中 is_view 初值是0
同时,这条记录的name是 host_summary ,也就是表名。
ecol =1
ccol=17
这是上次看到的变量值,对于查看变量值很关键
从第一个if 看,
detected_server == SERVER_TYPE_MYSQL //为True
row[17] 的值是 【0xa0393d "execute command denied to user 'dbbackup'@'%' for routine 'sys.format_time'"】 // 这个不符合【( row[ccol] == NULL || !strcmp(row[ccol],"VIEW") )】条件,所以 is_view变量值还是0
再看第二个if,
( !is_view && row[ecol] == NULL )
// !is_view 条件成立 ,row[1] 也确实是空值,条件成立 ,最终会打出告警信息
【** (mydumper:19312): WARNING **: Broken table detected, please review: sys.host_summary】
(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
(gdb) n
1962 if ( !is_view && row[ecol] == NULL ) {
(gdb) print is_view
$7 = 0
总结:
报错原因是mydumper切换到sys库,并且执行show table status命令获取所有表信息,并且获取Engine、Comment 这两个列的信息,并用Comment这个列的信息来判断是否是视图,如果为空值或者内容值是 VIEW ,则标记为视图,如果Comment列信息不是这些内容,并且Engine列的内容为空,则告警这个视图是损坏的。
问题的关键是show table status 结果中Comment的内容是【execute command denied to user 'dbbackup'@'%' for routine 'sys.format_time'】
所以mydumper认为这不是一个视图
我们使用的备份账号权限都是一样的,但只有比较早的RDS实例会有出现告警信息,应该是云上RDS小版本的原因,每次的权限范围或其它显示方面不太一样,最终导致了mydumper告警的原因。