摘要:本文分析了.7到8.0演进过程中数据字典DD的重构(缓存、持久化)以及DDL的关键实现。
本文分享自华为云社区《【华为云MySQL技术专栏】数据字典重构源码解读-云社区-华为云》,作者:数据库
一、背景介绍
在使用.7版本的实践中,我们很容易遇到DDL crash导致的数据不一致的问题。具体场景描述如下:
主备高可用架构部署下,在备机上回放执行DROP TABLE过程中,触发了其他社区Bug,导致备机进程崩溃。备机重启后,由于没有同时清理FRM文件和存储表结构的表空间IBD,导致执行DROP TABLE再次失败,需要手动清理备机的物理文件,这带来了自动化运维面临很大障碍。
这个问题的本质是.7版本的DDL是非原子的,数据字典架构有缺陷。 MySQL社区从5.7版本到8.0版本的演进过程中,一大变化就是数据字典(Data,以下简称DD)的重构以及对相关原子数据(DDL)的支持。重构的动机来自于5.7版本数据字典的以下问题[1]:
(1)层和存储引擎插件层的数据字典不统一。 DD信息由存储引擎分别维护,造成部分DD信息冗余,进而带来DD信息不同步的隐患。
(2)不同类型的DD文件缺乏统一的访问API,不利于后续的维护和扩展。
(3)非原子DDL:数据字典存储在非事务表中。如果DDL中途崩溃,就会造成数据残留和复制问题。
(4)的表现受到了批评。在5.7版本中,表定义是临时表。这些临时表的数据来自FRM文件、存储引擎统计信息等,主要缺点是与表结构文件FRM交互会导致大量的I/O开销和性能较差[2]。
下面将分析.0版本数据字典重构相关的代码,并讲解重构后如何解决5.7版本中的相关问题。
2.数据字典的变化
DD的重构对DDL语句流程和锁系统有何影响?可以从最常见的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
在层的进入之前,5.7版本和8.0版本的主要区别在于元数据的持久存储。 5.7版本写入FRM文件,8.0版本直接将元数据写入表中。详细信息请参见下面的第 2.2 章。另外,在8.0版本代码中,重构了图层数据字典缓存机制。详细信息请参见下面的第 2.3 章。进入后,表的元数据缓存结构的锁持有粒度在8.0版本中也变得更加细化。详细信息请参见下面的第 2.4 章。
2.2 元数据持久化策略的变化
对比上述过程不难发现,在该层调用存储引擎接口之前,MySQL 5.7和8.0版本分别实现了(5.7)和e(8.0)中的一些持久化相关步骤。
5.7中该层首先写入FRM文件来持久化表结构,并检查是否存在同名的FRM文件,以保证不会重复创建同名表。
在8.0中,不再使用FRM文件。通过调用::store->::store,元数据的变化直接写入格式的数据字典表(DD表)中。这种元数据的改变是由引擎的能力来保证的。交易性的。它真正的持久化是在DDL事务提交之后。取消独立的FRM文件也避免了上面背景描述中提到的问题:在DDL过程中,如果进程崩溃,不能保证IBD和FRM文件会同时创建或清理。与5.7检查FRM同名文件冲突的方式相比,8.0版本使用元数据锁(Lock、MDL)来避免和创建同名表的场景。
2.3 DD缓存机制的变化
MySQL 8.0的层元数据缓存最重要的变化是引入了二级缓存,以及两种新类型的DD缓存:会话私有的本地缓存Local Cache和所有会话可见的全局共享缓存。
该层在查询DD时,首先通过dd::cache::类的接口查询会话本身的本地缓存Local Cache。如果自身Local Cache中没有命中,再查询全局缓存,则全局缓存中命中的DD对象会同时添加到会话的本地缓存中。
当两个缓存都未命中时,将调用存储引擎的接口查询。如果在存储引擎中查询对应的DD对象,返回的对象会同时更新到会话自己的本地缓存Local Cache和图层的全局缓存中。
比较5.7和8.0中层内元数据缓存机制的实现:5.7只有一层全局层。在创建表之前,会调用re进行重复性验证。
通过表名和元数据的映射关系来查找对应表元数据的内存结构。如果只有一层全局元数据缓存,为了保证多线程环境下的安全,必然会涉及到线程之间的锁竞争。 8.0版本引入的会话级本地缓存Local Cache,消除了发生命中时需要访问全局缓存的情况,可以显着降低锁冲突的频率,提升性能。
2.4的变化
内部维护了一套独立的元数据信息缓存,也就是我们常说的,它维护了当前打开的表的元数据信息。元数据信息缓存已从 5.7 扩展至 8.0。
创建表时,该层会读取该层传下来的新表的元数据信息,在内部创建相应的结构体进行维护,然后调用che将其添加到哈希表中。 ->mutex 是整个锁。 8.0中,->mutex在che调用前后获取和释放;而在5.7中,::中的大多数进程都持有这个锁,并从内存中的DD表对象申请堆内存。 ,填完后更新统计信息。这种差异影响并发建表的效率。
2.5的变化
5.7版本中基于临时表实现,依赖于独立的表结构FRM文件,导致大量的I/O开销,导致性能较差;而在8.0版本中,DD相关的表是基于引擎持久化存储的,定义成为基于这些DD表的View。与5.7版本相比,这种基于视图的方式避免了读取FRM文件时与磁盘的交互。基于DD表的视图查询还可以充分利用优化器和DD表本身的索引来提高性能。
2.6 DD变更总结
总结以上,DD从5.7版本到8.0版本的变化如表1所示。
表1
变化
5.7
8.0
冗余文件
FRM 文件独立于 IBD。当DDL过程中崩溃时,两者不一致。字符集文件db.opt、TRG触发文件等
取消FRM等独立文件,采用事务型DD表统一存储。
元数据持久化
表结构依赖独立的FRM文件
基于DD表,它是事务性的。
临时表,依赖FRM,IO开销高
视图、基于引擎的DD表、性能优化
分层缓存机制
全球水平
两层:
会话私有本地缓存,
全球车
DD缓存
锁粒度
在建表过程中,单个线程在整个创建过程中一直持有锁,影响并发。
细粒度的锁定/解锁
3.原子DDL和DDL日志表
DDL原子性是通过8.0中的新功能来保证的,这些新功能与DD重构有关。一方面,元数据存储在表中,这本身就保证了事务性;另一方面,元数据基于层存储到DD表后,实现了后续DDL过程中相关数据文件处理的原子性。比如建表过程中索引的创建、IBD文件的生成都是由另一个DD表来保证的。
为了保证DDL的原子性,在DDL过程中,每一个修改文件或者修改相关内存对象的动作都会记录在基于引擎的DD表DDL日志中。它的类定义和内存中的实例是:
class Log_DDL
dict_table_t *ddl_log;
DDL的每个关键步骤执行完毕后,这张DDL日志表直接记录了执行步骤对应的回滚操作。以创建不包含二级索引的表为例,该层将执行以下函数调用:
->->,创建B+树索引后,会有::->::的调用。
::会记录两种日志:一种是“ index”对应的回滚日志,即删除对应索引的操作;另一个是删除日志,删除上面的回滚日志。
如果DDL事务最终提交,则删除日志会被提交,不会执行索引创建对应的回滚操作;而如果DDL事务最终被回滚,那么删除日志本身也会被回滚,并且索引创建时会执行相应的回滚操作,最终回滚新创建的索引,完成DDL的真正回滚。如果DDL涉及其他文件或内存操作,则按照相同的逻辑执行回滚日志和删除日志记录,以保证DDL提交和回滚后相应的文件和内存被正确清理和重置。
::中回滚日志的具体内容是“创建索引”的回滚操作:创建B+树对应的操作,即释放索引对应的B+树。 DDL日志中新增一条记录表中途时的索引信息: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日志记录包括:
1、ALTER TABLE时有::og,分别记录新旧表名。
2. 创建表空间时::og。
3、上面建表过程中che保存内存DD结构后,::og。
4. DROP TABLE::时,记录要删除的表id。
在事务处理结束或者重启后的crash过程中,无论事务应该提交还是回滚,该层接口结构的接口都会调用对应存储引擎的实现,进入后的功能接口为-> ::.
步骤期间,如果事务最终提交,那么正如上面所说,DDL日志中的回滚日志将被彻底删除,不会执行回滚,不需要对索引创建等进行额外的操作提交之前已执行的步骤。 。在某些场景下,会执行文件操作日志,比如表删除操作的最终清理:对比上面DDL日志记录的命名,我们可以发现只有drop table::的接口名没有命名“回滚已执行的步骤”操作”,而不是drop自己。这是因为drop表只会在DDL事务提交时真正执行删除操作并进行最终清理;如果没有,则不需要实际回滚当删除实际上没有发生时删除。
如果DDL事务最终被回滚,那么上面提到的DDL日志中的删除日志也会被回滚,并且会执行DDL日志中的回滚日志。根据回滚类型,创建的索引将被删除。 ,表名将恢复为旧表名,并且层元数据缓存中存储的内存结构将被清除。
4. MDL锁的部分改动 4.1 代码架构重构
前面提到,8.0版本对DD进行了重构,对元数据锁(Lock、MDL)更直观的改变就是代码架构的重构。
8.0 在sql/dd/impl/中,dd封装了公共表和级别独占和共享的MDL接口。例如:当该层第一次进入TABLE过程时,在界面中,将整个库中的(IX)级MDL加锁步骤封装在类dd::中。
对于这些常见的表级和库级MDL操作,5.7版本通过宏来管理MDL请求。这些宏的直接调用分散在各个接口的实现中,缺乏统一的功能封装,可维护性较差。在8.0版本中,即使在最底层调用dd下的接口,它们仍然是宏。这种设计模式也体现了8.0 DD重构后分层统一管理DD的思想。
4.2 MDL锁类型的扩展
枚举值记录了MDL锁的不同类型的对象。除了常规的库、表、触发器、函数等之外,8.0中新增的MDL枚举值还包括:
SRID,
ACL_CACHE,
COLUMN_STATISTICS,
RESOURCE_GROUPS,
FOREIGN_KEY,
CHECK_CONSTRAINT,
BACKUP_TABLES,
BACKUP_LOCK,
这些MDL枚举值细化了MDL的粒度。比如枚举值,在ALTER TABLE过程中,当Key重命名时,会单独在外键名称上加一个MDL锁;在执行用户身份验证更改的语句期间,枚举值被锁定。此时,其他新创建的连接无法获取ACL缓存的MDL锁,则无法对该连接进行认证。
4.3 新增SDI MDL
在8.0版本中,由于表结构的原因,DD不再依赖图层的FRM文件。除了各层共享的 DD 表外,该信息还以 (SDI) 格式存储在物理文件 (.IBD) 中。
此 SDI 元数据旨在使工具能够在发生 DD 错误时获取表结构并基于单个 IBD 文件恢复数据。将SDI信息写入同一个IBD文件的方法比5.7版本基于独立的FRM文件且缺乏原子性更加可靠;当DD表损坏时,单个IBD文件仍然可以通过其自身的SDI信息从表中恢复。结构,即表数据文件是自描述的,不依赖于DD解析本身(尽管在8.0版本中SDI仍然被视为单独的文件)。
这种新的SDI机制在删除表/时需要MDL锁,并且在事务提交时自动释放。它的接口是:/d_mdl。不过这个MDL锁不会和其他库表冲突,因为输入的表名和库名都会经过特殊处理,比如库名,表名使用SDI_前缀实现与真实空间id的字符匹配。字符串拼接。
由于DD的重构,MDL还发生了很多其他的变化,本文不再讨论。
5. 总结
本文分析了社区从 7.0 到 8.0 演进过程中数据字典 DD(缓存、持久化)的重构以及 DDL 的关键实现: 层上将 FRM 文件替换为引擎的数据字典表,保证元数据的事务性存储,并通过Local Cache和Che二级缓存,减少锁冲突,提高性能。 DDL的关键实现是基于引擎的数据字典表DDL日志。元数据和DDL操作存储在事务存储引擎的数据字典表中,有效保证元数据的一致性。
6. 参考资料
[1]
[2]
点击关注,第一时间了解华为云新技术~
华为云博客_大数据博客_AI博客_云计算博客_开发者中心-华为云
扫一扫在手机端查看
我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求,请立即点击咨询我们或拨打咨询热线: 13761152229,我们会详细为你一一解答你心中的疑难。