【作者】

王栋:携程技术保障中心数据库专家,对数据库疑难问题的排查和数据库自动化智能化运维工具的开发有强烈的兴趣。

【问题描述】

最近碰到有台MySQL实例出现了MySQL服务短暂hang死,表现为瞬间的并发线程上升,连接数暴增。

排查Error Log文件中有page_cleaner超时的信息,引起我们的关注:

2019-08-24T23:47:09.361836+08:00 0 [Note] InnoDB: page_cleaner: 1000ms intended loop took 24915ms. The settings might not be optimal. (flushed=182 and evicted=0, during the time.)
2019-08-24T23:47:16.211740+08:00 0 [Note] InnoDB: page_cleaner: 1000ms intended loop took 4849ms. The settings might not be optimal. (flushed=240 and evicted=0, during the time.)
2019-08-24T23:47:23.362286+08:00 0 [Note] InnoDB: page_cleaner: 1000ms intended loop took 6151ms. The settings might not be optimal. (flushed=215 and evicted=0, during the time.)

【问题分析】

1、 error log中page_cleaner信息是如何产生的

通过源码storage/innobase/buf/buf0flu.cc可以看出,当满足curr_time > next_loop_time + 3000条件(大于4秒)时,会打印上面信息到error log中。next_loop_time为1000ms,即1秒钟做一次刷新页的操作。

 if (ret_sleep == OS_SYNC_TIME_EXCEEDED) {
ulint curr_time = ut_time_ms(); if (curr_time > next_loop_time + 3000) {
if (warn_count == 0) {
ib::info() n_flushed_last
n_evicted
300) {
warn_interval = 600;
} else {
warn_interval *= 2;
} warn_count = warn_interval;
} else {
--warn_count;
}
} else {
/* reset counter */
warn_interval = 1;
warn_count = 0;
} next_loop_time = curr_time + 1000;
n_flushed_last = n_evicted = 0;
}

后半部分(flushed=182 and evicted=0, during the time.)日志,对应n_flushed_last和n_evicted两个变量,这两个变量来自于下面2个变量。

 n_evicted += n_flushed_lru;
n_flushed_last += n_flushed_list;

从源码注释中可以看出,n_flushed_lru表示从LRU list尾部刷新的页数,也就是日志中如evicted=0指标的所表示的值。

n_flushed_list:这个是从flush_list刷新列表中刷新的页数,也就是脏页数,日志中flushed=182的值。

/**
Wait until all flush requests are finished.
@param n_flushed_lru number of pages flushed from the end of the LRU list.
@param n_flushed_list number of pages flushed from the end of the
flush_list.
@return true if all flush_list flushing batch were success. */
static
bool
pc_wait_finished(
ulint* n_flushed_lru,
ulint* n_flushed_list)

从pc_wait_finished函数可以看出page_cleaner线程包含了LRU list和flush_list两部分刷新,而且需要等待两部分都完成刷新。

2、MySQL5.7.4引入了多线程page cleaner,但由于LRU列表刷新和脏页列表刷新仍然耦合在一起,在遇到高负载,或是热数据的buffer pool instance时,仍存在性能问题。

1) LRU List刷脏在先,Flush list的刷脏在后,但是是互斥的。也就是说在进Flush list刷脏的时候,LRU list不能继续去刷脏,必须等到下一个循环周期才能进行。

2) 另外一个问题是,刷脏的时候,page cleaner coodinator会等待所有的page cleaner线程完成之后才会继续响应刷脏请求。这带来的问题就是如果某个buffer pool instance比较热的话,page cleaner就不能及时进行响应。

Percona版本对LRU list刷脏做了很多优化。

3、分析问题实例的binlog日志,可以看到从2019-08-24 23:46:15到2019-08-24 23:47:25之间没有记录任何日志,说明这段时间内mysql服务无法正常处理请求,短暂hang住了

mysqlbinlog -vvv binlog --start-datetime='2019-08-24 23:46:15' --stop-datetime='2019-08-24 23:47:25'|less

