mysql FTWRL


转自: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)。

原创文章,作者:kirin,如若转载,请注明出处:https://blog.ytso.com/tech/database/273833.html

(0)
上一篇 2022年7月12日 16:38
下一篇 2022年7月12日 16:38

相关推荐

发表回复

登录后才能评论