最近一個平常實例在作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 //添加一個創建/刪除索引的任務