關於Group Commit網上的資料其實已經足夠多了,我這裏只簡單的介紹一下。 php
衆所周知,在MySQL5.6以前的版本,因爲引入了Binlog/InnoDB的XA,Binlog的寫入和InnoDB commit徹底串行化執行,大概的執行序列以下: html
InnoDB prepare (持有prepare_commit_mutex); write/sync Binlog; InnoDB commit (寫入COMMIT標記後釋放prepare_commit_mutex)。當sync_binlog=1時,很明顯上述的第二步會成爲瓶頸,並且仍是持有全局大鎖,這也是爲何性能會急劇降低。
很快Mariadb就提出了一個Binlog Group Commit方案,即在準備寫入Binlog時,維持一個隊列,最先進入隊列的是leader,後來的是follower,leader爲蒐集到的隊列中的線程依次寫Binlog文件, 並commit事務。Percona 的Group Commit實現也是Port自Mariadb。不過仍在使用Percona Server5.5的朋友須要注意,該Group Commit實現可能破壞掉Semisync的行爲,感興趣的點擊 bug#1254571 mysql
Oracle MySQL 在5.6版本開始也支持Binlog Group Commit,使用了和Mariadb相似的思路,但將Group Commit的過程拆分紅了三個階段:flush stage 將各個線程的binlog從cache寫到文件中; sync stage 對binlog作fsync操做(若是須要的話);commit stage 爲各個線程作引擎層的事務commit。每一個stage同時只有一個線程在操做。 算法
Tips:當引入Group Commit後,sync_binlog的含義就變了,假定設爲1000,表示的不是1000個事務後作一次fsync,而是1000個事務組。 sql
Oracle MySQL的實現的優點在於三個階段能夠併發執行,從而提高效率。 數據庫
XA Recover 緩存
在Binlog打開的狀況下,MySQL默認使用MySQL_BIN_LOG來作XA協調者,大體流程爲: 性能優化
1.掃描最後一個Binlog文件,提取其中的xid;
2.InnoDB維持了狀態爲Prepare的事務鏈表,將這些事務的xid和Binlog中記錄的xid作比較,若是在Binlog中存在,則提交,不然回滾事務。
經過這種方式,可讓InnoDB和Binlog中的事務狀態保持一致。顯然只要事務在InnoDB層完成了Prepare,而且寫入了Binlog,就能夠從崩潰中恢復事務,這意味着咱們無需在InnoDB commit時顯式的write/fsync redo log。 網絡
Tips:MySQL爲什麼只須要掃描最後一個Binlog文件呢 ? 緣由是每次在rotate到新的Binlog文件時,老是保證沒有正在提交的事務,而後fsync一次InnoDB的redo log。這樣就能夠保證老的Binlog文件中的事務在InnoDB老是提交的。 session
問題
其實問題很簡單:每一個事務都要保證其Prepare的事務被write/fsync到redo log文件。儘管某個事務可能會幫助其餘事務完成redo 寫入,但這種行爲是隨機的,而且依然會產生明顯的log_sys->mutex開銷。
優化
從XA恢復的邏輯咱們能夠知道,只要保證InnoDB Prepare的redo日誌在寫Binlog前完成write/sync便可。所以咱們對Group Commit的第一個stage的邏輯作了些許修改,大概描述以下:
Step1. InnoDB Prepare,記錄當前的LSN到thd中;
Step2. 進入Group Commit的flush stage;Leader蒐集隊列,同時算出隊列中最大的LSN。
Step3. 將InnoDB的redo log write/fsync到指定的LSN
Step4. 寫Binlog並進行隨後的工做(sync Binlog, InnoDB commit , etc)
經過延遲寫redo log的方式,顯式的爲redo log作了一次組寫入,並減小了log_sys->mutex的競爭。
目前官方MySQL已經根據咱們report的bug#73202鎖提供的思路,對5.7.6的代碼進行了優化,對應的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.
性能數據
簡單測試了下,使用sysbench, update_non_index.lua, 100張表,每張10w行記錄,innodb_flush_log_at_trx_commit=2, sync_binlog=1000,關閉Gtid
併發線程 原生 修改後 32 25600 27000 64 30000 35000 128 33000 39000 256 29800 38000
背景
項目的快速迭代開發和在線業務須要保持持續可用的要求,致使MySQL的ddl變成了DBA很頭疼的事情,並且常常致使故障發生。本篇介紹RDS分支上作的一個功能改進,DDL fast fail。主要解決:DDL操做由於沒法獲取MDL排它鎖,進入等待隊列的時候,阻塞了應用全部的讀寫請求問題。
MDL鎖機制介紹
首先介紹一下MDL(METADATA LOCK)鎖機制,MySQL爲了保證表結構的完整性和一致性,對錶的全部訪問都須要得到相應級別的MDL鎖,好比如下場景:
session 1: start transaction; select * from test.t1;
session 2: alter table test.t1 add extra int;
session 3: select * from test.t1;
這種場景就是目前由於MDL鎖致使的很經典的阻塞問題,若是session1長時間未提交,或者查詢持續過長時間,那麼後續對t1表的全部讀寫操做,都被阻塞。 對於在線的業務來講,很容易致使業務中斷。
aliyun RDS分支改進
DDL fast fail並無解決真正DDL過程當中的阻塞問題,但避免了由於DDL操做沒有獲取鎖,進而致使業務其餘查詢/更新語句阻塞的問題。
其實現方式以下:
alter table test.t1 no_wait/wait 1 add extra int;
在ddl語句中,增長了no_wait/wait 1語法支持。
其處理邏輯以下:
首先嚐試獲取t1表的MDL_EXCLUSIVE級別的MDL鎖:
另外,除了alter語句之外,還支持rename,truncate,drop,optimize,create index等ddl操做。
與Oracle的比較
在Oracle 10g的時候,DDL操做常常會遇到這樣的錯誤信息:
ora-00054:resource busy and acquire with nowait specified 即DDL操做沒法獲取表上面的排它鎖,而fast fail。
其實DDL獲取排他鎖的設計,須要考慮的就是兩個問題:
在Oracle 11g的時候,引入了DDL_LOCK_TIMEOUT參數,若是你設置了這個參數,那麼DDL操做將使用排隊阻塞模式,能夠在session和global級別設置, 給了用戶更多選擇。
背景
MySQL從5.6版本開始支持GTID特性,也就是所謂全局事務ID,在整個複製拓撲結構內,每一個事務擁有本身全局惟一標識。GTID包含兩個部分,一部分是實例的UUID,另外一部分是實例內遞增的整數。
GTID的分配包含兩種方式,一種是自動分配,另一種是顯式設置session.gtid_next,下面簡單介紹下這兩種方式:
自動分配
若是沒有設置session級別的變量gtid_next,全部事務都走自動分配邏輯。分配GTID發生在GROUP COMMIT的第一個階段,也就是flush stage,大概能夠描述爲:
顯式設置
用戶經過設置session級別變量gtid_next能夠顯式指定一個GTID,流程以下:
備庫SQL線程使用的就是第二種方式,由於備庫在apply主庫的日誌時,要保證GTID是一致的,SQL線程讀取到GTID事件後,就根據其中記錄的GTID來設置其gtid_next變量。
問題
因爲在實例內,GTID須要保證惟一性,所以無論是操做gtid_executed集合和gtid_owned集合,仍是分配GTID,都須要加上一個大鎖。咱們的優化主要集中在第一種GTID分配方式。
對於GTID的分配,因爲處於Group Commit的第一個階段,由該階段的leader線程爲其follower線程分配GTID及刷Binlog,所以不會產生競爭。
而在Step 5,各個線程在完成事務提交後,各自去從gtid_owned集合中刪除其使用的gtid。這時候每一個線程都須要獲取互斥鎖,很顯然,併發越高,這種競爭就越明顯,咱們很容易從pt-pmp輸出中看到以下相似的trace:
ha_commit_trans—>MySQL_BIN_LOG::commit—>MySQL_BIN_LOG::ordered_commit—>MySQL_BIN_LOG::finish_commit—>Gtid_state::update_owned_gtids_impl—>lock_sidno
這同時也會影響到GTID的分配階段,致使TPS在高併發場景下的急劇降低。
解決
實際上對於自動分配GTID的場景,並無必要維護gtid_owned集合。咱們的修改也很是簡單,在自動分配一個GTID後,直接加入到gtid_executed集合中,避免維護gtid_owned,這樣事務提交時就無需去清理gtid_owned集合了,從而能夠徹底避免鎖競爭。
固然爲了保證一致性,若是分配GTID後,寫入Binlog文件失敗,也須要從gtid_executed集合中刪除。不過這種場景很是罕見。
性能數據
使用sysbench,100張表,每張10w行記錄,update_non_index.lua,純內存操做,innodb_flush_log_at_trx_commit = 2,sync_binlog = 1000
併發線程 原生 修改後 32 24500 25000 64 27900 29000 128 30800 31500 256 29700 32000 512 29300 31700 1024 27000 31000
從測試結果能夠看到,優化前隨着併發上升,性能出現降低,而優化後則能保持TPS穩定。
問題重現
先從問題入手,重現下這個 bug
use test; drop table if exists t1; create table t1(id int auto_increment, a int, primary key (id)) engine=innodb; insert into t1 values (1,2); insert into t1 values (null,2); insert into t1 values (null,2); select * from t1; +----+------+ | id | a | +----+------+ | 1 | 2 | | 2 | 2 | | 3 | 2 | +----+------+ delete from t1 where id=2; delete from t1 where id=3; select * from t1; +----+------+ | id | a | +----+------+ | 1 | 2 | +----+------+
這裏咱們關閉MySQL,再啓動MySQL,而後再插入一條數據
insert into t1 values (null,2); select * FROM T1; +----+------+ | id | a | +----+------+ | 1 | 2 | +----+------+ | 2 | 2 | +----+------+
咱們看到插入了(2,2),而若是我沒有重啓,插入一樣數據咱們獲得的應該是(4,2)。 上面的測試反映了MySQLd重啓後,InnoDB存儲引擎的表自增id可能出現重複利用的狀況。
自增id重複利用在某些場景下會出現問題。依然用上面的例子,假設t1有個歷史表t1_history用來存t1錶的歷史數據,那麼MySQLd重啓前,ti_history中可能已經有了(2,2)這條數據,而重啓後咱們又插入了(2,2),當新插入的(2,2)遷移到歷史表時,會違反主鍵約束。
緣由分析
InnoDB 自增列出現重複值的緣由:
MySQL> show create table t1\G; *************************** 1. row *************************** Table: t1 Create Table: CREATE TABLE `t1` ( `id` int(11) NOT NULL AUTO_INCREMENT, `a` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=innodb AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 1 row in set (0.00 sec)
建表時能夠指定 AUTO_INCREMENT值,不指定時默認爲1,這個值表示當前自增列的起始值大小,若是新插入的數據沒有指定自增列的值,那麼自增列的值即爲這個起始值。對於InnoDB表,這個值沒有持久到文件中。而是存在內存中(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是實時存儲內存中的。那麼,MySQLd 重啓後,從哪裏獲得AUTO_INCREMENT呢? 內存值確定是丟失了。實際上MySQL採用執行相似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表重啓是不會出現自增id重複的問題。
問題修復
MyISAM選擇將AUTO_INCREMENT實時存儲在.MYI文件頭部中。實際上.MYI頭部還會實時存其餘信息,也就是說寫AUTO_INCREMENT只是個順帶的操做,其性能損耗能夠忽略。InnoDB 表若是要解決這個問題,有兩種方法。
1)將AUTO_INCREMENT最大值持久到frm文件中。
2)將 AUTO_INCREMENT最大值持久到彙集索引根頁trx_id所在的位置。
第一種方法直接寫文件性能消耗較大,這是一額外的操做,而不是一個順帶的操做。咱們採用第二種方案。爲何選擇存儲在彙集索引根頁頁頭trx_id,頁頭中存儲trx_id,只對二級索引頁和insert buf 頁頭有效(MVCC)。而彙集索引根頁頁頭trx_id這個值是沒有使用的,始終保持初始值0。正好這個位置8個字節可存放自增值的值。咱們每次更新AUTO_INCREMENT值時,同時將這個值修改到彙集索引根頁頁頭trx_id的位置。 這個寫操做跟真正的數據寫操做同樣,遵照write-ahead log原則,只不過這裏只須要redo log ,而不須要undo log。由於咱們不須要回滾AUTO_INCREMENT的變化(即回滾後自增列值會保留,即便insert 回滾了,AUTO_INCREMENT值不會回滾)。
所以,AUTO_INCREMENT值存儲在彙集索引根頁trx_id所在的位置,其實是對內存根頁的修改和多了一條redo log(量很小),而這個redo log 的寫入也是異步的,能夠說是原有事務log的一個順帶操做。所以AUTO_INCREMENT值存儲在彙集索引根頁這個性能損耗是極小的。
修復後的性能對比,咱們新增了全局參數innodb_autoinc_persistent 取值on/off; on 表示將AUTO_INCREMENT值實時存儲在彙集索引根頁。off則採用原有方式只存儲在內存。
./bin/sysbench --test=sysbench/tests/db/insert.lua --MySQL-port=4001 --MySQL-user=root \--MySQL-table-engine=innodb --MySQL-db=sbtest --oltp-table-size=0 --oltp-tables-count=1 \--num-threads=100 --MySQL-socket=/u01/zy/sysbench/build5/run/MySQL.sock --max-time=7200 --max-requests run set global innodb_autoinc_persistent=off; tps: 22199 rt:2.25ms set global innodb_autoinc_persistent=on; tps: 22003 rt:2.27ms
能夠看出性能損耗在%1如下。
改進
新增參數innodb_autoinc_persistent_interval 用於控制持久化AUTO_INCREMENT值的頻率。例如:innodb_autoinc_persistent_interval=100,auto_incrememt_increment=1時,即每100次insert會控制持久化一次AUTO_INCREMENT值。每次持久的值爲:當前值+innodb_autoinc_persistent_interval。
測試結論
innodb_autoinc_persistent=ON, innodb_autoinc_persistent_interval=1時性能損耗在%1如下。
innodb_autoinc_persistent=ON, innodb_autoinc_persistent_interval=100時性能損耗能夠忽略。
限制
注意:若是咱們使用須要開啓innodb_autoinc_persistent,應該在參數文件中指定
innodb_autoinc_persistent= on
若是這樣指定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表居然也有這個要求,感受是多餘的。
前言
與oracle 不一樣,MySQL 的主庫與備庫的同步是經過 binlog 實現的,而redo日誌只作爲MySQL 實例的crash recovery使用。MySQL在4.x 的時候放棄redo 的同步策略而引入 binlog的同步,一個重要緣由是爲了兼容其它非事務存儲引擎,不然主備同步是沒有辦法進行的。
redo 日誌同步屬於物理同步方法,簡單直接,將修改的物理部分傳送到備庫執行,主備共用一致的 LSN,只要保證 LSN 相同便可,同一時刻,只能主庫或備庫一方接受寫請求; binlog的同步方法屬於邏輯複製,分爲statement 或 row 模式,其中statement記錄的是SQL語句,Row 模式記錄的是修改以前的記錄與修改以後的記錄,即前鏡像與後鏡像;備庫經過binlog dump 協議拉取binlog,而後在備庫執行。若是拉取的binlog是SQL語句,備庫會走和主庫相同的邏輯,若是是row 格式,則會調用存儲引擎來執行相應的修改。
本文簡單說明5.5到5.7的主備複製性能改進過程。
replication improvement (from 5.5 to 5.7)
(1) 5.5 中,binlog的同步是由兩個線程執行的
io_thread: 根據binlog dump協議從主庫拉取binlog, 並將binlog轉存到本地的relaylog;
sql_thread: 讀取relaylog,根據位點的前後順序執行binlog event,進而將主庫的修改同步到備庫,達到主備一致的效果; 因爲在主庫的更新是由多個客戶端執行的,因此當壓力達到必定的程度時,備庫單線程執行主庫的binlog跟不上主庫執行的速度,進而會產生延遲形成備庫不可用,這也是分庫的緣由之一,其SQL線程的執行堆棧以下:
sql_thread: exec_relay_log_event apply_event_and_update_pos apply_event rows_log_event::apply_event storage_engine operation update_pos
(2) 5.6 中,引入了多線程模式,在多線程模式下,其線程結構以下
io_thread: 同5.5
Coordinator_thread: 負責讀取 relay log,將讀取的binlog event以事務爲單位分發到各個 worker thread 進行執行,並在必要時執行binlog event(Description_format_log_event, Rotate_log_event 等)。
worker_thread: 執行分配到的binlog event,各個線程之間互不影響;
多線程原理
sql_thread 的分發原理是依據當前事務所操做的數據庫名稱來進行分發,若是事務是跨數據庫行爲的,則須要等待已分配的該數據庫的事務所有執行完畢,纔會繼續分發,其分配行爲的僞碼能夠簡單的描述以下:
get_slave_worker if (contains_partition_info(log_event)) db_name= get_db_name(log_event); entry {db_name, worker_thread, usage} = map_db_to_worker(db_name); while (entry->usage > 0) wait(); return worker; else if (last_assigned_worker) return last_assigned_worker; else push into buffer_array and deliver them until come across a event that have partition info
須要注意的細節
整體上說,5.6 的並行複製打破了5.5 單線程的複製的行爲,只是在單庫下用處不大,而且5.6的並行複製的改動引入了一些重量級的bug
(3) 5.7中,並行複製的實現添加了另一種並行的方式,即主庫在 ordered_commit中的第二階段的時候,將同一批commit的 binlog 打上一個相同的seqno標籤,同一時間戳的事務在備庫是能夠同時執行的,所以大大簡化了並行複製的邏輯,並打破了相同 DB 不能並行執行的限制。備庫在執行時,具備同一seqno的事務在備庫能夠並行的執行,互不干擾,也不須要綁定信息,後一批seqno的事務須要等待前一批相同seqno的事務執行完後才能夠執行。
詳細實現可參考: http://bazaar.launchpad.net/~MySQL/MySQL-server/5.7/revision/6256 。
reference: http://geek.rohitkalhans.com/2013/09/enhancedMTS-deepdive.html
本文說明一個物理升級致使的 "數據丟失"。
現象
在MySQL 5.1下新建key分表,能夠正確查詢數據。
drop table t1; create table t1 (c1 int , c2 int) PARTITION BY KEY (c2) partitions 5; insert into t1 values(1,1785089517),(2,null); MySQL> select * from t1 where c2=1785089517; +------+------------+ | c1 | c2 | +------+------------+ | 1 | 1785089517 | +------+------------+ 1 row in set (0.00 sec) MySQL> select * from t1 where c2 is null; +------+------+ | c1 | c2 | +------+------+ | 2 | NULL | +------+------+ 1 row in set (0.00 sec)
而直接用MySQL5.5或MySQL5.6啓動上面的5.1實例,發現(1,1785089517)這行數據不能正確查詢出來。
alter table t1 PARTITION BY KEY ALGORITHM = 1 (c2) partitions 5; MySQL> select * from t1 where c2 is null; +------+------+ | c1 | c2 | +------+------+ | 2 | NULL | +------+------+ 1 row in set (0.00 sec) MySQL> select * from t1 where c2=1785089517; Empty set (0.00 sec)
緣由分析
跟蹤代碼發現,5.1 與5.5,5.6 key hash算法是有區別的。
5.1 對於非空值的處理算法以下
void my_hash_sort_bin(const CHARSET_INFO *cs __attribute__((unused)), const uchar *key, size_t len,ulong *nr1, ulong *nr2) { const uchar *pos = key; key+= len; for (; pos < (uchar*) key ; pos++) { nr1[0]^=(ulong) ((((uint) nr1[0] & 63)+nr2[0]) * ((uint)*pos)) + (nr1[0] << 8); nr2[0]+=3; } }
經過此算法算出數據(1,1785089517)在第3個分區
5.5和5.6非空值的處理算法以下
void my_hash_sort_simple(const CHARSET_INFO *cs, const uchar *key, size_t len, ulong *nr1, ulong *nr2) { register uchar *sort_order=cs->sort_order; const uchar *end; /* Remove end space. We have to do this to be able to compare 'A ' and 'A' as identical */ end= skip_trailing_space(key, len); for (; key < (uchar*) end ; key++) { nr1[0]^=(ulong) ((((uint) nr1[0] & 63)+nr2[0]) * ((uint) sort_order[(uint) *key])) + (nr1[0] << 8); nr2[0]+=3; } }
經過此算法算出數據(1,1785089517)在第5個分區,所以,5.5,5.6查詢不能查詢出此行數據。
5.1,5.5,5.6對於空值的算法仍是一致的,以下
if (field->is_null()) { nr1^= (nr1 << 1) | 1; continue; }
都能正確算出數據(2, null)在第3個分區。所以,空值能夠正確查詢出來。
那麼是什麼致使非空值的hash算法走了不一樣路徑呢?在5.1下,計算字段key hash固定字符集就是my_charset_bin,對應的hash 函數就是前面的my_hash_sort_simple。而在5.5,5.6下,計算字段key hash的字符集是隨字段變化的,字段c2類型爲int對應my_charset_numeric,與之對應的hash函數爲my_hash_sort_simple。具體能夠參考函數Field::hash
那麼問題又來了,5.5後爲何算法會變化呢?緣由在於官方關於字符集策略的調整,詳見WL#2649 。
兼容處理
前面講到,因爲hash 算法變化,用5.5,5.6啓動5.1的實例,致使不能正確查詢數據。那麼5.1升級5.5,5.6就必須兼容這個問題.MySQL 5.5.31之後,提供了專門的語法 ALTER TABLE ... PARTITION BY ALGORITHM=1 [LINEAR] KEY ... 用於兼容此問題。對於上面的例子,用5.5或5.6啓動5.1的實例後執行
MySQL> alter table t1 PARTITION BY KEY ALGORITHM = 1 (c2) partitions 5; Query OK, 2 rows affected (0.02 sec) Records: 2 Duplicates: 0 Warnings: 0
MySQL> select * from t1 where c2=1785089517; +------+------------+ | c1 | c2 | +------+------------+ | 1 | 1785089517 | +------+------------+ 1 row in set (0.00 sec)
數據能夠正確查詢出來了。
而實際上5.5,5.6的MySQL_upgrade升級程序已經提供了兼容方法。MySQL_upgrade 執行check table xxx for upgrade 會檢查key分區表是否用了老的算法。若是使用了老的算法,會返回
MySQL> CHECK TABLE t1 FOR UPGRADE\G *************************** 1. row *************************** Table: test.t1 Op: check Msg_type: error Msg_text: KEY () partitioning changed, please run: ALTER TABLE `test`.`t1` PARTITION BY KEY /*!50611 ALGORITHM = 1 */ (c2) PARTITIONS 5 *************************** 2. row *************************** Table: test.t1 Op: check Msg_type: status Msg_text: Operation failed 2 rows in set (0.00 sec)
檢查到錯誤信息後會自動執行如下語句進行兼容。
ALTER TABLE `test`.`t1` PARTITION BY KEY /*!50611 ALGORITHM = 1 */ (c2) PARTITIONS 5。
背景
客戶使用MySQLdump導出一張表,而後使用MySQL -e 'source test.dmp'的過程當中client進程crash,爆出內存的segment fault錯誤,致使沒法導入數據。
問題定位
test.dmp文件大概50G左右,查看了一下文件的前幾行內容,發現:
A partial dump from a server that has GTIDs will by default include the GTIDs of all transactions, even those that changed suppressed parts of the database If you don't want to restore GTIDs pass set-gtid-purged=OFF. To make a complete dump, pass... -- MySQL dump 10.13 Distrib 5.6.16, for Linux (x86_64) -- -- Host: 127.0.0.1 Database: carpath -- ------------------------------------------------------ -- Server version 5.6.16-log /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
問題定位到第一行出現了不正常warning的信息,是因爲客戶使用MySQLdump命令的時候,重定向了stderr。即:
MySQLdump ...>/test.dmp 2>&1
致使error或者warning信息都重定向到了test.dmp, 最終致使失敗。
問題引伸
問題雖然定位到了,但卻有幾個問題沒有弄清楚:
問題1. 不正常的sql,執行失敗,報錯出來就能夠了,爲何會致使crash?
MySQL.cc::add_line函數中,在讀第一行的時候,讀取到了don't,發現有一個單引號,因此程序死命的去找匹配的另一個單引號,致使不斷的讀取文件,分配內存,直到crash。
假設沒有這個單引號,MySQL讀到第六行,發現;號,就會執行sql,並正常的報錯退出。
問題2. 那代碼中對於大小的邊界究竟是多少?好比insert語句支持batch insert時,語句的長度多少,又好比遇到clob字段呢?
因此,正常狀況下,max_allowed_packet如今的最大字段長度和MAX_BATCH_BUFFER_SIZE限制的最大insert語句,是匹配的。
RDS問題修復原則
從問題的定位上來看,這一例crash屬於客戶錯誤使用MySQLdump致使的問題,Aliyun RDS分支對內存致使的crash問題,都會定位並反饋給用戶。 但此例不作修復,而是引導用戶正確的使用MySQLdump工具。
bug描述
Oracle 最新發布的版本 5.6.22 中有這樣一個關於GTID的bugfix,在主備場景下,若是咱們在主庫上 SET GLOBAL GTID_PURGED = "some_gtid_set",而且 some_gtid_set 中包含了備庫還沒複製的事務,這個時候若是備庫接上主庫的話,預期結果是主庫返回錯誤,IO線程掛掉的,可是實際上,在這種場景下主庫並不報錯,只是默默的把本身 binlog 中包含的gtid事務發給備庫。這個bug的形成的結果是看起來複制正常,沒有錯誤,但實際上備庫已經丟事務了,主備極可能就不一致了。
背景知識
binlog 中記錄的和GTID相關的事件主要有2種,Previous_gtids_log_event 和 Gtid_log_event,前者表示以前的binlog中包含的gtid的集合,後者就是一個gtid,對應一個事務。一個 binlog 文件中只有一個 Previous_gtids_log_event,放在開頭,有多個 Gtid_log_event,以下面所示
Previous_gtids_log_event // 此 binlog 以前的全部binlog文件包含的gtid集合 Gtid_log_event // 單個gtid event Transaction Gtid_log_event Transaction . . . Gtid_log_event Transaction
咱們知道備庫的複製線程是分IO線程和SQL線程2種的,IO線程經過GTID協議或者文件位置協議拉取主庫的binlog,而後記錄在本身的relay log中;SQL線程經過執行realy log中的事件,把其中的操做都本身作一遍,記入本地binlog。在GTID協議下,備庫向主庫發送拉取請求的時候,會告知主庫本身已經有的全部的GTID的集合,Retrieved_Gtid_Set + Executed_Gtid_Set,前者對應 realy log 中全部的gtid集合,表示已經拉取過的,後者對應binlog中記錄有的,表示已經執行過的;主庫在收到這2個總集合後,會掃描本身的binlog,找到合適的binlog而後開始發送。
主庫將備庫發送過來的總合集記爲 slave_gtid_executed,而後調用 find_first_log_not_in_gtid_set(slave_gtid_executed),這個函數的目的是從最新到最老掃描binlog文件,找到第一個含有不存在 slave_gtid_executed 這個集合的gtid的binlog。在這個掃描過程當中並不須要從頭至尾讀binlog中全部的gtid,只須要讀出 Previous_gtids_log_event ,若是Previous_gtids_log_event 不是 slave_gtid_executed的子集,就繼續向前找binlog,直到找到爲止。
這個查找過程總會中止的,中止條件以下:
- 找到了這樣的binlog,其Previous_gtids_log_event 是slave_gtid_executed子集
- 在往前讀binlog的時候,發現沒有binlog文件了(如被purge了),可是還沒找到知足條件的Previous_gtids_log_event,這個時候主庫報錯
- 一直往前找,發現Previous_gtids_log_event 是空集
在條件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的場景都會觸發這個bug,下面就分析下什麼狀況下才會觸發bug。
bug 分析
假設有這樣的場景,咱們要用已經有MySQL實例的備份從新作一對主備實例,無論是用 xtrabackup 這種物理備份工具或者MySQLdump這種邏輯備份工具,都會有2步操做,
- 導入數據
- SET GLOBAL GTID_PURGED ="xxxx"
步驟2是爲了保證GTID的完備性,由於新實例已經導入了數據,就須要把生成這些數據的事務對應的GTID集合也設置進來。
正常的操做是主備都要作這2步的,若是咱們只在主庫上作了這2步,備庫什麼也不作,而後就直接用 GTID 協議把備庫連上來,按照咱們的預期這個時候是應該出錯的,主備不一致,而且主庫的binlog中沒東西,應該報以前中止條件2報的錯。可是使人大跌眼鏡的是主庫不報錯,複製看起來是徹底正常的。
爲啥會這樣呢,SET GLOBAL GTID_PURGED 操做會調用 MySQL_bin_log.rotate_and_purge切換到一個新的binlog,並把這個GTID_PURGED 集合記入新生成的binlog的Previous_gtids_log_event,假設原有的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 實例的數據增加到不少的時候,就會考慮經過庫或者表級別的拆分,把當前實例的數據分散到多個實例上去,假設原實例爲A,想把其中的5個庫(db1/db2/db3/db4/db5)拆分到5個實例(B1/B2/B3/B4/B5)上去。
拆分過程通常會這樣作,先把A的相應庫的數據導出,而後導入到對應的B實例上,可是在這個導出導入過程當中,A庫的數據仍是在持續更新的,因此還需在導入完後,在全部的B實例和A實例間創建複製關係,拉取缺失的數據,在業務不繁忙的時候將業務切換到各個B實例。
在複製搭建時,每一個B實例只須要複製A實例上的一個庫,因此只須要重放對應庫的binlog便可,這個經過 replicate-do-db 來設置過濾條件。若是咱們用備庫上執行 show slave status\G 會看到Executed_Gtid_Set是斷斷續續的,間斷很是多,致使這一列很長很長,看到的直接效果就是被刷屏了。
爲啥會這樣呢,由於設了replicate-do-db,就只會執行對應db對應的event,其它db的都不執行。主庫的執行是不分db的,對各個db的操做互相間隔,記錄在binlog中,因此備庫作了過濾後,就出現這種斷斷的現象。
除了這個看着不舒服外,還會致使其它問題麼?
假設咱們拿B1實例的備份作了一個新實例,而後接到A上,若是主庫A又按期purge了老的binlog,那麼新實例的IO線程就會出錯,由於須要的binlog在主庫上找不到了;即便主庫沒有purge 老的binlog,新實例還要把主庫的binlog都從頭從新拉過來,而後執行的時候又都過濾掉,不如不拉取。
有沒有好的辦法解決這個問題呢?SQL線程在執行的時候,發現是該被過濾掉的event,在不執行的同時,記一個空事務就行了,把原事務對應的GTID位置佔住,記入binlog,這樣備庫的Executed_Gtid_Set就是連續的了。
bug 修復
對這個問題,官方有一個相應的bugfix,參見 revno: 5860 ,有了這個patch後,備庫B1的 SQL 線程在遇到和 db2-db5 相關的SQL語句時,在binlog中把對應的GTID記下,同時對應記一個空事務。
這個 patch 只是針對Query_log_event,即 statement 格式的 binlog event,那麼row格式的呢? row格式原來就已是這種行爲,經過check_table_map 函數來過濾庫或者表,而後生成一個空事務。
另外這個patch還專門處理了下 CREATE/DROP TEMPORARY TABLE 這2種語句,咱們知道row格式下,對臨時表的操做是不會記入binlog的。若是主庫的binlog格式是 statement,備庫用的是 row,CREATE/DROP TEMPORARY TABLE 對應的事務傳到備庫後,就會消失掉,Executed_Gtid_Set集合看起來是不連續的,可是主庫的binlog記的gtid是連續的,這個 patch 讓這種狀況下的CREATE/DROP TEMPORARY TABLE在備庫一樣記爲一個空事務。
來自一個TokuDB用戶的「投訴」:
https://mariadb.atlassian.net/browse/MDEV-6207
現象大概是:
用戶有一個MyISAM的表test_table:
CREATE TABLE IF NOT EXISTS `test_table` ( `id` int(10) unsigned NOT NULL, `pub_key` varchar(80) NOT NULL, PRIMARY KEY (`id`), KEY `pub_key` (`pub_key`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1;
轉成TokuDB引擎後表大小爲92M左右:
47M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb 45M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb
執行"OPTIMIZE TABLE test_table":
63M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb 61M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb
再次執行"OPTIMIZE TABLE test_table":
79M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb 61M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb
繼續執行:
79M _tester_testdb_sql_61e7_1812_main_ad88a6b_1_19_B_0.tokudb 61M _tester_testdb_sql_61e7_1812_key_pub_key_ad88a6b_1_19_B_1.tokudb
基本穩定在這個大小。
主索引從47M-->63M-->79M,執行"OPTIMIZE TABLE"後爲何會愈來愈大?
這得從TokuDB的索引文件分配方式提及,當內存中的髒頁須要寫到磁盤時,TokuDB優先在文件末尾分配空間並寫入,而不是「覆寫」原塊,原來的塊暫時成了「碎片」。
這樣問題就來了,索引文件豈不是愈來愈大?No, TokuDB會把這些「碎片」在checkpoint時加入到回收列表,以供後面的寫操做使用,看似79M的文件其實還能夠裝很多數據呢!
嗯,這個現象解釋通了,但還有2個問題:
- 在執行這個語句的時候,TokuDB到底在作什麼呢? 在作toku_ft_flush_some_child,把內節點的緩衝區(message buffer)數據刷到最底層的葉節點。
- 在TokuDB裏,OPTIMIZE TABLE有用嗎? 做用很是小,不建議使用,TokuDB是一個"No Fragmentation"的引擎。
本文轉載自MySQL.taobao.org ,感謝淘寶數據庫項目組丁奇、鳴嵩、彭立勳、皓庭、項仲、劍川、武藏、祁奚、褚霸、一工。審校:劉亞瓊