转自:https://www.modb.pro/db/401005
【1】Flush Tables With Read Lock
一、FTWRL的原理
实际上这部分我们可以在函数mysql_execute_command寻找case SQLCOM_FLUSH 的部分,实际上主要调用函数为reload_acl_and_cache,其中核心部分为:
if (thd->global_read_lock.lock_global_read_lock(thd))//加 MDL GLOBAL 级别S锁
return 1; // Killed
if (close_cached_tables(thd, tables, //关闭表操作释放 share 和 cache
((options & REFRESH_FAST) ? FALSE : TRUE),
thd->variables.lock_wait_timeout)) //等待时间受lock_wait_timeout影响
{
/*
NOTE: my_error() has been already called by reopen_tables() within
close_cached_tables().
*/
result= 1;
}
if (thd->global_read_lock.make_global_read_lock_block_commit(thd)) // MDL COMMIT 锁
{
/* Don't leave things in a half-locked state */
thd->global_read_lock.unlock_global_read_lock(thd);
return 1;
}
更具体的关闭表的操作和释放table缓存的部分包含在函数close_cached_tables中,我就不详细写了。但是我们需要明白table缓存实际上包含两个部分:
-
table cache define:每一个表第一次打开的时候都会建立一个静态的表定义结构内存,当多个会话同时访问同一个表的时候,从这里拷贝成相应的instance供会话自己使用。由参数table_definition_cache定义大小,由状态值Open_table_definitions查看当前使用的个数。对应函数get_table_share。
-
table cache instance:同上所述,这是会话实际使用的表定义结构是一个instance。由参数table_open_cache定义大小,由状态值Open_tables查看当前使用的个数。对应函数open_table_from_share。
这里我统称为table缓存,好了下面是我总结的FTWRl的大概步骤:
第一步: 加MDL LOCK类型为GLOBAL 级别为S。如果出现等待状态为‘Waiting for global read lock’。注意select语句不会上GLOBAL级别上锁,但是DML/DDL/FOR UPDATE语句会上GLOBAL级别的IX锁,IX锁和S锁不兼容会出现这种等待。下面是这个兼容矩阵:
| Type of active |
Request | scoped lock |
type | IS(*) IX S X |
---------+------------------+
IS | + + + + |
IX | + + - - |
S | + - + - |
X | + - - - |
第二步:推进全局表缓存版本。源码中就是一个全局变量 refresh_version++。
第三步:释放没有使用的table 缓存。可自行参考函数close_cached_tables函数。
第四步:判断是否有正在占用的table缓存,如果有则等待,等待占用者释放。等待状态为'Waiting for table flush'。这一步会去判断table缓存的版本和全局表缓存版本是否匹配,如果不匹配则等待而等待的结束就是占用的table缓存的占用者释放,这个释放操作存在于函数close_thread_table中,如下:
if (table->s->has_old_version() || table->needs_reopen() ||
table_def_shutdown_in_progress)
{
tc->remove_table(table);//关闭 table cache instance
mysql_mutex_lock(&LOCK_open);
intern_close_table(table);//去掉 table cache define
mysql_mutex_unlock(&LOCK_open);
}
最终会调用函数MDL_wait::set_status将FTWRL唤醒,也就是说对于正在占用的table缓存释放者不是FTWRL会话而是占用者自己。不管怎么样最终整个table缓存将会被清空,如果经过FTWRL后去查看Open_table_definitions和Open_tables将会发现重新计数了。下面是唤醒函数的代码,也很明显:
bool MDL_wait::set_status(enum_wait_status status_arg) open_table
{
bool was_occupied= TRUE;
mysql_mutex_lock(&m_LOCK_wait_status);
if (m_wait_status == EMPTY)
{
was_occupied= FALSE;
m_wait_status= status_arg;
mysql_cond_signal(&m_COND_wait_status);//唤醒
}
mysql_mutex_unlock(&m_LOCK_wait_status);//解锁
return was_occupied;
}
第五步:加MDL LOCK类型COMMIT 级别为S。如果出现等待状态为‘Waiting for commit lock’。如果有大事务的提交很可能出现这种等待。
二、FTWRL堵塞和被堵塞场景
(1)被什么堵塞
- 长时间的DDL\DML\FOR UPDATE堵塞FTWRL,因为FTWRL需要获取 GLOBAL的S锁,而这些语句都会对GLOBAL持有IX(MDL_INTENTION_EXCLUSIVE)锁,根据兼容矩阵不兼容。等待为:Waiting for global read lock 。本文的案例1就是这种情况。
- 长时间的select堵塞FTWRL, 因为FTWRL会释放所有空闲的table缓存,如果有占用者占用某些table缓存,则会等待占用者自己释放这些table缓存。等待为:Waiting for table flush 。即便KILL FTWRL会话也不行,除非KILL掉长时间的select操作才行。实际上flush table也会存在这种堵塞情况。
- 长时间的commit(如大事务提交)也会堵塞FTWRL,因为FTWRL需要获取COMMIT的S锁,而commit语句会对commit持有IX(MDL_INTENTION_EXCLUSIVE)锁,根据兼容矩阵不兼容。
(2)堵塞什么
- FTWRL会堵塞DDL\DML\FOR UPDATE操作,堵塞点为 GLOBAL级别 的S锁,等待为:Waiting for global read lock 。
- FTWRL会堵塞commit操作,堵塞点为COMMIT的S锁,等待为Waiting for commit lock 。
- FTWRL不会堵塞select操作,因为select不会在GLOBAL级别上锁。
最后提醒一下很多备份工具都要执行FTWRL操作,包含mysqldump和5.7的XTRBACKUP ,一定要注意它的堵塞/被堵塞场景和特殊场景,当然xtrbackup 8.0有所改善,我们后面进行分析
三、慢查询对FTWRL的记录方式
通常来讲DML(SELECT FOR UPDATE)和SELECT都会堵塞FTWRL,具体参见上面,而且我们知道这都是MDL LOCK堵塞,在以往的慢查询认知中,MDL LOCK堵塞是计入到慢查询的LOCK time,但是FTWRL却不一样。因为FTWRL不会过接口mysql_lock_tables,因此MDL LOCK堵塞时间不会被慢查询记录,正常的语句关于慢查询的部分大概如下:
1、语句开始, 记录开始时间点为 X
2、获取MDL LOCK -> 正常语句如果本处超时(超过lock_wait_timeout)直接退出,也不会记录到下面一步记录的lock time时间中,因此lock time为0。
3、上MySQL层锁(比如myisam,记录lock time时间差量(1-3的时间差量Y)
4、优化器优化语句。
5、执行器开始执行语句,与innodb层交互, 如果每行需要等待innodb行锁,记录每次获取的lock time时间差量(第5步的每次等待innodb行锁的时间差量综合为Y)
6、语句结束,触发记录慢查询接口,并且获取当前时间点为(Z)判定记录慢查询如下。
如果(Z-X)-Y > 慢查询设置的时间则需要记录
也就是慢查询 query time:Z-X lock time为:Y
需要注意的是语句异常结束也会记录慢查询。
但是FTWRL不记录Y这个时间,因此慢查询中对于FTWRL 统计的方式变为了(Z-X),也就是它执行了多久就是多久,且lock time为0。*
超时(lock_wait_timeout设置为120)
# Time: 2022-05-04T16:26:35.226862+08:00
# User@Host: root[root] @ localhost [] Id: 10534
# Query_time: 120.004726 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0
SET timestamp=1651652675;
flush table with read lock;
kill FTWRL
# Time: 2022-05-04T16:27:22.215598+08:00
# User@Host: root[root] @ localhost [] Id: 10534
# Query_time: 30.727657 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0
SET timestamp=1651652811;
flush table with read lock;
上面两个例子表明了这一点,我们注意到Lock_time为0。还需要注意的是如果正常语句遇到lock_wait_timeout超时记录的慢查询的Lock_time也是0,原因一致(不过接口mysql_lock_tables),如下:
# Time: 2022-05-06T11:06:29.665024Z
# User@Host: root[root] @ localhost [] Id: 257
# Schema: test Last_errno: 1205 Killed: 0
# Query_time: 120.004564 Lock_time: 0.000000 Rows_sent: 0 Rows_examined: 0 Rows_affected: 0
# Bytes_sent: 67
SET timestamp=1651835189;
select * from testup11;
可以看到这是一个select由于Waiting for table flush等待超时,但是语句Lock_time为0,执行时间大约为120秒,也就是超时时间(lock_wait_timeout设置为120)。