昨晚发现有大量的sql执行失败, 使用show processlist
发现存在大量的Waiting for table flush
状态的连接.如下图(下图为复现环境)
就这么几个连接, 排除自己, 就只剩一个select的了, 总不能是select导致的其它表Waiting for table flush
吧, 我们再看下mdl相关信息:
-- 如果没有开启mdl, 可以使用如下sql开启
-- update performance_schema.setup_instruments set ENABLED='YES' where name ='wait/lock/metadata/sql/mdl';
-- 查询mdl锁
select * from performance_schema.metadata_locks;
都是SHARED_READ啊
首先我们来看下什么情况会产生Waiting for table flush
, 查询官网 发现: FLUSH TABLES, FLUSH TABLES tbl_name, ALTER TABLE, RENAME TABLE, REPAIR TABLE, ANALYZE TABLE, OPTIMIZE TABLE 均会导致产生这个状态.
第一个命令(FLUSH TABLES)是不是看着很眼熟? 就是我们使用mysqldump备份时会执行的, 而且也会因为存在大事务, 导致其状态变为Waiting for table flush
, 于是我们查看下备份时间点, 恰好能对得上. 说明就是我们备份导致的业务查询失败, 问题就解决了!
且慢, 我们备份已经跑完了啊, 而且也失败了. 那为啥还是会出现大量的Waiting for table flush
, 而且即使是新连接进来查询该表也会出现这个状态.
这种情况我们通常会猜测: 后面的查询都堵在某个队列里面, 只要最原始的sql不执行完, 后面的就永远只能堵着. 那么kill
掉第一个查询, 后面的sql就能跑完. 我们来验证下:
当我们Kill掉"罪魁祸首"之后, 后面的sql确实执行成功了. 那真的有这么个队列吗? 总感觉有点不太对
在我们深入分析前, 我们先复现下问题吧. 刚才都分析清楚了,是有大事务的时候,执行flush tables
就会导致后续该表的查询堵着.
-- 数据准备
drop procedure if exists pro_insert_nrows;
delimiter //
create procedure pro_insert_nrows( IN rows1 int)
begin
declare n int;
set n=1;
set autocommit=off;
while n <= rows1
do
insert into t20250627 values(n,n,md5(n));
set n=n+1;
end while;
commit;
end//
delimiter ;
create table t20250627(c1 int,c2 int,c3 varchar(200));
create table t20250627_2(c1 int,c2 int,c3 varchar(200));
create table t20250627_3(c1 int,c2 int,c3 varchar(200));
call pro_insert_nrows(200000);
insert into t20250627 select * from t20250627;
insert into t20250627 select * from t20250627;
insert into t20250627 select * from t20250627;
session 1: 模拟大事务
select count(*) from t20250627 as a,t20250627 as b where a.c2 in (select c1 from t20250627 order by c3);
session 2: 模拟备份
flush tables
session 3: 模拟正常业务查询
select * from t20250627 limit 1
用sleep模拟大事务应该也是可以的.
这次我们使用gdb查看堆栈信息, 先看下堵塞连接的线程id:
select * from performance_schema.threads where NAME='thread/sql/one_connection'
然后我们使用gdb来查看下该线程的堆栈信息
gdb -p `pidof mysqld`
使用info thread
查看线程信息, 然后使用thread n
切换到我们刚才查询到的堵塞的线程, 然后使用bt
查看堆栈信息
然后我们稍微整理下堆栈信息,得到如下:
pthread_cond_timedwait
native_cond_timedwait
my_cond_timedwait
inline_mysql_cond_timedwait
MDL_wait::timed_wait
TABLE_SHARE::wait_for_old_version
tdc_wait_for_old_version
open_table
open_and_process_table
open_tables
open_tables_for_query
execute_sqlcom_select
mysql_execute_command
mysql_parse
dispatch_command
do_command
handle_connection
也就是 解析SQL, 打开表时, wait_for_old_version了. 那么进入这个wait_for_old_version的依据是什么呢? 我们查询sql/sql_base.cc发现如下逻辑:
if (!(flags & MYSQL_OPEN_IGNORE_FLUSH))
{
if (share->has_old_version())
{
.......
wait_result= tdc_wait_for_old_version(thd, table_list->db,
table_list->table_name,
ot_ctx->get_timeout(),
deadlock_weight);
.....
}
}
也就是判断到has_old_version了. 我们在sql/table.h里面找到了has_old_version的逻辑:
struct TABLE_SHARE{
/**
TABLE_SHARE version, if changed the TABLE_SHARE must be reopened.
NOTE: The TABLE_SHARE will not be reopened during LOCK TABLES in
close_thread_tables!!!
*/
ulong version;
inline bool has_old_version() const
{
return version != refresh_version;
}
}
也就是这个TABLE_SHARE的版本如果低于refresh_version的话, 就会去wait_for_old_version, 然后堵着. 那么这个refresh_version又是啥呢? 我们发现其定义为: sql/mysqld.cc
ulong refresh_version; /* Increments on each reload */
看起来是每次reload就会++, 也就是我们的flush table可能触发了这个refresh_version++, 导致和table_share的版本. 我们来瞅瞅.
gdb -p `pidof mysqld` --batch --ex "print refresh_version" | grep '\$1'
我们发现即使我们的flush tables失败了, 这个refresh_version还是会加1 对应源码如下:
bool close_cached_tables(THD *thd, TABLE_LIST *tables,
bool wait_for_refresh, ulong timeout)
{
...
if (!tables)
{
/*
Force close of all open tables.
Note that code in TABLE_SHARE::wait_for_old_version() assumes that
incrementing of refresh_version and removal of unused tables and
shares from TDC happens atomically under protection of LOCK_open,
or putting it another way that TDC does not contain old shares
which don't have any tables used.
*/
refresh_version++;
table_cache_manager.free_all_unused_tables();
/* Free table shares which were not freed implicitly by loop above. */
while (oldest_unused_share->next)
(void) my_hash_delete(&table_def_cache, (uchar*) oldest_unused_share);
}
...
}
逻辑比较简单, 就是先refresh_version++; 然后遍历表并关闭. 我们来gdb看下堆栈呢(break close_cached_tables)
(gdb) break close_cached_tables
Breakpoint 1 at 0xd18122: file /var/lib/pb2/sb_1-12949965-1697025378.23/mysql-5.7.44/sql/sql_base.cc, line 1208.
(gdb) c
Continuing.
[Switching to Thread 0x7f94753ac700 (LWP 26720)]
Breakpoint 1, close_cached_tables (thd=0x7f94300008c0, tables=0x0, wait_for_refresh=true, timeout=31536000)
at /var/lib/pb2/sb_1-12949965-1697025378.23/mysql-5.7.44/sql/sql_base.cc:1208
1208 /var/lib/pb2/sb_1-12949965-1697025378.23/mysql-5.7.44/sql/sql_base.cc: No such file or directory.
(gdb) bt
#0 close_cached_tables (thd=0x7f94300008c0, tables=0x0, wait_for_refresh=true, timeout=31536000)
at /var/lib/pb2/sb_1-12949965-1697025378.23/mysql-5.7.44/sql/sql_base.cc:1208
#1 0x0000000000d9986a in reload_acl_and_cache (thd=0x7f94300008c0, options=4, tables=0x0, write_to_binlog=0x7f94753ab270)
at /var/lib/pb2/sb_1-12949965-1697025378.23/mysql-5.7.44/sql/sql_reload.cc:293
#2 0x0000000000d69712 in mysql_execute_command (thd=0x7f94300008c0, first_level=true)
at /var/lib/pb2/sb_1-12949965-1697025378.23/mysql-5.7.44/sql/sql_parse.cc:4150
#3 0x0000000000d6e1ad in mysql_parse (thd=0x7f94300008c0, parser_state=<optimized out>)
at /var/lib/pb2/sb_1-12949965-1697025378.23/mysql-5.7.44/sql/sql_parse.cc:5584
#4 0x0000000000d6f9e8 in dispatch_command (thd=0x7f94300008c0, com_data=0x7f94753abda0, command=COM_QUERY)
at /var/lib/pb2/sb_1-12949965-1697025378.23/mysql-5.7.44/sql/sql_parse.cc:1492
#5 0x0000000000d70594 in do_command (thd=0x7f94300008c0)
at /var/lib/pb2/sb_1-12949965-1697025378.23/mysql-5.7.44/sql/sql_parse.cc:1031
#6 0x0000000000e43b2c in handle_connection (arg=<optimized out>)
at /var/lib/pb2/sb_1-12949965-1697025378.23/mysql-5.7.44/sql/conn_handler/connection_handler_per_thread.cc:313
#7 0x000000000123a884 in pfs_spawn_thread (arg=0x8cf69c0)
at /var/lib/pb2/sb_1-12949965-1697025378.23/mysql-5.7.44/storage/perfschema/pfs.cc:2197
#8 0x00007f956481fea5 in start_thread () from /lib64/libpthread.so.0
#9 0x00007f95632d796d in clone () from /lib64/libc.so.6
到这里基本上就能确定是flush时,refresh_version++, 然后由于大事务导致超时(其实无所谓了,只要执行了就会导致), 其它会话open_table的时候发现share->version != refresh_version 于是就去wait_for_old_version...
我们还发现flush tables会刷binlog, 以前都没注意过.
也就是备份给refresh_version++了, 然后后面的表就只能去open_table, 但是有个大事务'挡着'的, 就只能等着(知道超时)
差不多就是如下图:
本次问题算是之前 大事务导致备份失败的 后续问题分析吧, 但当时那个场景是没得业务查询的, 所以未发现这个隐藏的坑.
建议: 1. 优化业务逻辑, 减少大事务/长时间执行的sql 2.备份时间要选在无大事务时间点(但业务变更可能就撞上来了)
测试发现
flush tables with read lock
也会导致refresh_version++, 也就是备份myisam表时, 如果有大事务, 也会gg
参考:
https://dev.mysql.com/doc/refman/8.0/en/general-thread-states.html
https://dev.mysql.com/doc/refman/8.0/en/performance-schema-metadata-locks-table.html
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。