<b>bug描述</b>
oracle 最新釋出的版本 5.6.22 中有這樣一個關于gtid的bugfix,在主備場景下,如果我們在主庫上 set global gtid_purged = "some_gtid_set",并且 some_gtid_set 中包含了備庫還沒複制的事務,這個時候如果備庫接上主庫的話,預期結果是主庫傳回錯誤,io線程挂掉的,但是實際上,在這種場景下主庫并不報錯,隻是默默的把自己 binlog 中包含的gtid事務發給備庫。這個bug的造成的結果是看起來複制正常,沒有錯誤,但實際上備庫已經丢事務了,主備很可能就不一緻了。
<b>背景知識</b>
一) binlog gtid事件
binlog 中記錄的和gtid相關的事件主要有2種,previous_gtids_log_event 和 gtid_log_event,前者表示之前的binlog中包含的gtid的集合,後者就是一個gtid,對應一個事務。一個 binlog 檔案中隻有一個 previous_gtids_log_event,放在開頭,有多個 gtid_log_event,如下面所示
二) 備庫發送gtid集合給主庫
我們知道備庫的複制線程是分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然後開始發送。
三)主庫如何找到要發送給備庫的第一個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,直到找到為止。
這個查找過程總會停止的,停止條件如下:
找到了這樣的binlog,其previous_gtids_log_event 是slave_gtid_executed子集
在往前讀binlog的時候,發現沒有binlog檔案了(如被purge了),但是還沒找到滿足條件的previous_gtids_log_event,這個時候主庫報錯
一直往前找,發現previous_gtids_log_event 是空集
在條件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。
<b>bug 分析</b>
假設有這樣的場景,我們要用已經有mysql執行個體的備份重新做一對主備執行個體,不管是用 xtrabackup 這種實體備份工具或者mysqldump這種邏輯備份工具,都會有2步操作,
導入資料
set global gtid_purged ="xxxx"
步驟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是肯定會停下來的,并且不報錯。
<b>bug 修複</b>
官方的修複就是在主庫掃描查找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.