[MySQL 5.6] GTID实现、运维变化及存在的bug
[MySQL 5.6] GTID實現、運維變化及存在的bug
http://www.tuicool.com/articles/NjqQju
由于之前沒太多深入關注gtid,這里給自己補補課,本文是我看文檔和代碼的整理記錄。
本文的主要目的是記下跟gtid相關的backtrace,用于以后的問題排查。另外也會討論目前在MySQL5.6.11版本中存在的bug。前言:什么是GTID
什么是GTID呢, 簡而言之,就是全局事務ID(global transaction identifier ),最初由google實現,官方MySQL在5.6才加入該功能,本文的起因在于5.6引入一大堆的gtid相關變量,深感困惑。 去年年中的時候,也寫過一片簡短的博客,大致介紹了下gtid是什么,http://mysqllover.com/?p=87??。本文也不打算太多文字的介紹,因為網絡上已經有大量的類似文章。 GTID的格式類似于: 7a07cd08-ac1b-11e2-9fcf-0010184e9e08:1 這是在我的一臺服務器上生成的gtid記錄,它在binlog中表現的事件類型就是: GTID_LOG_EVENT:用于表示隨后的事務的GTID 另外還有兩種類型的GTID事件: ANONYMOUS_GTID_LOG_EVENT :匿名GTID事件類型(暫且不論) PREVIOUS_GTIDS_LOG_EVENT: 用于表示當前binlog文件之前已經執行過的GTID集合,記錄在Binlog文件頭,例如: # at 120? #130502 23:23:27 server id 119821? end_log_pos 231 CRC32 0x4f33bb48???? Previous-GTIDs? # 10a27632-a909-11e2-8bc7-0010184e9e08:1,? # 7a07cd08-ac1b-11e2-9fcf-0010184e9e08:1-1129 這個字符串,用“:”分開,前面表示這個服務器的server_uuid,這是一個128位的隨機字符串,在第一次啟動時生成(函數generate_server_uuid),對應的variables是只讀變量server_uuid。 它能以極高的概率保證全局唯一性,并存到文件DATA/auto.cnf中。因此要注意保護這個文件不要被刪除或修改,不然就麻煩了。 第二部分是一個自增的事務ID號,事務id號+server_uuid來唯一標示一個事務。 除了單獨的GTID外,還有一個GTID SET的概念。一個GTID SET的表示類似于: 7a07cd08-ac1b-11e2-9fcf-0010184e9e08:1-31 GTID_EXECUTED和GTID_PURGED是典型的GTID SET類型變量;在一個復制拓撲中,GTID_EXECUTED 可能包含好幾組數據,例如:mysql> show global variables like ‘%gtid_executed%’\G
*************************** 1. row *************************** Variable_name: gtid_executed Value: 10a27632-a909-11e2-8bc7-0010184e9e08:1-4, 153c0406-a909-11e2-8bc7-0010184e9e08:1-3, 7a07cd08-ac1b-11e2-9fcf-0010184e9e08:1-31,f914fb74-a908-11e2-8bc6-0010184e9e08:1
?
本文討論的內容包括:
?
一.主庫上的gtid產生及記錄 二.備庫如何使用GTID復制 三.主備運維的變化 四.MySQL5.6.11存在的bug一、主庫上的Gtid
a.相關變量
主庫上每個事務的Gtid包括變化的部分和不變的部分。在討論之前,要弄清楚GTID維護的四個變量: GTID_PURGED?:已經被刪除的binlog的事務,它是GTID_EXECUTED的子集,從MySQL5.6.9開始,該變量無法被設置。 GTID_OWNED?:??表示正在執行的事務的gtid以及對應的線程ID。 例如如下:mysql> show global variables like ‘%gtid_owned%’\G
*************************** 1. row *************************** Variable_name: gtid_owned Value: 7a07cd08-ac1b-11e2-9fcf-0010184e9e08:11560057#67:11560038#89:11560059#7:11560034#32:11560053#56:11560052#112:11560055#128:11560054#65:11559997#96:11560056#90:11560051#85:11560058#39:11560061#12:11560060#125:11560035#62:11560062#5 1 row in set (0.01 sec) GTID_EXECUTED??表示已經在該實例上執行過的事務; 執行RESET MASTER 會將該變量置空; 我們還可以通過設置GTID_NEXT執行一個空事務,來影響GTID_EXECUTED GTID_NEXT?是SESSION級別變量,表示下一個將被使用的GTID 在內存中也維護了與GTID_PURGED, GTID_OWNED, GTID_EXECUTED相對應的全局對象gtid_state。 gtid_state中維護了三個集合,其中logged_gtids對應GTID_EXECUTED,?lost_gtids對應GTID_PURGED,owned_gtids對應GTID_OWNEDb.如何分配和使用GTID
在主庫執行一個事務的過程中,關于Gtid主要涉及到以下幾個部分: 事務開始,執行第一條SQL時?,在寫入第一個“BEGIN” 的QUERY EVENT 之前, 為binlog cache 的Group_cache中分配一個group(Group_cache::add_logged_group),并寫入一個Gtid_log_event,此時并未為其分配事務id,backtrace 如下: handler::ha_write_row->binlog_log_row->write_locked_table_maps->THD::binlog_write_table_map->binlog_start_trans_and_stmt->binlog_cache_data::write_event->Group_cache::add_logged_group 暫時還不清楚什么時候一個事務里會有多個gtid的group_cache. 在binlog group commit的flush階段?: 第一步,調用Group_cache::generate_automatic_gno來為當前線程生成一個gtid,分配給thd->owned_gtid,并加入到owned_gtids中,backtrace如下: MYSQL_BIN_LOG::process_flush_stage_queue->MYSQL_BIN_LOG::flush_thread_caches->binlog_cache_mngr::flush->binlog_cache_data::flush->gtid_before_write_cache->Group_cache::generate_automatic_gno->Gtid_state::acquire_ownership->Owned_gtids::add_gtid_owner? 也就是說,直到事務完成,準備把binlog刷到binlog cache時,才會去為其分配gtid. 當gtid_next的類型為AUTOMATIC時,調用generate_automatic_gno生成事務id(gno),分配流程大概如下: 1.gtid_state->lock_sidno(automatic_gtid.sidno) , 為當前sidno加鎖,分配過程互斥 2.gtid_state->get_automatic_gno(automatic_gtid.sidno); 獲取事務ID |–>初始化候選(candidate)gno為1 |–>從logged_gtids[$sidno]中掃描,獲取每個gno區間(iv): |–>當candidate < iv->start(或者MAX_GNO,如果iv為NULL)時,判斷candidate是否有被占用,如果沒有的話,則使用該candidate,從函數返回,否則candidate++,繼續本步驟 |–>將candidate設置為iv->end,iv指向下一個區間,繼續第二步 從該過程可以看出,這里兼顧了區間存在碎片的場景,有可能分配的gno并不是全局最大的gno. 不過在主庫不手動設置gtid_next的情況下,我們可以認為主庫上的gno總是遞增的。 3.gtid_state->acquire_ownership(thd, automatic_gtid); |–>加入到owned_gtids集合中(owned_gtids.add_gtid_owner),并賦值給thd->owned_gtid= gtid 4.gtid_state->unlock_sidno(automatic_gtid.sidno); ?解鎖 第二步, 調用Gtid_state::update_on_flush將當前事務的grid加入到logged_gtids中,backtrace如下: MYSQL_BIN_LOG::process_flush_stage_queue->MYSQL_BIN_LOG::flush_thread_caches->binlog_cache_mngr::flush->binlog_cache_data::flush->MYSQL_BIN_LOG::write_cache->Gtid_state::update_on_flush? 在bin log group commit的commit階段 調用Gtid_state::update_owned_gtids_impl 從owned_gtids中將當前事務的gtid移除,backtrace 如下: MYSQL_BIN_LOG::ordered_commit->MYSQL_BIN_LOG::finish_commit->Gtid_state::update_owned_gtids_impl? 上述步驟涉及到的是對logged_gtids和owned_gtids的修改。而lost_gtids除了啟動時維護外,就是在執行Purge操作時維護。 例如,當我們執行purge binary logs to ‘mysql-bin.000205′ 時, mysql-bin.index先被更新掉,然后再根據index文件找到第一個binlog文件的PREVIOUS_GTIDS_LOG_EVENT事件,更新lost_gtids集合,backtrace如下: purge_master_logs->MYSQL_BIN_LOG::purge_logs->MYSQL_BIN_LOG::init_gtid_sets->read_gtids_from_binlog->Previous_gtids_log_event::add_to_set->Gtid_set::add_gtid_encoding->Gtid_set::add_gno_interval? 關于binlog group commit,參見之前寫的博客:?http://mysqllover.com/?p=581??c.如何持久化GTID
當重啟MySQL后,我們看到GTID_EXECUTED和GTID_PURGED和重啟前是一致的。 持久化GTID,是通過全局對象gtid_state來管理的。gtid_state在系統啟動時調用函數gtid_server_init分配內存;如果打開了binlog,則會做進一步的初始化工作: quoted code:5419?????? if (mysql_bin_log.init_gtid_sets(
5420???????????? const_cast<Gtid_set *>(gtid_state->get_logged_gtids()), 5421???????????? const_cast<Gtid_set *>(gtid_state->get_lost_gtids()), 5422???????????? opt_master_verify_checksum, 5423???????????? true/*true=need lock*/)) 5424???????? unireg_abort(1); gtid_state 包含3個gtid集合:logged_gtids,?lost_gtids,?owned_gtids,前兩個都是gtid_set類型, owned_gtids類型為Owned_gtids MYSQL_BIN_LOG::init_gtid_sets 主要用于初始化logged_gtids和lost_gtids,該函數的邏輯簡單描述下: 1.掃描mysql-index文件,搜集binlog文件名,并加入到filename_list中 2.從最后一個文件開始往前讀,依次調用函數read_gtids_from_binlog: |–>打開binlog文件,如果讀取到PREVIOUS_GTIDS_LOG_EVENT事件 (1)無論如何,將其加入到logged_gtids(prev_gtids_ev->add_to_set(all_gtids)) (2)如果該文件是第一個binlog文件,將其加入到lost_gtids(prev_gtids_ev->add_to_set(prev_gtids))中. |–>獲取GTID_LOG_EVENT事件 (1) 讀取該事件對應的sidno,sidno= gtid_ev->get_sidno(false); 這是一個32位的整型,用sidno來代表一個server_uuid,從1開始計算,這主要處于節省內存的考慮。維護在全局對象global_sid_map中。 當sidno還沒加入到map時,調用global_sid_map->add_sid(sid),sidno從1開始遞增。 (2) all_gtids->ensure_sidno(sidno) all_gtids是gtid_set類型,可以理解為一個集合,ensure_sidno就是要確保這個集合至少可以容納sidno個元素 (3) all_gtids->_add_gtid(sidno, gtid_ev->get_gno() ? 將該事件中記錄的gtid加到all_gtids[sidno]中(最終調用Gtid_set::add_gno_interval,這里實際上是把(gno, gno+1)這樣一個區間加入到其中,這里 面涉及到區間合并,交集等操作 ? ?) 當第一個文件中既沒有PREVIOUS_GTIDS_LOG_EVENT, 也沒有GTID_LOG_EVENT時,就繼續讀上一個文件 如果只存在PREVIOUS_GTIDS_LOG_EVENT事件,函數read_gtids_from_binlog返回GOT_PREVIOUS_GTIDS 如果還存在GTID_LOG_EVENT事件,返回GOT_GTIDS 這里很顯然存在一個問題,即如果在重啟前,我們并沒有使用gtid_mode,并且產生了大量的binlog,在這次重啟后,我們就可能需要掃描大量的binlog文件。這是一個非常明顯的Bug, 后面再集中討論。 3.如果第二部掃描,沒有到達第一個文件,那么就從第一個文件開始掃描,和第2步流程類似,讀取到第一個PREVIOUS_GTIDS_LOG_EVENT事件,并加入到lost_gtids中。 簡單的講,如果我們一直打開的gtid_mode,那么只需要讀取第一個binlog文件和最后一個binlog文件,就可以確定logged_gtids和lost_gtids這兩個GTID SET了。二、備庫上的GTID
a.如何保持主備GTID一致
由于在binlog中記錄了每個事務的GTID,因此備庫的復制線程可以通過設置線程級別GTID_NEXT來保證主庫和備庫的GTID一致。 默認情況下,主庫上的thd->variables.gtid_next.type為AUTOMATIC_GROUP,而備庫為GTID_GROUP 備庫SQL線程gtid_next輸出: (gdb) p thd->variables.gtid_next? $2 = {? type = GTID_GROUP,? gtid = {? sidno = 2,? gno = 1127,? static MAX_TEXT_LENGTH = 56? },? static MAX_TEXT_LENGTH = 56? } 這些變量在執行Gtid_log_event時被賦值:Gtid_log_event::do_apply_event,大體流程為: 1.rpl_sidno sidno= get_sidno(true); ?獲取sidno 2.thd->variables.gtid_next.set(sidno, spec.gtid.gno); ?設置gtid_next 3.gtid_acquire_ownership_single(thd);?? ? ?|–>檢查該gtid是否在logged_gtids集合中,如果在的話,則返回(gtid_pre_statement_checks會忽略該事務)
|–>如果該gtid已經被其他線程擁有,則等待(gtid_state->wait_for_gtid(thd, gtid_next)),否則將當前線程設置為owner(gtid_state->acquire_ownership(thd, gtid_next)) 在上面提到,有可能當前事務的GTID已經在logged_gtids中,因此在執行Rows_log_event::do_apply_event或者mysql_execute_command函數中,都會去調用函數gtid_pre_statement_checks 該函數也會在每個SQL執行前,檢查gtid是否合法,主要流程包括: 1.當打開選項enforce_gtid_consistency時,檢查DDL是否被允許執行(thd->is_ddl_gtid_compatible()),若不允許,返回GTID_STATEMENT_CANCEL 2.檢查當前SQL是否會產生隱式提交并且gtid_next被設置(gtid_next->type != AUTOMATIC_GROUP),如果是的話,則會拋出錯誤ER_CANT_DO_IMPLICIT_COMMIT_IN_TRX_WHEN_GTID_NEXT_IS_SET 并返回GTID_STATEMENT_CANCEL,注意這里會導致bug#69045 3.對于BEGIN/COMMIT/ROLLBACK/(SET OPTION 或者 SELECT )且沒有使用存儲過程/ 這幾種類型的SQL,總是允許執行,返回GTID_STATEMENT_EXECUTE 4.gtid_next->type為UNDEFINED_GROUP,拋出錯誤ER_GTID_NEXT_TYPE_UNDEFINED_GROUP,返回GTID_STATEMENT_CANCEL 5.gtid_next->type == GTID_GROUP且thd->owned_gtid.sidno == 0時, 返回GTID_STATEMENT_SKIP 其中第五步中處理了函數gtid_acquire_ownership_single的特殊情況b.備庫如何發起DUMP請求
引入GTID,最大的好處當然是我們可以隨心所欲的切換主備拓撲結構了。在一個正常運行的復制結構中,我們可以在備庫簡單的執行如下SQL:
CHANGE MASTER TO MASTER_USER=’$USERNAME’, MASTER_HOST=’ ‘, MASTER_PORT=’ ‘, MASTER_AUTO_POSITION=1; 打開GTID后,我們就無需指定binlog文件或者位置,MySQL會自動為我們做這些事情。這里的關鍵就是MASTER_AUTO_POSITION。IO線程連接主庫,可以大概分為以下幾步: 1.IO線程在和主庫建立TCP鏈接后,會去獲取主庫的uuid(get_master_uuid),然后在主庫上設置一個用戶變量@slave_uuid(io_thread_init_commands) 2.之后,在主庫上注冊SLAVE(register_slave_on_master) 在主庫上調用register_slave來注冊備庫,將備庫的host,user,password,port,server_id等信息記錄到slave_list哈希中。 3.調用request_dump,開始向主庫請求數據,這里分兩種情況: MASTER_AUTO_POSITION=0時,向主庫發送命令的類型為COM_BINLOG_DUMP,這是傳統的請求BINLOG的模式 MASTER_AUTO_POSITION=1時,命令類型為COM_BINLOG_DUMP_GTID,這是新的方式。 這里我們只討論第二種。第二種情況下,會先去讀取備庫已經執行的gtid集合 quoted code in rpl_slave.cc :2974?? if (command == COM_BINLOG_DUMP_GTID)
2975?? { 2976???? // get set of GTIDs 2977???? Sid_map sid_map(NULL/*no lock needed*/); 2978???? Gtid_set gtid_executed(&sid_map); 2979???? global_sid_lock->wrlock(); 2980???? gtid_state->dbug_print(); 2981???? if (gtid_executed.add_gtid_set(mi->rli->get_gtid_set()) != RETURN_STATUS_OK || 2982???????? gtid_executed.add_gtid_set(gtid_state->get_logged_gtids()) !=2983???????? RETURN_STATUS_OK)
?
構建完成發送包后,發送給主庫。在主庫上接受到命令后,調用入口函數com_binlog_dump_gtid,流程如下:
?
1.slave_gtid_executed.add_gtid_encoding(packet_position, data_size) ;讀取備庫傳來的GTID SET? 2.讀取備庫的uuid(get_slave_uuid),被根據uuid來kill僵尸線程(kill_zombie_dump_threads) 這也是之前SLAVE IO線程執行SET @SLAVE_UUID的用處。 3.進入mysql_binlog_send函數: |–>調用MYSQL_BIN_LOG::find_first_log_not_in_gtid_set,從最后一個Binlog開始掃描,獲取文件頭部的PREVIOUS_GTIDS_LOG_EVENT,如果它是slave_gtid_executed的子集,保存當前binlog文件名,否則繼續向前掃描。 這一步的目的就是為了找出備庫執行到的最后一個Binlog文件。 |–>從這個文件頭部開始掃描,遇到GTID_EVENT時,會去判斷該GTID是否包含在slave_gtid_executed中: Gtid_log_event gtid_ev(packet->ptr() + ev_offset, packet->length() – checksum_size, p_fdle); skip_group= slave_gtid_executed->contains_gtid(gtid_ev.get_sidno(sid_map), gtid_ev.get_gno()); 主庫通過GTID決定是否可以忽略事務,從而決定執行開始的位置? 注意,在使用MASTER_LOG_POSITION后,就不要指定binlog的位置,否則會報錯。三、運維操作
?
a.如何忽略復制錯誤
當備庫復制出錯時,傳統的跳過錯誤的方法是設置sql_slave_skip_counter,然后再START SLAVE。 但如果打開了GTID,就會設置失敗:mysql> set global sql_slave_skip_counter = 1;
ERROR 1858 (HY000): sql_slave_skip_counter can not be set when the server is running with @@GLOBAL.GTID_MODE = ON. Instead, for each transaction that you want to skip, generate an empty transaction with the same GTID as the transaction
?
提示的錯誤信息告訴我們,可以通過生成一個空事務來跳過錯誤的事務。 我們手動產生一個備庫復制錯誤: Last_SQL_Error: Error ‘Unknown table ‘test.t1” on query. Default database: ‘test’. Query: ‘DROP TABLE `t1` /* generated by server */’ 查看binlog中,該DDL對應的GTID為7a07cd08-ac1b-11e2-9fcf-0010184e9e08:1131 在備庫上執行:mysql> STOP SLAVE;
Query OK, 0 rows affected (0.00 sec) mysql> SET SESSION GTID_NEXT = ’7a07cd08-ac1b-11e2-9fcf-0010184e9e08:1131′; Query OK, 0 rows affected (0.00 sec) mysql> BEGIN; COMMIT; Query OK, 0 rows affected (0.00 sec)Query OK, 0 rows affected (0.00 sec)
?
mysql> SET SESSION GTID_NEXT = AUTOMATIC;
Query OK, 0 rows affected (0.00 sec)
?
mysql> START SLAVE; 再查看show slave status,就會發現錯誤事務已經被跳過了。這種方法的原理很簡單,空事務產生的GTID加入到GTID_EXECUTED中,這相當于告訴備庫,這個GTID對應的事務已經執行了。b.重指主庫
使用change master to …. , MASTER_AUTO_POSITION=1; 注意在整個復制拓撲中,都需要打開gtid_modec.新的until條件
5.6提供了新的util condition,可以根據GTID來決定備庫復制執行到的位置 SQL_BEFORE_GTIDS:在指定的GTID之前停止復制 SQL_AFTER_GTIDS :在指定的GTID之后停止復制 判斷函數為Relay_log_info::is_until_satisfied 詳細文檔見:?http://dev.mysql.com/doc/refman/5.6/en/start-slave.html??d.適當減小binlog文件的大小
如果開啟GTID,理論上最好調小每個binlog文件的最大值,以縮小掃描文件的時間。四、存在的bug
bug#69097?, 即使關閉了gtid_mode,也會在啟動時去掃描binlog文件。 當在重啟前沒有使用gtid_mode,重啟后可能會去掃描所有的binlog文件,如果Binlog文件很多的話,這顯然是不可接受的。 bug#69096?,無法通過GTID_NEXT_LIST來跳過復制錯誤,因為默認編譯下,GTID_NEXT_LIST未被編譯進去。 TODO:GTID_NEXT_LIST的邏輯上面均未提到,有空再看。 bug#69095?,將備庫的復制模式設置為STATEMENT/MIXED。 主庫設置為ROW模式,執行DML 會導致備庫復制中斷 Last_SQL_Error: Error executing row event: ‘Cannot execute statement: impossible to write to binary log since statement is in row format and BINLOG_FORMAT = STATEMENT.’ 判斷報錯的backtrace: handle_slave_worker->slave_worker_exec_job->Rows_log_event::do_apply_event->open_and_lock_tables->open_and_lock_tables->lock_tables->THD::decide_logging_format解決辦法:將備庫的復制模式設置為’ROW’ ,保持主備一致
?
該bug和GTID無關 bug#69045?, 當主庫執行類似 FLUSH PRIVILEGES這樣的動作時,如果主庫和備庫都開啟了gtid_mode,會導致復制中斷 Last_SQL_Error: Error ‘Cannot execute statements with implicit commit inside a transaction when @@SESSION.GTID_NEXT != AUTOMATIC or @@SESSION.GTID_NEXT_LIST != NULL.’ on query. Default database: ”. Query: ‘flush privileges’ 也是一個很低級的bug,在MySQL5.6.11版本中,如果有可能導致隱式提交的事務, 則gtid_next必須等于AUTOMATIC,對備庫復制線程而言,很容易就中斷了,判斷邏輯在函數gtid_pre_statement_checks中參考文檔
1.阿里長源的三篇博客(?一?,??二?, 三) 2.MySQL5.6.11源代碼總結
以上是生活随笔為你收集整理的[MySQL 5.6] GTID实现、运维变化及存在的bug的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从零开始-android 4.2之旅
- 下一篇: Foundations of Machi