暂无图片
暂无图片
2
暂无图片
暂无图片
暂无图片

before_dml hook 引发的drop table阻塞事件

MySQLLabs 2020-09-16
1877


其实关于MySQL drop table操作存在的风险,是个老生常谈的问题, 百度随便搜一下就能看到很多关于它的讨论,相关的链接上千万。

结论总结下就是:

  1. 删除表的过程需要从操作系统层面删除物理文件(其实是调用unlink),文件越大,需要的时间越长,并且占用IO资源。

  2. 删除表的过程涉及到很多其他的内部流程,比如释放AHI,缓存等等。

  3. drop table主要耗时在InnoDB层全局的字典信息mutext锁保护下进行,所以会和其他dml冲突。

给出的解决方案可能是:

  1. 使用硬连接加速文件移除的过程(drop table伪移除ibd文件)

  2. 关闭AHI


然而之所以会在深夜发文,肯定是有不一样的发现,正如标题所示。


现象:一条drop命令后,几乎所有写入操作都阻塞在Opening tables 
通过pstack进行线程栈跟踪,可以看到被阻塞的线程栈几乎一致:
Thread 2 (Thread 0x7fab1c0c3700 (LWP 2371)):
#0 0x0000003fb360b68c in pthread_cond_wait@@GLIBC_2.3.2 () from lib64/libpthread.so.0
#1 0x0000000001039c5b in os_event::wait_low(long) ()
#2 0x00000000010e49f9 in sync_array_wait_event(sync_array_t*, sync_cell_t*&) ()
#3 0x0000000000fb3efa in TTASEventMutex<GenericPolicy>::wait ()
#4 0x0000000000fcaaf5 in PolicyMutex<TTASEventMutex<GenericPolicy> >::enter(unsigned int, unsigned int, char const*, unsigned int) ()
#5 0x0000000000fc250a in ha_innobase::get_foreign_key_list(THD*, List<st_foreign_key_info>*) ()
#6 0x0000000000c605df in has_cascade_foreign_key(TABLE*, THD*) ()
#7 0x0000000000c612a0 in Trans_delegate::prepare_table_info(THD*, Trans_table_info*&, unsigned int&) ()
#8 0x0000000000c64088 in Trans_delegate::before_dml(THD*, int&) ()
#9 0x0000000000c9ade1 in run_before_dml_hook(THD*) ()
#10 0x0000000000d7d327 in Sql_cmd_update::try_single_table_update(THD*, bool*) ()
#11 0x0000000000d7e3cc in Sql_cmd_update::execute(THD*) ()
#12 0x0000000000cf6149 in mysql_execute_command(THD*, bool) ()
#13 0x0000000000cfa725 in mysql_parse(THD*, Parser_state*) ()
#14 0x0000000000cfb948 in dispatch_command(THD*, COM_DATA const*, enum_server_command) ()
#15 0x0000000000cfc834 in do_command(THD*) ()
#16 0x0000000000dc9b9c in handle_connection ()
#17 0x0000000000f46844 in pfs_spawn_thread ()
#18 0x0000003fb3607aa1 in start_thread () from lib64/libpthread.so.0
#19 0x0000003fb32e8aad in clone () from lib64/libc.so.6

