通過這種方式,可以讓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中;通過延遲寫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;這種場景就是目前因為MDL鎖導致的很經典的阻塞問題,如果session1長時間未提交,或者查詢持續過長時間,那么后續對t1表的所有讀寫操作,都被阻塞。 對于在線的業務來說,很容易導致業務中斷。
aliyun RDS分支改進
DDL fast fail并沒有解決真正DDL過程中的阻塞問題,但避免了因為DDL操作沒有獲取鎖,進而導致業務其他查詢/更新語句阻塞的問題。
其實現方式如下:
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操作經常會遇到這樣的錯誤信息:
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文件中。第一種方法直接寫文件性能消耗較大,這是一額外的操作,而不是一個順帶的操作。我們采用第二種方案。為什么選擇存儲在聚集索引根頁頁頭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,應該在參數文件中指定
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
詳細實現可參考: 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。問題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,直到找到為止。
這個查找過程總會停止的,停止條件如下:
在條件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步操作,
步驟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個問題:
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com