最近一个日常实例在做 DDL 过程中,直接把数据库给干趴下了,问题还是比较严重的,于是赶紧排查问题,撸了下 crash 堆栈和 alert 日志,发现是在去除唯一约束的场景下,MyRocks 存在一个严重的 bug,于是紧急向官方提了一个 bug。其实问题比较隐蔽,因为直接一条 DDL 语句,数据库是不会挂了,而是在特定情况下,并且对同一个索引操作多次才会发生,因此排查问题也费了一些时间,具体 bug 排查和复现过程不在此展开,有兴趣的童鞋可以直接看 bug 链接:https://github.com/facebook/mysql-5.6/issues/602。借着排查问题的机会,我梳理了 MyRocks DDL 的工作流程,下文主要包括 3 方面内容:MyRocks 数据字典,DDL 操作除了修改数据本身,很重要的一个工作是维护数据字典,第二部分是 MyRocks DDL 的流程,主要围绕增加 / 删除索引的场景展开,最后一部分是分析 DDL 异常处理逻辑。
数据字典 所谓数据字典,就是存储引擎元数据的地方。数据字典可以从两个维度来看,从用户角度来看,数据字典就是 information_schema 表中的 RocksDB 相关的表,主要包括 ROCKSDB_DDL,ROCKSDB_INDEX_FILE_MAP 等。而从 RockDB 内部实现角度来看,所有元数据都以 KV 对的方式存储在 system column family 中。我们看到的 information_schema 中表的信息,其实都是通过 system column family 中的元数据构造出来的,同时在 mysqld 启动时,也会构造一份元数据存储在内存中,方便快速检索查询。下面我会列出 RocksDB 数据字典的几种类型,并列出每种类型 KV 对的形式。// Data dictionary types
- enum DATA_DICT_TYPE {
- DDL_ENTRY_INDEX_START_NUMBER = 1,
- //表与索引映射关系
- INDEX_INFO = 2,
- //索引
- CF_DEFINITION = 3,
- //column family
- BINLOG_INFO_INDEX_NUMBER = 4,
- //binlog位点信息
- DDL_DROP_INDEX_ONGOING = 5,
- //删除索引字典任务
- INDEX_STATISTICS = 6,
- //索引统计信息
- MAX_INDEX_ID = 7,
- //当前最大index_id
- DDL_CREATE_INDEX_ONGOING = 8,
- //添加索引字典任务
- END_DICT_INDEX_ID = 255
- };
1). DDL_ENTRY_INDEX_START_NUMBER 表和索引之间的映射关系 key: Rdb_key_def::DDL_ENTRY_INDEX_START_NUMBER(0x1) + dbname.tablenamevalue: version + {global_index_id}*n_indexes_of_the_table
2). INDEX_INFO 索引 id 和索引属性的关系 key: Rdb_key_def::INDEX_INFO(0x2) + global_index_idvalue: version, index_type, key_value_format_version
index_type: 主键 / 二级索引 / 隐式主键 key_value_format_version: 记录存储格式的版本
3). CF_DEFINITIONcolumn family 属性 key: Rdb_key_def::CF_DEFINITION(0x3) + cf_idvalue: version, {is_reverse_cf, is_auto_cf}
is_reverse_cf: 是否是 reverse column familyis_auto_cf: column family 名字是否是 $per_index_cf,名字自动由 table.indexname 组成
4). BINLOG_INFO_INDEX_NUMBERbinlog 位点及 gtid 信息,binlog_commit 更新此信息 key: Rdb_key_def::BINLOG_INFO_INDEX_NUMBER (0x4)value: version, {binlog_name,binlog_pos,binlog_gtid}
5). DDL_DROP_INDEX_ONGOING 删除的索引任务 key: Rdb_key_def::DDL_DROP_INDEX_ONGOING(0x5) + global_index_idvalue: version
6). INDEX_STATISTICS 索引统计信息 key: Rdb_key_def::INDEX_STATISTICS(0x6) + global_index_idvalue: version, {materialized PropertiesCollector::IndexStats}
7). MAX_INDEX_ID 当前的 index_id,每次创建索引 index_id 都从这个获取和更新 key: Rdb_key_def::CURRENT_MAX_INDEX_ID(0x7)value: version, current max index id
8). DDL_CREATE_INDEX_ONGOING 待创建的索引任务 key: Rdb_key_def::DDL_CREATE_INDEX_ONGOING(0x8) + global_index_idvalue: version
DDL 流程 RocksDB 引擎并没有类似 InnoDB 引擎的增量 row_log 机制,因此 MyRocks 还不支持 Online DDL,只是对部分 DDL 操作支持了 inplace 方式,从 check_if_supported_inplace_alter 接口实现可知,对于 DROP_INDEX, DROP_UNIQUE_INDEX 和 ADD_INDEX 这三个操作,可以通过 inplace 的方式完成 DDL,inplace 方式的优点就是不需要拷贝表,间接减少了锁表时间,其它操作都只能通过重建表的方式来实现。下面我以 inplace 的方式说明 DDL 的执行流程,copy 方式相对会更简单一些。总的入口函数是 mysql_inplace_alter_table,主要包含 4 个阶段。1). 检查存储引擎是否支持 inplace 的 DDL 操作接口:ha_rocksdb::check_if_supported_inplace_alterMyRocks 支持 inplace 方式操作类型是 HA_ALTER_INPLACE_SHARED_LOCK_AFTER_PREPARE,意味着执行 DDL 过程中会堵塞写。2). 准备阶段接口:ha_rocksdb::prepare_inplace_alter_table 对于 RocksDB 引擎来说,支持 inplace 方式主要是添加和删除索引,因此这个过程主要做的事情是搜集需要添加、删除的索引信息。涉及到数据字典操作具体入口函数是 create_key_defs,最终调用 create_key_def 接口,每个索引对应一个 Rdb_key_def 对象。这里涉及到一个主要操作是为索引产生全局有序的 index_id(ddl_manager.get_and_update_next_number)。
3). 执行阶段接口:ha_rocksdb::inplace_alter_table 这里主要是添加二级索引操作,具体实现在 inplace_populate_sk 接口。主要包括两部分内容,更新数据字典和创建索引。a. 更新数据字典数据字典维护通过最终通过接口 start_ongoing_index_operation 完成,为新建索引构造 KV 对,写入 system column family。,所有添加的索引的 KV 对会作为一个事务 commit,表示一批待创建索引的任务。
- begin
- put-KV:(DDL_CREATE_INDEX_ONGOING,cf_id,index_id)->(DDL_CREATE_INDEX_ONGOING_VERSION)
- commit
b. 创建索引 接下来就是真正创建索引的操作,通过遍历 PK 索引,构造出新增二级索引的格式记录,然后写入索引,主要实现接口在 update_sk 里。由于 RockDB 行锁实现中,每个 key 对应一把锁,并且锁对象不能复用,因此锁消耗的总内存与 key 大小和 key 数量相关,为了保证系统运行中内存可控,一般开启 rocksdb_commit_in_the_middle 避免大事务。因此这个这个过程也会触发是否提前提交事务的检查,主要实现接口在 do_bulk_commit 里面。
4). 提交或回滚阶段接口:commit_inplace_alter_tablea. 处理待删除的索引,最终通过接口 start_ongoing_index_operation(drop) 完成。b. 对于新增索引,写入索引字典信息 c. 写入表和索引的映射关系 对表进行 alter 操作后,会增一些索引,并删除一些索引,因此表对应的索引关系需要重建,主要实现接口在 Rdb_tbl_def::put_dict 里面。 第 1),2),3) 涉及的字典操作整个作为一个事务提交。
- begin put - KV: (DDL_DROP_INDEX_ONGOING, cf_id, index_id) - >(DDL_DROP_INDEX_ONGOING_VERSION) put - KV: (INDEX_INFO + cf_id + index_id) - >INDEX_INFO_VERSION_VERIFY_KV_FORMAT + index_type + kv_version put - KV: (DDL_ENTRY_INDEX_START_NUMBER, dbname_tablename) - >version + {
- key_entry,
- key_entry,
- key_entry,
- ...
- },key_entry-->(cf_id, index_nr) commit
d. 维护数据字典在内存中对象 m_ddl_hash。主要工作是从 hash 表中摘掉老的 tbl 对象,写入新的 tbl 对象,主要实现接口在 Rdb_ddl_manager::put 里面。
e. 清理 DDL_CREATE_INDEX_ONGOING 标记。正常执行到这里,表示新建的索引已经成功执行,需要清理 DDL_CREATE_INDEX_ONGOING 标记。主要实现接口在 finish_indexes_operation 里面,最终调用 end_ongoing_index_operation 将之前加入的 KV 对进行删除动作。(DDL_CREATE_INDEX_ONGOING,cf_id,index_id)->(DDL_CREATE_INDEX_ONGOING_VERSION),并将整个操作作为一个事务 commit。我们可以看到,整个过程已经执行完毕,但并没有看到哪里将删除的索引真正清理掉,RocksDB 里面删除索引实质是一个异步的过程,真正删除索引的动作通过后台线程 Rdb_drop_index_thread 完成。所以,到这里会主动触发一次唤醒 rdb_drop_idx_thread 的动作,告知线程有活干了。
Rdb_drop_index_thread 工作流程 1). 获取待删除索引列表 key=(DDL_DROP_INDEX_ONGOING)2). 逐一遍历每个需要删除的索引,按照(index_id,index_id+1)key 范围来删除记录 3). 并调用 CompactRange 触发合并 4). 通过 index_id 来查找 key,若不存在 index-id 相同的 key,则认为 index 已经被清理 5). 最后调用 finish_indexes_operation(DDL_DROP_INDEX_ONGOING) 清理待删除索引标记,并将索引字典信息从数据字典中删除,具体实现参考 delete_index_info。
- begin
- delete-key: (DDL_DROP_INDEX_ONGOING,cf_id,index_id)
- delete-key: (INDEX_INFO+cf_id+index_id)
- batch-commit
DDL 异常处理 从上述的实现来看,我们执行一个 DDL 操作,除了本身索引操作的事务,涉及数据字典的操作的事务也有好几个,所以整个 DDL 操作并不是一个原子操作。比如在执行阶段的第 1 步,字典相关的操作提交后,实例 crash 了,那么这些字典操作内容就残留在 system Column family 中了,但从业务角度来看,并不影响。上面介绍的 mysql_inplace_alter_table 包含了 DDL 的主要执行过程,实际上,在此之前还会通过 mysql_prepare_alter_table 创建临时表定义 frm 文件,(文件名一般以 #sql 开头),该文件包含了目标表的 schema 定义;并在 DDL 结束的时候,通过 mysql_rename_table 更新为目标表名. frm。如果在 rename 之前,实例 crash 了,就会导致 frm 文件的内容仍然是老版本,但 RocksDB 引擎字典已经更新。从表现形式来看,就会发现 show create table xxx,显示的索引内容与 information_schema.ROCKSDB_DDL 的数据字典不一致。前面讨论的两种情况都是 inplace 方式带来的问题,对于 copy 方式,由于需要重建表,会将临时表 #sqlxxx 的信息写入数据字典,如果这个动作完成后,实例 crash,会导致数据字典中残留有临时表的信息。mysqld 重启时,会根据字典的信息检查表是否存在,主要通过接口 validate_schemas 实现,具体而言,通过数据字典中的表名查找对应的 frm 文件,并且查找过程中会忽略# 开头的临时 frm 文件,因此会导致只要数据字典中包含了临时表的字典信息,则会导致 mysqld 启动失败,并报如下错误。
- error:
- [Warning] RocksDB: Schema mismatch - Table test.#sql-b54_1 is registered in RocksDB but does not have a .frm file
- [ERROR] RocksDB: Problems validating data dictionary against .frm files, exiting
- [ERROR] RocksDB: Failed to initialize DDL manager.
如果想正常启动,可以临时通过参数 rocksdb_validate_tables=2 设置忽略这个错误,毕竟临时表的数据字典不影响业务表的使用。从我这里分析来看,目前 DDL 在异常处理这块还处理的不够好,根本原因还在于 DDL 不是一个原子操作,server 层和引擎层的修改在某些情况下无法保持一致,导致问题出现。
相关实现文件和接口 storage/rocksdb/rdb_datadic.cc // 数据字典相关代码 storage/rocksdb/rdb_i_s.cc //information_schema 相关代码 myrocks::ha_rocksdb::inplace_populate_sk // 更新二级索引 Rdb_dict_manager::get_max_index_id // 获取最大 index_idha_rocksdb::check_if_supported_inplace_alter // 检查是否支持 inplacemyrocks::ha_rocksdb::create //copy 方式建表接口 myrocks::ha_rocksdb::create_key_def // 建立 key 对象 myrocks::Rdb_ddl_manager::get_and_update_next_number // 获取下一个 index_idRdb_dict_manager::start_ongoing_index_operation // 添加一个建立 / 删除索引的任务
来源: http://www.cnblogs.com/cchust/p/6716823.html