最近一個日常實例在做DDL過程中,直接把資料庫給乾趴下了,問題還是比較嚴重的,於是趕緊排查問題,擼了下crash堆棧和alert日誌,發現是在去除唯一約束的場景下,MyRocks存在一個嚴重的bug,於是緊急向官方提了一個bug。其實問題比較隱蔽,因為直接一條DDL語句,資料庫是不會掛了,而是在特定 ...
最近一個日常實例在做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.tablename
value: version + {global_index_id}*n_indexes_of_the_table
2). INDEX_INFO
索引id和索引屬性的關係
key: Rdb_key_def::INDEX_INFO(0x2) + global_index_id
value: version, index_type, key_value_format_version
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}
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 //添加一個建立/刪除索引的任務