編者按: MySQL是目前使用最多的開源數據庫,可是MySQL數據庫的默認設置性能很是的差,必須進行不斷的優化,而優化是一個複雜的任務,本文描述淘寶數據庫團隊針對MySQL數據庫 Metadata Lock子系統的優化, hash_scan 算法的實現解析 的性能優化, TokuDB·版本優化 ,以及 MariaDB·的性能優化。 本文來自淘寶團隊內部經驗分享。php
往期文章: 淘寶內部分享:怎麼跳出MySQL的10個大坑html
背景node
引入MDL鎖的目的,最初是爲了解決著名的bug#989,在MySQL 5.1及以前的版本,事務執行過程當中並不維護涉及到的全部表的Metatdata 鎖,極易出現複製中斷,例如以下執行序列:mysql
Session 1: BEGIN;linux
Session 1: INSERT INTO t1 VALUES (1);算法
Session 2: Drop table t1; --------SQL寫入BINLOGsql
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#7304 , WL#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 )
問題描述
首先,咱們執行下面的TestCase:
--source include/master-slave.inc --source include/have_binlog_format_row.inc connection slave; set global slave_rows_search_algorithms='TABLE_SCAN'; connection master; create table t1(id int, name varchar(20); insert into t1 values(1,'a'); insert into t2 values(2, 'b'); ...... insert into t3 values(1000, 'xxx'); delete from t1; ---source include/rpl_end.inc
隨着 t1 數據量的增大,rpl_hash_scan.test 的執行時間會隨着 t1 數據量的增大而快速的增加,由於在執行 'delete from t1;' 對於t1的每一行刪除操做,備庫都要掃描t1,即全表掃描,若是 select count(*) from t1 = N, 則須要掃描N次 t1 表, 則讀取記錄數爲: O(N + (N-1) + (N-2) + .... + 1) = O(N^2),在 replication 沒有引入 hash_scan,binlog_format=row時,對於無索引表,是經過 table_scan 實現的,若是一個update_rows_log_event/delete_rows_log_event 包含多行修改時,每一個修改都要進行全表掃描來實現,其 stack 以下:
#0 Rows_log_event::do_table_scan_and_update #1 0x0000000000a3d7f7 in Rows_log_event::do_apply_event #2 0x0000000000a28e3a in Log_event::apply_event #3 0x0000000000a8365f in apply_event_and_update_pos #4 0x0000000000a84764 in exec_relay_log_event #5 0x0000000000a89e97 in handle_slave_sql (arg=0x1b3e030) #6 0x0000000000e341c3 in pfs_spawn_thread (arg=0x2b7f48004b20) #7 0x0000003a00a07851 in start_thread () from /lib64/libpthread.so.0 #8 0x0000003a006e767d in clone () from /lib64/libc.so.6
這種狀況下,每每會形成備庫延遲,這也是無索引表所帶來的複製延遲問題。
如何解決問題:
RDS 爲了解這個問題,會在每一個表建立的時候檢查一下表是否包含主建或者惟一建,若是沒有包含,則建立一個隱式主建,此主建對用戶透明,用戶無感,相應的show create, select * 等操做會屏蔽隱式主建,從而能夠減小無索引錶帶來的影響;
官方爲了解決這個問題,在5.6.6 及之後版本引入參數 slave_rows_search_algorithms ,用於指示備庫在 apply_binlog_event時使用的算法,有三種算法TABLE_SCAN,INDEX_SCAN,HASH_SCAN,其中 table_scan與index_scan是已經存在的,本文主要研究HASH_SCAN的實現方式,關於參數 slave_rows_search_algorithms的設置。
hash_scan 的實現方法:
簡單的講,在 apply rows_log_event時,會將 log_event 中對行的更新緩存在兩個結構中,分別是:m_hash, m_distinct_key_list。 m_hash:主要用來緩存更新的行記錄的起始位置,是一個hash表; m_distinct_key_list:若是有索引,則將索引的值push 到m_distinct_key_list,若是表沒有索引,則不使用這個List結構; 其中預掃描整個調用過程以下: Log_event::apply_event
Rows_log_event::do_apply_event Rows_log_event::do_hash_scan_and_update Rows_log_event::do_hash_row (add entry info of changed records) if (m_key_index < MAX_KEY) (index used instead of table scan) Rows_log_event::add_key_to_distinct_keyset ()
當一個event 中包含多個行的更改時,會首先掃描全部的更改,將結果緩存到m_hash中,若是該表有索引,則將索引的值緩存至m_distinct_key_list List 中,若是沒有,則不使用這個緩存結構,而直接進行全表掃描;
執行 stack 以下:
#0 handler::ha_delete_row #1 0x0000000000a4192b in Delete_rows_log_event::do_exec_row #2 0x0000000000a3a9c8 in Rows_log_event::do_apply_row #3 0x0000000000a3c1f4 in Rows_log_event::do_scan_and_update #4 0x0000000000a3c5ef in Rows_log_event::do_hash_scan_and_update #5 0x0000000000a3d7f7 in Rows_log_event::do_apply_event #6 0x0000000000a28e3a in Log_event::apply_event #7 0x0000000000a8365f in apply_event_and_update_pos #8 0x0000000000a84764 in exec_relay_log_event #9 0x0000000000a89e97 in handle_slave_sql #10 0x0000000000e341c3 in pfs_spawn_thread #11 0x0000003a00a07851 in start_thread () #12 0x0000003a006e767d in clone ()
執行過程說明:
Rows_log_event::do_scan_and_update
open_record_scan() do next_record_scan() if (m_key_index > MAX_KEY) ha_rnd_next(); else ha_index_read_map(m_key from m_distinct_key_list) entry= m_hash->get() m_hash->del(entry); do_apply_row() while (m_hash->size > 0);
從執行過程上能夠看出,當使用hash_scan時,只會全表掃描一次,雖然會屢次遍歷m_hash這個hash表,可是這個掃描是O(1),因此,代價很小,所以能夠下降掃描次數,提升執行效率。
hash_scan 的一個 bug
bug詳情:
http://bugs.mysql.com/bug.php?id=72788
bug緣由:m_distinct_key_list 中的index key 不是惟一的,因此存在着對已經刪除了的記錄重複刪除的問題。
bug修復: http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/8494
問題擴展:
在沒有索引的狀況下,是否是把 hash_scan 打開就能提升效率,下降延遲呢? 不必定,若是每次更新操做只一條記錄,此時仍然須要全表掃描,而且因爲entry 的開銷,應該會有後退的狀況;
一個event中能包含多少條記錄的更新呢? 這個和表結構以及記錄的數據大小有關,一個event 的大小不會超過9000 bytes, 沒有參數能夠控制這個size;
hash_scan 有沒有限制呢? hash_scan 只會對更新、刪除操做有效,對於binlog_format=statement 產生的 Query_log_event 或者binlog_format=row 時產生的 Write_rows_log_event 不起做用;
TokuDB 7.5.0大版本已發佈,是一個里程碑的版本,這裏談幾點優化,以饗存儲引擎愛好者們。
a) shutdown加速
有用戶反饋TokuDB在shutdown的時候,半個小時還沒完事,很是不可接受。在shutdown的時候,TokuDB在幹什麼呢?在作checkpoint,把內存中的節點數據序列化並壓縮到磁盤。
那爲何如此耗時呢?若是tokudb_cache_size開的比較大,內存中的節點會很是多,在shutdown的時候,你們都排隊等着被壓縮到磁盤(串行的)。
在7.5.0版本,TokuDB官方針對此問題進行了優化,使多個節點並行壓縮來縮短期。
BTW: TokuDB在早期設計的時候已保留並行接口,只是一直未開啓。
b) 內節點讀取加速
在內存中,TokuDB內節點(internal node)的每一個message buffer都有2個重要數據結構:
1) FIFO結構,保存{key, value}2) OMT結構,保存{key, FIFO-offset}
因爲FIFO不具有快速查找特性,就利用OMT來作快速查找(根據key查到value)。 這樣,當內節點發生cache miss的時候,索引層須要作:
1) 從磁盤讀取節點內容到內存
2) 構造FIFO結構
3) 根據FIFO構造OMT結構(作排序)
因爲TokuDB內部有很多性能探(ji)針(shu),他們發現步驟3)是個不小的性能消耗點,由於每次都要把message buffer作下排序構造出OMT,因而在7.5.0版本,把OMT的FIFO-offset(已排序)也持久化到磁盤,這樣排序的損耗就沒了。
c) 順序寫加速
當寫發生的時候,會根據當前的key在pivots裏查找(二分)當前寫要落入哪一個mesage buffer,若是寫是順序(或局部順序,數據走向爲最右邊路徑)的,就能夠避免由"查找"帶來的額外開銷。
如何判斷是順序寫呢?TokuDB使用了一種簡單的啓發式方法(heurstic):seqinsert_score積分式。 若是:
1) 當前寫入落入最右節點,對seqinsert_score加一分(原子)2) 當前寫入落入非最右節點,對seqinsert_score清零(原子)
當seqinsert_score大於100的時候,就能夠認爲是順序寫,當下次寫操做發生時,首先與最右的節點pivot進行對比判斷,若是確實爲順序寫,則會被寫到該節點,省去很多compare開銷。方法簡單而有效。
從MySQL 5.6.2/MariaDB 10.0.0版本開始,MySQL/MariaDB針對"ORDER BY ...LIMIT n"語句實現了一種新的優化策略。當n足夠小的時候,優化器會採用一個容積爲n的優先隊列來進行排序,而不是排序全部數據而後取出前n條。 這個新算法能夠這麼描述:(假設是ASC排序)
創建一個只有n個元素的優先隊列(堆),根節點爲堆中最大元素
根據其餘條件,依次從表中取出一行數據
若是當前行的排序關鍵字小於堆頭,則把當前元素替換堆頭,從新Shift保持堆的特性
再取一條數據重複2步驟,若是沒有下一條數據則執行5
依次取出堆中的元素(從大到小排序),逆序輸出(從小到大排序),便可得ASC的排序結果
這樣的算法,時間複雜度爲m*log(n),m爲索引過濾後的行數,n爲LIMIT的行數。而原始的全排序算法,時間複雜度爲m*log(m)。只要n遠小於m,這個算法就會頗有效。
不過在MySQL 5.6中,除了optimizer_trace,沒有好的方法來看到這個新的執行計劃到底起了多少做用。MariaDB 10.013開始,提供一個系統狀態,能夠查看新執行計劃調用的次數:
Sort_priority_queue_sorts
描述: 經過優先隊列實現排序的次數。(總排序次數=Sort_range+Sort_scan)
範圍: Global, Session
數據類型: numeric
引入版本: MariaDB 10.0.13
此外,MariaDB還將此信息打入了Slow Log中。只要指定 log_slow_verbosity=query_plan,就能夠在Slow Log中看到這樣的記錄:
# Time: 140714 18:30:39 # User@Host: root[root] @ localhost [] # Thread_id: 3 Schema: test QC_hit: No # Query_time: 0.053857 Lock_time: 0.000188 Rows_sent: 11 Rows_examined: 100011 # Full_scan: Yes Full_join: No Tmp_table: No Tmp_table_on_disk: No # Filesort: Yes Filesort_on_disk: No Merge_passes: 0 Priority_queue: Yes SET timestamp=1405348239;SET timestamp=1405348239; select * from t1 where col1 between 10 and 20 order by col2 limit 100;
"Priority_queue: Yes" 就表示這個Query利用了優先隊列的執行計劃(pt-query-digest 目前已經能夠解析 Priority_queue 這個列)。更多精彩內容,敬請期待!
本文轉載自MySQL.taobao.org ,感謝淘寶數據庫項目組丁奇、鳴嵩、彭立勳、皓庭、項仲、劍川、武藏、祁奚、褚霸、一工。審校:劉亞瓊
本文來自:Linux學習教程網