1 基於binlog的主從複製

Mysql 5.0以後,支持通過binary log(二進位日誌)以支持主從複製。複製允許將來自一個MySQL資料庫伺服器(master) 的數據複製到一個或多個其他MySQL資料庫伺服器(slave),以實現災難恢復、水平擴展、統計分析、遠程數據分發等功能。

二進位日誌中存儲的內容稱之為事件,每一個資料庫更新操作(Insert、Update、Delete,不包括Select)等都對應一個事件。

注意:本文不是講解mysql主從複製,而是講解binlog的應用場景,binlog中包含哪些類型的event,這些event的作用是什麼。你可以理解為,是對主從複製中關於binlog解析的細節進行深度剖析。而講解主從複製主要是為了理解binlog的工作流程。

下面以mysql主從複製為例,講解一個從庫是如何從主庫拉取binlog,並回放其中的event的完整流程。mysql主從複製的流程如下圖所示:

主要分為3個步驟:

  • 第一步:master在每次準備提交事務完成數據更新前,將改變記錄到二進位日誌(binary log)中(這些記錄叫做二進位日誌事件,binary log event,簡稱event)
  • 第二步:slave啟動一個I/O線程來讀取主庫上binary log中的事件,並記錄到slave自己的中繼日誌(relay log)中。
  • 第三步:slave還會起動一個SQL線程,該線程從relay log中讀取事件並在備庫執行,從而實現備庫數據的更新。

2 binlog的應用場景

binlog本身就像一個螺絲刀,它能發揮什麼樣的作用,完全取決你怎麼使用。就像你可以使用螺絲刀來修電器,也可以用其來固定傢具。

2.1 讀寫分離

最典型的場景就是通過Mysql主從之間通過binlog複製來實現橫向擴展,來實現讀寫分離。如下圖所示:

在這種場景下:

  • 有一個主庫Master,所有的更新操作都在master上進行
  • 同時會有多個Slave,每個Slave都連接到Master上,獲取binlog在本地回放,實現數據複製。
  • 在應用層面,需要對執行的sql進行判斷。所有的更新操作都通過Master(Insert、Update、Delete等),而查詢操作(Select等)都在Slave上進行。由於存在多個slave,所以我們可以在slave之間做負載均衡。通常業務都會藉助一些資料庫中間件,如tddl、sharding-jdbc等來完成讀寫分離功能。

因為工作性質的原因,筆者見過最多的一個業務,一個master,後面掛了20多個slave。筆者之前寫過一篇關於資料庫中間件實現原理的文章,感興趣的讀者可以參考:資料庫中間件詳解

2.2 數據恢復

一些同學可能有誤刪除資料庫記錄的經歷,或者因為誤操作導致資料庫存在大量臟數據的情況。例如筆者,曾經因為誤操作污染了業務方几十萬數據記錄。

如何將臟數據恢復成原來的樣子?如果恢復已經被刪除的記錄?

這些都可以通過反解binlog來完成,筆者也是通過這個手段,來恢復業務方的記錄。

2.3 數據最終一致性

在實際開發中,我們經常會遇到一些需求,在資料庫操作成功後,需要進行一些其他操作,如:發送一條消息到MQ中、更新緩存或者更新搜索引擎中的索引等。

如何保證資料庫操作與這些行為的一致性,就成為一個難題。以資料庫與redis緩存的一致性為例:操作資料庫成功了,可能會更新redis失敗;反之亦然。很難保證二者的完全一致。

遇到這種看似無解的問題,最好的辦法是換一種思路去解決它:不要同時去更新資料庫和其他組件,只是簡單的更新資料庫即可。

如果資料庫操作成功,必然會產生binlog。之後,我們通過一個組件,來模擬的mysql的slave,拉取並解析binlog中的信息。通過解析binlog的信息,去非同步的更新緩存、索引或者發送MQ消息,保證資料庫與其他組件中數據的最終一致。

在這裡,我們將模擬slave的組件,統一稱之為binlog同步組件。你並不需要自己編寫這樣的一個組件,已經有很多開源的實現,例如linkedin的databus,阿里巴巴的canal,美團點評的puma等。

當我們通過binlog同步組件完成數據一致性時,此時架構可能如下圖所示:

增量索引

通常索引分為全量索引和增量索引。對於增量索引的部分,可以通過監聽binlog變化,根據binlog中包含的信息,轉換成es語法,進行實時索引更新。當然,你可能並沒有使用es,而是solr,這裡只是以es舉例。

