MySQL5.7GTID淺析

zysql發表於2017-01-07

GTID 簡介

GTID (global transaction identifier)在MySQL5.6時引入,GTID是事務的全域性唯一標識。GTID結構如下

GTID = source_id:transaction_id

source_id:執行事務的原始例項的sever_uuid, 此事務GTID在備庫apply時也不變。
transaction_id:事務的執行編號,binlog_order_commits=1時,此編號根據事務的提交順序嚴格遞增。GTID是在binlog flush時生成的,因此同一個serverid的GTID在binglog中是嚴格有序的。binlog_order_commits=0時,GTID在binlog中也是序的,但並不一定與提交的順序一致。

binlog_order_commits=0會影響XtraBackup工具的備份,但不會影響innobackup工具的備份
XtraBackup會從innodb事務page獲取最後提交事務的binlog位點資訊,binlog_order_commits=0時事務提交順序和binlog順序不一定一致,這樣此位點前可能存在部分prepare狀態的事務,這些事務在備份恢復後會丟失。
而innobackup的位點資訊是在加備份鎖前提下從show master status/show slave status中獲取的,位點前的事務都是已提交的。

支援GTID後,備庫啟動時不再需要通過位點資訊從主庫來拉取binlog,而是根據備庫本身已執行和拉取的gtid去主庫查詢第一個未執行的GTID,從此GTID位置開始拉取binlog。

新增了COM_BINLOG_DUMP_GTID命令

  • 備庫
    備庫封裝COM_BINLOG_DUMP_GTID命令,包含備庫的gtid_executed(已執行的GTID和當前已拉取的GTID的並集)

    request_dump():
      gtid_executed.add_gtid_set(mi->rli->get_gtid_set())
      gtid_executed.add_gtid_set(gtid_state->get_executed_gtids())
  • 主庫
    主庫接收COM_BINLOG_DUMP_GTID命令,從最新的binlog開始反向遍歷查詢的binlog, 依次讀取PREVIOUS_GTIDS_LOG_EVENT, 直到PREVIOUS_GTIDS_LOG_EVENT記錄的gtid_set是備庫發過來的gtid子集為止。
