天天看點

MariaDB · 版本特性 · MariaDB 的 GTID 介紹

簡單來說,mariadb(mysql)的複制機制是這樣的:

在master端所有資料庫的變更(包括dml和ddl)都會以 binlog event 的方式寫入binlog中。slave會連上master然後讀取 binlog event,再重放這些操作到自身的資料中。一個執行個體可以既是master同時又是slave,做成雙向複制。也可以一級一級串聯,做成級聯複制,binlog event 中包含的 server_id 可以識别産生 event 的執行個體,避免重複執行。

slave會儲存最後一次收到和應用的binlog的位置,是以slave重連master時可以從中斷的位置繼續開始複制。也可以在暫停slave後,将其整體拷貝到新的位置,然後作為一個新的slave繼續複制。

全局事務id(global transaction id,gtid)為每個 event group (就是一系列 event 組成的一個原子單元,要麼一起送出要麼都無法送出)引入了一個辨別,是以 gtid 是辨別“事務”的最佳方式(盡管 event 裡面還包含一些非事務的dml語句和ddl,它們可以作為一個單獨的 event group )。每當一個 event group 從master複制到slave時,它的 gtid 也通過 gtid event 被傳到slave。因為每個 gtid 在整個複制拓撲結構中都是一個唯一标志,是以這使得在不同的執行個體之間識别相同的 binlog events 非常簡單,然而在有 gtid 之前,想做到這點是很困難的。mariadb 從 10.0.2 開始提供 gtid 支援,但是 mariadb 的 gtid 與 mysql 的 gtid 在實作原理上并不相同,因為 mariadb 支援像多源複制啊、多主複制等官方暫時還沒考慮的複制模型。下面我們來看看 mariadb 的 gtid 的實作。

使用 gtid 有兩個主要的優勢:

在級聯複制、一主多從等複雜的複制場景下,可以更簡單地将一個slave的複制修改到另一個master上,而不用人工去尋找複制的起始位點。從5.0一路走來的同學應該很能了解這種痛苦。

這是因為slave會儲存最後一個執行的 event group 的 gtid,是以可以通過這個 gtid 很容易地在新master上找到相應的複制起點。而在使用 binlog file 和 binlog pos 的時代,這是很難辦到的。

slave的狀态是 crash-safe 的。

slave的執行狀态(最後一個執行的 gtid)被記錄在 <code>mysql.gtid_slave_pos</code> 系統表中。如果這張表使用的是事務引擎(例如innodb,預設就是),那麼修改使用者表的資料和修改slave狀态的系統表這兩個操作在就可以放在一個事務中完成,這就保證了slave狀态是 crash-safe 的,如果slave崩潰了,那麼 crash recovery 就可以在重新開機的時候把使用者資料表和slave狀态系統表恢複到一個一緻的位點。而在非 gtid 複制的舊版本中,這也是做不到的,slave狀态隻是簡單的存放在 relay-log.info 檔案中 (mysql是可以把 binlog file 和 binlog pos 也存在 <code>slave_relay_log_info</code> 和 <code>slave_master_info</code> 系統表中),而且需要靠不斷的 <code>fsync()</code> 調用才能同步到磁盤上,一旦當機很可能導緻slave狀态跟實際不一緻(但是也隻有事務引擎的dml能保證一緻,非事務引擎和ddl本身就不是crash-safe的)。

基于這兩個優勢,通常我們都建議使用gtid複制。并且傳統的基于binlog檔案位置的複制方式,和 gtid 的複制方式,在 mariadb 中是可以互相之間平滑的切換的。

每個gtid,都包含三個數字部分,分别用’-‘号隔開,例如:

0-1-10

第一個數字’0’是domain id,這是一個32位的無符号整型。

第二個數字’1’是server id,這跟傳統的主備複制中 server id 的含義是一樣的,也是一個32位無符号整型。是以在一個複制拓撲中每個執行個體的server id必須是唯一的。

