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住的关键原因。
所以,如何尽可能的避免这些问题,应该是有了答案了。