一开始在线下进行复测时,通过sysbenck进行压力测试,并同时drop不相干的表,虽然也会出现阻塞,但基本上不会出现阻塞在Opening tables。对比pstack结果和源代码才发现这其中另有隐情。可以简单看下run_before_dml_hook函数,
    int run_before_dml_hook(THD *thd)
    {
    int out_value= 0;
    (void) RUN_HOOK(transaction, before_dml, (thd, out_value));


    if (out_value)
    my_error(ER_BEFORE_DML_VALIDATION_ERROR, MYF(0));


    return out_value;
    }
    RUN_HOOK宏定义
      /*
      if there is no observers in the delegate, we can return 0
      immediately.
      */
      #define RUN_HOOK(group, hook, args) \
      (group ##_delegate->is_empty() ? \
      0 : group ##_delegate->hook args)
      MySQL的插件机制提供了很多观察点,插件可以通过注册来实现某个关键节点的功能扩展。而此处涉及到的就是before_dml hook,就是在执行dml前去回调插件注册的相关函数。由于复测环境并没有安装半同步复制插件,导致无法进入和生产一样的逻辑,也观察不到一致的现象(任何不一致的实验条件都有可能造成结果的偏差,即使它看起来毫无关联)。
      回来继续,只有在加载半同步复制master插件(或者其他插件向transaction_delegate进行过注册)后,每次dml操作,都会触发对于before_dml这个hook的回调,以一个insert操作为例,过程如下:
      ...//省略若干层调用
      error= mysql_execute_command(thd, true);
      mysql_execute_command(THD*, bool)
      Sql_cmd_insert::execute(THD*)
      Sql_cmd_insert::mysql_insert(THD*, TABLE_LIST*)
      run_before_dml_hook(THD*)
      Trans_delegate::before_dml(THD*, int&)
      Trans_delegate::prepare_table_info(THD*, Trans_table_info*&, unsigned int&)
      has_cascade_foreign_key(TABLE*, THD*)
      ha_innobase::get_foreign_key_list(THD*, List<st_foreign_key_info>*)

      在进行函数回调前,需要去获取如下两个信息,并且进行封装当作参数传递给插件处理
      prepare_table_info(thd, param.tables_info, param.number_of_tables);
      prepare_transaction_context(thd, param.trans_ctx_info);

      其中table_info包含了表名,存储引擎类型,外键信息等等,trans_ctx_info则包含了事务上下文信息。而问题点就在于获取表的外键信息时,需要对inondb的dict_sys加锁,可以参照函数
      int
      ha_innobase::get_foreign_key_list(
      /*==============================*/
      THD* thd, /*!< in: user thread handle */
      List<FOREIGN_KEY_INFO>* f_key_list) /*!< out: foreign key list */
      {
      update_thd(ha_thd());

      TrxInInnoDB trx_in_innodb(m_prebuilt->trx);
      m_prebuilt->trx->op_info = "getting list of foreign keys";
      mutex_enter(&dict_sys->mutex);//加锁
      for (dict_foreign_set::iterator it
      = m_prebuilt->table->foreign_set.begin();
      it != m_prebuilt->table->foreign_set.end();
      ++it) {

      FOREIGN_KEY_INFO* pf_key_info;
      dict_foreign_t* foreign = *it;

      pf_key_info = get_foreign_key_info(thd, foreign);

      if (pf_key_info != NULL) {
      f_key_list->push_back(pf_key_info);
      }
      }
      mutex_exit(&dict_sys->mutex); //释放锁
      m_prebuilt->trx->op_info = "";
      return(0);
      }

      即便你对innodb的dictionary system不熟悉也是很好理解这个逻辑的,这个过程和drop table的主要逻辑是冲突的。然而通过加锁保护获取到的这些信息仅对于某些插件有用,对于半同步复制插件来说完全没用,而有用的插件你可能完全没有加载。可以看下半同步master插件在注册事务观察者时传入的关于before_dml这个钩子的函数
      //semisync_master_plugin.cc:407
      Trans_observer trans_observer = {
      sizeof(Trans_observer), // len

      repl_semi_report_before_dml, //before_dml
      repl_semi_report_before_commit, // before_commit
      repl_semi_report_before_rollback, // before_rollback
      repl_semi_report_commit, // after_commit
      repl_semi_report_rollback, // after_rollback
      };

      参照Trans_observer结构体定义
      /**
      Observes and extends transaction execution
      */

      typedef struct Trans_observer {
      uint32 len;
      int (*before_dml)(Trans_param *param, int& out_val);
      int (*before_commit)(Trans_param *param);
      int (*before_rollback)(Trans_param *param);
      int (*after_commit)(Trans_param *param);
      int (*after_rollback)(Trans_param *param);
      } Trans_observer;

      befor_dml函数指针在rpl_semi_sync_master中指向函数repl_semi_report_before_dml,但它的实现是空的,rpl_semi_sync_master真的是对执行dml之前毫无兴趣(仅限官方版本,其他分支没有去查看)。
      //semisync_master_plugin.cc:77
      int repl_semi_report_before_dml(Trans_param *param, int& out)
      {
      return 0;
      }

      而且如果加载了半同步master插件的情况下,即便不开启半同步 ,每次dml操作前都需要去获取table_info以及trans_ctx_info,函数调用结束后再销毁这些对象。实际上,这些信息可以用于MGR对于外键的检测,和半同步插件原理类似,参照MGR源代码函数group_replication_trans_before_dml(MGR很多限制的检测都是在这里进行的)
      //observer_trans.cc
      /*
      Transaction lifecycle events observers.
      */


      int group_replication_trans_before_dml(Trans_param *param, int &out)
      {
      DBUG_TRACE;
      ... //省略部分代码

      if ((out += (param->trans_ctx_info.transaction_write_set_extraction ==
      HASH_ALGORITHM_OFF))) {
      /* purecov: begin inspected */
      LogPluginErr(ERROR_LEVEL, ER_GRP_RPL_TRANS_WRITE_SET_EXTRACTION_NOT_SET);
      return 0;
      /* purecov: end */
      }

      if (local_member_info->has_enforces_update_everywhere_checks() &&
      (out += (param->trans_ctx_info.tx_isolation == ISO_SERIALIZABLE))) {
      LogPluginErr(ERROR_LEVEL, ER_GRP_RPL_UNSUPPORTED_TRANS_ISOLATION);
      return 0;
      }

      for (uint table = 0; out == 0 && table < param->number_of_tables; table++) {
      if (param->tables_info[table].db_type != DB_TYPE_INNODB) {
      LogPluginErr(ERROR_LEVEL, ER_GRP_RPL_NEEDS_INNODB_TABLE,
      param->tables_info[table].table_name);
      out++;
      }

      if (param->tables_info[table].number_of_primary_keys == 0) {
      LogPluginErr(ERROR_LEVEL, ER_GRP_RPL_PRIMARY_KEY_NOT_DEFINED,
      param->tables_info[table].table_name);
      out++;
      }
      if (local_member_info->has_enforces_update_everywhere_checks() &&
      param->tables_info[table].has_cascade_foreign_key) {
      LogPluginErr(ERROR_LEVEL, ER_GRP_RPL_FK_WITH_CASCADE_UNSUPPORTED,
      param->tables_info[table].table_name);
      out++;
      }
      }

      也并不是说:如果在Trans_delegate::before_dml函数中不进行table_info的获取,则drop table不会引发实例hang住的情况,而是它很大程度增加了这种风险。并且获取table_info并不是必须的,而为了在server层提供统一的逻辑(有人需要 ,有人不需要),进行了获取。
      总的来说,InnoDB层全局字典信息设计以及互斥锁的设计以及drop table的逻辑,让这种操作时刻都存在着各种风险,比如drop table过程中遇到如下情况:
      1. 其他线程出现行锁等待

      2. 出现行锁死锁需要回滚

      3. 对于information_schem中的innodb类型的表进行访问等等

      即便排除锁的影响,移除ibd文件时的IO消耗,也是导致实例短时间内hang住的关键原因。所以,如何尽可能的避免由于Drop table而引发的数据库hang死问题,心里应该是有了答案了,这里不做过多说明。
      最后修改时间:2020-09-17 10:21:13
      文章转载自MySQLLabs,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

      评论