网易迁移方案设计 DDB 迁移到 TiDB
1591
2023-06-02
MySQL 深潜 - 一文详解 MySQL Data Dictionary
一、背景
在 MySQL 8.0 之前,Server 层和存储引擎(比如 InnoDB)会各自保留一份元数据(schema name, table definition 等),不仅在信息存储上有着重复冗余,而且可能存在两者之间存储的元数据不同步的现象。不同存储引擎之间(比如 InnoDB 和 MyISAM)有着不同的元数据存储形式和位置(.FRM, .PAR, .OPT, .TRN and .TRG files),造成了元数据无法统一管理。此外,将元数据存放在不支持事务的表和文件中,使得 DDL 变更不会是原子的,crash recovery 也会成为一个问题。
为了解决上述问题,MySQL 在 8.0 中引入了 data dictionary 来进行 Server 层和不同引擎间统一的元数据管理,这些元数据都存储在 InnoDB 引擎的表中,自然的支持原子性,且 Server 层和引擎层共享一份元数据,不再存在不同步的问题。
二、整体架构
data dictionary 提供了统一的 client API 供 Server 层和引擎层使用,包含对元数据访问的 acquire() / drop() / store() / update() 基本操作。底层实现了对 InnoDB 引擎存放的数据字典表的读写操作,包含开表(open table)、构造主键、主键查找等过程。client 和底层存储之间通过两级缓存来加速对元数据对象的内存访问,两级缓存都是基于 hash map 实现的,一层缓存是 local 的,由每个 client(每个线程对应一个 client)独享;二级缓存是 share 的,为所有线程共享的全局缓存。下面我将对 data dictionary 的数据结构和实现架构做重点介绍,也会分享一个支持原子的 DDL 在 data dictionary 层面的实现过程。
三、metadata 在内存和引擎层面的表示
data dictionary (简称DD)中的数据结构是完全按照多态、接口/实现的形式来组织的,接口通过纯虚类来实现(比如表示一个表的 Table),其实现类(Table_impl)为接口类的名字加 _impl 后缀。下面以 Table_impl 为例介绍一个表的元数据对象在 DD cache 中的表示。
1.Table_impl
Table_impl 也是代码实现中 client 最常访问的内存结构,开发者想要增加新的属性,直接在这个类中添加和初始化即可,但是仅仅如此不会自动将该属性持久化到存储引擎中。除了上述简单属性之外,还包括与一个表相关的复杂属性,比如列信息、索引信息、分区信息等,这些复杂属性都是存在其他的 DD 表中,在内存 cache 中也都会集成到 Table_impl 对象里。
从Abstract_table_impl继承来的 Collection m_columns 就表示表的所有列集合,集合中的每一个对象 Column_impl 表示该列的元信息,包括数值类型、是否为 NULL、是否自增、默认值等。同时也包含指向 Abstract_table_impl 的指针,将该列与其对应的表联系起来。
class Column_impl : public Entity_object_impl, public Column { // Fields. enum_column_types m_type; bool m_is_nullable; bool m_is_zerofill; bool m_is_unsigned; bool m_is_auto_increment; bool m_is_virtual; bool m_default_value_null; String_type m_default_value; // References to tightly-coupled objects. Abstract_table_impl *m_table; };
此外 Table_impl 中也包含所有分区的元信息集合 Collection m_partitions,存放每个分区的 id、引擎、选项、范围值、父子分区等。
因此获取到一个表的 Table_impl,我们就可以获取到与这个表相关联的所有元信息。
2.Table_impl 是如何持久化存储和访问的
DD cache 中的元信息都是在 DD tables 中读取和存储的,每个表存放一类元信息的基本属性字段,比如 tables、columns、indexes等,他们之间通过主外键关联连接起来,组成 Table_impl 的全部元信息。DD tables 存放在 mysql 的表空间中,在 release 版本对用户隐藏,只能通过 INFORMATION SCHEMA 的部分视图查看;在 debug 版本可通过设置 SET debug='+d,skip_dd_table_access_check' 直接访问查看。比如:
通过以上 mysql.tables 的表定义可以获得存储引擎中实际存储的元信息字段。DD tables 包括 tables、schemata、columns、column_type_elements、indexes、index_column_usage、foreign_keys、foreign_key_column_usage、table_partitions、table_partition_values、index_partitions、triggers、check_constraints、view_table_usage、view_routine_usage 等。
Storage_adapter 是访问持久存储引擎的处理类,包括 get() / drop() / store() 等接口。当初次获取一个表的元信息时,会调用 Storage_adapter::get() 接口,处理过程如下:
Storage_adapter::get() // 根据访问对象类型,将依赖的 DD tables 加入到 open table list 中 |--Open_dictionary_tables_ctx::register_tables
上述在获取列和属性的对应关系时,根据的是 Tables 对象的枚举类型下标,按顺序包含了该类型 DD 表中的所有列,与上述表定义是一一对应的。因此如果我们需要新增 DD 表中存储的列时,也需要往下面枚举类型定义中加入对应的列,并且在 Table_impl::restore_attributes() / Table_impl::store_attributes() 函数中添加对新增列的读取和存储操作。
class Tables : public Entity_object_table_impl { enum enum_fields { FIELD_ID, FIELD_SCHEMA_ID, FIELD_NAME, FIELD_TYPE, FIELD_ENGINE, FIELD_MYSQL_VERSION_ID, FIELD_ROW_FORMAT, FIELD_COLLATION_ID, FIELD_COMMENT, FIELD_HIDDEN, FIELD_OPTIONS, FIELD_SE_PRIVATE_DATA, FIELD_SE_PRIVATE_ID, FIELD_TABLESPACE_ID, FIELD_PARTITION_TYPE, FIELD_PARTITION_EXPRESSION, FIELD_PARTITION_EXPRESSION_UTF8, FIELD_DEFAULT_PARTITIONING, FIELD_SUBPARTITION_TYPE, FIELD_SUBPARTITION_EXPRESSION, FIELD_SUBPARTITION_EXPRESSION_UTF8, FIELD_DEFAULT_SUBPARTITIONING, FIELD_CREATED, FIELD_LAST_ALTERED, FIELD_VIEW_DEFINITION, FIELD_VIEW_DEFINITION_UTF8, FIELD_VIEW_CHECK_OPTION, FIELD_VIEW_IS_UPDATABLE, FIELD_VIEW_ALGORITHM, FIELD_VIEW_SECURITY_TYPE, FIELD_VIEW_DEFINER, FIELD_VIEW_CLIENT_COLLATION_ID, FIELD_VIEW_CONNECTION_COLLATION_ID, FIELD_VIEW_COLUMN_NAMES, FIELD_LAST_CHECKED_FOR_UPGRADE_VERSION_ID, NUMBER_OF_FIELDS // Always keep this entry at the end of the enum }; };
四、多级缓存
两级缓存的底层实现很统一,都是基于 hash map 的,目前的实现是 std::map。Local_multi_map 和 Shared_multi_map都是派生于 Multi_map_base。
template
之所以叫 Multi_map_base,是因为其中包含了多个 hash map,适合用户根据不同类型的 key 来获取缓存对象,比如 id、name、DD cache 本身等。Element_map 就是对 std::map 的一个封装,key 为前述几种类型之一,value 为 DD cache 对象指针的一个封装 Cache_element,封装了对象本身和引用计数。
Multi_map_base 对象实现了丰富的 m_map() 模板函数,可以很方便的根据 key 的类型不同选择到对应的 hash map。
Shared_multi_map 与 Local_multi_map 的不同在于,Shared_multi_map 还引入了一组 latch 与 condition variable 用于并发访问中的线程同步与 cache miss 的处理。同时对 Cache_element 对象做了内存管理和复用的相关能力。
1.局部缓存
2.共享缓存
共享缓存是 Server 全局唯一的,使用单例 Shared_dictionary_cache 来实现。与上述局部缓存中 Object_registry 相似,Shared_dictionary_cache 也需要包含针对各种类型对象的缓存。与 Multi_map_base 实现根据 key 类型自动选取对应 hash map 的模版函数相似,Object_registry 和 Shared_dictionary_cache 也都实现了根据访问对象的类型选择对应缓存的 m_map() 函数,能够很大程度上简化函数调用。
class Shared_dictionary_cache { Shared_multi_map
与局部缓存可以无锁访问 hash map 不同,共享缓存在获取 / 释放 DD cache object 时都需要加锁来完成引用计数的调整和防止访问过程中被 destroy 掉。
3.缓存获取过程
用户通过 client 调用元数据对象获取函数,传入元数据的 name 字符串,然后构建出对应的 name key,通过 key 去缓存中获取元数据对象。获取的整体过程就是一级局部缓存 -> 二级共享缓存 -> 存储引擎。
Cache miss
// Get a wrapper element from the map handling the given key type. template
由于开表访问 DD tables,构建 DD cache object 的过程相对耗时,不会一直给 Shared_multi_map 加锁,因此需要对并发访问的 client 做并发控制。DD 的实现方法是第一个访问的 client 会将 cache miss 的 key 加入到 Shared_multi_map的 m_missed 集合中,这个集合包含着现在所有正在读取元数据的对象 key 值。之后访问的 client 看到目标 key 值在 m_missed 集合中就会进入等待。
当第一个 client 获取到完整的 DD cache object,加入到共享缓存之后,移除 m_missed 集合中对应的 key,并通过广播的方式通知之前等待的线程重新在共享缓存中获取。
五、Auto_releaser
在嵌套函数调用过程中,可能在每一层都会有自己的 Auto_releaser,他们之间通过一个简单的链表指针连接起来。在函数返回时将本层需要 release 的对象 release 掉,需要返回给上层使用的 DD cache 对象交给上层的 Auto_releaser 来负责。通过 transfer_release() 可以在不同层次的 Auto_releaser 对象间转移需要 release 的对象,可以灵活的指定不再需要 DD cache 对象的层次。
六、应用举例:inplace DDL 过程中对 DD 的操作
在 MySQL inplace DDL 执行过程中,会获取当前表定义的 DD cache 对象,然后根据实际的 DDL 操作内容构造出新对应的 DD 对象。然后依次调用 client 的接口完成对当前表定义的删除和新表定义的存储。
在 drop() 过程中,会将当前表定义的 DD cache 对象对应的数据从存储引擎中删除,然后从共享缓存中移除(这要求当前对象的引用计数仅为1,即只有当前线程使用),之后加入到 dropped 局部缓存中。
在这个过程中,由于 MDL(metadata lock) 的存在,不会有其他的线程尝试访问正在变更对象的 DD object,所以可以安全的对 Shared_dictionary_cache 进行操作。当 DDL 操作结束(提交或回滚),释放 EXCLUSIVE 锁之后,新的线程就可以重新从存储引擎上加载新的表定义。
七、总结
MySQL data dictionary 解决了背景所述旧架构中的诸多问题,使元数据的访问更加安全,存储和管理成本更低。架构实现非常的精巧,通过大量的模版类实现使得代码能够最大程度上被复用。多层缓存的实现也能显著提升访问效率。通过 client 简洁的接口,让 Server 层和存储层能在任何地方方便的访问元数据。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。