淘宝内部分享:MySQL MariaDB性能优化
淘寶內(nèi)部分享:MySQL & MariaDB性能優(yōu)化
摘要:MySQL是目前使用最多的開(kāi)源數(shù)據(jù)庫(kù),但是MySQL數(shù)據(jù)庫(kù)的默認(rèn)設(shè)置性能非常的差,必須進(jìn)行不斷的優(yōu)化,而優(yōu)化是一個(gè)復(fù)雜的任務(wù),本文描述淘寶數(shù)據(jù)庫(kù)團(tuán)隊(duì)針對(duì)MySQL相關(guān)的數(shù)據(jù)庫(kù)優(yōu)化方案。
編者按:MySQL是目前使用最多的開(kāi)源數(shù)據(jù)庫(kù),但是MySQL數(shù)據(jù)庫(kù)的默認(rèn)設(shè)置性能非常的差,必須進(jìn)行不斷的優(yōu)化,而優(yōu)化是一個(gè)復(fù)雜的任務(wù),本文描述淘寶數(shù)據(jù)庫(kù)團(tuán)隊(duì)針對(duì)MySQL數(shù)據(jù)庫(kù)Metadata Lock子系統(tǒng)的優(yōu)化,hash_scan 算法的實(shí)現(xiàn)解析的性能優(yōu)化,TokuDB·版本優(yōu)化,以及MariaDB·的性能優(yōu)化。本文來(lái)自淘寶團(tuán)隊(duì)內(nèi)部經(jīng)驗(yàn)分享。
往期文章:淘寶內(nèi)部分享:怎么跳出MySQL的10個(gè)大坑
MySQL· 5.7優(yōu)化·Metadata Lock子系統(tǒng)的優(yōu)化
背景
引入MDL鎖的目的,最初是為了解決著名的bug#989,在MySQL 5.1及之前的版本,事務(wù)執(zhí)行過(guò)程中并不維護(hù)涉及到的所有表的Metatdata 鎖,極易出現(xiàn)復(fù)制中斷,例如如下執(zhí)行序列:
Session 1: BEGIN;Session 1: INSERT INTO t1 VALUES (1);
Session 2: Drop table t1; --------SQL寫(xiě)入BINLOG
Session 1: COMMIT; -----事務(wù)寫(xiě)入BINLOG
在備庫(kù)重放 binlog時(shí),會(huì)先執(zhí)行DROP TABLE,再I(mǎi)NSERT數(shù)據(jù),從而導(dǎo)致復(fù)制中斷。
在MySQL 5.5版本里,引入了MDL, 在事務(wù)過(guò)程中涉及到的所有表的MDL鎖,直到事務(wù)結(jié)束才釋放。這意味著上述序列的DROP TABLE 操作將被Session 1阻塞住直到其提交。
不過(guò)用過(guò)5.5的人都知道,MDL實(shí)在是個(gè)讓人討厭的東西,相信不少人肯定遇到過(guò)在使用mysqldump做邏輯備份時(shí),由于需要執(zhí)行FLUSH TABLES WITH READ LOCK (以下用FTWRL縮寫(xiě)代替)來(lái)獲取全局GLOBAL的MDL鎖,因此經(jīng)??梢钥吹健皐ait for global read lock”之類(lèi)的信息。如果備庫(kù)存在大查詢,或者復(fù)制線程正在執(zhí)行比較漫長(zhǎng)的DDL,并且FTWRL被block住,那么隨后的QUERY都會(huì)被block住,導(dǎo)致業(yè)務(wù)不可用引發(fā)故障。
為了解決這個(gè)問(wèn)題,Facebook為MySQL增加新的接口替換掉FTWRL 只創(chuàng)建一個(gè)read view ,并返回與read view一致的binlog位點(diǎn);另外Percona Server也實(shí)現(xiàn)了一種類(lèi)似的辦法來(lái)繞過(guò)FTWRL,具體點(diǎn)擊文檔連接以及percona的博客,不展開(kāi)闡述。
MDL解決了bug#989,卻引入了一個(gè)新的熱點(diǎn),所有的MDL鎖對(duì)象被維護(hù)在一個(gè)hash對(duì)象中;對(duì)于熱點(diǎn),最正常的想法當(dāng)然是對(duì)其進(jìn)行分區(qū)來(lái)分散熱點(diǎn),不過(guò)這也是Facebook的大神Mark Callaghan在report了bug#66473后才加入的,當(dāng)時(shí)Mark觀察到MDL_map::mutex的鎖競(jìng)爭(zhēng)非常高,進(jìn)而推動(dòng)官方改變。因此在MySQL 5.6.8及之后的版本中,引入了新參數(shù)metadata_locks_hash_instances來(lái)控制對(duì)mdl hash的分區(qū)數(shù)(Rev:4350);
不過(guò)故事還沒(méi)結(jié)束,后面的測(cè)試又發(fā)現(xiàn)哈希函數(shù)有問(wèn)題,somedb. someprefix1 … .somedb .someprefix8 的hash key值相同,都被hash到同一個(gè)桶下面了,相當(dāng)于hash分區(qū)沒(méi)生效。這屬于hash算法的問(wèn)題,喜歡考古的同學(xué)可以閱讀下bug#66473后面Dmitry Lenev的分析。
Mark進(jìn)一步的測(cè)試發(fā)現(xiàn)Innodb的hash計(jì)算算法比my_hash_sort_bin要更高效, Oracle的開(kāi)發(fā)人員重開(kāi)了個(gè)bug#68487來(lái)跟蹤該問(wèn)題,并在MySQL5.6.15對(duì)hash key計(jì)算函數(shù)進(jìn)行優(yōu)化,包括fix 上面說(shuō)的hash計(jì)算問(wèn)題(Rev:5459),使用MurmurHash3算法來(lái)計(jì)算mdl key的hash值。
MySQL 5.7 對(duì)MDL鎖的優(yōu)化
在MySQL 5.7里對(duì)MDL子系統(tǒng)做了更為徹底的優(yōu)化。主要從以下幾點(diǎn)出發(fā):
第一,盡管對(duì)MDL HASH進(jìn)行了分區(qū),但由于是以表名+庫(kù)名的方式作為key值進(jìn)行分區(qū),如果查詢或者DML都集中在同一張表上,就會(huì)hash到相同的分區(qū),引起明顯的MDL HASH上的鎖競(jìng)爭(zhēng)。
針對(duì)這一點(diǎn),引入了LOCK-FREE的HASH來(lái)存儲(chǔ)MDL_lock,LF_HASH無(wú)鎖算法基于論文"Split-Ordered Lists: Lock-Free Extensible Hash Tables",實(shí)現(xiàn)還比較復(fù)雜。 注:實(shí)際上LF_HASH很早就被應(yīng)用于Performance Schema,算是比較成熟的代碼模塊。由于引入了LF_HASH,MDL HASH分區(qū)特性自然直接被廢除了 。對(duì)應(yīng)WL#7305, PATCH(Rev:7249)
第二,從廣泛使用的實(shí)際場(chǎng)景來(lái)看,DML/SELECT相比DDL等高級(jí)別MDL鎖類(lèi)型,是更為普遍的,因此可以針對(duì)性的降低DML和SELECT操作的MDL開(kāi)銷(xiāo)。
為了實(shí)現(xiàn)對(duì)DML/SELECT的快速加鎖,使用了類(lèi)似LOCK-WORD的加鎖方式,稱之為FAST-PATH,如果FAST-PATH加鎖失敗,則走SLOW-PATH來(lái)進(jìn)行加鎖。
每個(gè)MDL鎖對(duì)象(MDL_lock)都維持了一個(gè)long long類(lèi)型的狀態(tài)值來(lái)標(biāo)示當(dāng)前的加鎖狀態(tài),變量名為MDL_lock::m_fast_path_state 舉個(gè)簡(jiǎn)單的例子:(初始在sbtest1表上對(duì)應(yīng)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隊(duì)列
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請(qǐng)求加的MDL_SHARED_UPGRADABLE類(lèi)型鎖被視為unobtrusive lock,可以認(rèn)為這個(gè)是比上述SQL的MDL鎖級(jí)別更高的鎖,并且不相容,因此被強(qiáng)制走slow path。而slow path是需要加MDL_lock::m_rwlock的寫(xiě)鎖。m_fast_path_state = m_fast_path_state | MDL_lock::HAS_SLOW_PATH | MDL_lock::HAS_OBTRUSIVE
注:DDL還會(huì)獲得庫(kù)級(jí)別的意向排他MDL鎖或者表級(jí)別的共享可升級(jí)鎖,但為了表述方便,這里直接忽略了,只考慮涉及的同一個(gè)MDL_lock鎖對(duì)象。
Session 4: SELECT * FROM sbtest1 WHERE id =3; // 檢查m_fast_path_state &HAS_OBTRUSIVE,如果DDL還沒(méi)跑完,就會(huì)走slow path。
從上面的描述可以看出,MDL子系統(tǒng)顯式的對(duì)鎖類(lèi)型進(jìn)行了區(qū)分(OBTRUSIVE or UNOBTRUSIVE),存儲(chǔ)在數(shù)組矩陣m_unobtrusive_lock_increment。 因此對(duì)于相容類(lèi)型的MDL鎖類(lèi)型,例如DML/SELECT,加鎖操作幾乎沒(méi)有任何讀寫(xiě)鎖或MUTEX開(kāi)銷(xiāo)。對(duì)應(yīng)WL#7304,?WL#7306?, PATCH(Rev:7067,Rev:7129)(Rev:7586)
第三,由于引入了MDL鎖,實(shí)際上早期版本用于控制Server和引擎層表級(jí)并發(fā)的THR_LOCK 對(duì)于Innodb而言已經(jīng)有些冗余了,因此Innodb表完全可以忽略這部分的開(kāi)銷(xiāo)。
不過(guò)在已有的邏輯中,Innodb依然依賴THR_LOCK來(lái)實(shí)現(xiàn)LOCK TABLE tbname READ,因此增加了新的MDL鎖類(lèi)型來(lái)代替這種實(shí)現(xiàn)。實(shí)際上代碼的大部分修改都是為了處理新的MDL類(lèi)型,Innodb的改動(dòng)只有幾行代碼。對(duì)應(yīng)WL#6671,PATCH(Rev:8232)
第四,Server層的用戶鎖(通過(guò)GET_LOCK函數(shù)獲取)使用MDL來(lái)重新實(shí)現(xiàn)。
用戶可以通過(guò)GET_LOCK()來(lái)同時(shí)獲取多個(gè)用戶鎖,同時(shí)由于使用MDL來(lái)實(shí)現(xiàn),可以借助MDL子系統(tǒng)實(shí)現(xiàn)死鎖的檢測(cè)。注意由于該變化,導(dǎo)致用戶鎖的命名必須小于64字節(jié),這是受MDL子系統(tǒng)的限制導(dǎo)致。對(duì)應(yīng)WL#1159, PATCH(Rev:8356)
MySQL·性能優(yōu)化·hash_scan 算法的實(shí)現(xiàn)解析
問(wèn)題描述
首先,我們執(zhí)行下面的TestCase:
[js] view plaincopyprint?[js] view plaincopyprint?
如何解決問(wèn)題:
hash_scan 的實(shí)現(xiàn)方法:
簡(jiǎn)單的講,在 apply rows_log_event時(shí),會(huì)將 log_event 中對(duì)行的更新緩存在兩個(gè)結(jié)構(gòu)中,分別是:m_hash, m_distinct_key_list。 m_hash:主要用來(lái)緩存更新的行記錄的起始位置,是一個(gè)hash表; m_distinct_key_list:如果有索引,則將索引的值push 到m_distinct_key_list,如果表沒(méi)有索引,則不使用這個(gè)List結(jié)構(gòu); 其中預(yù)掃描整個(gè)調(diào)用過(guò)程如下: Log_event::apply_event
[js] view plaincopyprint?執(zhí)行 stack 如下:
執(zhí)行過(guò)程說(shuō)明:
Rows_log_event::do_scan_and_update
[js] view plaincopyprint?hash_scan 的一個(gè) bug
bug詳情:http://bugs.mysql.com/bug.php?id=72788bug原因:m_distinct_key_list 中的index key 不是唯一的,所以存在著對(duì)已經(jīng)刪除了的記錄重復(fù)刪除的問(wèn)題。
bug修復(fù):http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/8494
問(wèn)題擴(kuò)展:
- 在沒(méi)有索引的情況下,是不是把 hash_scan 打開(kāi)就能提高效率,降低延遲呢?不一定,如果每次更新操作只一條記錄,此時(shí)仍然需要全表掃描,并且由于entry 的開(kāi)銷(xiāo),應(yīng)該會(huì)有后退的情況;
- 一個(gè)event中能包含多少條記錄的更新呢?這個(gè)和表結(jié)構(gòu)以及記錄的數(shù)據(jù)大小有關(guān),一個(gè)event 的大小不會(huì)超過(guò)9000 bytes, 沒(méi)有參數(shù)可以控制這個(gè)size;
- hash_scan 有沒(méi)有限制呢?hash_scan 只會(huì)對(duì)更新、刪除操作有效,對(duì)于binlog_format=statement 產(chǎn)生的 Query_log_event 或者binlog_format=row 時(shí)產(chǎn)生的 Write_rows_log_event 不起作用;
TokuDB·版本優(yōu)化·7.5.0
TokuDB 7.5.0大版本已發(fā)布,是一個(gè)里程碑的版本,這里談幾點(diǎn)優(yōu)化,以饗存儲(chǔ)引擎愛(ài)好者們。
a) shutdown加速
有用戶反饋TokuDB在shutdown的時(shí)候,半個(gè)小時(shí)還沒(méi)完事,非常不可接受。在shutdown的時(shí)候,TokuDB在干什么呢?在做checkpoint,把內(nèi)存中的節(jié)點(diǎn)數(shù)據(jù)序列化并壓縮到磁盤(pán)。
那為什么如此耗時(shí)呢?如果tokudb_cache_size開(kāi)的比較大,內(nèi)存中的節(jié)點(diǎn)會(huì)非常多,在shutdown的時(shí)候,大家都排隊(duì)等著被壓縮到磁盤(pán)(串行的)。
在7.5.0版本,TokuDB官方針對(duì)此問(wèn)題進(jìn)行了優(yōu)化,使多個(gè)節(jié)點(diǎn)并行壓縮來(lái)縮短時(shí)間。
BTW: TokuDB在早期設(shè)計(jì)的時(shí)候已保留并行接口,只是一直未開(kāi)啟。
b) 內(nèi)節(jié)點(diǎn)讀取加速
在內(nèi)存中,TokuDB內(nèi)節(jié)點(diǎn)(internal node)的每個(gè)message buffer都有2個(gè)重要數(shù)據(jù)結(jié)構(gòu):
1) FIFO結(jié)構(gòu),保存{key, value}?2) OMT結(jié)構(gòu),保存{key, FIFO-offset}
由于FIFO不具備快速查找特性,就利用OMT來(lái)做快速查找(根據(jù)key查到value)。這樣,當(dāng)內(nèi)節(jié)點(diǎn)發(fā)生cache miss的時(shí)候,索引層需要做:
1) 從磁盤(pán)讀取節(jié)點(diǎn)內(nèi)容到內(nèi)存?2) 構(gòu)造FIFO結(jié)構(gòu)
?3) 根據(jù)FIFO構(gòu)造OMT結(jié)構(gòu)(做排序)
由于TokuDB內(nèi)部有不少性能探(ji)針(shu),他們發(fā)現(xiàn)步驟3)是個(gè)不小的性能消耗點(diǎn),因?yàn)槊看味家裮essage buffer做下排序構(gòu)造出OMT,于是在7.5.0版本,把OMT的FIFO-offset(已排序)也持久化到磁盤(pán),這樣排序的損耗就沒(méi)了。
c) 順序?qū)懠铀?/strong>
當(dāng)寫(xiě)發(fā)生的時(shí)候,會(huì)根據(jù)當(dāng)前的key在pivots里查找(二分)當(dāng)前寫(xiě)要落入哪個(gè)mesage buffer,如果寫(xiě)是順序(或局部順序,數(shù)據(jù)走向?yàn)樽钣疫吢窂?的,就可以避免由"查找"帶來(lái)的額外開(kāi)銷(xiāo)。
如何判斷是順序?qū)懩?#xff1f;TokuDB使用了一種簡(jiǎn)單的啟發(fā)式方法(heurstic):seqinsert_score積分式。如果:
1) 當(dāng)前寫(xiě)入落入最右節(jié)點(diǎn),對(duì)seqinsert_score加一分(原子)2) 當(dāng)前寫(xiě)入落入非最右節(jié)點(diǎn),對(duì)seqinsert_score清零(原子) 當(dāng)seqinsert_score大于100的時(shí)候,就可以認(rèn)為是順序?qū)?#xff0c;當(dāng)下次寫(xiě)操作發(fā)生時(shí),首先與最右的節(jié)點(diǎn)pivot進(jìn)行對(duì)比判斷,如果確實(shí)為順序?qū)?#xff0c;則會(huì)被寫(xiě)到該節(jié)點(diǎn),省去不少compare開(kāi)銷(xiāo)。方法簡(jiǎn)單而有效。
MariaDB· 性能優(yōu)化·filesort with small LIMIT optimization
從MySQL 5.6.2/MariaDB 10.0.0版本開(kāi)始,MySQL/MariaDB針對(duì)"ORDER BY ...LIMIT n"語(yǔ)句實(shí)現(xiàn)了一種新的優(yōu)化策略。當(dāng)n足夠小的時(shí)候,優(yōu)化器會(huì)采用一個(gè)容積為n的優(yōu)先隊(duì)列來(lái)進(jìn)行排序,而不是排序所有數(shù)據(jù)然后取出前n條。 這個(gè)新算法可以這么描述:(假設(shè)是ASC排序)
這樣的算法,時(shí)間復(fù)雜度為m*log(n),m為索引過(guò)濾后的行數(shù),n為L(zhǎng)IMIT的行數(shù)。而原始的全排序算法,時(shí)間復(fù)雜度為m*log(m)。只要n遠(yuǎn)小于m,這個(gè)算法就會(huì)很有效。
不過(guò)在MySQL 5.6中,除了optimizer_trace,沒(méi)有好的方法來(lái)看到這個(gè)新的執(zhí)行計(jì)劃到底起了多少作用。MariaDB 10.013開(kāi)始,提供一個(gè)系統(tǒng)狀態(tài),可以查看新執(zhí)行計(jì)劃調(diào)用的次數(shù):
Sort_priority_queue_sorts?描述: 通過(guò)優(yōu)先隊(duì)列實(shí)現(xiàn)排序的次數(shù)。(總排序次數(shù)=Sort_range+Sort_scan)
?范圍: Global, Session
?數(shù)據(jù)類(lèi)型: numeric
?引入版本: MariaDB 10.0.13 此外,MariaDB還將此信息打入了Slow Log中。只要指定 log_slow_verbosity=query_plan,就可以在Slow Log中看到這樣的記錄:
[js] view plaincopyprint?
本文轉(zhuǎn)載自MySQL.taobao.org ,感謝淘寶數(shù)據(jù)庫(kù)項(xiàng)目組丁奇、鳴嵩、彭立勛、皓庭、項(xiàng)仲、劍川、武藏、祁奚、褚霸、一工。審校:劉亞瓊
總結(jié)
以上是生活随笔為你收集整理的淘宝内部分享:MySQL MariaDB性能优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 周鸿祎对企业的深刻反思,太有借鉴意义了!
- 下一篇: 10年前,三星差点收购了安卓