最近一个平常实例在作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异常处理逻辑。mysql
数据字典
所谓数据字典,就是存储引擎元数据的地方。数据字典能够从两个维度来看,从用户角度来看,数据字典就是information_schema表中的
RocksDB相关的表,主要包括ROCKSDB_DDL,ROCKSDB_INDEX_FILE_MAP等。而从RockDB内部实现角度来看,全部元数据都以KV对的方式存储在system column family中。咱们看到的information_schema中表的信息,其实都是经过system column family中的元数据构造出来的,同时在mysqld启动时,也会构造一份元数据存储在内存中,方便快速检索查询。下面我会列出RocksDB数据字典的几种类型,并列出每种类型KV对的形式。
// Data dictionary typesgit
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.tablename
value: version + {global_index_id}*n_indexes_of_the_tablegithub
2). INDEX_INFO
索引id和索引属性的关系
key: Rdb_key_def::INDEX_INFO(0x2) + global_index_id
value: version, index_type, key_value_format_versionsql
index_type:主键/二级索引/隐式主键
key_value_format_version: 记录存储格式的版本数据库
3). CF_DEFINITION
column family属性
key: Rdb_key_def::CF_DEFINITION(0x3) + cf_id
value: version, {is_reverse_cf, is_auto_cf}异步
is_reverse_cf: 是不是reverse column family
is_auto_cf: column family名字是不是$per_index_cf,名字自动由table.indexname组成函数
4). BINLOG_INFO_INDEX_NUMBER
binlog位点及gtid信息,binlog_commit更新此信息
key: Rdb_key_def::BINLOG_INFO_INDEX_NUMBER (0x4)
value: version, {binlog_name,binlog_pos,binlog_gtid}spa
5). DDL_DROP_INDEX_ONGOING
删除的索引任务
key: Rdb_key_def::DDL_DROP_INDEX_ONGOING(0x5) + global_index_id
value: version线程
6). INDEX_STATISTICS
索引统计信息
key: Rdb_key_def::INDEX_STATISTICS(0x6) + global_index_id
value: 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_id
value: 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_alter
MyRocks支持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_table
a.处理待删除的索引,最终经过接口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_id
ha_rocksdb::check_if_supported_inplace_alter //检查是否支持inplace
myrocks::ha_rocksdb::create //copy方式建表接口
myrocks::ha_rocksdb::create_key_def //创建key对象
myrocks::Rdb_ddl_manager::get_and_update_next_number //获取下一个index_id
Rdb_dict_manager::start_ongoing_index_operation //添加一个创建/删除索引的任务