第三個數字是序列号(sequence number)。這是一個64位的無符号整型。每個新産生的 event group 記錄到binlog時都會新生成一個單調遞增的序列号。

這個規則使得 (server_id, sequence_number) 總是唯一的,是以gtid也是全局唯一的。

使用64位數字可以提供充足序列号支援龐大的 event group 數量,在可預見的時間内,應該來說是沒有溢出風險的。但是一個可見的風險是,人為地設定一個很高的 <code>gtid_seq_no</code> 值,導緻 gtid 的起始序列号就很高,是可能導緻序列号逼近64位數值的上限的。

當 events 從master複制到slave時,events 總是按照從master讀取的順序記錄在slave的binlog中。是以,如果同一時刻隻有一個master接收變更操作(不包括複制帶來的變更操作),那麼 binlog 中 events 的順序在所有參與複制關系的執行個體上應該都是一樣的。

這種一緻的 binlog 順序,可以被slave用來追蹤目前複制的位置。隻要slave記住最後一個從master複制過來的 event group 的 gtid,重連到master時(不管是原來的master還是新的master),就可以發送這個 gtid 給master,然後master就可以開始繼續發送這個 gtid 之後的 event.

然而,如果使用者同時在多個執行個體上做了更新,那麼一般來說各個執行個體上的binlog順序是不可能一樣的。當使用多源複制、或者執行個體之間構成環狀拓撲結構時,這種情況是可能出現的;或者人為手動對一個slave做了更新操作,也可能發生這種情況。如果 binlog 順序在新老master之間不一樣,那麼僅僅使用一個獨立的gtid(不包含 domain id)并不足以記錄目前的狀态。

而 domain id 的任務, 就是為了解決這種情況。

通常,binlog 并非是一個單一有序的資料流(strem),相反,它是由許多不同的資料流組成的,每個資料流都由一個自己的 domain id 來識别。對每個資料流,gtid 總是以相同的 binlog 順序存儲在每個執行個體中。但是,不同的資料流可以以不同的方式在不同的執行個體中交錯。

slave通過記錄每個複制資料流(replication stream)中最後一次應用的 gtid 位置來跟蹤複制的位點。當連上一個新的master時,slave可以為每個 domain id 從不同的 binlog 位點開始複制。

後面有一節我們專門闡述了如何設定 domain id,以及如何在多源複制、多主複制的場景下利用 domain id.

隻有一個master的簡單場景是不用考慮 domain id 的,任意時刻隻有一個master會有應用去更新,是以隻需要一個單獨的 replication stream 即可,domain id 可以直接忽略,在所有執行個體上用預設值0就行了。

從 mariadb 10.0.2 開始,gtid 是預設自動打開的。每個 event group 寫到 binlog 時會先收到一個gtid_event,用mariadb的 mysqlbinlog 工具或者 show binlog events 指令可以看到這個event。

slave自動記錄了最後一次應用的 event group 的 gtid,可以通過 <code>gtid_slave_pos</code> 變量來檢視:

當slave連接配接到master時,可以選擇是否使用 gtid 方式,或者使用原來的檔案位置的方式來判斷起始的複制點位。如果使用 gtid 方式複制,那麼在 change master 的時候使用 <code>master_use_gtid</code> 選項來設定:

<code>change master to master_use_gtid=slave_pos</code> 将把slave配置為使用 gtid 方式。當slave連接配接到master時,master将從最後一個gtid開始給slave複制 binlog,可以通過 <code>@@gtid_slave_pos</code> 這個變量來檢視目前最後一個gtid是什麼。由于gtid在所有參與複制的執行個體之間都是相同的,是以slave可以被指向不同的master,master可以自動決定正确的複制起始位置。

但是,假設我們設定了兩個執行個體a和b,并且讓a是master,b是他的slave。運作一段時間後,我們關閉a,然後讓b成為新的master,然後一段時間後我們再把a加回來作為b的slave。

