MySQL内核月报 2014.11-MySQL· 5.7优化·Metadata Lock子系统的优化

背景

引入MDL锁的目的,最初是为了解决著名的bug#989,在MySQL 5.1及之前的版本,事务执行过程中并不维护涉及到的所有表的Metatdata 锁,极易出现复制中断,例如如下执行序列:

Session 1: BEGIN;
Session 1: INSERT INTO t1 VALUES (1);
Session 2: Drop table t1; --------SQL写入BINLOG
Session 1: COMMIT; -----事务写入BINLOG

在备库重放 binlog时,会先执行DROP TABLE,再INSERT数据,从而导致复制中断。

在MySQL 5.5版本里,引入了MDL, 在事务过程中涉及到的所有表的MDL锁,直到事务结束才释放。这意味着上述序列的DROP TABLE 操作将被Session 1阻塞住直到其提交。

不过用过5.5的人都知道,MDL实在是个让人讨厌的东西,相信不少人肯定遇到过在使用mysqldump做逻辑备份时,由于需要执行FLUSH TABLES WITH READ LOCK (以下用FTWRL缩写代替)来获取全局GLOBAL的MDL锁,因此经常可以看到“wait for global read lock”之类的信息。如果备库存在大查询,或者复制线程正在执行比较漫长的DDL,并且FTWRL被block住,那么随后的QUERY都会被block住,导致业务不可用引发故障。

为了解决这个问题,Facebook为MySQL增加新的接口替换掉FTWRL 只创建一个read view ,并返回与read view一致的binlog位点;另外Percona Server也实现了一种类似的办法来绕过FTWRL,具体点击文档连接以及percona的博客,不展开阐述。

MDL解决了bug#989,却引入了一个新的热点,所有的MDL锁对象被维护在一个hash对象中;对于热点,最正常的想法当然是对其进行分区来分散热点,不过这也是Facebook的大神Mark Callaghan在report了bug#66473后才加入的,当时Mark观察到MDL_map::mutex的锁竞争非常高,进而推动官方改变。因此在MySQL 5.6.8及之后的版本中,引入了新参数metadata_locks_hash_instances来控制对mdl hash的分区数(Rev:4350);

不过故事还没结束,后面的测试又发现哈希函数有问题,类似somedb.someprefix1….somedb.someprefix8的hash key值相同,都被hash到同一个桶下面了,相当于hash分区没生效。这属于hash算法的问题,喜欢考古的同学可以阅读下bug#66473后面Dmitry Lenev的分析。

Mark进一步的测试发现Innodb的hash计算算法比my_hash_sort_bin要更高效, Oracle的开发人员重开了个bug#68487来跟踪该问题,并在MySQL5.6.15对hash key计算函数进行优化,包括fix 上面说的hash计算问题(Rev:5459),使用MurmurHash3算法来计算mdl key的hash值。

MySQL 5.7 对MDL锁的优化

在MySQL 5.7里对MDL子系统做了更为彻底的优化。主要从以下几点出发:

第一,尽管对MDL HASH进行了分区,但由于是以表名+库名的方式作为key值进行分区,如果查询或者DML都集中在同一张表上,就会hash到相同的分区,引起明显的MDL HASH上的锁竞争

针对这一点,引入了LOCK-FREE的HASH来存储MDL_lock,LF_HASH无锁算法基于论文"Split-Ordered Lists: Lock-Free Extensible Hash Tables",实现还比较复杂。 注:实际上LF_HASH很早就被应用于Performance Schema,算是比较成熟的代码模块。

由于引入了LF_HASH,MDL HASH分区特性自然直接被废除了 。

对应WL#7305, PATCH(Rev:7249)

第二,从广泛使用的实际场景来看,DML/SELECT相比DDL等高级别MDL锁类型,是更为普遍的,因此可以针对性的降低DML和SELECT操作的MDL开销。

为了实现对DML/SELECT的快速加锁,使用了类似LOCK-WORD的加锁方式,称之为FAST-PATH,如果FAST-PATH加锁失败,则走SLOW-PATH来进行加锁。

