原文: http://mp.weixin.qq.com/s?__biz=MzAxNjAzMTQyMA==&mid=209773318&idx=1&sn=e9600d3db80ba3a3811a6e672d08aded&scene=1&srcid=10132stoFdOVIDasHsVOlGzR&key=2877d24f51fa53848e3b6d036cca266f7f31c08154ea4286ecc5cd73ea382806ef4ed0b4431d5b1c2e74d11b43a1815a&ascene=0&uin=Mjk1ODMyNTYyMg%3D%3D&devicetype=iMac+MacBookPro11%2C4+OSX+OSX+10.11+build(15A284)&version=11020201&pass_ticket=arNrQT6cCQuwOIGtmblNyqDr0Ft81AyT7cWa7HHUOK1lFPgxXHS%2Brs65tOnMLmzkphp
關於Group Commit網上的資料其實已經足夠多了,我這裏只簡單的介紹一下。html
衆所周知,在MySQL5.6以前的版本,因爲引入了Binlog/InnoDB的XA,Binlog的寫入和InnoDB commit徹底串行化執行,大概的執行序列以下:sql
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性能優化
Oracle MySQL 在5.6版本開始也支持Binlog Group Commit,使用了和Mariadb相似的思路,但將Group Commit的過程拆分紅了三個階段:flush stage 將各個線程的binlog從cache寫到文件中; sync stage 對binlog作fsync操做(若是須要的話);commit stage 爲各個線程作引擎層的事務commit。每一個stage同時只有一個線程在操做。session
Tips:當引入Group Commit後,sync_binlog的含義就變了,假定設爲1000,表示的不是1000個事務後作一次fsync,而是1000個事務組。多線程
Oracle MySQL的實現的優點在於三個階段能夠併發執行,從而提高效率。併發
XA Recoveroracle
在Binlog打開的狀況下,MySQL默認使用MySQL_BIN_LOG來作XA協調者,大體流程爲:app
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老是提交的。
問題
其實問題很簡單:每一個事務都要保證其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;
1. session 1對t1表作查詢,首先須要獲取t1表的MDL_SHARED_READ級別MDL鎖。鎖一直持續到commit結束,而後釋放。
2. session 2對t1表作DDL,須要獲取t1表的MDL_EXCLUSIVE級別MDL鎖,由於MDL_SHARED_READ與MDL_EXCLUSIVE不相容,因此session 2被session 1阻塞,而後進入等待隊列。
3. session 3對t1表作查詢,由於等待隊列中有MDL_EXCLUSIVE級別MDL鎖請求,因此session3也被阻塞,進入等待隊列。
這種場景就是目前由於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鎖:
1. 當語句指定的是no_wait,若是獲取失敗,客戶端將獲得報錯信息:ERROR : Lock wait timeout exceeded; try restarting transaction。
2. 當語句指定的是wait 1,若是獲取失敗,最多等待1s,而後獲得報錯信息:ERROR : Lock wait timeout exceeded; try restarting transaction。
另外,除了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獲取排他鎖的設計,須要考慮的就是兩個問題:
雪崩,若是你採用排隊阻塞的機制,那麼DDL若是長時間沒法獲取鎖,就會致使應用的雪崩效應,對於高併發的業務,也是災難。
餓死,若是你採用強制式的機制,那麼要防止DDL一直沒法獲取鎖的狀況,在業務高峯期,可能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,大概能夠描述爲:
Step 1:事務過程當中,碰到第一條DML語句須要記錄Binlog時,分配一段Gtid事件的cache,但不分配實際的GTID
Step 2:事務完成後,進入commit階段,分配一個GTID並寫入Step1預留的Gtid事件中,該GTID必須保證不在gtid_owned集合和gtid_executed集合中。 分配的GTID隨後被加入到gtid_owned集合中。
Step 3:將Binlog 從線程cache中刷到Binlog文件中。
Step 4:將GTID加入到gtid_executed集合中。
Step 5:在完成sync stage 和commit stage後,各個會話將其使用的GTID從gtid_owned中移除。
顯式設置
用戶經過設置session級別變量gtid_next能夠顯式指定一個GTID,流程以下:
Step 1:設置變量gtid_next,指定的GTID被加入到gtid_owned集合中。
Step 2:執行任意事務SQL,在將binlog從線程cache刷到binlog文件後,將GTID加入到gtid_executed集合中。
Step 3:在完成事務COMMIT後,從gtid_owned中移除。
備庫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=on, innodb_autoinc_persistent_interval=N>1時,自增N次後持久化到彙集索引根頁,每次持久的值爲當前AUTO_INCREMENT+(N-1)*innodb_autoextend_increment。重啓後讀取持久化的AUTO_INCREMENT值會偏大,形成一些浪費但不會重複。innodb_autoinc_persistent_interval=1 每次都持久化沒有這個問題。
若是innodb_autoinc_persistent=on,頻繁設置auto_increment_increment的可能會致使持久化到彙集索引根頁的值不許確。由於innodb_autoinc_persistent_interval計算沒有考慮auto_increment_increment變化的狀況,參看dict_table_autoinc_update_if_greater。而設置auto_increment_increment的狀況極少,能夠忽略。
注意:若是咱們使用須要開啓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
須要注意的細節
內存的分配與釋放。relay thread 每讀取一個log_event, 則須要 malloc 必定的內存,在work線程執行完後,則須要free掉;
數據庫名 與 worker 線程的綁定信息在一個hash表中進行維護,hash表以entry爲單位,entry中記錄當前entry所表明的數據庫名,有多少個事務相關的已被分發,執行這些事務的worker thread等信息;
維護一個綁定信息的array , 在分發事務的時候,更新綁定信息,增長相應 entry->usage, 在執行完一個事務的時候,則須要減小相應的entry->usage;
slave worker 信息的維護,即每一個 worker thread執行了哪些事務,執行到的位點是在哪,延遲是如何計算的,若是執行出錯,mts_recovery_group 又是如何恢復的;
分配線程是以數據庫名進行分發的,當一個實例中只有一個數據庫的時候,不會對性能有提升,相反,因爲增長額外的操做,性能還會有一點回退;
臨時表的處理,臨時表是和entry綁定在一塊兒的,在執行的時候將entry的臨時表掛在執行線程thd下面,但沒有固化,若是在臨時表操做期間,備庫crash,則重啓後備庫會有錯誤;
整體上說,5.6 的並行複製打破了5.5 單線程的複製的行爲,只是在單庫下用處不大,而且5.6的並行複製的改動引入了一些重量級的bug
MySQL slave sql thread memory leak (http://bugs.MySQL.com/bug.php?id=71197)
Relay log without xid_log_event may case parallel replication hang (http://bugs.MySQL.com/bug.php?id=72794)
Transaction lost when relay_log_info_repository=FILE and crashed (http://bugs.MySQL.com/bug.php?id=73482)
(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