由于a從來沒有成為slave,它沒記錄任何之前複制的gtid,是以<code>@@gtid_slave_pos</code>是空的。如果要讓a自動被加為slave,可以使用 <code>master_use_gtid=current_pos</code> 這個方法。這樣做在連接配接的時候,slave會把<code>@@gtid_current_pos</code>存的gtid發給master,而不是 <code>@@gtid_slave_pos</code>,這樣就把a做master時産生的最後一個gtid發送給了b,然後從這個位置開始複制。

使用<code>master_use_gtid=current_pos</code>可能是最簡單的方式,因為這樣不需要考慮之前這個執行個體是作為master還是slave。但是,必須注意的是,如果這樣做的話,就不要在slave上做任何非複制帶來的修改操作,否則就亂了。如果出現了這種情況,複制可能無法繼續,因為這些事務在master運作時并沒有在master上出現過,當切換master和slave身份時,就出現了未知的gtid。為了避免這種情況,可以在slave上設定 <code>@@sql_log_bin=0</code>。

如果這不是slave期望的運作方式,slave上就是可能有一些資料變更,那麼就應該使用<code>master_use_gtid=slave_pos</code>方式。這樣slave總是使用最後一次複制的gtid發送給master來擷取之後的event group。這可以避免上面的方式在一些不可控因素修改了slave本地的資料卻沒有在binlog之中有所記錄的問題。

當gtid嚴格模式開啟時(<code>@@global.gtid_strict_mode=1</code>),通常最好是用<code>current_pos</code>。在嚴格模式下,不允許有額外的事務。

如果slave沒有開啟binlog,那麼<code>current_pos</code>和<code>slave_pos</code>是一回事。

即使當slave被配置為舊的複制方式時(<code>change master to master_log_file=..., master_log_pos=...</code>),mariadb依然會跟蹤目前的gtid位置并儲存在 <code>@@global.gtid_slave_pos</code>。這意味着一個用非gtid模式複制的slave可以很容易地修改為gtid方式:

slave會儲存 <code>master_use_gtid=slave_pos|master_pos</code>的資訊以為後來進行連接配接,直到複制方式被修改為指定 <code>master_log_file/pos=...</code> 或者 <code>master_use_gtid=no</code>。目前的複制方式可以通過show slave status的using_gtid列來判斷:

slave内部用<code>mysql.gtid_slave_pos</code>表來存儲gtid位置(是以重新開機後<code>@@global.gtid_slave_pos</code>的值會重新被填充)。更新mariadb的版本到10.0之後,必須使用<code>mysql_upgrade</code>來保證這些表和列被建立了。

為了保證crash-safe,這張表必須使用事務引擎,例如innodb。當mariadb第一次安裝或者更新到10.0.2+時,這張表會用預設的存儲引擎建立 - 預設就是innodb。當然如果把預設引擎改成myisam的話,這張表就會被建立成myisam。如果需要修改引擎,可以直接用alter table的方式:

<code>mysql.gtid_slave_pos</code> 表不應該被slave線程之外的方式修改。尤其是不要嘗試直接修改表的資料來改變slave的gtid位置,如果要修改應該用這種方式:

設定一個新的使用gtid的slave跟設定一個非gtid方式複制的slave差别很大,基本步驟是:

配置一個新的執行個體并且載入初始資料。

從相應的master binlog位置開始slave的複制。

出于測試的目的,最簡單的方式就是建立一個新的空執行個體,然後從master複制所有的binlog(在實際生産環境中這幾乎是不可能的,因為最早的binlog檔案應該早就被清除了)。

正常方式安裝的slave執行個體,預設情況下gtid位點都是空的,是以可以從master的第一個binlog檔案開始複制。但是如果slave之前被用作其他目的,那麼初始位置需要手動設定為空:

