淘寶內(nèi)部分享:怎么跳出MySQL的10個大坑摘要:淘寶自從2010開始規(guī)模使用MySQL,替換了之前商品、交易、用戶等原基于IOE方案的核心數(shù)據(jù)庫,目前已部署數(shù)千臺規(guī)模。本文涉及以下幾個方向:單機,提升單機數(shù)據(jù)庫的性能;集群,提供擴展可靠性;IO存儲體系等。 編者按:淘寶自從2010開始規(guī)模使用MySQL,替換了之前商品、交易、用戶等原基于IOE方案的核心數(shù)據(jù)庫,目前已部署數(shù)千臺規(guī)模。同時和Oracle, Percona, Mariadb等上游廠商有良好合作,共向上游提交20多個Patch。目前淘寶核心系統(tǒng)研發(fā)部數(shù)據(jù)庫組,根據(jù)淘寶的業(yè)務(wù)需求,改進數(shù)據(jù)庫和提升性能,提供高性能、可擴展的、穩(wěn)定可靠的數(shù)據(jù)庫(存儲)解決方案。 目前有以下幾個方向:單機,提升單機數(shù)據(jù)庫的性能,增加我們所需特性;集群,提供性能擴展,可靠性,可能涉及分布式事務(wù)處理;IO存儲體系,跟蹤IO設(shè)備變化潮流, 研究軟硬件結(jié)合,輸出高性能存儲解決方案。本文是來自淘寶內(nèi)部數(shù)據(jù)庫內(nèi)容分享。MySQL · 性能優(yōu)化· Group Commit優(yōu)化背景關(guān)于Group Commit網(wǎng)上的資料其實已經(jīng)足夠多了,我這里只簡單的介紹一下。 眾所周知,在MySQL5.6之前的版本,由于引入了Binlog/InnoDB的XA,Binlog的寫入和InnoDB commit完全串行化執(zhí)行,大概的執(zhí)行序列如下: 當sync_binlog=1時,很明顯上述的第二步會成為瓶頸,而且還是持有全局大鎖,這也是為什么性能會急劇下降。很快Mariadb就提出了一個Binlog Group Commit方案,即在準備寫入Binlog時,維持一個隊列,最早進入隊列的是leader,后來的是follower,leader為搜集到的隊列中的線程依次寫B(tài)inlog文件, 并commit事務(wù)。Percona 的Group Commit實現(xiàn)也是Port自Mariadb。不過仍在使用Percona Server5.5的朋友需要注意,該Group Commit實現(xiàn)可能破壞掉Semisync的行為,感興趣的點擊 bug#1254571 Oracle MySQL 在5.6版本開始也支持Binlog Group Commit,使用了和Mariadb類似的思路,但將Group Commit的過程拆分成了三個階段:flush stage 將各個線程的binlog從cache寫到文件中; sync stage 對binlog做fsync操作(如果需要的話);commit stage 為各個線程做引擎層的事務(wù)commit。每個stage同時只有一個線程在操作。 Tips:當引入Group Commit后,sync_binlog的含義就變了,假定設(shè)為1000,表示的不是1000個事務(wù)后做一次fsync,而是1000個事務(wù)組。 Oracle MySQL的實現(xiàn)的優(yōu)勢在于三個階段可以并發(fā)執(zhí)行,從而提升效率。 XA Recover 在Binlog打開的情況下,MySQL默認使用MySQL_BIN_LOG來做XA協(xié)調(diào)者,大致流程為: 1.掃描最后一個Binlog文件,提取其中的xid; 通過這種方式,可以讓InnoDB和Binlog中的事務(wù)狀態(tài)保持一致。顯然只要事務(wù)在InnoDB層完成了Prepare,并且寫入了Binlog,就可以從崩潰中恢復事務(wù),這意味著我們無需在InnoDB commit時顯式的write/fsync redo log。 Tips:MySQL為何只需要掃描最后一個Binlog文件呢 ? 原因是每次在rotate到新的Binlog文件時,總是保證沒有正在提交的事務(wù),然后fsync一次InnoDB的redo log。這樣就可以保證老的Binlog文件中的事務(wù)在InnoDB總是提交的。 問題 其實問題很簡單:每個事務(wù)都要保證其Prepare的事務(wù)被write/fsync到redo log文件。盡管某個事務(wù)可能會幫助其他事務(wù)完成redo 寫入,但這種行為是隨機的,并且依然會產(chǎn)生明顯的log_sys->mutex開銷。 優(yōu)化 從XA恢復的邏輯我們可以知道,只要保證InnoDB Prepare的redo日志在寫B(tài)inlog前完成write/sync即可。因此我們對Group Commit的第一個stage的邏輯做了些許修改,大概描述如下: Step1. InnoDB Prepare,記錄當前的LSN到thd中; 通過延遲寫redo log的方式,顯式的為redo log做了一次組寫入,并減少了log_sys->mutex的競爭。 目前官方MySQL已經(jīng)根據(jù)我們report的bug#73202鎖提供的思路,對5.7.6的代碼進行了優(yōu)化,對應(yīng)的Release Note如下: When using InnoDB with binary logging enabled, concurrent transactions written in the InnoDB redo log are now grouped together before synchronizing to disk when innodb_flush_log_at_trx_commit is set to 1, which reduces the amount of synchronization operations. This can lead to improved performance. 性能數(shù)據(jù) 簡單測試了下,使用sysbench, update_non_index.lua, 100張表,每張10w行記錄,innodb_flush_log_at_trx_commit=2, sync_binlog=1000,關(guān)閉Gtid MySQL · 新增特性· DDL fast fail背景 項目的快速迭代開發(fā)和在線業(yè)務(wù)需要保持持續(xù)可用的要求,導致MySQL的ddl變成了DBA很頭疼的事情,而且經(jīng)常導致故障發(fā)生。本篇介紹RDS分支上做的一個功能改進,DDL fast fail。主要解決:DDL操作因為無法獲取MDL排它鎖,進入等待隊列的時候,阻塞了應(yīng)用所有的讀寫請求問題。 MDL鎖機制介紹 首先介紹一下MDL(METADATA LOCK)鎖機制,MySQL為了保證表結(jié)構(gòu)的完整性和一致性,對表的所有訪問都需要獲得相應(yīng)級別的MDL鎖,比如以下場景: session 1: start transaction; select * from test.t1;
這種場景就是目前因為MDL鎖導致的很經(jīng)典的阻塞問題,如果session1長時間未提交,或者查詢持續(xù)過長時間,那么后續(xù)對t1表的所有讀寫操作,都被阻塞。 對于在線的業(yè)務(wù)來說,很容易導致業(yè)務(wù)中斷。 aliyun RDS分支改進 DDL fast fail并沒有解決真正DDL過程中的阻塞問題,但避免了因為DDL操作沒有獲取鎖,進而導致業(yè)務(wù)其他查詢/更新語句阻塞的問題。 其實現(xiàn)方式如下: alter table test.t1 no_wait/wait 1 add extra int; 其處理邏輯如下: 首先嘗試獲取t1表的MDL_EXCLUSIVE級別的MDL鎖:
另外,除了alter語句以外,還支持rename,truncate,drop,optimize,create index等ddl操作。 與Oracle的比較 在Oracle 10g的時候,DDL操作經(jīng)常會遇到這樣的錯誤信息: ora-00054:resource busy and acquire with nowait specified 即DDL操作無法獲取表上面的排它鎖,而fast fail。 其實DDL獲取排他鎖的設(shè)計,需要考慮的就是兩個問題:
在Oracle 11g的時候,引入了DDL_LOCK_TIMEOUT參數(shù),如果你設(shè)置了這個參數(shù),那么DDL操作將使用排隊阻塞模式,可以在session和global級別設(shè)置, 給了用戶更多選擇。 MySQL · 性能優(yōu)化· 啟用GTID場景的性能問題及優(yōu)化背景 MySQL從5.6版本開始支持GTID特性,也就是所謂全局事務(wù)ID,在整個復制拓撲結(jié)構(gòu)內(nèi),每個事務(wù)擁有自己全局唯一標識。GTID包含兩個部分,一部分是實例的UUID,另一部分是實例內(nèi)遞增的整數(shù)。 GTID的分配包含兩種方式,一種是自動分配,另外一種是顯式設(shè)置session.gtid_next,下面簡單介紹下這兩種方式: 自動分配 如果沒有設(shè)置session級別的變量gtid_next,所有事務(wù)都走自動分配邏輯。分配GTID發(fā)生在GROUP COMMIT的第一個階段,也就是flush stage,大概可以描述為:
顯式設(shè)置 用戶通過設(shè)置session級別變量gtid_next可以顯式指定一個GTID,流程如下:
備庫SQL線程使用的就是第二種方式,因為備庫在apply主庫的日志時,要保證GTID是一致的,SQL線程讀取到GTID事件后,就根據(jù)其中記錄的GTID來設(shè)置其gtid_next變量。 問題 由于在實例內(nèi),GTID需要保證唯一性,因此不管是操作gtid_executed集合和gtid_owned集合,還是分配GTID,都需要加上一個大鎖。我們的優(yōu)化主要集中在第一種GTID分配方式。 對于GTID的分配,由于處于Group Commit的第一個階段,由該階段的leader線程為其follower線程分配GTID及刷Binlog,因此不會產(chǎn)生競爭。 而在Step 5,各個線程在完成事務(wù)提交后,各自去從gtid_owned集合中刪除其使用的gtid。這時候每個線程都需要獲取互斥鎖,很顯然,并發(fā)越高,這種競爭就越明顯,我們很容易從pt-pmp輸出中看到如下類似的trace: 這同時也會影響到GTID的分配階段,導致TPS在高并發(fā)場景下的急劇下降。 解決 實際上對于自動分配GTID的場景,并沒有必要維護gtid_owned集合。我們的修改也非常簡單,在自動分配一個GTID后,直接加入到gtid_executed集合中,避免維護gtid_owned,這樣事務(wù)提交時就無需去清理gtid_owned集合了,從而可以完全避免鎖競爭。 當然為了保證一致性,如果分配GTID后,寫入Binlog文件失敗,也需要從gtid_executed集合中刪除。不過這種場景非常罕見。 性能數(shù)據(jù) 使用sysbench,100張表,每張10w行記錄,update_non_index.lua,純內(nèi)存操作,innodb_flush_log_at_trx_commit = 2,sync_binlog = 1000 從測試結(jié)果可以看到,優(yōu)化前隨著并發(fā)上升,性能出現(xiàn)下降,而優(yōu)化后則能保持TPS穩(wěn)定。 MySQL · 捉蟲動態(tài)· InnoDB自增列重復值問題問題重現(xiàn) 先從問題入手,重現(xiàn)下這個 bug 這里我們關(guān)閉MySQL,再啟動MySQL,然后再插入一條數(shù)據(jù) 我們看到插入了(2,2),而如果我沒有重啟,插入同樣數(shù)據(jù)我們得到的應(yīng)該是(4,2)。 上面的測試反映了MySQLd重啟后,InnoDB存儲引擎的表自增id可能出現(xiàn)重復利用的情況。 自增id重復利用在某些場景下會出現(xiàn)問題。依然用上面的例子,假設(shè)t1有個歷史表t1_history用來存t1表的歷史數(shù)據(jù),那么MySQLd重啟前,ti_history中可能已經(jīng)有了(2,2)這條數(shù)據(jù),而重啟后我們又插入了(2,2),當新插入的(2,2)遷移到歷史表時,會違反主鍵約束。 原因分析 InnoDB 自增列出現(xiàn)重復值的原因: 建表時可以指定 AUTO_INCREMENT值,不指定時默認為1,這個值表示當前自增列的起始值大小,如果新插入的數(shù)據(jù)沒有指定自增列的值,那么自增列的值即為這個起始值。對于InnoDB表,這個值沒有持久到文件中。而是存在內(nèi)存中(dict_table_struct.autoinc)。那么又問,既然這個值沒有持久下來,為什么我們每次插入新的值后, show create table t1看到AUTO_INCREMENT值是跟隨變化的。其實show create table t1是直接從dict_table_struct.autoinc取得的(ha_innobase::update_create_info)。 知道了AUTO_INCREMENT是實時存儲內(nèi)存中的。那么,MySQLd 重啟后,從哪里得到AUTO_INCREMENT呢? 內(nèi)存值肯定是丟失了。實際上MySQL采用執(zhí)行類似select max(id)+1 from t1;方法來得到AUTO_INCREMENT。而這種方法就是造成自增id重復的原因。 MyISAM自增值 MyISAM也有這個問題嗎?MyISAM是沒有這個問題的。myisam會將這個值實時存儲在.MYI文件中(mi_state_info_write)。MySQLd重起后會從.MYI中讀取AUTO_INCREMENT值(mi_state_info_read)。因此,MyISAM表重啟是不會出現(xiàn)自增id重復的問題。 問題修復 MyISAM選擇將AUTO_INCREMENT實時存儲在.MYI文件頭部中。實際上.MYI頭部還會實時存其他信息,也就是說寫AUTO_INCREMENT只是個順帶的操作,其性能損耗可以忽略。InnoDB 表如果要解決這個問題,有兩種方法。 1)將AUTO_INCREMENT最大值持久到frm文件中。 第一種方法直接寫文件性能消耗較大,這是一額外的操作,而不是一個順帶的操作。我們采用第二種方案。為什么選擇存儲在聚集索引根頁頁頭trx_id,頁頭中存儲trx_id,只對二級索引頁和insert buf 頁頭有效(MVCC)。而聚集索引根頁頁頭trx_id這個值是沒有使用的,始終保持初始值0。正好這個位置8個字節(jié)可存放自增值的值。我們每次更新AUTO_INCREMENT值時,同時將這個值修改到聚集索引根頁頁頭trx_id的位置。 這個寫操作跟真正的數(shù)據(jù)寫操作一樣,遵守write-ahead log原則,只不過這里只需要redo log ,而不需要undo log。因為我們不需要回滾AUTO_INCREMENT的變化(即回滾后自增列值會保留,即使insert 回滾了,AUTO_INCREMENT值不會回滾)。 因此,AUTO_INCREMENT值存儲在聚集索引根頁trx_id所在的位置,實際上是對內(nèi)存根頁的修改和多了一條redo log(量很?。?而這個redo log 的寫入也是異步的,可以說是原有事務(wù)log的一個順帶操作。因此AUTO_INCREMENT值存儲在聚集索引根頁這個性能損耗是極小的。 修復后的性能對比,我們新增了全局參數(shù)innodb_autoinc_persistent 取值on/off; on 表示將AUTO_INCREMENT值實時存儲在聚集索引根頁。off則采用原有方式只存儲在內(nèi)存。 可以看出性能損耗在%1以下。 改進 新增參數(shù)innodb_autoinc_persistent_interval 用于控制持久化AUTO_INCREMENT值的頻率。例如:innodb_autoinc_persistent_interval=100,auto_incrememt_increment=1時,即每100次insert會控制持久化一次AUTO_INCREMENT值。每次持久的值為:當前值+innodb_autoinc_persistent_interval。 測試結(jié)論 innodb_autoinc_persistent=ON, innodb_autoinc_persistent_interval=1時性能損耗在%1以下。 限制
注意:如果我們使用需要開啟innodb_autoinc_persistent,應(yīng)該在參數(shù)文件中指定 如果這樣指定set global innodb_autoinc_persistent=on;重啟后將不會從聚集索引根頁讀取AUTO_INCREMENT最大值。 疑問:對于InnoDB表,重啟通過select max(id)+1 from t1得到AUTO_INCREMENT值,如果id上有索引那么這個語句使用索引查找就很快。那么,這個可以解釋MySQL 為什么要求自增列必須包含在索引中的原因。 如果沒有指定索引,則報如下錯誤, ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key 而myisam表竟然也有這個要求,感覺是多余的。 MySQL · 優(yōu)化改進· 復制性能改進過程前言 與oracle 不同,MySQL 的主庫與備庫的同步是通過 binlog 實現(xiàn)的,而redo日志只做為MySQL 實例的crash recovery使用。MySQL在4.x 的時候放棄redo 的同步策略而引入 binlog的同步,一個重要原因是為了兼容其它非事務(wù)存儲引擎,否則主備同步是沒有辦法進行的。 redo 日志同步屬于物理同步方法,簡單直接,將修改的物理部分傳送到備庫執(zhí)行,主備共用一致的 LSN,只要保證 LSN 相同即可,同一時刻,只能主庫或備庫一方接受寫請求; binlog的同步方法屬于邏輯復制,分為statement 或 row 模式,其中statement記錄的是SQL語句,Row 模式記錄的是修改之前的記錄與修改之后的記錄,即前鏡像與后鏡像;備庫通過binlog dump 協(xié)議拉取binlog,然后在備庫執(zhí)行。如果拉取的binlog是SQL語句,備庫會走和主庫相同的邏輯,如果是row 格式,則會調(diào)用存儲引擎來執(zhí)行相應(yīng)的修改。 本文簡單說明5.5到5.7的主備復制性能改進過程。 replication improvement (from 5.5 to 5.7) (1) 5.5 中,binlog的同步是由兩個線程執(zhí)行的 io_thread: 根據(jù)binlog dump協(xié)議從主庫拉取binlog, 并將binlog轉(zhuǎn)存到本地的relaylog; sql_thread: 讀取relaylog,根據(jù)位點的先后順序執(zhí)行binlog event,進而將主庫的修改同步到備庫,達到主備一致的效果; 由于在主庫的更新是由多個客戶端執(zhí)行的,所以當壓力達到一定的程度時,備庫單線程執(zhí)行主庫的binlog跟不上主庫執(zhí)行的速度,進而會產(chǎn)生延遲造成備庫不可用,這也是分庫的原因之一,其SQL線程的執(zhí)行堆棧如下: (2) 5.6 中,引入了多線程模式,在多線程模式下,其線程結(jié)構(gòu)如下 io_thread: 同5.5 Coordinator_thread: 負責讀取 relay log,將讀取的binlog event以事務(wù)為單位分發(fā)到各個 worker thread 進行執(zhí)行,并在必要時執(zhí)行binlog event(Description_format_log_event, Rotate_log_event 等)。 worker_thread: 執(zhí)行分配到的binlog event,各個線程之間互不影響; 多線程原理 sql_thread 的分發(fā)原理是依據(jù)當前事務(wù)所操作的數(shù)據(jù)庫名稱來進行分發(fā),如果事務(wù)是跨數(shù)據(jù)庫行為的,則需要等待已分配的該數(shù)據(jù)庫的事務(wù)全部執(zhí)行完畢,才會繼續(xù)分發(fā),其分配行為的偽碼可以簡單的描述如下: 需要注意的細節(jié)
總體上說,5.6 的并行復制打破了5.5 單線程的復制的行為,只是在單庫下用處不大,并且5.6的并行復制的改動引入了一些重量級的bug
(3) 5.7中,并行復制的實現(xiàn)添加了另外一種并行的方式,即主庫在 ordered_commit中的第二階段的時候,將同一批commit的 binlog 打上一個相同的seqno標簽,同一時間戳的事務(wù)在備庫是可以同時執(zhí)行的,因此大大簡化了并行復制的邏輯,并打破了相同 DB 不能并行執(zhí)行的限制。備庫在執(zhí)行時,具有同一seqno的事務(wù)在備庫可以并行的執(zhí)行,互不干擾,也不需要綁定信息,后一批seqno的事務(wù)需要等待前一批相同seqno的事務(wù)執(zhí)行完后才可以執(zhí)行。 詳細實現(xiàn)可參考: http://bazaar./~MySQL/MySQL-server/5.7/revision/6256 。 reference: http://geek./2013/09/enhancedMTS-deepdive.html MySQL · 談古論今· key分區(qū)算法演變分析本文說明一個物理升級導致的 "數(shù)據(jù)丟失"。 現(xiàn)象 在MySQL 5.1下新建key分表,可以正確查詢數(shù)據(jù)。 而直接用MySQL5.5或MySQL5.6啟動上面的5.1實例,發(fā)現(xiàn)(1,1785089517)這行數(shù)據(jù)不能正確查詢出來。 原因分析 跟蹤代碼發(fā)現(xiàn),5.1 與5.5,5.6 key hash算法是有區(qū)別的。 5.1 對于非空值的處理算法如下 通過此算法算出數(shù)據(jù)(1,1785089517)在第3個分區(qū) 5.5和5.6非空值的處理算法如下 通過此算法算出數(shù)據(jù)(1,1785089517)在第5個分區(qū),因此,5.5,5.6查詢不能查詢出此行數(shù)據(jù)。 5.1,5.5,5.6對于空值的算法還是一致的,如下 都能正確算出數(shù)據(jù)(2, null)在第3個分區(qū)。因此,空值可以正確查詢出來。 那么是什么導致非空值的hash算法走了不同路徑呢?在5.1下,計算字段key hash固定字符集就是my_charset_bin,對應(yīng)的hash 函數(shù)就是前面的my_hash_sort_simple。而在5.5,5.6下,計算字段key hash的字符集是隨字段變化的,字段c2類型為int對應(yīng)my_charset_numeric,與之對應(yīng)的hash函數(shù)為my_hash_sort_simple。具體可以參考函數(shù)Field::hash 那么問題又來了,5.5后為什么算法會變化呢?原因在于官方關(guān)于字符集策略的調(diào)整,詳見WL#2649 。 兼容處理 前面講到,由于hash 算法變化,用5.5,5.6啟動5.1的實例,導致不能正確查詢數(shù)據(jù)。那么5.1升級5.5,5.6就必須兼容這個問題.MySQL 5.5.31以后,提供了專門的語法 ALTER TABLE ... PARTITION BY ALGORITHM=1 [LINEAR] KEY ... 用于兼容此問題。對于上面的例子,用5.5或5.6啟動5.1的實例后執(zhí)行 數(shù)據(jù)可以正確查詢出來了。 而實際上5.5,5.6的MySQL_upgrade升級程序已經(jīng)提供了兼容方法。MySQL_upgrade 執(zhí)行check table xxx for upgrade 會檢查key分區(qū)表是否用了老的算法。如果使用了老的算法,會返回 檢查到錯誤信息后會自動執(zhí)行以下語句進行兼容。 MySQL · 捉蟲動態(tài)· MySQL client crash一例背景 客戶使用MySQLdump導出一張表,然后使用MySQL -e 'source test.dmp'的過程中client進程crash,爆出內(nèi)存的segment fault錯誤,導致無法導入數(shù)據(jù)。 問題定位 test.dmp文件大概50G左右,查看了一下文件的前幾行內(nèi)容,發(fā)現(xiàn): 問題定位到第一行出現(xiàn)了不正常warning的信息,是由于客戶使用MySQLdump命令的時候,重定向了stderr。即: MySQLdump ...>/test.dmp 2>&1 導致error或者warning信息都重定向到了test.dmp, 最終導致失敗。 問題引申 問題雖然定位到了,但卻有幾個問題沒有弄清楚: 問題1. 不正常的sql,執(zhí)行失敗,報錯出來就可以了,為什么會導致crash? MySQL.cc::add_line函數(shù)中,在讀第一行的時候,讀取到了don't,發(fā)現(xiàn)有一個單引號,所以程序死命的去找匹配的另外一個單引號,導致不斷的讀取文件,分配內(nèi)存,直到crash。 問題2. 那代碼中對于大小的邊界到底是多少?比如insert語句支持batch insert時,語句的長度多少,又比如遇到clob字段呢?
所以,正常情況下,max_allowed_packet現(xiàn)在的最大字段長度和MAX_BATCH_BUFFER_SIZE限制的最大insert語句,是匹配的。 RDS問題修復原則 從問題的定位上來看,這一例crash屬于客戶錯誤使用MySQLdump導致的問題,Aliyun RDS分支對內(nèi)存導致的crash問題,都會定位并反饋給用戶。 但此例不做修復,而是引導用戶正確的使用MySQLdump工具。 MySQL · 捉蟲動態(tài)· 設(shè)置 gtid_purged 破壞AUTO_POSITION復制協(xié)議bug描述 Oracle 最新發(fā)布的版本 5.6.22 中有這樣一個關(guān)于GTID的bugfix,在主備場景下,如果我們在主庫上 SET GLOBAL GTID_PURGED = "some_gtid_set",并且 some_gtid_set 中包含了備庫還沒復制的事務(wù),這個時候如果備庫接上主庫的話,預(yù)期結(jié)果是主庫返回錯誤,IO線程掛掉的,但是實際上,在這種場景下主庫并不報錯,只是默默的把自己 binlog 中包含的gtid事務(wù)發(fā)給備庫。這個bug的造成的結(jié)果是看起來復制正常,沒有錯誤,但實際上備庫已經(jīng)丟事務(wù)了,主備很可能就不一致了。 背景知識
binlog 中記錄的和GTID相關(guān)的事件主要有2種,Previous_gtids_log_event 和 Gtid_log_event,前者表示之前的binlog中包含的gtid的集合,后者就是一個gtid,對應(yīng)一個事務(wù)。一個 binlog 文件中只有一個 Previous_gtids_log_event,放在開頭,有多個 Gtid_log_event,如下面所示
我們知道備庫的復制線程是分IO線程和SQL線程2種的,IO線程通過GTID協(xié)議或者文件位置協(xié)議拉取主庫的binlog,然后記錄在自己的relay log中;SQL線程通過執(zhí)行realy log中的事件,把其中的操作都自己做一遍,記入本地binlog。在GTID協(xié)議下,備庫向主庫發(fā)送拉取請求的時候,會告知主庫自己已經(jīng)有的所有的GTID的集合,Retrieved_Gtid_Set + Executed_Gtid_Set,前者對應(yīng) realy log 中所有的gtid集合,表示已經(jīng)拉取過的,后者對應(yīng)binlog中記錄有的,表示已經(jīng)執(zhí)行過的;主庫在收到這2個總集合后,會掃描自己的binlog,找到合適的binlog然后開始發(fā)送。
主庫將備庫發(fā)送過來的總合集記為 slave_gtid_executed,然后調(diào)用 find_first_log_not_in_gtid_set(slave_gtid_executed),這個函數(shù)的目的是從最新到最老掃描binlog文件,找到第一個含有不存在 slave_gtid_executed 這個集合的gtid的binlog。在這個掃描過程中并不需要從頭到尾讀binlog中所有的gtid,只需要讀出 Previous_gtids_log_event ,如果Previous_gtids_log_event 不是 slave_gtid_executed的子集,就繼續(xù)向前找binlog,直到找到為止。 這個查找過程總會停止的,停止條件如下:
在條件2下,報錯信息是這樣的 Got fatal error 1236 from master when reading data from binary log: 'The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires. 其實上面的條件3是條件1的特殊情況,這個bugfix針對的場景就是條件3這種,但并不是所有的符合條件3的場景都會觸發(fā)這個bug,下面就分析下什么情況下才會觸發(fā)bug。 bug 分析 假設(shè)有這樣的場景,我們要用已經(jīng)有MySQL實例的備份重新做一對主備實例,不管是用 xtrabackup 這種物理備份工具或者MySQLdump這種邏輯備份工具,都會有2步操作,
步驟2是為了保證GTID的完備性,因為新實例已經(jīng)導入了數(shù)據(jù),就需要把生成這些數(shù)據(jù)的事務(wù)對應(yīng)的GTID集合也設(shè)置進來。 正常的操作是主備都要做這2步的,如果我們只在主庫上做了這2步,備庫什么也不做,然后就直接用 GTID 協(xié)議把備庫連上來,按照我們的預(yù)期這個時候是應(yīng)該出錯的,主備不一致,并且主庫的binlog中沒東西,應(yīng)該報之前停止條件2報的錯。但是令人大跌眼鏡的是主庫不報錯,復制看起來是完全正常的。 為啥會這樣呢,SET GLOBAL GTID_PURGED 操作會調(diào)用 MySQL_bin_log.rotate_and_purge切換到一個新的binlog,并把這個GTID_PURGED 集合記入新生成的binlog的Previous_gtids_log_event,假設(shè)原有的binlog為A,新生成的為B,主庫剛啟動,所以A就是主庫的第一個binlog,它之前啥也沒有,A的Previous_gtids_log_event就是空集,并且A中也不包含任何GTID事件,否則SET GLOBAL GTID_PURGED是做不了的。按照之前的掃描邏輯,掃到A是肯定會停下來的,并且不報錯。 bug 修復 官方的修復就是在主庫掃描查找binlog之前,判斷一下 gtid_purged 集合不是不比slave_gtid_executed大,如果是就報錯,錯誤信息和條件2一樣 Got fatal error 1236 from master when reading data from binary log: 'The slave is connecting using CHANGE MASTER TO MASTER_AUTO_POSITION = 1, but the master has purged binary logs containing GTIDs that the slave requires。 MySQL · 捉蟲動態(tài)· replicate filter 和 GTID 一起使用的問題問題描述 當單個 MySQL 實例的數(shù)據(jù)增長到很多的時候,就會考慮通過庫或者表級別的拆分,把當前實例的數(shù)據(jù)分散到多個實例上去,假設(shè)原實例為A,想把其中的5個庫(db1/db2/db3/db4/db5)拆分到5個實例(B1/B2/B3/B4/B5)上去。 拆分過程一般會這樣做,先把A的相應(yīng)庫的數(shù)據(jù)導出,然后導入到對應(yīng)的B實例上,但是在這個導出導入過程中,A庫的數(shù)據(jù)還是在持續(xù)更新的,所以還需在導入完后,在所有的B實例和A實例間建立復制關(guān)系,拉取缺失的數(shù)據(jù),在業(yè)務(wù)不繁忙的時候?qū)I(yè)務(wù)切換到各個B實例。 在復制搭建時,每個B實例只需要復制A實例上的一個庫,所以只需要重放對應(yīng)庫的binlog即可,這個通過 replicate-do-db 來設(shè)置過濾條件。如果我們用備庫上執(zhí)行 show slave status\G 會看到Executed_Gtid_Set是斷斷續(xù)續(xù)的,間斷非常多,導致這一列很長很長,看到的直接效果就是被刷屏了。 為啥會這樣呢,因為設(shè)了replicate-do-db,就只會執(zhí)行對應(yīng)db對應(yīng)的event,其它db的都不執(zhí)行。主庫的執(zhí)行是不分db的,對各個db的操作互相間隔,記錄在binlog中,所以備庫做了過濾后,就出現(xiàn)這種斷斷的現(xiàn)象。 除了這個看著不舒服外,還會導致其它問題么? 假設(shè)我們拿B1實例的備份做了一個新實例,然后接到A上,如果主庫A又定期purge了老的binlog,那么新實例的IO線程就會出錯,因為需要的binlog在主庫上找不到了;即使主庫沒有purge 老的binlog,新實例還要把主庫的binlog都從頭重新拉過來,然后執(zhí)行的時候又都過濾掉,不如不拉取。 有沒有好的辦法解決這個問題呢?SQL線程在執(zhí)行的時候,發(fā)現(xiàn)是該被過濾掉的event,在不執(zhí)行的同時,記一個空事務(wù)就好了,把原事務(wù)對應(yīng)的GTID位置占住,記入binlog,這樣備庫的Executed_Gtid_Set就是連續(xù)的了。 bug 修復 對這個問題,官方有一個相應(yīng)的bugfix,參見 revno: 5860 ,有了這個patch后,備庫B1的 SQL 線程在遇到和 db2-db5 相關(guān)的SQL語句時,在binlog中把對應(yīng)的GTID記下,同時對應(yīng)記一個空事務(wù)。 這個 patch 只是針對Query_log_event,即 statement 格式的 binlog event,那么row格式的呢? row格式原來就已經(jīng)是這種行為,通過check_table_map 函數(shù)來過濾庫或者表,然后生成一個空事務(wù)。 另外這個patch還專門處理了下 CREATE/DROP TEMPORARY TABLE 這2種語句,我們知道row格式下,對臨時表的操作是不會記入binlog的。如果主庫的binlog格式是 statement,備庫用的是 row,CREATE/DROP TEMPORARY TABLE 對應(yīng)的事務(wù)傳到備庫后,就會消失掉,Executed_Gtid_Set集合看起來是不連續(xù)的,但是主庫的binlog記的gtid是連續(xù)的,這個 patch 讓這種情況下的CREATE/DROP TEMPORARY TABLE在備庫同樣記為一個空事務(wù)。 TokuDB·特性分析· Optimize Table來自一個TokuDB用戶的“投訴”: https://mariadb./browse/MDEV-6207 現(xiàn)象大概是: 用戶有一個MyISAM的表test_table: 轉(zhuǎn)成TokuDB引擎后表大小為92M左右: 執(zhí)行"OPTIMIZE TABLE test_table": 再次執(zhí)行"OPTIMIZE TABLE test_table": 繼續(xù)執(zhí)行: 基本穩(wěn)定在這個大小。 主索引從47M-->63M-->79M,執(zhí)行"OPTIMIZE TABLE"后為什么會越來越大? 這得從TokuDB的索引文件分配方式說起,當內(nèi)存中的臟頁需要寫到磁盤時,TokuDB優(yōu)先在文件末尾分配空間并寫入,而不是“覆寫”原塊,原來的塊暫時成了“碎片”。 這樣問題就來了,索引文件豈不是越來越大?No, TokuDB會把這些“碎片”在checkpoint時加入到回收列表,以供后面的寫操作使用,看似79M的文件其實還可以裝不少數(shù)據(jù)呢! 嗯,這個現(xiàn)象解釋通了,但還有2個問題:
本文轉(zhuǎn)載自MySQL.taobao.org ,感謝淘寶數(shù)據(jù)庫項目組丁奇、鳴嵩、彭立勛、皓庭、項仲、劍川、武藏、祁奚、褚霸、一工。審校:劉亞瓊 |
|