可靠消息

可靠消息是指的是:保證本地事務與發送消息到MQ行為的一致性。一些業務使用本地事務表或者獨立消息服務,來保證二者的最終一致。Apache RocketMQ在4.3版本開源了事務消息,也是用於完成此功能。事實上,這兩種方案,都有一定侵入性,對業務不透明。通過訂閱binlog來發送可靠消息,則是一種解耦、無侵入的方案。關於可靠消息,筆者最近寫了一篇文章, 感興趣的讀者可以參考:可靠消息一致性的奇淫技巧。

緩存一致性

業務經常遇到的一個問題是,如何保證資料庫中記錄和緩存中數據的一致性。不妨換一種思路,只更新資料庫,資料庫更新成功後,通過拉取binlog來非同步的更新緩存(通常是刪除,讓業務回源到資料庫)。如果資料庫更新失敗,沒有對應binlog,那麼也不會去更新緩存,從而實現最終一致性。

可以看到,binlog是一把利器,可以保證資料庫與與其他任何組件(es、mq、redis等)的最終一致。這是一種優雅的、通用的、無業務入侵的、徹底的解決方案。我們沒有必要再單獨的研究某一種其他組件如何與資料庫保持最終一致,可以通過binlog來實現統一的解決方案。

在實際開發中,你可以簡單的像上圖那樣,每個應用場景都模擬一個slave,各自連接到Mysql上去拉取binlog,master會給每個連接上來的slave一份完整的binlog拷貝,業務拿到各自的binlog之後進行消費,彼此之間互不影響。但是這樣,有一些弊端,多個slave會給master帶來一些額外管理上的開銷,網卡流量也將翻倍的增長。

我們可以進行一些優化,之所以不同場景模擬多個slave來連接master獲取同一份binlog,本質上要滿足的是:一份binlog數據,同時提供給多個不同業務場景使用,彼此之間互不影響。

顯然,消息中間件是一個很好的解決方案。現在很多主流的消息中間件,都支持consumer group的概念,如kafka、rocketmq等。同一個topic中的數據,可以由多個不同consumer group來消費,且不同的consumer group之間是相互隔離的,例如:當前消費到的位置(offset)。

因此,我們完全可以將binlog,統一都發送到MQ中,不同的應用場景使用不同的consumer group來消費,彼此之間互不影響。此時架構如下圖所示:

通過這樣方式,我們巧妙的達到了一份數據多個應用場景來使用。一般,一個Mysql實例中可能會創建多個庫(Database),通常我們會將一個庫的binlog放到一個對應的MQ中的Topic中。

當將binlog發送到MQ中後,我們就可以利用MQ的一些高級特性了。例如binlog發送到MQ過快,消費方來不及消費,可以利用MQ的消息堆積能力進行流量削峯。還可以利用MQ的消息回溯功能,例如一個業務需要消費歷史的binlog,此時MQ中如果還有保存,那麼就可以直接進行回溯。

當然,有一些binlog同步組件可能實現了類似於MQ的功能,此時你就無序再單獨的使用MQ。

2.4 異地多活

一個更大的應用場景,異地多活場景下,跨數據中心之間的數據同步。這種場景的下,多個數據中心都需要寫入數據,並且往對方同步。以下是一個簡化的示意圖:

這裡有一些特殊的問題需要處理。典型的包括:

  • 數據衝突:雙方同時插入了一個相同主鍵的值,那麼往對方同步時,就會出現主鍵衝突的錯誤。
  • 數據迴環:一個庫A中插入的數據,通過binlog同步到另外一個庫B中,依然會產生binlog。此時庫B的數據再次同步回庫A,如此反覆,就形成了一個死循環。

如何解決數據衝突、數據迴環,就變成了binlog同步組件要解決的問題。同樣,業界也有了成熟的實現,比較知名的有阿里開源的otter,以及摩拜(已經屬於美團)的DRC等。

筆者之前寫過一篇文章,介紹如何在多機房進行數據同步,感興趣的讀者可以參考以下文章:異地多活場景下的數據同步之道

2.5 小結

如前所屬,binlog的作用如此強大。因此,你可能想知道binlog文件中到底包含了哪些內容,為什麼具有如此的魔力?在進行一些資料庫操作時,例如:Insert、Update、Delete等,到底會對binlog產生什麼樣的影響?這正是本文要下來要講解的內容。

3 Binlog事件詳解

