MySQL核心月報2014.11-MySQL· 5.7優化·MetadataLock子系統的優化

db匠發表於2016-05-23

背景

引入MDL鎖的目的,最初是為了解決著名的bug#989,在MySQL 5.1及之前的版本,事務執行過程中並不維護涉及到的所有表的Metatdata 鎖,極易出現複製中斷,例如如下執行序列:

Session 1: BEGIN;
Session 1: INSERT INTO t1 VALUES (1);
Session 2: Drop table t1; ——–SQL寫入BINLOG
Session 1: COMMIT; —–事務寫入BINLOG

在備庫重放 binlog時,會先執行DROP TABLE,再INSERT資料,從而導致複製中斷。


在MySQL 5.5版本里,引入了MDL, 在事務過程中涉及到的所有表的MDL鎖,直到事務結束才釋放。這意味著上述序列的DROP TABLE 操作將被Session 1阻塞住直到其提交。


不過用過5.5的人都知道,MDL實在是個讓人討厭的東西,相信不少人肯定遇到過在使用mysqldump做邏輯備份時,由於需要執行FLUSH TABLES WITH READ LOCK (以下用FTWRL縮寫代替)來獲取全域性GLOBAL的MDL鎖,因此經常可以看到“wait for global read lock”之類的資訊。如果備庫存在大查詢,或者複製執行緒正在執行比較漫長的DDL,並且FTWRL被block住,那麼隨後的QUERY都會被block住,導致業務不可用引發故障。


為了解決這個問題,Facebook為MySQL增加新的介面替換掉FTWRL 只建立一個read view ,並返回與read view一致的binlog位點;另外Percona Server也實現了一種類似的辦法來繞過FTWRL,具體點選文件連線以及percona的部落格,不展開闡述。


MDL解決了bug#989,卻引入了一個新的熱點,所有的MDL鎖物件被維護在一個hash物件中;對於熱點,最正常的想法當然是對其進行分割槽來分散熱點,不過這也是Facebook的大神Mark Callaghan在report了bug#66473後才加入的,當時Mark觀察到MDL_map::mutex的鎖競爭非常高,進而推動官方改變。因此在MySQL 5.6.8及之後的版本中,引入了新引數metadata_locks_hash_instances來控制對mdl hash的分割槽數(Rev:4350);


不過故事還沒結束,後面的測試又發現雜湊函式有問題,類似somedb.someprefix1….somedb.someprefix8的hash key值相同,都被hash到同一個桶下面了,相當於hash分割槽沒生效。這屬於hash演算法的問題,喜歡考古的同學可以閱讀下bug#66473後面Dmitry Lenev的分析。


Mark進一步的測試發現Innodb的hash計算演算法比my_hash_sort_bin要更高效, Oracle的開發人員重開了個bug#68487來跟蹤該問題,並在MySQL5.6.15對hash key計算函式進行優化,包括fix 上面說的hash計算問題(Rev:5459),使用MurmurHash3演算法來計算mdl key的hash值。


MySQL 5.7 對MDL鎖的優化

在MySQL 5.7裡對MDL子系統做了更為徹底的優化。主要從以下幾點出發:


第一,儘管對MDL HASH進行了分割槽,但由於是以表名+庫名的方式作為key值進行分割槽,如果查詢或者DML都集中在同一張表上,就會hash到相同的分割槽,引起明顯的MDL HASH上的鎖競爭

針對這一點,引入了LOCK-FREE的HASH來儲存MDL_lock,LF_HASH無鎖演算法基於論文”Split-Ordered Lists: Lock-Free Extensible Hash Tables”,實現還比較複雜。 注:實際上LF_HASH很早就被應用於Performance Schema,算是比較成熟的程式碼模組。

由於引入了LF_HASH,MDL HASH分割槽特性自然直接被廢除了 。

對應WL#7305, PATCH(Rev:7249)


第二,從廣泛使用的實際場景來看,DML/SELECT相比DDL等高階別MDL鎖型別,是更為普遍的,因此可以針對性的降低DML和SELECT操作的MDL開銷。

為了實現對DML/SELECT的快速加鎖,使用了類似LOCK-WORD的加鎖方式,稱之為FAST-PATH,如果FAST-PATH加鎖失敗,則走SLOW-PATH來進行加鎖。

每個MDL鎖物件(MDL_lock)都維持了一個long long型別的狀態值來標示當前的加鎖狀態,變數名為MDL_lock::m_fast_path_state 舉個簡單的例子:(初始在sbtest1表上對應MDL_lock::m_fast_path_state值為0)

Session 1: BEGIN;
Session 1: SELECT * FROM sbtest1 WHERE id =1; //m_fast_path_state = 1048576, MDL ticket 不加MDL_lock::m_granted佇列
Session 2: BEGIN;
Session 2: SELECT * FROM sbtest1 WHERE id =2; //m_fast_path_state=1048576+1048576=2097152,同上,走FAST PATH
Session 3: ALTER TABLE sbtest1 ENGINE = INNODB; //DDL請求加的MDL_SHARED_UPGRADABLE型別鎖被視為unobtrusive lock,可以認為這個是比上述SQL的MDL鎖級別更高的鎖,並且不相容,因此被強制走slow path。而slow path是需要加MDL_lock::m_rwlock的寫鎖。m_fast_path_state = m_fast_path_state | MDL_lock::HAS_SLOW_PATH | MDL_lock::HAS_OBTRUSIVE
注:DDL還會獲得庫級別的意向排他MDL鎖或者表級別的共享可升級鎖,但為了表述方便,這裡直接忽略了,只考慮涉及的同一個MDL_lock鎖物件。
Session 4: SELECT * FROM sbtest1 WHERE id =3; // 檢查m_fast_path_state &HAS_OBTRUSIVE,如果DDL還沒跑完,就會走slow path。

從上面的描述可以看出,MDL子系統顯式的對鎖型別進行了區分(OBTRUSIVE or UNOBTRUSIVE),儲存在陣列矩陣m_unobtrusive_lock_increment。 因此對於相容型別的MDL鎖型別,例如DML/SELECT,加鎖操作幾乎沒有任何讀寫鎖或MUTEX開銷。

對應WL#7304WL#7306 , PATCH(Rev:7067,Rev:7129)(Rev:7586)


第三,由於引入了MDL鎖,實際上早期版本用於控制Server和引擎層表級併發的THR_LOCK 對於Innodb而言已經有些冗餘了,因此Innodb表完全可以忽略這部分的開銷。

不過在已有的邏輯中,Innodb依然依賴THR_LOCK來實現LOCK TABLE tbname READ,因此增加了新的MDL鎖型別來代替這種實現。

實際上程式碼的大部分修改都是為了處理新的MDL型別,Innodb的改動只有幾行程式碼。

對應WL#6671,PATCH(Rev:8232)


第四,Server層的使用者鎖(通過GET_LOCK函式獲取)使用MDL來重新實現。

使用者可以通過GET_LOCK()來同時獲取多個使用者鎖,同時由於使用MDL來實現,可以藉助MDL子系統實現死鎖的檢測。

注意由於該變化,導致使用者鎖的命名必須小於64位元組,這是受MDL子系統的限制導致。

對應WL#1159, PATCH(Rev:8356)


相關文章