天天看點

[MySQL 5.6] GTID内部實作、運維變化及存在的bug前言:什麼是GTID一、主庫上的Gtid二、備庫上的GTID三、運維操作四、存在的bug

由于之前沒太多深入關注gtid,這裡給自己補補課,本文是我看文檔和代碼的整理記錄。

本文的主要目的是記下跟gtid相關的backtrace,用于以後的問題排查。另外也會讨論目前在mysql5.6.11版本中存在的bug。

本文讨論的内容包括

一.主庫上的gtid産生及記錄

二.備庫如何使用gtid複制

三.主備運維的變化

四.mysql5.6.11存在的bug

什麼是gtid呢, 簡而言之,就是全局事務id(global transaction identifier ),最初由google實作,官方mysql在5.6才加入該功能,本文的起因在于5.6引入一大堆的gtid相關變量,深感困惑。

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

gtid字元串,用“:”分開,前面表示這個伺服器的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維護的四個變量:

gtid_purged:已經被删除的binlog的事務,它是gtid_executed的子集,從mysql5.6.9開始,該變量無法被設定。

gtid_owned:  表示正在執行的事務的gtid以及對應的線程id。

例如如下:

mysql> show global variables like ‘%gtid_owned%’\g

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_owned

在主庫執行一個事務的過程中,關于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指向下一個區間,繼續第2步

        從該過程可以看出,這裡兼顧了區間存在碎片的場景,有可能配置設定的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将目前事務的gtid加入到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 

當重新開機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了。

由于在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

},

}

這些變量在執行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的特殊情況

引入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的位置,否則會報錯。

當備庫複制出錯時,傳統的跳過錯誤的方法是設定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′;

mysql> begin; commit;

mysql> set session gtid_next = automatic;

mysql> start slave;

再檢視show slave status,就會發現錯誤事務已經被跳過了。這種方法的原理很簡單,空事務産生的gtid加入到gtid_executed中,這相當于告訴備庫,這個gtid對應的事務已經執行了。

使用change master to …. , master_auto_position=1;

注意在整個複制拓撲中,都需要打開gtid_mode

5.6提供了新的util condition,可以根據gtid來決定備庫複制執行到的位置

sql_before_gtids:在指定的gtid之前停止複制

sql_after_gtids :在指定的gtid之後停止複制

判斷函數為relay_log_info::is_until_satisfied

如果開啟gtid,理論上最好調小每個binlog檔案的最大值,以縮小掃描檔案的時間。

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中