下一步就是用change master來指向master,指定<code>master_host</code>什麼的。隻是不再用<code>master_log_file</code>和<code>master_log_pos</code>,而是用<code>master_use_gtid=current_pos</code>(或者slave_pos):

一般來說建立slave的方式都是通過備份集來恢複出一個新的執行個體,然後找到master上複制的起始點建立複制關系。

因而找到正确的複制起始位置是非常重要的,否則slave可能因為資料與master不一緻而導緻複制中斷。

一般來說備份都是用 xtrabackup 或者 mysqldump。這兩種方式都可以在非阻塞的情況下獲得備份時正确的binlog位點(所有表都要是事務引擎),當然,如果備份時不會有寫入,那麼 show master status 也能提供正确的位點。

一旦擷取了備份時正确的binlog位點(檔案名和偏移量),那麼就可以用<code>binlog_gtid_pos()</code>函數來計算gtid:

從mariadb 10.0.13版本開始,mysqldump會自動完成這個工作,并且把gtid的寫在導出檔案中,隻要設定 –master-data 或 –dump-slave 的同時設定 –gtid 即可。

這樣的話新的slave就可以通過設定 <code>@@gtid_slave_pos</code> 的值來設定複制的起始位置,用 change master 把這個值傳給主庫,然後開始複制:

用master備份搭建一個slave時這種方式尤其有用。不過一定要記得確定master和slave的server_id要設定成不一樣的。

如果備份是從現有的slave執行個體建立的,那麼gtid的位置已經存在<code>mysql.gtid_slave_pos</code>表中了,并且跟其他事務引擎表都是在一個一緻狀态。這種情況下,就沒必要去找gtid的位置然後設定變量之類的,因為已經從<code>mysql.gtid_slave_pos</code>載入了正确的值。然而從master做備份就沒這個福利了,因為正确的gtid位點資訊在binlog中,而不是在<code>mysql.gtid_slave_pos</code>。

如果已經有一個slave運作在binlog檔案名和偏移量的複制模式下,那麼可以直接修改為gtid模式。對于更新來說這是很有用的方式。

當一個slave通過binlog位點的方式跟master連接配接,并且master支援gtid,那麼slave會自動把gtid位置的資訊也擷取過來,并且在複制過程中會不斷更新。是以,當一個slave已經連上了一個支援gtid的master,那麼并不需要額外的動作就可以把複制切換為使用gtid:

(後面更新的版本可能會增加一種方式,如果第一次連接配接的時候使用的是原來的binlog檔案位點的方式,那麼隻要主庫是支援gtid的,後面再連接配接的時候就自動切換為gtid方式)

一旦複制運作在gtid模式下(<code>master_use_gtid=current_pos|slave_pos</code>),slave就可以很容易地用change master更換到新的master:

slave已經記錄了最後執行的舊master的gtid,而且由于gtid在整個複制拓撲中都是全局唯一的,是以slave隻需要根據gtid在新master的binlog裡找到合适的位置,繼續複制就行了。

binlog是一組有序的events資料流(或者多個資料流,每個複制域(replication domain)都是一個資料流,參照接下來那一節),資料流内的event在每個slave上總是按照同一順序被應用。mariadb 的 gtid 憑借這個順序,使得它足以記住每個資料流中的這個點。由于event順序在每個執行個體上是相同的,切換到另一個執行個體的binlog中相同的gtid點将會得到一樣的結果。

比較通俗易懂的講,就是mariadb的gtid複制是全異步的,并且是非常靈活的。甚至binlog順序的一緻性被破壞了,切換master後,gtid依然可以嘗試從目前gtid位點繼續複制。

binlog順序在不同的執行個體之間産生不一緻,最常見的情況是使用者或者dba直接更新了slave執行個體(并且把日志寫到binlog了)。這會導緻slave上的有些event,在master和其他slave上都沒有。雖然可以通過設定<code>sql_log_bin=0</code>來避免這些變更寫到binlog。

