做者:高鹏(网名八怪)
文章末尾有他著做的《深刻理解 MySQL 主从原理 32 讲》,深刻透彻理解 MySQL 主从,GTID 相关技术知识。
本文来源:转载自公众号-老叶茶馆,
(做者简书: https://www.jianshu.com/p/40b...)
*爱可生开源社区出品,原创内容未经受权不得随意使用,转载请联系小编并注明来源。
今天遇到一个朋友的线上问题,大概意思就是说,我有一个线上的大事务大概 100G 左右,正在作回滚,当前看起来彷佛影响了线上的业务,而且回滚很慢,是否能够减轻对线上业务的影响。而且朋友已经取消了双 1 设置,可是没有任何改观。版本:MySQL 5.6
首先咱们须要知道的是,MySQL 并不适合大事务,大概列举一些 MySQL 中大事务的影响:php
基于如上一些不彻底的列举,咱们应该在线上尽量的避免大事务。好了咱们下面来进行问题讨论。html
前面已经说了,咱们已经取消了双 1 设置,所谓的双 1 就是 sync_binlog=1 和 innodb_flush_log_at_trx_commit=1。这两个参数线上要保证为 1,前者保证 binlog 的安全,后者保证 redo 的安全,它们在数据库 crash recovery 的时候起到了关键做用,不设置为双 1 可能致使数据丢失。具体的参数含义不作过多讨论。可是这里的问题是即使取消了双 1,没有任何改观,所以彷佛说明 IO问题不是主要瓶颈呢?mysql
下面咱们来看几个截图:ios
咱们重点观察 vmstat 的 r 和 b 列发现,IO 队列没有什么问题,而且 wa% 并不大。咱们观察 iostat 中的 %util 和读写数据大小来看问题不大,而且 tps 远没达到极限(SSD 盘)。咱们 top -Hu 能够观察到 %us 不小,而且有线程已经打满了(99.4%CPU)一个 CPU 核。 所以咱们能够将方向转为研究 CPU 瓶颈的产生,但愿可以对问题有帮助,而后从提供的 perf top 中咱们有以下发现:算法
好了咱们将问题先锁定到 lock_number_of_rows_locked 这个函数上。sql
朋友用的 5.6,可是我这里以 5.7.26 的版本进行描述。而后下一节描述 5.6 和 5.7 算法上的关键差别。不知道你们是否注意过 show engine innodb status 中的这样一个标志:数据库
这个标记就来自函数 lock_number_of_rows_locked,含义为当前事务加行锁的行数。而这个函数包裹在函数 lock_print_info_all_transactions 下面,lock_print_info_all_transactions 函数是打印咱们一般看到 show engine innodb status 中事务部分的核心参数。咱们来看一下简单的流程:安全
PrintNotStarted print_not_started(file);//创建一个结构体,目的是作not start 事务的打印 ut_list_map(trx_sys->mysql_trx_list, print_not_started); //这个地方打印出那些事务状态是no start的事务。mysql_trx_list是全事务。 consttrx_t* trx; TrxListIterator trx_iter; //这个迭代器是trx_sys->rw_trx_list 这个链表的迭代器 consttrx_t* prev_trx = 0; /* Control whether a block should be fetched from the buffer pool. */ bool load_block = true; bool monitor = srv_print_innodb_lock_monitor && (srv_show_locks_held != 0); while((trx = trx_iter.current()) != 0) { //经过迭代器进行迭代 ,显然这里不会有只读事务的信息,所有是读写事务。 ... /* If we need to print the locked record contents then we need to fetch the containing block from the buffer pool. */ if(monitor) { /* Print the locks owned by the current transaction. */ TrxLockIterator& lock_iter = trx_iter.lock_iter(); if(!lock_trx_print_locks( //打印出锁的详细信息 file, trx, lock_iter, load_block))
简单的说就是先打印哪些处于 not start 的事务,而后打印那些读写事务的信息,固然咱们的回滚事务确定也包含在其中了,须要注意的是只读事务 show engine 不会打印。对于处于回滚状态的事务咱们能够在 show engine 中观察到以下信息:函数
函数 trx_print_low 能够看到大部分的信息,这里就不详细解释了。既然如此咱们须要明白 lock_number_of_rows_locked 是如何计算的,下面进行讨论。工具
上面咱们说了函数 lock_number_of_rows_locked 函数会打印出当前事务加行锁的行数。那么咱们来看一下 5.6 和 5.7 算法的不一样。
5.7.26
实际上只有以下一句话:
return(trx_lock->n_rec_locks);
咱们能够看到这是返回了一个计数器,而这个计数器的递增就是在每行记录加锁后完成的,在函数 lock_rec_set_nth_bit 的末尾能够看到 ++lock->trx->lock.nreclocks,所以这是一种预先计算的机制。所以这样的计算代价很低,也不会因为某个事务持有了大量的锁,而致使计算代价太高。
5.6.22
随后我翻了一下 5.6.22 的代码,发现彻底不一样以下:
for(lock= UT_LIST_GET_FIRST(trx_lock->trx_locks); //使用for循环每一个获取的锁结构 lock!= NULL; lock= UT_LIST_GET_NEXT(trx_locks, lock)) { if(lock_get_type_low(lock) == LOCK_REC) { //过滤为行锁 ulint n_bit; ulint n_bits = lock_rec_get_n_bits(lock); for(n_bit = 0; n_bit < n_bits; n_bit++) {//开始循环每个锁结构的每个bit位进行统计 if(lock_rec_get_nth_bit(lock, n_bit)) { n_records++; } } } } return(n_records);
咱们知道循环自己是一种 CPU 密集型的操做,这里使用了嵌套循环实现。所以若是在 5.6 中若是出现大事务操做了大量的行,那么获取行锁记录的个数的时候,将会出现高耗 CPU 的状况。
有了上面的分析咱们很清楚了,触发的缘由有以下几点:
这样当在统计这个大事务行锁个数的时候,就会进行大量的循环操做。从现象上看就是线程消耗了大量的 CPU 资源,而且处于 perf top 的第一位。
知道了缘由就很简单了,找出为频繁使用 show engine innodb status 的监控工具,随后业务所有恢复正常,IO 利用率也上升了以下:
固然若是可以使用更新的版本好比 5.7 及 8.0 版本将不会出现这个问题,能够考虑使用更高版本。分析性能问题须要首先找到性能的瓶颈而后进行集中突破,好比本例中 CPU 资源消耗更加严重。也许解决问题就在一瞬间。
最后经过朋友后面查询的 bug 以下:https://bugs.mysql.com/bug.ph... 发现印风(翟卫翔)已经在多年前提出过了这个问题,而且作出了修改意见,而且这个修改意见官方采纳了,也就是上面咱们分析的算法改变。通过印风(翟卫翔)的测试有 bug 中有以下描述:
也就是 CPU 消耗会高达 20%。
下面是 5.7.26 调用栈帧:
最后推荐高鹏的专栏《深刻理解 MySQL 主从原理 32 讲》,想要透彻了解学习 MySQL 主从原理的朋友不容错过。