MyRocks 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 //添加一個建立/刪除索引的任務

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM