基于binlog的主從復制
Mysql 5.0以后,支持通過binary log(二進制日志)以支持主從復制。復制允許將來自一個MySQL數據庫服務器(master) 的數據復制到一個或多個其他MySQL數據庫服務器(slave),以實現災難恢復、水平擴展、統計分析、遠程數據分發等功能。
二進制日志中存儲的內容稱之為事件,每一個數據庫更新操作(Insert、Update、Delete,不包括Select)等都對應一個事件。
下面以mysql主從復制為例,講解一個從庫是如何從主庫拉取binlog,并回放其中的event的完整流程。mysql主從復制的流程如下圖所示:
主要分為3個步驟:
第一步:master在每次準備提交事務完成數據更新前,將改變記錄到二進制日志(binary log)中(這些記錄叫做二進制日志事件,binary log event,簡稱event)
第二步:slave啟動一個I/O線程來讀取主庫上binary log中的事件,并記錄到slave自己的中繼日志(relay log)中。
第三步:slave還會起動一個SQL線程,該線程從relay log中讀取事件并在備庫執行,從而實現備庫數據的更新。
binlog的應用場景
讀寫分離
最典型的場景就是通過Mysql主從之間通過binlog復制來實現橫向擴展,來實現讀寫分離。如下圖所示:
在這種場景下:
有一個主庫Master,所有的更新操作都在master上進行
同時會有多個Slave,每個Slave都連接到Master上,獲取binlog在本地回放,實現數據復制。
在應用層面,需要對執行的sql進行判斷。所有的更新操作都通過Master(Insert、Update、Delete等),而查詢操作(Select等)都在Slave上進行。由于存在多個slave,所以我們可以在slave之間做負載均衡。通常業務都會借助一些數據庫中間件,如tddl、sharding-jdbc等來完成讀寫分離功能。
數據最終一致性
在實際開發中,我們經常會遇到一些需求,在數據庫操作成功后,需要進行一些其他操作,如:發送一條消息到MQ中、更新緩存或者更新搜索引擎中的索引等。
如何保證數據庫操作與這些行為的一致性,就成為一個難題。以數據庫與redis緩存的一致性為例:操作數據庫成功了,可能會更新redis失??;反之亦然。很難保證二者的完全一致。
遇到這種看似無解的問題,最好的辦法是換一種思路去解決它:不要同時去更新數據庫和其他組件,只是簡單的更新數據庫即可。如果數據庫操作成功,必然會產生binlog。之后,我們通過一個組件,來模擬的mysql的slave,拉取并解析binlog中的信息。通過解析binlog的信息,去異步的更新緩存、索引或者發送MQ消息,保證數據庫與其他組件中數據的最終一致。
在這里,我們將模擬slave的組件,統一稱之為binlog同步組件。你并不需要自己編寫這樣的一個組件,已經有很多開源的實現,例如linkedin的databus,阿里巴巴的canal,美團點評的puma等。
當我們通過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事件詳解
Mysql已經經歷了多個版本的發布,最新已經到8.x,然而目前企業中主流使用的還是Mysql 5.6或5.7。不同版本的Mysql中,binlog的格式和事件類型可能會有些細微的變化,不過暫時我們并不討論這些細節。
總的來說,binlog文件中存儲的內容稱之為二進制事件,簡稱事件。我們的每一個數據庫更新操作(Insert、Update、Delete等),都會對應的一個事件。
從大的方面來說,binlog主要分為2種格式:
- Statement模式:binlog中記錄的就是我們執行的SQL;
- Row模式:binlog記錄的是每一行記錄的每個字段變化前后得到值。
當我們選擇不同的binlog模式時,在binlog文件包含的事件類型也不相同,如:
1)在Statement模式下,我們就看不到Row模式下獨有的事件類型。
2)有一些類型的event,必須在我們開啟某些特定配置的情況下,才會出現;
3)當然也會有一些公共的event類型,在任何模式下都會出現。
Mysql中定義了30多個event類型,這里并不打算將所有的事件類型提前列出,這樣沒有意義,只會讓讀者茫然不知所措。筆者將會在必要的地方,介紹遇到的每一種event類型的作用。目前我們先從宏觀的角度對binlog有一個感性的認知。
多文件存儲
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左右的磁盤空間。
Binlog管理事件
所謂binlog管理事件,官方稱之為binlog managent events,你可以認為是一些在任何模式下都有可能會出現的事件,不管你的配置binlog_format是Row、Statement還是Mixed。
以下通過“show binlog events”語法進行查看一個空的binlog文件,也就是只包含(部分)管理事件,沒有其他數據更新操作對應的事件。如下:
在當前binlog v4版本中,每個binlog文件總是以Format Description Event作為開始,以Rotate 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:當前事件的描述信息
實戰
開啟binlog日志
- 查詢mysql配置文件所在位置
which mysqld
/usr/sbin/mysqld
/usr/sbin/mysqld --verbose --help | grep -A 1 "Default options"
2020-04-07T08:42:21.418712Z 0 [Warning] Changed limits: max_open_files: 1024 (requested 5000)
2020-04-07T08:42:21.418799Z 0 [Warning] Changed limits: table_open_cache: 431 (requested 2000)
Default options are read from the following files in the given order:
/etc/my.cnf /etc/mysql/my.cnf /usr/etc/my.cnf ~/.my.cnf
2.編輯配置文件/etc/my.cnf
在文件尾部添加:
log-bin=/var/lib/mysql/mysql-bin
server-id=123454 (5.7以上需要指定,用來在集群中區別服務器)
3.重啟mysql
service mysqld restart
4.登陸mysql查看配置信息
show variables like '%log_bin%'
常用binlog日志操作命令
1.查看所有binlog日志列表
mysql> show master logs;
2.查看master狀態,即最后(最新)一個binlog日志的編號名稱,及其最后一個操作事件pos結束點(Position)值
mysql> show master status;
3.刷新log日志,自此刻開始產生一個新編號的binlog日志文件
mysql> flush logs;
注:每當mysqld服務重啟時,會自動執行此命令,刷新binlog日志;在mysqldump備份數據時加 -F 選項也會刷新binlog日志;
4.重置(清空)所有binlog日志
mysql> reset master;
查看某個binlog日志內容,常用有兩種方式:
1.使用mysqlbinlog自帶查看命令法:
注: binlog是二進制文件,普通文件查看器cat more vi等都無法打開,必須使用自帶的 mysqlbinlog 命令查看binlog日志與數據庫文件在同目錄中
/usr/local/mysql/bin/mysqlbinlog /usr/local/mysql/data/mysql-bin.000013
下面截取一個片段分析:
...............................................................................
# at 552
#131128 17:50:46 server id 1 end_log_pos 665 Query thread_id=11 exec_time=0 error_code=0 ---->執行時間:17:50:46;pos點:665
SET TIMESTAMP=1385632246/*!*/;
update zyyshop.stu set name='李四' where id=4 ---->執行的SQL
/*!*/;
# at 665
#131128 17:50:46 server id 1 end_log_pos 692 Xid = 1454 ---->執行時間:17:50:46;pos點:692
...............................................................................
注: server id 1 數據庫主機的服務號;
end_log_pos 665 pos點
thread_id=11 線程號
2.上面這種辦法讀取出binlog日志的全文內容較多,不容易分辨查看pos點信息,這里介紹一種更為方便的查詢命令:
mysql> show binlog events [IN 'log_name'] [FROM pos] [LIMIT [offset,] row_count];
選項解析:
IN 'log_name' 指定要查詢的binlog文件名(不指定就是第一個binlog文件)
FROM pos 指定從哪個pos起始點開始查起(不指定就是從整個文件首個pos點開始算)
LIMIT [offset,] 偏移量(不指定就是0)
row_count 查詢總條數(不指定就是所有行)
截取部分查詢結果:
*************************** 20. row ***************************
Log_name: mysql-bin.000021 ----------------------------------------------> 查詢的binlog日志文件名
Pos: 11197 ----------------------------------------------------------> pos起始點:
Event_type: Query ----------------------------------------------------------> 事件類型:Query
Server_id: 1 --------------------------------------------------------------> 標識是由哪臺服務器執行的
End_log_pos: 11308 ----------------------------------------------------------> pos結束點:11308(即:下行的pos起始點)
Info: use `zyyshop`; INSERT INTO `team2` VALUES (0,345,'asdf8er5') ---> 執行的sql語句
*************************** 21. row ***************************
Log_name: mysql-bin.000021
Pos: 11308 ----------------------------------------------------------> pos起始點:11308(即:上行的pos結束點)
Event_type: Query
Server_id: 1
End_log_pos: 11417
Info: use `zyyshop`; /*!40000 ALTER TABLE `team2` ENABLE KEYS */
*************************** 22. row ***************************
Log_name: mysql-bin.000021
Pos: 11417
Event_type: Query
Server_id: 1
End_log_pos: 11510
Info: use `zyyshop`; DROP TABLE IF EXISTS `type`
A.查詢第一個(最早)的binlog日志:
mysql> show binlog events\G;
B.指定查詢 mysql-bin.000021 這個文件:
mysql> show binlog events in 'mysql-bin.000021'\G;
C.指定查詢 mysql-bin.000021 這個文件,從pos點:8224開始查起:
mysql> show binlog events in 'mysql-bin.000021' from 8224\G;
D.指定查詢 mysql-bin.000021 這個文件,從pos點:8224開始查起,查詢10條
mysql> show binlog events in 'mysql-bin.000021' from 8224 limit 10\G;
E.指定查詢 mysql-bin.000021 這個文件,從pos點:8224開始查起,偏移2行,查詢10條
mysql> show binlog events in 'mysql-bin.000021' from 8224 limit 2,10\G;
Binlog結構和內容
日志由一組二進制日志文件(Binlog),加上一個索引文件(index);Binlog是一個二進制文件集合,每個Binlog以一個4字節的魔數開頭,接著是一組Events;
1.魔數:0xfe62696e對應的是0xfebin;
2.Event:每個Event包含header和data兩個部分;header提供了Event的創建時間,哪個服務器等信息,data部分提供的是針對該Event的具體信息,如具體數據的修改;
3.第一個Event用于描述binlog文件的格式版本,這個格式就是event寫入binlog文件的格式;
4.其余的Event按照第一個Event的格式版本寫入;
5.最后一個Event用于說明下一個binlog文件;
6.Binlog的索引文件是一個文本文件,其中內容為當前的binlog文件列表
Binlog的Event類型
官方提供的可能Event類型有36種,具體看下面的枚舉:
enum Log_event_type {
UNKNOWN_EVENT= 0,
START_EVENT_V3= 1,
QUERY_EVENT= 2,
STOP_EVENT= 3,
ROTATE_EVENT= 4,
INTVAR_EVENT= 5,
LOAD_EVENT= 6,
SLAVE_EVENT= 7,
CREATE_FILE_EVENT= 8,
APPEND_BLOCK_EVENT= 9,
EXEC_LOAD_EVENT= 10,
DELETE_FILE_EVENT= 11,
NEW_LOAD_EVENT= 12,
RAND_EVENT= 13,
USER_VAR_EVENT= 14,
FORMAT_DESCRIPTION_EVENT= 15,
XID_EVENT= 16,
BEGIN_LOAD_QUERY_EVENT= 17,
EXECUTE_LOAD_QUERY_EVENT= 18,
TABLE_MAP_EVENT = 19,
PRE_GA_WRITE_ROWS_EVENT = 20,
PRE_GA_UPDATE_ROWS_EVENT = 21,
PRE_GA_DELETE_ROWS_EVENT = 22,
WRITE_ROWS_EVENT = 23,
UPDATE_ROWS_EVENT = 24,
DELETE_ROWS_EVENT = 25,
INCIDENT_EVENT= 26,
HEARTBEAT_LOG_EVENT= 27,
IGNORABLE_LOG_EVENT= 28,
ROWS_QUERY_LOG_EVENT= 29,
WRITE_ROWS_EVENT = 30,
UPDATE_ROWS_EVENT = 31,
DELETE_ROWS_EVENT = 32,
GTID_LOG_EVENT= 33,
ANONYMOUS_GTID_LOG_EVENT= 34,
PREVIOUS_GTIDS_LOG_EVENT= 35,
ENUM_END_EVENT
/* end marker */
};
Event結構官網提供了3個版本,分別是v1,v3,v4:
v1:用在MySQL 3.23
v3:用在MySQL 4.0.2-4.1
v4:用在MySQL 5.0之后
現在MySQL的版本基本都使用5.0之后的版本,可以直接看v4,具體如下:
名字后面的兩個數字表示:offset : length即從第幾個字節開始,后面多少個字節用來存放數據.比如:timestamp(0 : 4)表示從第0個字節開始,往后四個字節用來存放timestamp
目前來說x=19,所有extra_headers是空的,y是fixed part的長度,不同的Event長度不一樣。
Event簡要分析
1.從一個最簡單的實例來分析其中的Event,包括創建表,插入數據,更新數據,刪除數據;binlog_format使用的是默認的STATEMENT;
CREATE TABLE `btest` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`age` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
insert into btest values(1,100,'zhaohui');
update btest set name='zhaohui_new' where id=1;
delete from btest where id=1;
2.查看所有的Events
show binlog events;
上圖中一共出現了3中類型的Event,分別是Format_desc,Query和Xid,下面進行簡單的分析
2.1Format_desc_Event
官網格式如下:
使用十六進制方式打開文件bin-log.000001,以下是Format_desc_Event的十六進制代碼:
可以先看前面的4+103=107個字節,4字節是binlog的魔數,103字節是Format_desc_Event,其中有19字節是header;
注:Binlog日志是小端字節順序
0x5A0504AA四個字節是timestamp;0x0F一個字節表示type_code;0x00000001四個字節為server_id;0x00000067四個字節是event_length,對應的十進制就是103;0x0000006b四個字節是next_position,即下一個Event的開始位置為107;ox0001兩個字節是flags;header總計19字節。data總字節數=103-19即84字節,排除掉前面的57個字節,剩余27字節表示post-header lengths for all event types;post-header lengths:從START_EVENT_V3開始到第27個Event,每個Event的fixed part lengths;Format_desc_Event位置是15,可以在圖中找到15的位置是0x54,對應十進制是84,即fixed part lengths=84,而這個值剛好是57+27=84,所以Format_desc_Event不存在variable part;
Query_Event
以下的create table產生的Query_Event的十六進制代碼:
header共19字節,0x02一個字節表示type_code(Query_Event=2);0x00000101四個字節是event_length,對應的十進制就是257(pos=107,End_log_pos=364);Query_Event在post-header的第二個位置0x0d,所有fix part lengths=13;variable part=257-19-13=225字節,具體fix data和variable data:
在創建表產生一個Query_Event,insert、update以及delete執行之后分別產生了2個Query_Event和一個Xid_Event。
Xid_Event
以下的更新數據產生的Xid_Event的十六進制代碼:
header共19字節,0x10一個字節表type_code(XID_EVENT=16);0x0000001b四個字節是event_length,對應的十進制就是27(pos=536,End_log_pos=563);2Xid_Event在post-header的第十六個位置0x00,所有fix part lengths=0;variable part=27-19=8字節,8字節:The XID transaction number。
insert、update以及delete執行之后分別產生了Xid_Event,事務提交產生的事件。