com_binlog_dump_gtid():
  Binlog_sender::init
       Binlog_sender::check_start_file(
             mysql_bin_log.find_first_log_not_in_gtid_set
  Binlog_sender::run

mysql5.7 gtid相對5.6主要有以下變化

  • gtid_mode可以動態設定,支援gtid模式和非gtid模式之間的複製
  • 增加了gtid_executed表

gtid_mode

MySQL5.7(>= 5.7.6) gtid_mode支援動態修改,gtid_mode取值可選擇如下

OFF: Both new and replicated transactions must be anonymous.

OFF_PERMISSIVE: New transactions are anonymous. Replicated transactions can be either anonymous or GTID transactions.

ON_PERMISSIVE: New transactions are GTID transactions. Replicated transactions can be either anonymous or GTID transactions.

ON: Both new and replicated transactions must be GTID transactions.

OFF_PERMISSIVE時支援GTID模式的例項向非GTID模式的例項的複製。

ON_PERMISSIVE:時支援非GTID模式的例項向GTID模式的例項的複製。此模式下,可支援低版本例項(<=5.5)向5.7高版本GTID例項的複製,從而為低版本例項(不支援GTID)平滑升級為5.7GTID例項提供了便利。

需要吐槽的是MySQL5.6目前還不支援低版本例項(<=5.5)向5.6高版本GTID例項的複製, 需要修改程式碼開啟此限制才可以。

另外,gtid_mode動態修改不支援跳躍修改。例如,如果當前值為OFF_PERMISSIV,只支援修改為OFF或ON_PERMISSIVE,不支援修改為ON。

MySQL 5.7 gtid_mode=on時需要設定enforce_gtid_consistency=1. MySQL5.6還需要另外設定 –log-bin, –log-slave-updates,而5.7是不需要的,這得益於5.7的gtid_executed表。

gtid_executed表

gtid_executed表儲存的是已執行的GTID集合資訊,此資訊不一定是實時的。gtid_executed表結構如下

CREATE TABLE gtid_executed (
    source_uuid CHAR(36) NOT NULL,
    interval_start BIGINT(20) NOT NULL,
    interval_end BIGINT(20) NOT NULL,
    PRIMARY KEY (source_uuid, interval_start)
)      

gtid_executed表的益處

有了gtid_executed表後,GTID模式下允許關閉binlog,允許設定log-slave-updates=0。這樣帶來的以下好處

  • 開啟GTID模式下,可以關閉備庫的binlog或設定log-slave-updates=0,GTID資訊仍然會儲存在gtid_executed表中。這樣備庫依然可以正常複製,同時省去了記錄binlog的開銷。

AliSQL 5.6在這塊也做了優化,備庫SQL執行緒的產生的binlog只記錄GTID EVENT資訊,不記錄實際操作的event, 因此減少了binglog的量, 並且能夠保證正常的複製。

  • 開啟GTID模式下,由於gtid_executed表是持久化的,即使人為刪除了備庫的binlog,複製依然可以通過gtid_executed表恢復。

gtid_executed表的更新

gtid_executed在binlog開啟和關閉的情況下都會更新

  • binlog開啟
    每次rotate或shutdown時儲存PREVIOUS_GTIDS_LOG_EVENT,只記錄最後一個binlog的gtid資訊。參考save_gtids_of_last_binlog_into_table
  • binlog關閉或log_slave_updates=0

每次事務提交時都儲存GTID

MYSQL_BIN_LOG::gtid_end_transaction():
  if (!opt_bin_log || (thd->slave_thread && !opt_log_slave_updates))
      gtid_state->save(thd)
ha_commit_trans():
   if (!opt_bin_log || (thd->slave_thread && !opt_log_slave_updates))
        gtid_state->save(thd)
  • reset master

reset master 會重置表,以delete方式刪除所有資料(非truncate)

 Gtid_table_persistor::reset
        delete_all(table)

gtid_executed表的compress

更新gtid_executed表資訊時,每次都是insert一條資料,而不是update方式,update容易產生行衝突,insert可以提高併發。而insert的副作用是導致gtid_executed錶行記錄數不斷增加。因此,專門提供了一個compress執行緒用來壓縮gtid_executed表。
以原始碼中的註釋來說明compress過程,具體可參考Gtid_table_persistor::compress_in_single_transaction

    Read each row by the PK(sid, gno_start) in increasing order,
    compress the first consecutive range of gtids.
    For example,
      1 1
      2 2
      3 3
      6 6
      7 7
      8 8
    After the compression, the gtids in the table is compressed as following:
      1 3
      6 6
      7 7
      8 8

全表掃描,依次找到連續一行刪一行,刪除(2,2),(3,3),最後更新第一行的結束值(1,1)更新為(1,3)

這裡有個有趣的bug, 設定super_read_only導致compress事務在提交時檢查read_only失敗,然後回滾事務。隨著gtid_executed表資料的增加,compress執行緒的事務越來越大,更新失敗然後回滾的代價越來越大。

compress執行緒是被動觸發的

mysql_cond_wait(&COND_compress_gtid_table, &LOCK_compress_gtid_table);

以下兩種情況會喚醒compress執行緒

  mysql_cond_signal(&COND_compress_gtid_table); 
  • 插入單個GTID時通過引數gtid_executed_compression_period來控制喚醒compress,此種情況發生在binlog關閉或log_slave_updates=0事務提交時。
  • 插入GTID集合每次都會喚醒compress,這種情況發生在binlog開啟時, binlog rotate或例項關閉時。

啟動時gtid_executed表的處理

例項啟動時,會讀取gtid_execute表資訊來構建以下資訊
executed_gtids:已執行的gtid資訊,是gtid_executed表和binlog中gtid的並集。即gtid_executed。
lost_gtids:已經purged的gtid。即gtid_purged。

  • 構建executed_gtids

1 讀gtid_executed表的GTID資訊賦值給exeucted_gtids, 參考read_gtid_executed_from_table
2 將binlog中比gtid_executed表中多的GTID補進來

gitds_in_binlog_not_in_table.add_gtid_set(&gtids_in_binlog);
gtids_in_binlog_not_in_table.remove_gtid_set(executed_gtids);
gtid_state->save(&gtids_in_binlog_not_in_table) //將binlog比表中多的補進來
executed_gtids->add_gtid_set(&gtids_in_binlog_not_in_table);

gtids_in_binlog是逆向查詢binlog,直到找到第一個包含PREVIOUS_GTIDS_LOG_EVENT的binlog為止, 讀取此binlog檔案的PREVIOUS_GTIDS_LOG_EVENT和GTID_LOG_EVENT構成gtids_in_binlog

  • 構建 lost_gtids

    lost_gtids = executed_gtids -
                     (gtids_in_binlog - purged_gtids_from_binlog)
                     = gtids_only_in_table + purged_gtids_from_binlog;

purged_gtids_from_binlog是正向查詢binlog,可以從第一個包含GTID_LOG_EVENT的binlog的PREVIOUS_GTIDS_LOG_EVENT中獲取。

有一種情況比較特殊,5.6 升級5.7時,有一種情況會導致binlog中有PREVIOUS_GTIDS_LOG_EVENT但沒有GTID_LOG_EVENT。如下面的註釋所示,真正的purged_gtids_from_binlog應該從master-bin.N+2的PREVIOUS_GTIDS_LOG_EVENT中獲取

 /*
      This branch is only reacheable by a binary log. The relay log
      don`t need to get lost_gtids information.

      A 5.6 server sets GTID_PURGED by rotating the binary log.

      A 5.6 server that had recently enabled GTIDs and set GTID_PURGED
      would have a sequence of binary logs like:

      master-bin.N  : No PREVIOUS_GTIDS (GTID wasn`t enabled)
      master-bin.N+1: Has an empty PREVIOUS_GTIDS and a ROTATE
                      (GTID was enabled on startup)
      master-bin.N+2: Has a PREVIOUS_GTIDS with the content set by a
                      SET @@GLOBAL.GTID_PURGED + has GTIDs of some
                      transactions.

      If this 5.6 server be upgraded to 5.7 keeping its binary log files,
      this routine will have to find the first binary log that contains a
      PREVIOUS_GTIDS + a GTID event to ensure that the content of the
      GTID_PURGED will be correctly set (assuming binlog_gtid_simple_recovery
      is not enabled).
    */

原因在於MySQL5.6在set gtid_purged時是通過切換檔案(rotate_and_purge )將gtid_purged儲存在PREVIOUS_GTIDS_LOG_EVENT中.

而MySQL5.7在set gtid_purged時並不切換檔案,gtid_purged直接儲存到gtid_executed表中。

引數 binlog_gtid_simple_recovery

官網對binlog_gtid_simple_recovery=false進行了詳細的解釋。主要說明了binlog_gtid_simple_recovery=false時正向查詢binlog獲取gtid_purged(對應上節的lost_gtids)和逆向查詢binlog獲取gtid_executed(對應上節的executed_gtids)可能需要遍歷較多的binlog檔案,上節也介紹了遍歷查詢的方法。
但官網只是簡單的介紹了binlog_gtid_simple_recovery=true時只需要查詢最新或最老的binlog檔案即可,至於為什麼可以這樣做沒有明確說明。
以下是我的個人理解,

對於gtid_executed只需要讀最新的binlog檔案,即使最新的binlog檔案沒有PREVIOUS_GTIDS_LOG_EVENT也沒有關係,因為最老的PREVIOUS_GTIDS_LOG_EVENT在binlog roate時已經寫入gtid_executed表,根據上節的gtid_executed獲取邏輯會讀取gtid_executed表,最後獲取的gtid_executed是完整的。

對於gtid_purged只需要讀取老的binlog檔案, 如果最老的binlog檔案沒有PREVIOUS_GTIDS_LOG_EVENT,同時最新的binlog檔案也沒有PREVIOUS_GTIDS_LOG_EVENT的情況下,根據上節的lost_gtids恢復邏輯

lost_gtids = executed_gtids -
                   (gtids_in_binlog - purged_gtids_from_binlog)

gtids_in_binlog和purged_gtids_from_binlog都為空,最後lost_gtids=executed_gtids,這顯然是不正確的。這裡我認為lost_gtids並不是一個重要的值,只在set gtid_purge時會修改,即使不正確也不影響正常複製。

GTID三個限制

enforce-gtid-consistency=ON時,以下三類語句時不支援的

  • CREATE TABLE … SELECT statements
  • CREATE TEMPORARY TABLE or DROP TEMPORARY TABLE statements inside transactions
  • Transactions or statements that update both transactional and nontransactional tables. There is an exception that nontransactional DML is allowed in the same transaction or in the same statement as transactional DML, if all nontransactional tables are temporary.

而實際上這個限制沒有必要這麼嚴格,

  • CREATE TABLE … SELECT statements

對於binlog_format=row, gtid_next=`automatic`時可以放開限制。
生成的binlog包含兩個GTID, 一個是建表語句,一個是包含多個insert的事務。

  • 事務中包含事務表和非事務表

對於gtid_next=`automatic`時可以放開限制。
生成的binlog包含兩個GTID, 一個是所有非事務表的,一個是所有事務表的。
對update多表(包含事務表和非事務表)此時需額外要求binlog_format=row。

總結

MySQL 5.7 在GTID上有了較大改進,但GTID的三個使用限制仍然存在,期待後期有所改進。