• 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)。

  • 相关阅读:
    Kafka写入流程和副本策略
    Kafka消费分组和分区分配策略
    Kafka安装和常用操作命令
    面试:谈谈你对大数据的理解
    HDFS写数据和读数据流程
    java笔记之分支、循环语句
    java笔记之分支、循环语句
    java笔记之运算符
    java笔记之运算符
    java笔记之java内存结构
  • 原文地址:https://www.cnblogs.com/gered/p/16468945.html
Copyright © 2020-2023  润新知