从计数器指标可以看出期间并发线程积压,QPS处理能力下降,稍后MySQL服务恢复,积压的请求集中释放,导致并发连接进一步上升

4、从Innodb_buffer_pool_pages_misc和Innodb_buffer_pool_pages_free指标来看,在问题时间段buffer pool在1分钟内集中释放了约16*(546893-310868)=3776400,3.7G可用内存。

可能期间LRU list的mutex资源锁定,导致page cleaner线程在LRU list刷新时阻塞,从而表现出page_cleaner线程执行时间过长。

innodb_buffer_pool_pages_misc与adaptive hash index有关,下面是官网的描述

•	Innodb_buffer_pool_pages_misc
The number of pages in the InnoDB buffer pool that are busy because they have been allocated for administrative overhead, such as row locks or the adaptive hash index. This value can also be calculated as Innodb_buffer_pool_pages_total − Innodb_buffer_pool_pages_free − Innodb_buffer_pool_pages_data. When using compressed tables, Innodb_buffer_pool_pages_misc may report an out-of-bounds value (Bug #59550).

5、为什么AHI短时间内会释放大量的内存呢,通过慢查询日志我们排查到期间有drop table的操作,但drop的表容量约50G,并不是很大,drop table为什么会导致MySQL服务短暂hang死,它对服务器性能会产生多大的影响,我们做了模拟测试。

【测试重现过程】

为了进一步验证,我们在测试环境下模拟测试了drop table对高并发MySQL性能的影响。

1、 使用sysbench工具做压测,首先在测试环境下创建了8张2亿条记录的表,单表容量48G

2、 开启innodb_adaptive_hash_index,用olap模板压测1个小时,填充目标8张表所对应的AHI内存

3、 再次开启压测线程只对sbtest1表做访问,观察MySQL的访问情况

4、 新建会话运行drop table test.sbtest2,看到drop一张48G的表消耗了64.84秒



5、用自定义脚本检测每秒Innodb_buffer_pool_pages_misc和Innodb_buffer_pool_pages_free指标的变化情况,看到在drop table期间Innodb_buffer_pool_pages_misc大量释放,Innodb_buffer_pool_pages_free值同时增长,释放和增加的内容总量基本一致,如下图所示



6、drop table期间,MySQL处于hang死状态,QPS长时间跌0

7、errorlog中也重现了page_cleaner的日志信息



至此复现了问题现象。

【为什么MySQL会短暂hang死】

1、 压测期间,抓取了pstack,show engine innodb status,以及performance_schema下events_waits_summary_global_by_event_name表中相关mutex等现场信息

2、 在SEMAPHORES相关信息中,可以看到hang死期间大量Thread请求S-lock,被同一个线程140037411514112持有的锁所阻塞,时间持续了64秒

--Thread 140037475792640 has waited at row0purge.cc line 862 for 64.00 seconds the semaphore:
S-lock on RW-latch at 0x966f6e38 created in file dict0dict.cc line 1183
a writer (thread id 140037411514112) has reserved it in mode exclusive
number of readers 0, waiters flag 1, lock_word: 0
Last time read locked in file row0purge.cc line 862
Last time write locked in file /mysql-5.7.23/storage/innobase/row/row0mysql.cc line 4253
--Thread 140037563102976 has waited at srv0srv.cc line 1982 for 57.00 seconds the semaphore:
X-lock on RW-latch at 0x966f6e38 created in file dict0dict.cc line 1183
a writer (thread id 140037411514112) has reserved it in mode exclusive
number of readers 0, waiters flag 1, lock_word: 0
Last time read locked in file row0purge.cc line 862
Last time write locked in file /mysql-5.7.23/storage/innobase/row/row0mysql.cc line 4253

3、通过ROW OPERATIONS相关信息,可以看到MySQL的Main Thread也被同一个线程140037411514112阻塞,状态处于enforcing dict cache limit状态

3 queries inside InnoDB, 0 queries in queue
17 read views open inside InnoDB
Process ID=39257, Main thread ID=140037563102976, state: enforcing dict cache limit
Number of rows inserted 1870023456, updated 44052478, deleted 22022445, read 9301843315
0.00 inserts/s, 0.00 updates/s, 0.00 deletes/s, 0.00 reads/s

4、可以看到线程140037411514112执行的SQL就是drop table test.sbtest2语句,说明drop table期间持有的锁,阻塞了Main Thread及其他线程

---TRANSACTION 44734025, ACTIVE 64 sec dropping table——
10 lock struct(s), heap size 1136, 7 row lock(s), undo log entries 1
MySQL thread id 440836, OS thread handle 140037411514112, query id 475074428 localhost root checking permissions
drop table test.sbtest2

5、下面是抓到的drop table 的简化后的调用栈信息,对比可以看出,64秒的时间,drop table 都在执行函数btr_search_drop_page_hash_index,清空对应的AHI记录信息

Thread 32 (Thread 0x7f5d002b2700 (LWP 8156)):
\#0 ha_remove_all_nodes_to_page
\#1 btr_search_drop_page_hash_index
\#2 btr_search_drop_page_hash_when_freed
\#3 fseg_free_extent
\#4 fseg_free_step
\#5 btr_free_but_not_root
\#6 btr_free_if_exists
\#7 dict_drop_index_tree
\#8 row_upd_clust_step
\#9 row_upd
\#10 row_upd_step
\#11 que_thr_step
\#12 que_run_threads_low
\#13 que_run_threads
\#14 que_eval_sql
\#15 row_drop_table_for_mysql
\#16 ha_innobase::delete_table
\#17 ha_delete_table
\#18 mysql_rm_table_no_locks
\#19 mysql_rm_table
\#20 mysql_execute_command
\#21 mysql_parse
\#22 dispatch_command
\#23 do_command
\#24 handle_connection
\#25 pfs_spawn_thread
\#26 start_thread ()
\#27 clone ()

6、通过代码我们可以看到drop table调用row_drop_table_for_mysql函数时,在row_mysql_lock_data_dictionary(trx);位置对元数据加了排它锁

row_drop_table_for_mysql(
const char* name,
trx_t* trx,
bool drop_db,
bool nonatomic,
dict_table_t* handler)
{
dberr_t err;
dict_foreign_t* foreign;
dict_table_t* table = NULL;
char* filepath = NULL;
char* tablename = NULL;
bool locked_dictionary = false;
pars_info_t* info = NULL;
mem_heap_t* heap = NULL;
bool is_intrinsic_temp_table = false;
DBUG_ENTER("row_drop_table_for_mysql");
DBUG_PRINT("row_drop_table_for_mysql", ("table: '%s'", name));
ut_a(name != NULL);
/* Serialize data dictionary operations with dictionary mutex:
no deadlocks can occur then in these operations */
trx->op_info = "dropping table";
if (handler != NULL && dict_table_is_intrinsic(handler)) {
table = handler;
is_intrinsic_temp_table = true;
}
if (table == NULL) {
if (trx->dict_operation_lock_mode != RW_X_LATCH) {
/* Prevent foreign key checks etc. while we are
dropping the table */
row_mysql_lock_data_dictionary(trx);
locked_dictionary = true;
nonatomic = true;
}

7、以Main Thread为例,在调用srv_master_evict_from_table_cache函数获取X-lock on RW-latch时被阻塞

/********************************************************************//**
Make room in the table cache by evicting an unused table.
@return number of tables evicted. */
static
ulint
srv_master_evict_from_table_cache(
/*==============================*/
ulint pct_check) /*! rw_lock_x_lock(dict_operation_lock);
dict_mutex_enter_for_mysql();
n_tables_evicted = dict_make_room_in_cache(
innobase_get_table_cache_size(), pct_check);
dict_mutex_exit_for_mysql();
rw_lock_x_unlock(dict_operation_lock);
return(n_tables_evicted);
}

8、查看dict_operation_lock的注释,create drop table操作时需要X锁,而一些其他后台线程,比如Main Thread检查dict cache时,也需要获取dict_operation_lock的X锁,因此被阻塞

/** @brief the data dictionary rw-latch protecting dict_sys
table create, drop, etc. reserve this in X-mode; implicit or
backround operations purge, rollback, foreign key checks reserve this
in S-mode; we cannot trust that MySQL protects implicit or background
operations a table drop since MySQL does not know of them; therefore
we need this; NOTE: a transaction which reserves this must keep book
on the mode in trx_t::dict_operation_lock_mode */
rw_lock_t* dict_operation_lock;

9、期间用户线程由于获取不到锁而处于挂起状态,当无法立刻获得锁时,会调用row_mysql_handle_errors将错误码传到上层进行处理

/****************************************************************//*
Handles user errors and lock waits detected by the database engine.
@return true if it was a lock wait and we should continue running the
query thread and in that case the thr is ALREADY in the running state. */
bool
row_mysql_handle_errors

下面是简化后的用户线程调用栈信息:

Thread 29 (Thread 0x7f5d001ef700 (LWP 8159)):
#0 pthread_cond_wait@@GLIBC_2.3.2
#1 wait
#2 os_event::wait_low
#3 os_event_wait_low
#4 lock_wait_suspend_thread
#5 row_mysql_handle_errors
#6 row_search_mvcc
#7 ha_innobase::index_read
#8 handler::ha_index_read_map
#9 handler::read_range_first
#10 handler::multi_range_read_next
#11 QUICK_RANGE_SELECT::get_next
#12 rr_quick
#13 mysql_update
#14 Sql_cmd_update::try_single_table_update
#15 Sql_cmd_update::execute
#16 mysql_execute_command
#17 mysql_parse
#18 dispatch_command
#19 do_command
#20 handle_connection
#21 pfs_spawn_thread
#22 start_thread
#23 clone

10、page_cleaner后台线程没有抓到明显的阻塞关系,只看到如下正常的调用栈信息

Thread 55 (Thread 0x7f5c7fe15700 (LWP 39287)):
\#0 pthread_cond_timedwait@@GLIBC_2.3.2 ()
\#1 os_event::timed_wait
\#2 os_event::wait_time_low
\#3 os_event_wait_time_low
\#4 pc_sleep_if_needed
\#5 buf_flush_page_cleaner_coordinator
\#6 start_thread
\#7 clone
Thread 54 (Thread 0x7f5c7f614700 (LWP 39288)):
\#0 pthread_cond_wait@@GLIBC_2.3.2 ()
\#1 wait
\#2 os_event::wait_low
\#3 os_event_wait_low
\#4 buf_flush_page_cleaner_worker
\#5 start_thread
\#6 clone

【结论&解决思路】

drop table引起的MySQL 短暂hang死的问题,是由于drop 一张使用AHI空间较大的表时,调用执行AHI的清理动作,会消耗较长时间,执行期间长时间持有dict_operation_lock的X锁,阻塞了其他后台线程和用户线程;

drop table执行结束锁释放,MySQL积压的用户线程集中运行,出现了并发线程和连接数瞬间上升的现象。

规避问题的方法,可以考虑在drop table前关闭AHI。

Drop Table对MySQL的性能影响分析的更多相关文章

  1. GSO/TSO/GRO等对VirtIO虚机的网络性能影响分析(by quqi99)

    作者:张华  发表于:2016-04-05版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明 ( http://blog.csdn.net/quqi99 ) IP层 ...

  2. mysql limit 性能问题分析

    问题重现 // todo 参考文章: MySQL 单表分页 Limit 性能优化 Scalable MySQL: Avoid offset for large tables 证明为什么用limit时, ...

  3. 利用硬链接和truncate降低drop table对线上环境的影响

    众所周知drop table会严重的消耗服务器IO性能,如果被drop的table容量较大,甚至会影响到线上的正常. 首先,我们看一下为什么drop容量大的table会影响线上服务 直接执行drop ...

  4. MySQL删除大表时潜在的问题(drop table,truncate table)

    来源于:https://www.cnblogs.com/CtripDBA/p/11465315.html,侵删,纯截图,避免吸引流量之嫌 case1,删除大表时,因为清理自适应hash索引占用的内容导 ...

  5. mysql服务性能优化—my.cnf配置说明详解

    MYSQL服务器my.cnf配置文档详解硬件:内存16G [client]port = 3306socket = /data/3306/mysql.sock [mysql]no-auto-rehash ...

  6. mysql服务性能优化 my.cnf my.ini配置说明详解(16G内存)

    sort_buffer_size,join_buffer_size,read_buffer_size参数对应的分配内存也是每个连接独享 这配置已经优化的不错了,如果你的mysql没有什么特殊情况的话, ...

  7. mysql服务性能优化—my.cnf_my.ini配置说明详解(16G内存)

    这配置已经优化的不错了,如果你的mysql没有什么特殊情况的话,可以直接使用该配置参数 MYSQL服务器my.cnf配置文档详解硬件:内存16G [client]port = 3306socket = ...

  8. Mysql数据库性能

    Mysql数据库设计规范 https://www.cnblogs.com/Luke-Me/p/8994432.html 我们在项目一开始的设计中,就要忙着考虑数据库的设计,表.字段.索引.sql等等, ...

  9. MySQL--DROP TABLE与MySQL版本

    ======================================================================== DROP TABLE与MySQL版本 MySQL在5. ...

随机推荐

  1. 温故而知新,重温 Java 7 的那些“新”特性

    2009 年 4 月 20 日,Java 的亲生父亲 Sun 被养父 Oracle 以 74 亿美元收购,这在当时可是一件天大的事.有不少同学都担心 Java 的前途,我当时傻不啦叽地也很担心:自己刚 ...

  2. 实现万行级excel导出---poi--ooxm的应用和采坑

    xl_echo编辑整理,欢迎转载,转载请声明文章来源.欢迎添加echo微信(微信号:t2421499075)交流学习. 百战不败,依不自称常胜,百败不颓,依能奋力前行.--这才是真正的堪称强大!! - ...

  3. 原创:微信小程序如何使用自定义组件

    本博文是通过实际开发中的一个实例来讲解自定义组件的使用. 第一步:新建自定义组件目录,如图,我新建了个componts和tabList目录,然后右键tabList目录选择新建compont取名为tab ...

  4. JDK的命令行工具系列 (二) javap、jinfo、jmap

    javap: 反编译工具, 可用来查看java编译器生成的字节码 参数摘要: -help 帮助 -l 输出行和变量的表 -public 只输出public方法和域 -protected 只输出publ ...

  5. Hexo结合github制作博客

    https://blog.csdn.net/Hoshea_chx/article/details/78826689 hexo(themes) vuePress jekylly

  6. Linux服务部署Yapi项目(安装Node Mongdb Git Nginx等)

    Linux服务部署Yapi 一,介绍与需求 1,我的安装环境:CentOS7+Node10.13.0+MongoDB4.0.10. 2,首先安装wget,用于下载node等其他工具 yum insta ...

  7. JavaMail的简单使用(自测可以发邮件)

    在很多项目中我们都会遇到发送邮件的功能,发送邮件其实还是很实用的,正好今天做项目需要实现,现在来简单的整理一下发送邮件的实现. 建立邮件与服务器之间的会话 Properties props = new ...

  8. Android OTG之USB转串口模块通讯

    微信公众号:CodingAndroid CSDN:http://blog.csdn.net/xinpengfei521 1.背景简介 我们公司开发了一款室内机平板APP应用,要求平板能去控制智能门锁. ...

  9. 安装node.js、webpack、vue 和vue-cli 以及安装速度慢/不成功的解决方法

    1.安装node.js 地址:https://nodejs.org/en/  下载安装软件之后,点击下一步即可 打开dos窗口,输入cmd能快速打开,输入npm -v 和 node -v 能显示出版本 ...

  10. spring cloud 断路器 Hystrix

    一.微服务架构中使用断路器的原因 二.代码实现 1.在Ribbon中使用短路器 1.1.引入依赖 <dependency> <groupId>org.springframewo ...