before_dml hook 引发的drop table阻塞事件

before_dml_hook 引发的drop table阻塞事件

现象:一条drop命令后,几乎所有写入操作都阻塞在Opening tables

对于Drop table操作,历来会出现一些问题,比如:

https://www.cnblogs.com/CtripDBA/p/11465315.html

https://blog.csdn.net/zyz511919766/article/details/40539333

https://www.jianshu.com/p/f8e124116094?from=singlemessage

但这一次却跟以往不同。
通过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结果和源代码才发现这其中另有隐情。

MySQL的插件机制提供了很多观察点,插件可以通过注册来实现某个关键节点的功能扩展。而此处涉及到的就是before_dml,由于复测环境并没有安装半同步复制插件,导致无法进入和生产一样的逻辑,也观察不到一致的现象。

在加载半同步复制master插件后,每次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的主要逻辑缓存缓存和移除ibd文件等是冲突的。

但是这些信息仅对于某些插件有用,对于半同步复制插件来说完全没用,可以看下半同步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的钩子函数为repl_semi_report_before_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过程中其他线程出现行锁等待,出现死锁需要回滚,对于information_schem中的innodb类型的表进行访问等等。即便排除锁的影响,移除ibd文件时的IO消耗,也是导致实例短时间内hang住的关键原因。

所以,如何尽可能的避免这些问题,应该是有了答案了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值