這通常是避免執行個體之間binlog不一樣的最好的辦法。話雖然這麼說,但mariadb的複制就是為最大的靈活性而設計的,而且有時這種不一緻是有合理需求的。這種情況下,隻需要了解gtid位點在每個binlog資料流(每個replication domain就有資料流)中是一個單獨的點。

當一個複制拓撲中兩個master在同一時間都是活躍的時候,也可能出現不一緻。比如使用環形多主複制的時候。但是隻要保證舊master上的變更在完全複制到所有slave之前不允許切換到新的master。通常情況下,要切換master,首先原master上的寫應該先停止,然後等待所有變更複制到了新的master,然後寫開始發送到新的master。有意使用多個master寫的情況也是支援的,下一節會描述這種情況。

gtid嚴格模式将用于強制保證所有執行個體之間的binlog相同。當這個選項開啟,如果遇到任何不一緻情況都會導緻複制停止并且報錯。

mariadb的gtid支援同時有多個活躍的master。通常這種情況發生在多源複制或者環形多主情況下。

在這樣的設定下,每個活躍的master都必須用友自己獨特的replication domain id,<code>gtid_domain_id</code>。然後binlog實際上就會由多個獨立的資料流組成,每個活躍的master都有一個。在每個replication domain中,每個執行個體的binlog順序總是一樣的。但是兩個不同的資料流在不同的執行個體的binlog裡可能是交錯的。

這樣的話,一個gtid位點不單單是一個單獨的gtid了,它将是每個domain id下最後一個執行的event group的gtid,實際上這表示了每個binlog資料流達到的位置。當slave連上master的時候,它可以在不同的binlog位置上繼續複制一個資料流。由于一個資料流内的資料順序在不同執行個體上是一樣的,這使得slave可以在任何一個新的master上找到正确的位點繼續複制。

domain id是由dba按照應用的需要配置設定的,<code>@@global.gtid_domain_id</code>的預設值是0。對于大部分的複制場景這都是個合适的值,隻要複制拓撲中同時隻有一個活躍的master。mariadb永遠不會自己寫一個新的domain_id到binlog中。

當使用多源複制的時候,一個slave同時會連上多個master,每個master都應該配置一個不同的domain id。

同樣的,在環形多主複制拓撲結構中,所有的環上所有的master都會被應用并發寫入(可以通過一些機制來避免沖突,例如區分id段),每個執行個體也需要配置不同的domain id。(在環形多主情況下,如果應用程式保證同一時刻隻會更新一個master,那麼一個domain id就夠了)

正常情況下,一個slave執行個體不應該直接接受任何更新(這導緻了跟master的binlog不一緻)。是以在slave上<code>gtid_domain_id</code>被設為什麼值并不重要,盡管它可能是有意義的,例如把它設定為跟master一樣(如果不使用多主),來使其更容易把slave改成新的master。當然,如果slave自身是一個活躍的master,例如在環形多主拓撲中,那麼domain id還是應該設定的,因為這時候執行個體的角色是一個活躍的master。

需要注意的是,domain id和server id是不同的東西。雖然為每個執行個體設定不同的domain id也是可以的,但這通常不是所希望的情況。這會導緻目前的gtid位置(<code>@@global.gtid_slave_pos</code>)更難了解,并且失去了所有執行個體擁有一個一緻的binlog資料流的好處。隻建議在每個會被應用同時更新的活躍的master執行個體上配置domain id。

沒有正确的配置domain id本身并不是一個錯誤(比如根本沒配)。例如,一個5.5版本的環形多主複制環境,更新到10.0。這個環可以繼續像之前一樣繼續工作下去,即使所有的執行個體上domain id還是預設值0。甚至有可能還是使用gtid在執行個體之間複制。然而,當切換slave的master時必須小心翼翼,如果binlog順序在新舊master不一樣,那麼用一個單獨的gtid位置(沒有domain id)從新的master繼續複制,資料就可能出錯。