Mysql已經經歷了多個版本的發布,最新已經到8.x,然而目前企業中主流使用的還是Mysql 5.6或5.7。不同版本的Mysql中,binlog的格式和事件類型可能會有些細微的變化,不過暫時我們並不討論這些細節。

總的來說,binlog文件中存儲的內容稱之為二進位事件,簡稱事件。我們的每一個資料庫更新操作(Insert、Update、Delete等),都會對應的一個事件。

從大的方面來說,binlog主要分為2種格式:

  • Statement模式:binlog中記錄的就是我們執行的SQL;
  • Row模式:binlog記錄的是每一行記錄的每個欄位變化前後得到值。

熟悉主從複製的同學,應該知道,還有第三種模式Mixed(即混合模式),從嚴格意義上來說,這並不是一種新的binlog格式,只是結合了Statement和Row兩種模式而已。

當我們選擇不同的binlog模式時,在binlog文件包含的事件類型也不相同,如: 1)在Statement模式下,我們就看不到Row模式下獨有的事件類型。2)有一些類型的event,必須在我們開啟某些特定配置的情況下,才會出現;3)當然也會有一些公共的event類型,在任何模式下都會出現。

Mysql中定義了30多個event類型,這裡並不打算將所有的事件類型提前列出,這樣沒有意義,只會讓讀者茫然不知所措。筆者將會在必要的地方,介紹遇到的每一種event類型的作用。

目前我們先從宏觀的角度對binlog有一個感性的認知。

3.1 多文件存儲

mysql 將資料庫更新操作對應的event記錄到本地的binlog文件中,顯然在一個文件中記錄所有的event是不可能的,過大的文件會給我們的運維帶來麻煩,如刪除一個大文件,在I/O調度方面會給我們帶來不可忽視的資源開銷。

因此,目前基本上所有支持本地文件存儲的組件,如MQ、Mysql等,都會控制一個文件的大小。在數據量較多的情況下,就分配到多個文件進行存儲。

在mysql中,我們可以通過"show binary logs"語句,來查看當前有多少個binlog文件,以及每個binlog文件的大小,如下:

另外,mysql提供了:

  • max_binlog_size配置項,用於控制一個binlog文件的大小,默認是1G
  • expire_logs_days配置項,可以控制binlog文件保留天數,默認是0,也就是永久保留。

在實際生產環境中,一般無法保留所有的歷史binlog。因為一條記錄可能會變更多次,記錄依然是一條,但是對應的binlog事件就會有多個。在數據變更比較頻繁的情況下,就會產生大量的binlog文件。此時,則無法保留所有的歷史binlog文件。

在mysql的percona分支上,還提供了max_binlog_files配置項,用於設置可以保留的binlog文件數量,以便我們更精確的控制binlog文件佔用的磁碟空間。這是一個非常有用的配置,筆者曾經遇到一個庫,大約10分鐘就會產生一個binlog文件,也就是1G,按照這種增長速度,1天下來產生的binlog文件,就會佔用大概144G左右的空間,磁碟空間可能很快就會被使用完。通過此配置,我們可以顯示的控制binlog文件的數量,例如指定50,binlog文件最多隻會佔用50G左右的磁碟空間。

在更高版本的mysql中,支持按照秒級精度,來控制binlog文件的保留時間。下面我們將對binlog文件中的內容進行詳細的講解。

3.2 Binlog管理事件

所謂binlog管理事件,官方稱之為binlog managent events,你可以認為是一些在任何模式下都有可能會出現的事件,不管你的配置binlog_format是Row、Statement還是Mixed。

以下通過"show binlog events"語法進行查看一個空的binlog文件,也就是隻包含(部分)管理事件,沒有其他數據更新操作對應的事件。如下:

在當前binlog v4版本中,每個binlog文件總是以Format Description Event作為開始,以Rotate Event結束作為結束。如果你使用的是很古老的Mysql版本中,開始事件也有可能是START EVENT V3,而結束事件是Stop Event。在開始和結束之間,穿插著其他各種事件。

在Event_Type列中,我們看到了三個事件類型:

  • Format_desc:也就是我們所說的Format Description Event,是binlog文件的第一個事件。在Info列,我們可以看到,其標明瞭Mysql Server的版本是5.7.10,Binlog版本是4。
  • Previous_gtids:該事件完整名稱為,PREVIOUS_GTIDS_LOG_EVENT。熟悉Mysql 基於GTID複製的同學應該知道,這是表示之前的binlog文件中,已經執行過的GTID。需要我們開啟GTID選項,這個事件才會有值,在後文中,將會詳細的進行介紹。
  • Rotate:Rotate Event是每個binlog文件的結束事件。在Info列中,我們看到了其指定了下一個binlog文件的名稱是mysql-bin.000004。