每个MDL锁对象(MDL_lock)都维持了一个long long类型的状态值来标示当前的加锁状态,变量名为MDL_lock::m_fast_path_state 举个简单的例子:(初始在sbtest1表上对应MDL_lock::m_fast_path_state值为0)

Session 1: BEGIN;
Session 1: SELECT * FROM sbtest1 WHERE id =1; //m_fast_path_state = 1048576, MDL ticket 不加MDL_lock::m_granted队列
Session 2: BEGIN;
Session 2: SELECT * FROM sbtest1 WHERE id =2; //m_fast_path_state=1048576+1048576=2097152,同上,走FAST PATH
Session 3: ALTER TABLE sbtest1 ENGINE = INNODB; //DDL请求加的MDL_SHARED_UPGRADABLE类型锁被视为unobtrusive lock,可以认为这个是比上述SQL的MDL锁级别更高的锁,并且不相容,因此被强制走slow path。而slow path是需要加MDL_lock::m_rwlock的写锁。m_fast_path_state = m_fast_path_state | MDL_lock::HAS_SLOW_PATH | MDL_lock::HAS_OBTRUSIVE
注:DDL还会获得库级别的意向排他MDL锁或者表级别的共享可升级锁,但为了表述方便,这里直接忽略了,只考虑涉及的同一个MDL_lock锁对象。
Session 4: SELECT * FROM sbtest1 WHERE id =3; // 检查m_fast_path_state &HAS_OBTRUSIVE,如果DDL还没跑完,就会走slow path。

从上面的描述可以看出,MDL子系统显式的对锁类型进行了区分(OBTRUSIVE or UNOBTRUSIVE),存储在数组矩阵m_unobtrusive_lock_increment。 因此对于相容类型的MDL锁类型,例如DML/SELECT,加锁操作几乎没有任何读写锁或MUTEX开销。

对应WL#7304WL#7306 , PATCH(Rev:7067,Rev:7129)(Rev:7586)

第三,由于引入了MDL锁,实际上早期版本用于控制Server和引擎层表级并发的THR_LOCK 对于Innodb而言已经有些冗余了,因此Innodb表完全可以忽略这部分的开销。

不过在已有的逻辑中,Innodb依然依赖THR_LOCK来实现LOCK TABLE tbname READ,因此增加了新的MDL锁类型来代替这种实现。

实际上代码的大部分修改都是为了处理新的MDL类型,Innodb的改动只有几行代码。

对应WL#6671,PATCH(Rev:8232)

第四,Server层的用户锁(通过GET_LOCK函数获取)使用MDL来重新实现。

用户可以通过GET_LOCK()来同时获取多个用户锁,同时由于使用MDL来实现,可以借助MDL子系统实现死锁的检测。

注意由于该变化,导致用户锁的命名必须小于64字节,这是受MDL子系统的限制导致。

对应WL#1159, PATCH(Rev:8356)

时间: 2025-01-26 19:50:39

MySQL内核月报 2014.11-MySQL· 5.7优化·Metadata Lock子系统的优化的相关文章

MySQL内核月报 2014.12-MySQL· 优化改进· GTID启动优化

背景 GTID 可以说是 MySQL 5.6 版本的一个杀手级特性,它给主备复制带来了极大的便利,RDS只读实例就是强依赖于这个特性.然而GTID在给我们带来便利的同时,也埋下了许多坑,最近几期内核月报中GTID的频繁出现也说明了这一点,对其我们可以说是既爱又恨. GTID 并不是免费午餐,要使用它是要有代价的,为了保证GTID这个体系能够运转起来,需要做许多相关的工作,比如binlog里每个事务要多记 gtid_event 这种事件.写binlog的时候要生成 gtid.要维护几个GTID集合

数据库内核月报 - 2015 / 10-MySQL · 特性分析 · 跟踪Metadata lock

背景 MySQL 从5.5.3版本,对Metadata lock进行了调整,主要是MDL锁持有的周期从语句变成了事务, 其原因主要是解决两个问题: 问题1: 破坏事务隔离级别 在repeatable read的隔离级别下,多次的select语句执行过程中,会因为其它session的DDL语句,而导致select语句执行的结果不相同,破坏了RR的隔离级别. 问题2: 破坏binlog的顺序 在对表的DML过程中,会因为其它session的DDL语句,导致binlog里的event顺序在备库执行的结

