本文分享自华为云社区《【华为云MySQL技能专栏】MySQL8 数据字典重构源码解读-云社区-华为云》,作者:GaussDB 数据库
1.背景先容
在MySQL5.7 版本的利用实践过程中,我们很随意马虎碰着DDL崩溃后导致数据不一致的问题,详细场景描述如下:
主备高可用架构支配下,备机回放实行DROP TABLE的中途,因触发其它社区bug导致备机mysqld进程crash。重新拉起备机后,因存储表构造的FRM文件与表空间IBD没有被同时清理,导致再次实行DROP TABLE失落败,需手动清理备机物理文件,这给自动化运维带来了很大阻碍。

这个问题的本色是MySQL5.7 版本的DDL非原子性、数据字典的架构是有缺陷的。MySQL 社区从5.7到8.0版本的演进过程中,个中一大改动是对数据字典(Data dictionary, 下文缩写为DD)的重构及与之干系原子性Data Definition Language (Atomic DDL)的支持。重构的动机来源于5.7版本数据字典的以下问题[1]:
(1)Server层和存储引擎插件层的数据字典未统一。Server和存储引擎分别掩护了各自的DD信息,导致部分DD信息冗余,进而带来DD信息不同步的隐患。
(2)不同类型的DD文件缺少统一的访问API,不利于后续的掩护和拓展。
(3)非原子DDL:数据字典被存放在非事务的表里。如果mysqld在DDL中途crash,会导致数据残留和复制问题。
(4) Information_schema的性能受到批评。5.7版本中,Information_schema表的定义是临时表,这些临时表的数据来源于FRM文件、存储引擎的统计信息等。最紧张的缺陷是与表构造文件FRM的交互会导致大量I/O开销,性能较差[2]。
下文将剖析MySQL8.0 版本数据字典重构干系的代码,并阐明重构后如何办理在5.7 版本中存在的干系问题。
2. 数据字典的变革DD的重构是如何影响DDL语句流程和锁系统的呢?可以先从最常见的CREATE TABLE,即创非临时表DDL的场景入手不雅观察。
2.1 5.7 vs 8.0 创表流程比拟比拟5.7和8.0创表流程中的紧张接口:
5.7流程:
(server层)mysql_create_table|-open_tables->lock_table_names // 对schema上IX锁|-mysql_create_table_no_lock->create_table_impl|-access检讨是否有重名FRM|-get_cached_table_share // server层校验表名是否存在|-ha_table_exists_in_engine // 调存储引擎的HA接口,但大多数存储引擎没实现|-rea_create_table|-mysql_create_frm//持久化:server层表构造文件FRM|-ha_create_table->ha_create // 进入存储引擎,各自实现(引擎层)ha_innobase::create|-create_table_info_t::prepare_create_table //表名预处理等|-row_mysql_lock_data_dictionary //加dict_sys的锁dict_sys_mutex_enter();|-create_table_info_t::create_table|-create_table_def|-dict_mem_table_create // malloc dict_table_t类+填列|-dict_table_add_to_cache // 加入dict_sys的hash|-row_create_table_for_mysql|-...fil_ibd_create//写IBD文件|-create_index //创建二级索引/处理外键约束等|-innobase_commit_low|-create_table_update_dict;至此已经commit, 更新一些统计信息|-row_mysql_unlock_data_dictionary //开释dict_sys的锁 dict_sys_mutex_exit();
8.0流程:
(server层)mysql_create_table|-mdl_locker.ensure_locked(db) // 同5.7 对schema上IX锁|-各种初期检讨,但不包括FRM|- rea_create_base_table|-Dictionary_client::store //将新表信息写人DD的InnoDB表|-dd::acquire_for_modification // dd_client在线程内DD缓存加入新表元数据|-ha_create_table->ha_create // 进入存储引擎,各自实现(引擎层)ha_innobase::create->innobase_basic_ddl::create_impl|-create_table_info_t::prepare_create_table //表名预处理等|-create_table_info_t::create_table// 不在此处操作dict_sys->mutex|-create_table_def|-dict_mem_table_create // malloc dict_table_t类+填列|-row_create_table_for_mysql|-...fil_ibd_create/btr_sdi_create_index //写IBD文件和个中的SDI|-dict_sys_mutex_enter() // 8.0仅在dict_table_add_to_cache前后操作锁|-dict_table_add_to_cache // 加入dict_sys的hash|-dict_sys_mutex_exit();|-create_index (回到外层SQL)trans_commit_implicit->dd::cache::Dictionary_client::commit_modified_objects
在server层进入InnoDB之前,5.7 和8.0 版本最紧张的差异是元数据的持久化存储。5.7 版本写FRM文件,8.0 版本直接将元数据写入InnoDB表,详见下文2.2章节。除此之外,8.0 版本代码中,对server层数据字典缓存机制进行了重构,详见下文2.3章节。在进入InnoDB后,InnoDB表的元数据缓存构造dict_sys的持锁粒度,也在8.0 版本变得更风雅,详见下文2.4章节。
2.2 元数据持久化策略的变革比拟上述流程,不难创造,在server层调用存储引擎接口进入InnoDB之前,MySQL 5.7 和8.0 版本分别在rea_create_table(5.7)和 rea_create_base_table(8.0) 实现了一部分持久化干系步骤。
5.7 中,server层首先写FRM文件持久化表构造,并通过检讨同名FRM文件是否存在来担保同名表不会被重复创建。
8.0 中,不再利用FRM文件,通过Dictionary_client::store->Storage_adapter::store 的调用,直接将元数据的改动写入InnoDB格式的数据字典表(DD table)中,由InnoDB引擎的能力担保这条元数据改动的事务性。其真正持久化,是在DDL事务提交之后。取消独立FRM文件,也避免了上文背景描述中提到的问题:DDL过程中,mysqld进程崩溃的场合,无法担保IBD和FRM文件同时被创建或清理。比较5.7检讨同名FRM文件冲突的做法,8.0 版本由元数据锁(Metadata Lock, MDL)规避并发创同名表的场景。
2.3 DD缓存机制的变革MySQL 8.0 版本在server层元数据缓存的最紧张改动是引入了二级缓存,新增了两种类型DD的缓存:会话私有的局部缓存Local Cache和所有会话可见的全局共享缓存Shared_dictionary_cache。
server层查询DD时,首先通过dd::cache::Dictionary_client类的接口,查询会话自身的局部缓存Local Cache。如果在自身的Local Cache不命中,再去查询全局缓存Shared_dictionary_cache,在全局缓存中命中的DD工具将同时被加入会话的局部缓存。
当这两种缓存皆不命中时,才会去调存储引擎InnoDB的接口查询。如果在存储引擎查询到相应的DD工具,返回的工具将同时更新到会话自身的局部缓存Local Cache和Server层的全局缓存Shared_dictionary_cache。
比拟5.7 和8.0 在server层元数据缓存机制的实现:5.7 在 server层只有一层全局的table_def_cache,在创表之前,调用get_cached_table_share进行重复性校验。
get_cached_table_share通过HashMap中表名和元数据的映射关系,查找相应表元数据的内存构造。如果只有一层全局的元数据缓存,为了担保多线程环境下的安全,不可避免的会涉及线程间锁的竞争。8.0 版本引入的会话级局部缓存Local Cache,命中时不用再去访问全局的缓存,能够大幅减少锁冲突的频率,提升了性能。
2.4 InnoDB的dict_sys_t的变革在InnoDB内部,单独掩护了一套元数据信息缓存,也便是我们常说的dict_sys_t,里面掩护了当前已经在InnoDB打开的表的元数据信息。该InnoDB的元数据信息缓存从5.7 延续到了8.0 。
创表时,InnoDB层读取Server层通报下来新表的元数据信息,在其内部创建一个对应的dict_table_t构造来掩护,然后调用dict_table_add_to_cache将该dict_table_t加入到dict_sys的hash表中。dict_sys->mutex是InnoDB全体dict_sys的锁,8.0 在 dict_table_add_to_cache 调用的前后,才获取和开释dict_sys->mutex;而5.7 则在 ha_innobase::create的大部分流程都持有这把锁,从内存中DD表工具dict_table_t的堆内存申请、填写到commit后统计信息的更新。这个差异影响了并发创表的效率。
2.5 Information_schema的变革在5.7 版本,information_schema是基于临时表实现,其依赖于独立的表构造FRM文件,产生大量I/O开销,导致性能较差;而在8.0 版本,DD干系的表基于InnoDB引擎持久化存储,information_schema的定义成为基于这些DD table的视图。比较5.7 版本,这种基于视图的做法,避免了读取FRM文件时与磁盘的交互,基于DD表的视图查询,也能充分利用优化器和DD表本身的索引提升性能。
2.6 DD变革总结总结上文,DD从5.7版本到8.0版本的变革如表一。
表一
变革项
5.7
8.0
冗余文件
独立于IBD的FRM文件,DDL过程中crash时,两者不一致。字符集文件db.opt、TRG触发器文件等
撤消FRM等独立文件,统一用事务性的DD表存储。
元数据持久化
表构造依赖独立的FRM文件
基于InnoDB的DD表,有事务性。
Information Schema
临时表,依赖于FRM,IO开销大
视图,基于InnoDB引擎的DD表,性能优化
Server层缓存机制
一层全局的Table_def_cache
两层:会话私有的Local Cache,全局的Shared_dictionary_cache
InnoDB DD缓存dict_sys的锁粒度
创表流程中,单个线程持锁贯穿全体创建流程,影响并发度
细粒度持锁/解锁
3.原子性DDL与DDL log表DDL原子性由InnoDB在8.0的新能力担保,这部分能力与DD重构干系。一方面,元数据存储在InnoDB表中,本身就担保了事务性;另一方面,在server层存储元数据到基于InnoDB的DD表完成后,后续DDL流程中干系数据文件处理的原子性。例如,创表过程中索引的创建、IBD文件的天生,则由另一张DD表DDLlog担保。
为了担保DDL的原子性,在DDL过程中,每一个对文件修正或对干系内存工具修正的动作,都会记录在基于InnoDB引擎的DD表DDL log里。其类定义和内存中的实例为:
class Log_DDLdict_table_t ddl_log;
DDL每一个关键步骤实行完,这张DDL log表直接记录与该已实行步骤相对应的回滚操作。以创建一张不包含二级索引的表为例,InnoDB层会实行以下函数调用:
create_index->row_create_index_for_mysql->dict_create_index_tree_in_mem,创建完B+树索引后,会有Log_DDL::write_free_tree_log-> Log_DDL::insert_free_tree_log 的调用。
Log_DDL::write_free_tree_log会记录2条日志:一条是“创建索引”对应的回滚日志,即删除对应索引的操作;另一条是删除日志,对以上的回滚日志进行删除。
如果DDL事务终极是提交的,删除日志就会被提交,则创索引对应的回滚操作不会被实行;而如果DDL终极是被回滚的,那么删除日志本身也被回滚,而创索引对应的回滚操作就会被实行,终极该新建的索引会被回滚,以此来完成DDL真正的回滚。如果DDL涉及到其它文件或者内存操作,都是按照相同的逻辑进行回滚日志和删除日志的记录,以确保DDL的提交和回滚之后,对应的文件和内存得到精确的清理和复位。
Log_DDL::insert_free_tree_log中的回滚日志,详细内容即“创索引”的回滚操作:与创B+树对应的操作,即开释索引对应的B+树。在DDL log中新增的一条DDL_Record,记录了create table到一半时索引的信息:space、page、id等,实现如下:
DDL_Record record;record.set_id(id);record.set_thread_id(thread_id);record.set_type(Log_Type::FREE_TREE_LOG);record.set_space_id(index->space);record.set_page_no(index->page);record.set_index_id(index->id);{DDL_Log_Table ddl_log(trx);error = ddl_log.insert(record);}
类似的DDL log记录还有:
1. ALTER TABLE RENAME时,有Log_DDL::write_rename_table_log,分别记录新老表名。
2. 创建表空间时的Log_DDL::write_delete_space_log。
3. 上文创表过程中dict_table_add_to_cache 将InnoDB的内存DD构造存入dict_sys后,Log_DDL::write_remove_cache_log。
4. DROP TABLE时Log_DDL::write_drop_log,记录将要被drop的table id。
在事务处理的末了或在重启后crash recovery的流程中,无论事务该当提交还是回滚,server层接口 handlerton构造体的post_ddl 接口都会调用相应存储引擎的实现,进入InnoDB后的函数接口为innobase_post_ddl->Log_DDL::post_ddl。
Post_ddl步骤中,如果事务终极被提交,那么如前文所述,DDL log中的回滚日志会被彻底删除,回滚不会被实行,无需对提交前已经实行的创索引、RENAME等步骤做额外的动作。一些场景下,文件操作日志将会被实行,例如删表操作的终极清理:比拟上文DDL log记录的命名可以创造,只有drop table的接口名Log_DDL::write_drop_log的命名办法并非“已实行步骤的回滚操作”,而是drop自己。这是由于drop table只有在DDL事务提交时,才会真正实行删除操作,进行终极的清理;如果没有commit,删除没有真正发生时,并不须要真正地对删除进行回滚操作。
如果DDL事务终极被回滚,那么上文所述DDL log中的删除日志本身也被回滚,而DDL log中的回滚日志会被实行,根据不同的回滚类型,创建的索引会被删除,RENAME的表名会被退回老表名,存入InnoDB层元数据缓存dict_sys的内存构造将被清理。
4. MDL锁的部分变革4.1 代码架构的重构上文所说8.0 版本对DD的重构,对元数据锁(Metadata Lock,MDL)较为直不雅观的一个改变是代码架构的重构。
8.0 在sql/dd/impl/dictionary_impl.cc 中,dd的namespace内,封装了常见的table和tablespace级别的排他、共享MDL接口,例如:server层刚进入CREATE TABLE流程时,在mysql_create_table_no_lock接口中, 对全体库加intention exclusive(IX)级别MDL锁的步骤,将其封装在类dd::Schema_MDL_locker中。
对付这些常见的表级、库级的MDL操作,5.7 版本通过MDL_REQUEST_INIT等宏管理MDL要求,这些宏的直接调用分散在各种接口的实现中,缺少统一的函数封装,可掩护性较差。而在8.0 版本中,纵然这些dd namespace下的接口在最底层的调用仍旧为MDL_REQUEST_INIT宏不变,这种设计模式也表示了8.0 DD重构后server层对DD统一管理的思路。
4.2 MDL锁类型的拓展enum_mdl_namespace 列举值记录了MDL锁的不同类型工具,在常规的库、表、触发器、函数等之外,8.0新增的MDL列举值包括:
SRID,ACL_CACHE,COLUMN_STATISTICS,RESOURCE_GROUPS,FOREIGN_KEY,CHECK_CONSTRAINT,BACKUP_TABLES,BACKUP_LOCK,
这些MDL的列举值细化了MDL的粒度。例如FOREIGN_KEY列举值,在ALTER TABLE RENAME 流程中,重命名Foreign Key时,会单独对外键的名字加MDL锁;ACL_CACHE列举值是在用户鉴权发生变革的语句实行过程中持锁,此时其他新建立的连接如果拿不到ACL cache的MDL锁,则无法鉴权进行连接。
4.3 新增SDI的MDL在8.0 版本,DD由于表构造不再依赖于server层的FRM文件。除了server层共用的DD表之外,InnoDB还将这份信息以(Serialized dictionary information (SDI)格式存在了tablespace 的物理文件(.IBD)中。
这份SDI元数据是为了应对DD出错的情形下,能够基于单个IBD文件利用ibd2sdi工具获取表构造、规复数据。InnoDB将SDI信息写入同IBD文件的做法,比较5.7 版本基于独立FRM文件、缺少原子性的实现办法更可靠;在DD表破坏时,单个IBD文件仍旧可以通过自带的SDI信息,规复出表构造,即表数据文件自我描述的,可以不依赖于DD解析自身(只管MyISAM在8.0 版本仍把SDI作为独立文件)。
这个新增的SDI机制,在drop table/tablespace时须要MDL锁,在事务提交时自动开释,其接口为:dd_sdi_acquire_exclusive_mdl/dd_sdi_acquire_shared_mdl。但是,这把MDL锁不会与其它库表冲突,是由于其输入的表名和库名会被分外处理,如其库名为dummy_sdi_db,而表名利用SDI_的前缀,实现和真正的space id进行字符串拼接。
MDL因DD的重构,还有其他很多方面的变革,在本篇中不再展开。
5. 总结本文对社区MySQL5.7 到8.0 演进过程中数据字典DD的重构(缓存,持久化),Atomic DDL的关键实现进行了剖析:在server层,通过InnoDB为引擎的数据字典表取代了FRM文件,担保了元数据存储的事务性,并通过Local Cache、Shared_dictionary_cache二级缓存,减少锁冲突,提升性能。Atomic DDL的关键实现基于InnoDB为引擎的数据字典表DDL log,将元数据和DDL的操作存入事务性存储引擎的数据字典表中,有效担保了元数据的同等性。
6. 参考[1] https://dev.mysql.com/blog-archive/mysql-8-0-data-dictionary-background-and-motivation/
[2] https://dev.mysql.com/blog-archive/mysql-8-0-improvements-to-information_schema/
点击关注,第一韶光理解华为云新鲜技能~
华为云博客_大数据博客_AI博客_云打算博客_开拓者中央-华为云