關於"show binlog events"語法顯示的每一列的作用說明如下:

  • Log_name:當前事件所在的binlog文件名稱
  • Pos:當前事件的開始位置,每個事件都佔用固定的位元組大小,結束位置(End_log_position)減去Pos,就是這個事件佔用的位元組數。細心的讀者可以看到了,第一個事件位置並不是從0開始,而是從4。Mysql通過文件中的前4個位元組,來判斷這是不是一個binlog文件。這種方式很常見,很多格式的文件,如pdf、doc、jpg等,都會通常前幾個特定字元判斷是否是合法文件。
  • Event_type:表示事件的類型
  • Server_id:表示產生這個事件的mysql server_id,通過設置my.cnf中的server-id選項進行配置。
  • End_log_position:下一個事件的開始位置
  • Info:當前事件的描述信息

3.3 Statement模式下的事件

mysql5.0及之前的版本只支持基於語句的複製,也稱之為邏輯複製,也就是binary log文件中,直接記錄的就是數據更新對應的sql。

假設有名為test庫中有一張user表,如下:

現在,我們往user表中插入一條數據

1 insert into user(name) values("tianbowen");

之後,可以使用"show binlog events" 語法查看binary log中的內容,如下:

紅色框架中Event,是我們執行上面Insert語句產生的4個Event。下面進行詳細的說明:

(劃重點)首先,需要說明的是,每個事務都是以Query Event作為開始,其INFO列內容為"BEGIN",以Xid Event表示結束,其INFO列內容為COMMIT。即使對於單條更新SQL我們沒有開啟事務,Mysql也會默認的幫我們開啟事務。因此在上面的紅色框中,儘管我們只是執行了一個INSERT語句,沒有開啟事務,但是Mysql 默認幫我們開啟了事務,所以第一個Event是Query Event,最後一個是Xid Event。

接著,是一個Intvar Event,因為我們的Insert語句插入的表中,主鍵是自增的(AUTO_INCREMENT)列,Mysql首先會自增一個值,這就是Intvar Event的作用,這裡我們看到INFO列的值為INSERT_ID=1,也就是說,這次的自增主鍵id為1。需要注意的是,這個事件,只會在Statement模式下出現。

然後,還是一個Query Event,這裡記錄的就是我們插入的SQL。這也體現了Statement模式的作用,就是記錄我們執行的SQL。

Statement模式下還有一些不常用的Event,如USER_VAR_EVENT,這是用於記錄用戶設置的變數,僅僅在Statement模式起作用。如:

執行以下SQL:

set @name = tianshouzhi;
insert into user(name) values(@name);

這裡,我們插入sql的時候,通過引用一個變數。此時查看binlog變化,這裡為了易於觀察,在執行show binlog events時,指定了binlog文件和from的位置,即只查看指定binlog文件中從指定位置開始的event。如下:

可以看到,依然符合我們所說的,對於這個插入語句,依然默認開啟了事務。主鍵自曾值INSERT_ID=2。

當然,我們也看到了User var這個事件,其記錄了我們的設置的變數值,只不過以16進位顯示。

3.4 Row模式下的事件

mysql5.1開始支持基於行的複製,這種方式記錄的某條sql影響的所有行記錄變更前變更後的值。Row模式下主要有以下10個事件:

很直觀的,我們看到了INSERT、DELETE、UPDATE操作都有3個版本(v0、v1、v2),v0和v1已經過時,我們只需要關注V2版本。

此外,還有一個TABLE_MAP_EVENT,這個event我們需要特別關注,可以理解其作用就是記錄了INSERT、DELETE、UPDATE操作的表結構。

下面,我們通過案例演示,ROW模式是如何記錄變更前後記錄的值,而不是記錄SQL。這裡只演示UPDATE,INSERT和DELETE也是類似。

在前面的操作步驟中,我們已經插入了2條記錄,如下:

現在需要從Statement模式切換到Row模式,重啟Mysql之後,執行以下SQL更新這兩條記錄:

update user set name=wangxiaoxiao;

在binary log中,會把這2條記錄變更前後的值都記錄下來,以下是一個邏輯示意圖:

該邏輯示意圖顯示了,在默認情況下,受到影響的記錄行,每個欄位變更前的和變更後的值,都會被記錄下來,即使這個欄位的值沒有發生變化