MySQL内核月报 2014.08-MySQL· 参数故事·timed_mutexes

提要 MySQL 5.5.39 Release版本正式从源码里删除了全局参数timed_mutexes.timed_mutexes原本用来控制是否对Innodb引擎的mutex wait进行计时统计,以方便进行性能诊断.为什么要删除这个参数呢? 下面介绍下相关背景: Innodb的同步锁机制 Innodb封装了mutex和rw_lock结构来保护内存的变量和结构,进行多线程同步,考虑可移植性, mutex使用lock_word或者OS mutex来保证原子操作,并使用event条件变量进行阻塞和

MySQL内核月报 2014.08-MySQL· 捉虫动态·Count(Distinct) ERROR

背景 MySQL现行版本中存在一个count(distinct)语句返回结果错误的bug,表现为,实际结果存在值,但是用count(distinct)统计后返回的是0. 原因分析 Count(distinct f)的语义就是计算字段f的去重总数,计算流程大致如下: 流程一: 1. 构造一个unique集合A1(用tree实现) 2. 对每个值都试图插入集合A1中 3. 若和A1中现有item重复则直接跳过,不重复则插入并+1 4. 完成后计算集合中元素个数. 细心的同学会看到上面的语句中有一个s

MySQL内核月报 2014.11-MySQL· 5.7改进·Recovery改进

背景 InnoDB作为事务性引擎,使用write-ahead logging(WAL)机制保证ACID中的Atomicity和Durability,使用undo机制保证ACID中的Consistency和Isolation. 按照WAL和undo的机制,形成以下两个原则: 1. 数据块的更改需要先记录redo日志. 2. 数据块的更改需要先写入undo. 根据这两个原则,InnoDB更新数据的基本流程可以简单的总结为: 1. 记录需要更改undo record的redo log 2. 记录需要更

MySQL内核月报 2014.11-MySQL· 捉虫动态·SIGHUP 导致 binlog 写错

bug描述 这是5.6中和gtid相关的一个bug,当 mysqld 收到 sighup 信号 (比如 kill -1) 的时候,会 flush binlog,但是新生成binlog开头没写 Previous_gtids_log_event,这会导致下面 2 个问题: 这个时候 mysqld 重启的话,会发现再也起不来了,error log 里有这样的错 The binary log file 'mysql/mysql-bin.000020' is logically corrupted: Th

MySQL内核月报 2014.09-MySQL· 捉虫动态·GTID 和 DELAYED

描述 这是一个MySQL 5.6才有的bug,影响包含最新版本.涉及到的概念有GTID.DELAYED. 现象 在5.6主备都开启GTID-MODE的时候,备库同步线程停止,且Last_SQL_Error显示"When @@SESSION.GTID_NEXT is set to a GTID, you must explicitly set it to a different value after a COMMIT or ROLLBACK. Please check GTID_NEXT var

MySQL内核月报 2014.12-TokuDB· Binary Log Group Commit with TokuDB

MySQL在开启Binary Log的情况下,使用2PC(图1)来保证事务(XA)完整性的,每次提交事务需要做: 每个事务在提交的时候都要做3次fsync以确保日志真正的落盘,这样在log里,一个事务就会有3种状态: 这样,无论处于任何一个状态,事务的完整性都不会被破坏,但是每次提交会产生3次fsync,性能非常低. 为了提升性能,MySQL 5.6增加了group commit功能,当多个事务并发提交时,让多个都在等待fsync的事务合并做一次fsync,大大提升了吞吐量. 但是这个优化还需要

MySQL内核月报 2014.10-TokuDB· 主备复制·Read Free Replication

尽管MySQL 5.6和MariaDB 10.x在replication上已经做了不少优化,TokuDB 7.5也做了一个"进一步"的优化:Read Free Replication(RFR),目的是提高备库(slave)重放速度,减少主备延迟. RFR的原理比较简单:就是避免一些"不必要"的读来减少read IO. Read IO 当在slave上执行:UPDATE/DELETE操作的时候,MySQL进行read-modify-write操作,可能会产生read IO操作,而INSERT的时候