首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >[MYSQL] 出现大量的Waiting for table flush导致业务表查询不了

[MYSQL] 出现大量的Waiting for table flush导致业务表查询不了

原创
作者头像
大大刺猬
发布2025-06-27 17:04:54
发布2025-06-27 17:04:54
2550
举报
文章被收录于专栏:大大刺猬大大刺猬

导读

昨晚发现有大量的sql执行失败, 使用show processlist发现存在大量的Waiting for table flush状态的连接.如下图(下图为复现环境)

就这么几个连接, 排除自己, 就只剩一个select的了, 总不能是select导致的其它表Waiting for table flush吧, 我们再看下mdl相关信息:

代码语言:sql
复制
-- 如果没有开启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就会导致后续该表的查询堵着.

代码语言:sql
复制
-- 数据准备
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: 模拟大事务

代码语言:sql
复制
select count(*) from t20250627 as a,t20250627 as b where a.c2 in (select c1 from t20250627 order by c3);

session 2: 模拟备份

代码语言:sql
复制
flush tables

session 3: 模拟正常业务查询

代码语言:sql
复制
select * from t20250627  limit 1

用sleep模拟大事务应该也是可以的.

再次分析

这次我们使用gdb查看堆栈信息, 先看下堵塞连接的线程id:

代码语言:sql
复制
select * from performance_schema.threads where NAME='thread/sql/one_connection'

然后我们使用gdb来查看下该线程的堆栈信息

代码语言:shell
复制
gdb -p `pidof mysqld`

使用info thread 查看线程信息, 然后使用thread n 切换到我们刚才查询到的堵塞的线程, 然后使用bt查看堆栈信息

然后我们稍微整理下堆栈信息,得到如下:

代码语言:txt
复制
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发现如下逻辑:

代码语言:c++
复制
  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的逻辑:

代码语言:c++
复制
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

代码语言:c++
复制
ulong refresh_version;  /* Increments on each reload */

看起来是每次reload就会++, 也就是我们的flush table可能触发了这个refresh_version++, 导致和table_share的版本. 我们来瞅瞅.

代码语言:shell
复制
gdb -p `pidof mysqld` --batch --ex "print refresh_version" | grep '\$1'

我们发现即使我们的flush tables失败了, 这个refresh_version还是会加1 对应源码如下:

代码语言:c++
复制
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)

代码语言:txt
复制
(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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导读
  • 初步分析
  • 复现
  • 再次分析
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档