接著,我們還是通過"show binlog events"語法來驗證:

首先我們可以看到的是,在Row模式下,單條SQL依然會默認開啟事務,通過Query Event(值為BEGIN)開始,以Xid Event結束。

接著,我們看到了一個Table_map 事件,就是前面提到的TABLE_MAP_EVENT,在INFO列,我們可以看到其記錄table_id為108,操作的是test庫中user表。

最後,是一個Update_rows事件,然而其INFO,並沒有像Statement模式那樣,顯示一條SQL,我們無法直接看到其變更前後的值是什麼。

由於存儲的都是二進位內容,直接vim無法查看,我們需要藉助另外一個工具mysqlbinlog來查看其內容。如下:

截圖中顯示了2個event,第一個紅色框就是Table_map事件,第二個是Update_rows事件。

在第二個紅色框架中,顯示了兩個Update sql,這是隻是mysqlbinlog工具為了方便我們查看,反解成SQL而已。我們看到了WHERE以及SET子句中,並沒有直接列出欄位名,而是以@1@2這樣的表示欄位位於資料庫表中的順序。事實上,這裡顯示的內容,WHERE部分就是每個欄位修改前的值,而SET部分,則是每個欄位修改後的值,也就是變更前後的值都會記錄。

這裡我們思考以下mysqlbinlog工具的工作原理,其可以將二進位數據反解成SQL進行展示。那麼,如果我們可以自己解析binlog,就可以做數據恢復,這並非是什麼難事。例如用戶誤刪除的數據,執行的是DETELE語句,由於Row模式下會記錄變更之前的欄位的值,我們可以將其反解成一個INSERT語句,重新插入,從而實現數據恢復。

3.4.1 binlog_row_image參數

我們經常會看到一些Row模式和Statement模式的比較。ROW模式下,即使我們只更新了一條記錄的其中某個欄位,也會記錄每個欄位變更前後的值,binlog日誌就會變大,帶來磁碟IO上的開銷,以及網路開銷。

事實上,這個行為可以通過binlog_row_image控制其有3個值,默認為FULL:

  • FULL : 記錄列的所有修改,即使欄位沒有發生變更也會記錄。
  • MINIMAL :只記錄修改的列。
  • NOBLOB :如果是text類型或clob欄位,不記錄這些日誌。

我們可以將其修改為MINIMAL,則可以只記錄修改的列的值。

3.4.2 binlog_rows_query_log_events參數

在Statement模式下,直接記錄SQL比較直觀,事實上,在Row模式下,也可以記錄。mysql提供了一個binlog_rows_query_log_events參數,默認為值為FALSE,如果為true的情況下,會通過Rows Query Event來記錄SQL。

可以在my.cnf中添加以下配置,來開啟row模式下的原始sql記錄(需要重啟):

binlog-rows-query-log_events=1

之後,再插入數據數據時

insert into user(name) values("maoxinyi");

在binlog文件中,我們將看到Rows Query Event

3.5 GTID相關事件

從MySQL 5.6開始支持GTID複製。要開啟GTID,修改my.cnf文件,添加以下配置

gtid-mode=on
enforce-gtid-consistency=true

在這種情況下,每當我們執行一個事務之前,都會記錄一個GTID Event

insert into user("name") values("zhuyihan");

此時binlog內容如下:

而當我們切換到下一個binlog文件時,會記錄之前的已經執行過的GTID。這裡我們通過執行以下sql手工切換到一個新的binlog文件。

mysql> flush logs;
Query OK, 0 rows affected (0.00 sec)

後在新的binlog文件中,我們看到之前執行過的GTID在下一個文件中出現了。

本文不是專門講解GTID的文章,感興趣的讀者,可以自行查看相關資料。

4 總結

本文對mysql binlog的應用場景進行了深入的講解,並介紹了mysql中大部分binlog event的作用。

如果讀者想更加深入的去學習,例如如何模擬mysql的slave去解析binlog,可以參考一些開源的實現,不過這些生產級別的組件,因此通常代碼比較複雜。筆者自己也造過類似的輪子,僅僅模擬slave去拉取mysql的binlog,並對事件進行解析,對於理解binlog解析的核心原理應該有一些幫助。

推薦閱讀:

動力節點:阿里內推面試題:能不能自定義一個類叫java.lang.System?

zhuanlan.zhihu.com圖標

作者:田守枝

來自:田守枝的博客博客鏈接:田守枝Java技術博客

推薦閱讀:

相關文章