MySQL 8 複製(六)——拓撲與性能

目錄

一、複製拓撲

1. 一主一(多)從

2. 雙(多)主複製

4. 多源複製

5. Blackhole引擎與日誌服務器

二、複製性能

1. 測試規劃

2. sync_binlog與innodb_flush_log_at_trx_commit

3. 組提交與多線程複製

3. 基於WriteSet的多線程複製


        可以在任意個主從庫之間建立複雜的複製拓撲結構,如普通的一主一(多)從、雙(多)主複製、級聯複製,MySQL 5.7.2後新增的多源複製,特殊場景下使用的Blackhole引擎與日誌服務器等等。複製中的MySQL服務器須要遵循以下基本原則:

  • 拓撲中的每個服務器必須有一個唯一的server_id和server_uuid。
  • 一個主庫可以有多個從庫(或者說一個從庫可以有多個兄弟從庫)。
  • 如果打開了log_slave_updates選項,一個從庫可以把其對應主庫上的數據變化傳播到它的從庫。

一、複製拓撲

1. 一主一(多)從

(1)一級主從
        一主一從的基本配置是最簡單的拓撲結構,而一主多從的結構和基本配置差不多簡單,因爲從庫之間根本沒有交互,它們僅僅是連接到同一個主庫。圖1顯示了這種結構。

圖1

        儘管這是非常簡單的拓撲結構,但它非常靈活,能滿足多種需求。爲滿足數據一致性和主從切換需求,從庫原則上應爲只讀,下面是從庫的一些用途:

  • 爲不同的角色使用不同的從庫,例如爲滿足讀寫分離需求,在從庫添加不同於主庫的適合讀操作的索引(不要忘記在從庫執行任何寫操作前 set sql_log_bin=0)。
  • 把一臺從庫只當做備用主庫,除了複製沒有其它數據傳輸。
  • 將一臺從庫放到遠程數據中心,用作災難恢復。
  • 延遲複製一個從庫,以備恢復用戶錯誤。
  • 使用其中一個從庫,作爲備份或測試使用。

        這種結構流行原因是它避免了很多其它拓撲的複雜性。例如,可以方便地比較不同從庫重放的事件在主庫二進制日誌中的位置,當然如果啓用GTID就更跟簡單了,支持自動定位。換句話說,如果在同一個邏輯點停止所有從庫的複製,它們正在讀取的是主庫上同一個日誌文件的相同物理位置。這是個很好的特性,可以減輕管理員許多工作,例如把從庫提升爲主庫。

        這種特性只存在於兄弟從庫之間。在沒有直接的主從或者兄弟關係的服務器上去比較日誌文件的位置要複雜很多。例如樹形複製或分佈式主庫,很難計算出複製事件的邏輯順序。

(2)級聯主從
        如果正在將主庫複製到大量從庫中,不管是把數據分發到不同的地方,還是提供更高的讀性能,使用級聯複製都能更好地管理,如圖2所示。

圖2

        這種設計的好處是減輕了主庫的負擔,將讀負載分發到多個從庫。缺點是中間層出現的任何錯誤都會影響到多個服務器。如果每個從庫和主庫直接相連就不會存在這樣的問題。同時中間層次越多,處理故障就會越複雜和困難。

2. 雙(多)主複製

(1)主動-主動模式下的雙主複製
        雙主複製包含兩臺MySQL服務器,每一個都被配置爲對方的主庫和從庫,換句話說,它們是一對主庫。圖3顯示了該結構。

圖3

        主動-主動模式指的是兩臺服務器對於應用均可讀寫,通常用於特殊目的。例如一個可能的應用場景是兩個處於不同地理位置的辦公室,並且都需要一份可寫的數據拷貝。這種配置最大的問題時如何解決衝突,兩個可寫的互主服務器導致的問題非常多。這通常發生在兩臺服務器同時修改一行記錄,或同時在兩臺服務器上向一個包含auto_increment列的表裏插入數據。這些問題會經常發生,而且需要不易解決,因此並不推薦這種模式。下面演示兩個常見的問題。

  • 在兩臺機器更新順序不同導致數據不一致或1032錯誤。
-- 主1
mysql> select * from t1;
+---+
| a |
+---+
| 1 |
+---+
1 row in set (0.00 sec)

-- 主2
mysql> select * from t1;
+---+
| a |
+---+
| 1 |
+---+
1 row in set (0.00 sec)

-- 主2延遲複製,模擬兩個主庫不同的執行順序
stop slave;
change master to master_delay = 10;
start slave;

-- 主1
set binlog_format='statement';
update t1 set a=a+1;

-- 主2在複製之前(10秒之內)執行
set binlog_format='statement';
update t1 set a=a*2;

-- 10秒之後查詢
-- 主1
mysql> select * from t1;
+------+
| a    |
+------+
|    4 |
+------+
1 row in set (0.00 sec)

-- 主2
mysql> select * from t1;
+------+
| a    |
+------+
|    3 |
+------+
1 row in set (0.00 sec)

        複製沒有報告任何錯誤,但兩個庫的數據已經不一致。主1上先執行的a=a+1,緊接着由於複製執行a=a*2,最終結果是4。主2上由於延遲複製,先執行a=a*2,10秒後執行復制的a=a+1,最終結果是3。此實驗是在binlog_format='statement'下進行的,如果設置binlog_format='row',則兩個庫(首先是主1,10秒後是主2)的都會報1032錯誤,show slave status中顯示的錯誤如下:

Last_SQL_Errno: 1032
Last_SQL_Error: Could not execute Update_rows event on table test.t1; Can't find record in 't1', Error_code: 1032; handler error HA_ERR_END_OF_FILE; the event's master log binlog.000001, end_log_pos 2534

        報1032的原因是應用複製時記錄已經發生改變,找不到更新時間點的數據行。

  • 由auto_increment引起的1062錯誤。
-- 主1
use test;
create table t1(a int auto_increment primary key);

delimiter //
create procedure p1(a int)
begin
   declare i int default 1;
   while i<=a do
      insert into t1(a) select null;
  set i=i+1;
   end while;
end;
//

delimiter ;

call p1(1000);

-- 主2,在主1執行過程期間同時在主2執行
call p1(1000);

        show slave status中顯示如下錯誤:

Last_SQL_Errno: 1062
Last_SQL_Error: Could not execute Write_rows event on table test.t1; Duplicate entry '366' for key 'PRIMARY', Error_code: 1062; handler error HA_ERR_FOUND_DUPP_KEY; the event's master log binlog.000001, end_log_pos 101521

        因爲本機插入的數據與複製插入的數據產生衝突而報1062錯誤。通過在兩個服務器設置不同的auto_increment_offset、auto_increment_increment,可以讓MySQL自動爲insert語句選擇不互相沖突的值,稍微增加了點安全性。

-- 主1
set auto_increment_offset=1;
set auto_increment_increment=2;
call p1(1000);

-- 主2,在主1執行過程期間同時在主2執行
set auto_increment_offset=2;
set auto_increment_increment=2;
call p1(1000);

        主1上插入單數,主2插入雙數,複製與本機數據不衝突。過程執行完後,兩個庫都插入了2000條數據,但缺省配置 innodb_autoinc_lock_mode=2 會造成序列值不連續。

-- 主1
mysql> select count(*),min(a),max(a) from t1;
+----------+--------+--------+
| count(*) | min(a) | max(a) |
+----------+--------+--------+
|     2000 |      1 |   2414 |
+----------+--------+--------+
1 row in set (0.00 sec)

-- 主2
mysql> select count(*),min(a),max(a) from t1;
+----------+--------+--------+
| count(*) | min(a) | max(a) |
+----------+--------+--------+
|     2000 |      1 |   2414 |
+----------+--------+--------+
1 row in set (0.00 sec)

        可以看到複製正常,兩個服務器數據是一致。但這隻極端理想的場景:從空表開始插入數據,配置複製時沒有聯機訪問。如果在配置雙主複製時已經有數據,情況將複雜的多。同時允許向兩臺主庫寫入很危險,極易造成複製錯誤或數據不一致。數據不同步還僅僅是開始。當正常的複製發生錯誤停止了,但應用仍然在同時向兩臺服務器寫入數據,這時候會發生什麼呢?不能簡單地把數據從一臺服務器複製到另外一臺,因爲這兩臺機器上需要複製的數據都可能發生了變化。解決這個問題將非常困難。總的來說,允許向兩個服務器上同時寫入所帶來的麻煩遠遠大於其帶來的好處。只要作爲從庫可寫,就存在主從數據不一致的風險。

(2)主動-被動模式下的雙主複製
        這是前面描述的雙主結構的變體,主要區別在於其中的一臺服務器是隻讀的被動服務器。這種拓撲結構能夠避免之前討論的問題,也是構建容錯性和高可用性系統的強大方式。兩個服務器從硬件到操作系統再到MySQL配置都應該完全相同。爲便於故障轉移,只讀最好由客戶端應用保證,通過設置以下系統變量強制只讀僅作爲可選項。

set global read_only=1;
set global super_read_only=1;

        這種方式使得反覆切換主動和被動服務器非常方便,因爲服務器的配置是對稱的。這使得故障轉移和故障恢復相對容易。它也允許用戶在不關閉服務器的情況下執行維護、優化、升級操作系統(或者應用程序、硬件等)或其它任務。        例如,執行alter table操作可能會鎖住整個表,阻塞對錶的讀寫,這可能會花費很長時間並導致服務中斷。

use test;
create table t1(a int auto_increment primary key);
insert into t1 select -1;
commit;

-- session 1
set autocommit=0; 
insert into t1 select null;

-- session 2
alter table t1 add column (b int);

-- session 3
update t1 set a=-2 where a=-1;

-- session 4
show processlist;

+-----+-------------+-----------------+------+-------------+------+-----------------------------------------------------------------------------+-----------------------------------+
| Id  | User        | Host            | db   | Command     | Time | State                                                                       | Info                              |
+-----+-------------+-----------------+------+-------------+------+-----------------------------------------------------------------------------+-----------------------------------+
|   1 | root        | localhost:33309 | NULL | Binlog Dump | 6159 | Master has sent all binlog to slave; waiting for binlog to be updated       | NULL                              |
|   3 | system user |                 | NULL | Connect     | 6104 | Waiting for master to send event                                            | NULL                              |
|   4 | system user |                 | NULL | Connect     |  340 | Slave has read all relay log; waiting for the slave I/O thread to update it | NULL                              |
| 170 | root        | localhost:33981 | test | Query       |   17 | Waiting for table metadata lock                                             | alter table t1 add column (b int) |
| 171 | root        | localhost:33982 | test | Query       |    9 | Waiting for table metadata lock                                             | update t1 set a=-2 where a=-1     |
| 172 | root        | localhost:33983 | test | Query       |    0 | init                                                                        | show processlist                  |
| 173 | root        | localhost:33986 | test | Sleep       |   25 |                                                                             | NULL                              |
+-----+-------------+-----------------+------+-------------+------+-----------------------------------------------------------------------------+-----------------------------------+
7 rows in set (0.00 sec)

        可以看到,如果在執行alter table時,表上有未提交的事務,alter table 本身和其後的所有DML都會等待table metadata lock,而不論這些後續的DML操作的是哪些行,因爲metadata lock是一個表級鎖。當session 1的事務提交或回滾,session 2才能得以執行。高併發場景下,在線DDL極有可能造成災難性的後果。一種暴力的解決方案是,先kill掉所有sleep的MySQL線程,緊接着執行alter table,這樣不會因爲metadata lock而卡住後面的DML。在這個例子中,如果session 2可以先得到執行,即使操作需要很長時間,也不會對後面的DML造成等待。腳本文件的內容可能爲:

#!/bin/bash
source ~/.bashrc

rm -rf /tmp/kill.sql
mysql -u root -p123456 -P3306 -h127.0.0.1 -e "select * into outfile '/tmp/kill.sql' from (select concat('kill ',id,';') from information_schema.processlist where command='sleep' union all select 'set sql_log_bin=0;' union all select 'alter table test.t1 add column (b int);') t;"

mysql -u root -p123456 -P3306 -h127.0.0.1 < /tmp/kill.sql

        注意,將所有sleep的線程都殺掉這個操作會導致沒有提交的事務回滾,是有風險的,需要根據業務場景進行操作。主庫上大表的DDL操作可能引起從庫的複製延時變大。在不影響數據一致性的前提下(如drop、truncate等等),一種可能的解決方案是執行DDL前先set sql_log_bin=0,讓大的DDL操作不寫入binlog,從而不會複製到從庫,之後再在從庫手動執行一遍。

        然而在主動-被動模式的雙主配置下,在線DDL變得更具可操作性。可以先停止主動服務器上的複製線程,這樣就不會複製被動服務器上執行的任何更新。然後在被動服務器上執行alter table操作,交換角色,最後在先前的主動服務器上啓動複製線程。這個服務器將會讀取中繼日誌並執行相同的alter語句。這可能花費很長時間,但不要緊,因爲該服務器沒有爲任何活躍查詢提供服務。假設A、B庫配置了雙主複製,A爲主動庫提供服務,B爲被動庫進行復制。如果需要在一個大表上增加字段,可能的操作步驟如下:

  1. A庫stop slave,此時A不會複製B的更新。
  2. B庫執行 alter table,B此時仍然複製來自A的更新。
  3. 交互角色,B變爲主動提供讀寫服務,A變爲被動,這意味着應用連接需要切換到B。
  4. A庫start slave,此時A將重放B上的alter table語句和其它更新。

        整個過程不停庫,只需修改應用連接的數據庫即可。如果使用虛IP技術,則應用不需要做任何修改,原本可能導致服務中斷的DDL操作將對應用完全透明。下面的過程用於驗證第2步操作中,B上的alter table不會阻塞它對A的複製。

-- 1. A停止複製
stop slave;

-- 2. B上執行一個長時間的alter table操作
alter table t1 add column (b int);

-- 3. 在上一步執行過程中,A上操作同一個表
call p1(1000000);

--  4. B確認複製狀態和線程狀態
show slave status\G
show processlist;
select max(a) from t1;

-- 5. 前面的步驟都執行完後,A開啓複製
start slave;

        show slave status的Read_Master_Log_Pos和Exec_Master_Log_Pos不停改變,show processlist中的State沒有任何鎖,t1表的數據一直處於更新狀態,說明B對A的複製不會被其上的alter table阻塞。注意,如果在第2步執行前從庫被修改表上有未提交的事務(從主庫複製過來),依然會阻塞第2步執行。但情況要比在主庫上緩解很多,其一是因爲從庫缺省爲單線程複製,沒有併發,事務應該很快被提交。其次是從庫可以設置成autocommit=on,這也會縮短alter table語句被阻塞的時間。調換以上步驟2和3的執行順序,可以驗證B對A的複製同樣也不會阻塞其上的alter table語句執行。        上面的步驟並非無懈可擊,成立的前提是alter table與複製的語句兼容,否則會導致複製錯誤。但通常來說都是先修改數據庫表結構,再升級應用程序,這樣看來此前提成立是自然而然的,問題並不大。下面的過程只是演示一種出錯的情況。

-- 1. A停止複製
stop slave;

-- 2. A上執行一個長時間的操作
call p1(1000000);

-- 3. 在上一步執行過程中,B上alter table同一個表
alter table t1 add column b int,drop column a;

-- 4. B確認複製狀態和線程狀態
show slave status\G

        由於t1.a列被刪除,添加了一列b,而列a與列b的數據類型不兼容,導致B庫上的複製報錯:

Last_Errno: 1677
Last_Error: Column 0 of table 'test.t1' cannot be converted from type 'bigint' to type 'int(11)'

        執行下面的修復後複製繼續:

alter table t1 change b a bigint auto_increment primary key;
stop slave;
start slave;

        讓我們看看主動服務器上更新時會發生什麼事情。更新被記錄到二進制日誌中,通過複製傳遞給被動服務器的中繼日誌中。被動服務器重放中繼日誌裏的查詢,如果開啓了log_slave_updates選項,它還會將複製事件記錄到自己的二進制日誌中。由於複製事件的服務器ID與主動服務器相同,因此主動服務器將忽略這些事件,通過這種方式避複製免死循環。設置主動-被動的雙主拓撲結構在某種意義上類似於創建一個熱備份,但可以使用這個“備份”來提高性能,例如,用它來執行讀操作、備份、輪換維護以及升級等。

(3)擁有從庫的雙主結構
        另外一種相關的配置是爲每個主庫增加一個從庫,如圖4所示。

圖4

        這種配置的優點是增加了冗餘,對於不同地理位置的複製拓撲,能夠消除站點單點失效的問題。也可以像平常一樣,將讀查詢分配到從庫上。如果在本地爲了故障轉移使用雙主結構,這種配置同樣有用。當主庫失效時,有兩種不同的處理方式,一是用從庫代替主庫,二是把從庫指向另一個不同的主庫。以圖4爲例,假設主庫1失效,採用第一種方式,需要將從庫1提升爲新的主庫1,修改主庫2的複製配置,指向新的主庫1,並將新主庫指向主庫2,保持雙主配置。如果採用第二種方式,只需要將從庫1指向主庫2,但這樣拓撲已從雙主變爲一主兩從。

(4)環形複製
        如圖5所示,雙主結構實際上是環形結構的一種特例。環形結構可以有三個或更多的主庫。每個服務器都是在它之前的服務器的備庫,是在它之後的服務器的主庫。這種結構也稱爲環形複製(circular replication)。

圖5

        環形結構沒有雙主結構的一些優點,例如對稱配置和簡單的故障轉移,並且完全依賴於環上的每一個可用節點,這大大增加了整個系統失效的機率。如果從環中移除一個節點,這個節點發起的事件就會陷入無限循環:它將永遠繞着服務器循環。因爲唯一可以根據服務器ID將其過濾的服務器是創建這個事件的服務器。下面的步驟可以模擬這種場景,M1、M2、M3構成的三個主庫的環形複製,M1複製M3、M3複製M2、M2複製M1。

-- 1. M1停止sql_thread線程
stop slave sql_thread;

-- 2. M2停止sql_thread線程
stop slave sql_thread;

-- 3. M3做更新
insert into test.t1 values (1);
commit;

-- 4. M3停庫
mysqladmin -uroot -p123456 shutdown

-- 5. M1啓動sql_thread線程,此時M3的更新複製到M1
start slave sql_thread;

-- 6. M1複製M2,此時原環形複製中移除了M3,其中master_log_file和master_log_pos從M2的show master status的輸出得到。

stop slave;
change master to
master_host = '172.16.1.126',
master_port = 3306,
master_user = 'repl',
master_password = '123456',
master_auto_position = 0,
master_log_file='binlog.000002',
master_log_pos=664210;
start slave;

-- 7. M2啓動sql_thread線程,此時M2複製了來自M3的更新,並繼續傳遞給M1,複製陷入死循環。在M1、M2上查詢test.t1,可以看到記錄不停增長。
start slave sql_thread;

        如果三個主庫都啓用GTID複製,以上過程不會陷入死循環,因爲複製不再通過server_id過濾本地事件,而是通過server_uuid複製事務。總的來說,環形結構非常脆弱,應該儘量避免。可以通過爲每個節點增加從庫的方式來減少環形複製的風險,如圖6所示。但這僅僅防範了服務器失效的風險,斷電或者其它一些影響到網絡連接的問題都可能破壞整個環。

圖6

4. 多源複製

        MySQL 5.7.6開始支持多源複製(Multi-Source Replication)。多源複製不同於多主複製,多主複製指的是在複製拓撲中存在多個主庫,而多源複製是指一個從庫可以同時從多個主庫進行復制。圖7所示爲兩主一從的多源複製結構。

圖7

        多源複製可用於將來自多個服務器的數據合併或備份到單個服務器,如合併表分片。應用事務時,多源複製不會檢測或解決任何衝突,如果需要,這些任務將留給應用程序實現。在多源複製拓撲中,從庫爲每個接收其事務的主庫創建複製通道。

(1)複製通道
        複製通道是一個字符串,表示從主庫到從庫的複製路徑。爲提供與先前版本的兼容性,MySQL服務器在啓動時自動創建一個默認通道,其名稱爲空字符串("")。這個通道始終存在,不能被用戶創建或銷燬。如果沒有創建其它通道(具有非空名稱),則複製語句僅作用於缺省通道,以便舊版從庫的所有複製語句按預期運行。多源複製中,從庫打開多個命名通道,每個通道都有自己的中繼日誌和複製線程。一旦複製通道的I/O線程接收到事務,它們就會被添加到通道對應的中繼日誌文件中並傳遞給SQL線程。這使得每個通道能夠獨立運行。複製通道還與主機名和端口關聯。可以將多個通道分配給主機名和端口的相同組合。在MySQL 8.0中,添加到一個從庫的最大通道數爲256。每個複製通道獨立配置,必須具有唯一非空名稱。

(2)配置
        多源複製拓撲至少需要配置兩個主庫和一個從庫。可以將多源複製中的主庫配置爲使用基於全局事務標識符(GTID)的複製或基於二進制日誌位置的複製。配置多源複製的步驟如下。

        1. 將從庫的master_info_repository、relay_log_info_repository系統變量設置爲TABLE。

stop slave;
set global master_info_repository = 'table';
set global relay_log_info_repository = 'table';

        這是MySQL 8.0的默認值。多源複製拓撲中的從庫需要使用表存儲主庫二進制日誌和本身中繼日誌的信息,多源複製與基於文件(file)的存儲庫不兼容。現在不推薦將這兩個參數設置爲'file'。

        2. 將主庫添加到從庫

change master to
master_host = '172.16.1.125',
master_port = 3306,
master_user = 'repl',
master_password = '123456',
master_auto_position = 1
for channel 'master-125';

change master to
master_host = '172.16.1.126',
master_port = 3306,
master_user = 'repl',
master_password = '123456',
master_auto_position = 1
for channel 'master-126';

        這裏使用GTID複製,設置兩主一從的多源複製。CHANGE MASTER TO語句通過使用FOR CHANNEL子句將新主庫添加到複製通道。多源複製與自動定位兼容。

        3. 啓動從庫複製

-- 啓動所有線程所有通道的複製
start slave; 

-- 啓動所有通道的io_thread線程
start slave io_thread; 

-- 啓動所有通道的sql_thread線程
start slave sql_thread; 啓動所有通道的sql_thread線程

-- 啓用單個通道 
start slave for channel 'master_125';
start slave io_thread for channel 'master_125';
start slave sql_thread for channel 'master_125';

        停止複製命令也啓動複製類似,只是把Start換成stop。同樣重置也可以選擇重置所有和重置單一通道:

reset slave;
reset slave for channel 'master_125';

(3)監控
        監控可以使用performance_schema.replication*表,這些表的第一列都是Channel_Name。注意SHOW VARIABLES語句不適用於多個複製通道。這些變量的信息已遷移到複製性能表。在具有多個通道的拓撲中使用SHOW VARIABLES語句僅顯示默認通道的狀態。

-- 查詢特定通道的連接狀態
mysql> select * from replication_connection_status where channel_name='master-125'\G
*************************** 1. row ***************************
                                      CHANNEL_NAME: master-125
                                        GROUP_NAME: 
                                       SOURCE_UUID: 8eed0f5b-6f9b-11e9-94a9-005056a57a4e
                                         THREAD_ID: 10421
                                     SERVICE_STATE: ON
                         COUNT_RECEIVED_HEARTBEATS: 41
                          LAST_HEARTBEAT_TIMESTAMP: 2019-06-24 16:21:31.583443
                          RECEIVED_TRANSACTION_SET: 
                                 LAST_ERROR_NUMBER: 0
                                LAST_ERROR_MESSAGE: 
                              LAST_ERROR_TIMESTAMP: 0000-00-00 00:00:00.000000
                           LAST_QUEUED_TRANSACTION: 
 LAST_QUEUED_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
LAST_QUEUED_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
     LAST_QUEUED_TRANSACTION_START_QUEUE_TIMESTAMP: 0000-00-00 00:00:00.000000
       LAST_QUEUED_TRANSACTION_END_QUEUE_TIMESTAMP: 0000-00-00 00:00:00.000000
                              QUEUEING_TRANSACTION: 
    QUEUEING_TRANSACTION_ORIGINAL_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
   QUEUEING_TRANSACTION_IMMEDIATE_COMMIT_TIMESTAMP: 0000-00-00 00:00:00.000000
        QUEUEING_TRANSACTION_START_QUEUE_TIMESTAMP: 0000-00-00 00:00:00.000000
1 row in set (0.00 sec)

        使用SHOW SLAVE STATUS FOR CHANNEL監控特定通道的狀態,如果不加FOR CHANNEL子句,則返回所有複製通道的狀態,每個通道一行。(4)簡單測試

-- 主庫1
mysql> insert into test.t1 values(125);
Query OK, 1 row affected (0.01 sec)

-- 主庫2
mysql> insert into test.t1 values(126);
Query OK, 1 row affected (0.01 sec)

-- 從庫
mysql> select * from test.t1;
+------+
| a    |
+------+
|    1 |
|  125 |
|  126 |
+------+
3 rows in set (0.00 sec)

-- 主庫1
mysql> truncate table test.t1;
Query OK, 0 rows affected (0.01 sec)

-- 從庫
mysql> select * from test.t1;
Empty set (0.00 sec)

        兩個主庫新增的數據都複製到從庫,但只在一個主庫清空表,從庫表所有數據全部被清空。因此使用多源複製要避免多個主庫具有同名的數據庫。

-- 主庫1
mysql> create user 'u1'@'%' identified by '123456';
Query OK, 0 rows affected (0.01 sec)

-- 主庫2
mysql> create user 'u1'@'%' identified by '123456';
Query OK, 0 rows affected (0.01 sec)

-- 從庫
mysql> show slave status\G

        通道master-125複製狀態正常,但master-126報錯:

Last_SQL_Errno: 1396
Last_SQL_Error: Error 'Operation CREATE USER failed for 'u1'@'%'' on query. Default database: 'test'. Query: 'CREATE USER 'u1'@'%' IDENTIFIED WITH 'caching_sha2_password' AS '$A$005$*B_B^@}R;15egC4\nYdRPGtaEXbF.jB36e2UpAZEoXEPck87oeMl4j8rO6iu5''

        建用戶的時候報告1396錯誤,原因是mysql庫中已經有了這個用戶。恢復複製的過程如下:
        1. 停止從庫通道master-126的複製

stop slave for channel 'master-126';

        2. 在從庫上確認出錯的事務ID

show slave status for channel 'master-126'\G
...
Retrieved_Gtid_Set: 53442434-8bfa-11e9-bc15-005056a50f77:1008-1009
            Executed_Gtid_Set: 53442434-8bfa-11e9-bc15-005056a50f77:1-1008,
6a739bf0-961d-11e9-8dd8-005056a5497f:1-1885,
8eed0f5b-6f9b-11e9-94a9-005056a57a4e:1-24240
...

        可以看到,從庫從53442434-8bfa-11e9-bc15-005056a50f77接收到事務1009,但只執行到1008,所以確定報錯的事務爲:53442434-8bfa-11e9-bc15-005056a50f77:1009。

        3. 在從庫上注入一個空事務跳過錯誤

set gtid_next='53442434-8bfa-11e9-bc15-005056a50f77:1009';
begin;commit;
set gtid_next=automatic;
start slave for channel 'master-126';

        對於mysql庫,建議使用REPLICATE_IGNORE_DB將其屏蔽掉:

stop slave;
change replication filter replicate_ignore_db = (mysql);
start slave;

        在主庫上對mysql庫進行操作時,需要加use mysql,否則不會進行過濾。

5. Blackhole引擎與日誌服務器

(1)Blackhole存儲引擎與複製
        MySQL 8中show engines命令返回存儲引擎如下:

mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
9 rows in set (0.00 sec)

        像MyISAM、InnoDB一樣,BlackHole是另一種MySQL引擎。該引擎的功能可謂名副其實,任何寫入到此引擎的數據均會被丟棄掉,不做實際存儲,和Linux中的 /dev/null 文件所起的作用類似。創建一個blackhole的表時,MySQL服務器在數據庫目錄創建一個.frm表定義文件,沒有其他文件關聯到這個表。雖然blackhole表不存儲任何數據,但它卻能夠接收並重放二進制日誌,如果開啓了log_slave_updates,它也能把複製向下傳播,如同普通的級聯複製拓撲一樣。

        當從庫足夠多時,會對主庫造成很大的負載。每個從庫會在主庫上創建一個線程執行binlog dump命令。該命令讀取二進制文件中的數據並將其發送給從庫。每個從庫都會重複這樣的工作,它們不會共享binlog dump的資源。如果從庫很多,並且有大的事件時,例如binlog_format爲statement時一次很大的load data infile操作,主庫的負載會顯著上升,甚至可能由於從庫同時請求同樣的事件而耗盡內存並崩潰。另一方面,如果從庫請求的數據不在文件系統的緩存中,可能會導致大量的磁盤檢索,這同樣會影響主庫的性能並增加鎖的競爭。

        因此,如果需要多個從庫,一個好辦法是從主庫移除負載並利用blackhole進行分發,即所謂的分發主庫。分發主庫實際上也是一個從庫,它唯一的目的就是提取和提供主庫的二進制日誌。多個從庫連接到分發主庫,這使原來的主庫擺脫了負擔,如圖8所示。

圖8

        很難說當主庫數據達到多少時需要一個分發主庫。按照通用準則,如果主庫接近滿負載,不應該爲其建立10個以上的從庫。如果只有少量寫操作,或者只複製其中一部分表,則主庫可以提供更多的複製。如果需要,可以使用多個分發主庫向大量從庫進行復制,或者使用級聯的分發主庫。對於跨數據中心的複製,設置slave_compressed_protocol能節約一些主庫帶寬。該變量是全局系統變量,缺省值爲off,可以動態設置。

        還可以通過分發主庫實現其它目的,如對二進制日誌事件執行過濾和重放規則。這比在每個從庫上重複進行日誌記錄、重放和過濾要高效得多。使用blackhole存儲引擎可以支持更多的從庫。雖然會在分發主庫執行查詢,但代價極小,因爲blackhole表中沒有任何數據。

        一個常見的問題是如何確保分發服務器上的每個表都是blackhole存儲引擎。如果在主庫創建了一個表並指定了不同的存儲引擎呢?確實,不管何時在從庫上使用不同的存儲引擎總會導致同樣的問題。通常的解決方案是設置服務器的缺省存儲引擎:

default_storage_engine=blackhole

        這隻會影響那些沒有指定存儲引擎的create table的語句。如果有一個無法控制的應用,這種拓撲結構可能會非常脆弱。可以設置disabled_storage_engines禁用多個存儲引擎。該系統變量爲只讀,只能通過配置文件修改,並重啓MySQL服務器使之生效。下面演示如何聯機搭建一個blackhole的分發主庫。

  • 服務器角色分配:

172.16.1.125:主庫。假設爲生產主庫,可以在以下整個過程中存在負載。
172.16.1.126:blackhole分發主庫。一個初始化的MySQL服務器。
172.16.1.127:從庫。

  • MySQL服務器配置:

172.16.1.125:
[mysqld]
server_id=1125
gtid_mode=ON
enforce-gtid-consistency=true

172.16.1.126:
[mysqld]
server_id=1126
gtid_mode=ON
enforce-gtid-consistency=true
default_storage_engine=blackhole
default_tmp_storage_engine=blackhole
disabled_storage_engines='innodb'
secure_file_priv='/tmp'

172.16.1.127:
server_id=1127
gtid_mode=ON
enforce-gtid-consistency=true

        其它配置使用MySQL 8的缺省值。啓用GTID複製,三臺MySQL服務器均已創建複製賬號。

(1)初始化blackhole分發主庫
        在126執行執行內容如下的腳本文件init_blackhole.sh

source ~/.bashrc
# 全量導入主庫,無數據
mysqldump --single-transaction --all-databases --host=172.16.1.125 -d --user=wxy --password=123456 | mysql -uroot -p123456 

# 修改所有表的存儲引擎爲blackhole
rm -rf /tmp/black.sql
mysql -uroot -p123456 -e "
select concat('alter table ', table_schema, '.', table_name, ' engine=''blackhole''', ';') 
  from information_schema.tables 
 where table_schema not in ('information_schema','mysql','performance_schema','sys')
   and table_type='BASE TABLE' into outfile '/tmp/black.sql';"

# 在執行的SQL文件第一行加入sql_log_bin=0,否則下級從庫也會執行
sed -i '1i\set sql_log_bin=0;' /tmp/black.sql
mysql -uroot -p123456 < /tmp/black.sql

(2)初始化從庫
        因爲是聯機配置複製,使用xtrabackup初始化從庫。

# 將主庫備份到從庫,在125執行
xtrabackup -uroot -p123456 --socket=/tmp/mysql.sock --no-lock --backup --compress --stream=xbstream --parallel=4 --target-dir=./ | ssh [email protected] "xbstream -x -C /usr/local/mysql/data/ --decompress"

# 從庫執行應用日誌,在127執行
xtrabackup --prepare --target-dir=/usr/local/mysql/data/

# 啓動從庫,在127執行
mysqld_safe --defaults-file=/etc/my.cnf &

(3)啓動複製

-- 在126執行
change master to
master_host = '172.16.1.125',
master_port = 3306,
master_user = 'repl',
master_password = '123456',
master_auto_position = 1;
start slave;
show slave status\G

-- 在127執行
change master to
master_host = '172.16.1.126',
master_port = 3306,
master_user = 'repl',
master_password = '123456',
master_auto_position = 1;
start slave;
show slave status\G

        至此完成了分發主庫的複製拓撲結構的搭建。

(2)日誌服務器
        使用MySQL複製的另一種用途是創建沒有數據的日誌服務器。它唯一的目的就是更加容易重放或過濾二進制日誌事件。假設有一組二進制日誌或中繼日誌,可能從備份或者一臺崩潰的服務器上獲取,希望能夠重放這些日誌中的事件。最容易想到的是通過mysqlbinlog命令行工具從其中提取出事件,但更加方便和高效得方法是配置一個沒有任何應用數據的MySQL實例並使其認爲這些二進制日誌是它擁有的。因爲無須執行二進制日誌,日誌服務器也就不需要任何數據,它的目的僅僅是將複製事件提供給別的服務器。

        我們來看看該策略是如何工作的。假設日誌被命名爲binlog.000001、binlog.000002等等,將這些日誌放到日誌服務器的日誌文件夾中,假設爲/usr/local/mysql/data。然後在啓動服務器前編輯my.cnf文件:

log_bin = /usr/local/mysql/data/binlog
log_bin_index = /usr/local/mysql/data/binlog.index

        服務器不會自動發現日誌文件,因此還需要更新日誌的索引文件。Linux上可用下面的命令完成。

/bin/ls -1 /usr/local/mysql/data/binlog.[0-9]* > /usr/local/mysql/data/binlog.index

        確保運行MySQL的賬戶能夠讀寫日誌索引文件。現在可以啓動日誌服務器並通過show master logs命令來確保其找到日誌文件。當主庫失效但二進制日誌尚存,可以設置一個日誌服務器,把從庫指向它,然後讓所有從庫趕上主庫的失效點。

        相比於使用mysqlbinlog來實現恢復,日誌服務器有優勢主要體現在:

  • 速度快,因爲無須將語句從日誌導出來並傳給MySQL。
  • 可以觀察到複製過程。
  • 容易處理錯誤,如跳過執行失敗的語句。
  • 便於過濾複製事件。

二、複製性能

        我們可以將複製的時間分爲兩部分:一是事件從主庫到從庫的傳輸時間,二是事件在從庫上的執行時間。事件在主庫上記錄二進制日誌後到傳遞到從庫的時間理論上非常快,因爲它只取決於網絡速度。MySQL二進制日誌的dump線程不是通過輪詢方式請求事件,而是由主庫來通知從庫新的事件,因爲前者低效且緩慢。從主庫讀取一個二進制日誌事件是一個阻塞型網絡調用,當主庫記錄事件後,馬上就開始發送。因此可以說,只要I/O線程被喚醒並且能夠通過網絡傳輸數據,事件就會很快到達從庫。但如果網絡很慢並且二進制日誌事件很大,記錄二進制日誌和在從庫上執行的延遲可能會非常明顯。如果查詢需要執行很長時間而網絡很快,通常可以認爲重放時間佔據了更多的複製時間開銷。

        本節主要從日誌持久化、組提交與多線程複製,以及新增的WRITESET特性三個方面,討論對複製性能產生的影響。我們先簡要介紹每種特性的基礎知識,然後針對不同情況進行測試,最後由測試結果得出結論。所有測試均基於GTID的標準主從異步複製。

1. 測試規劃

        這裏使用的思路是:記錄主庫加壓前後的GTID,得到從庫需要執行的事務數。然後在從庫上執行復制,記錄執行時間,得到從庫的每秒執行事務數(TPS)作爲衡量複製性能的指標。測試目的在於對比不同情況下複製的性能,而不是針對測量絕對值進行優化。主庫加壓使用tpcc-mysql基準測試工具。

(1)測試環境
        測試環境如下,已經配置好GTID異步複製。

主庫:172.16.1.125
從庫:172.16.1.126
MySQL版本:8.0.16

測試通用參數:
主庫:
server_id=1125
gtid_mode=ON
enforce-gtid-consistency=true
innodb_buffer_pool_size=4G

從庫:
server_id=1126
gtid_mode=ON
enforce-gtid-consistency=true
innodb_buffer_pool_size=4G

(2)tpcc-mysql測試前準備

        TPC-C是專門針對聯機交易處理系統(OLTP系統)的規範,tpcc-mysql則是percona公司基於TPC-C衍生出來的產品,專用於MySQL基準測試,下載地址爲https://github.com/Percona-Lab/tpcc-mysql。這裏使用tpcc-mysql只是爲了給主庫加壓。使用tpcc-mysql開始測試前完成以下準備工作,所有步驟均在主庫上執行:
        1. 安裝

cd tpcc-mysql-master/src
make

        2. 建立測試庫

mysql -uroot -p123456 -e "create database tpcc_test;"

        3. 建表和索引

cd tpcc-mysql-master
mysql -uroot -p123456 -Dtpcc_test < create_table.sql
mysql -uroot -p123456 -Dtpcc_test < add_fkey_idx.sql

        4. 生成數據

tpcc_load -h127.0.0.1 -d tpcc_test -u root -p "123456" -w 10

        -w參數指定建立的倉庫數。

        5. 備份測試庫

mysqldump --databases tpcc_test -uroot -p123456 --set-gtid-purged=off > tpcc_test.sql

        爲在同等環境下進行比較,每次測試前都要重新生成測試庫中的表、索引和數據,因此這裏做一個測試庫的邏輯備份。一定要加--set-gtid-purged=off,因爲將備份導入主庫時,需要在從庫通過複製同時生成。        下面是每次測試在從庫執行的自動化腳本:

# 初始化tpcc數據
mysql -uwxy -p123456 -h172.16.1.125 < tpcc_test.sql

# 讀取主庫的二進制座標
read master_file master_pos < <(mysql -uwxy -p123456 -h172.16.1.125 -e "show master status;" --skip-column-names | awk '{print $1,$2}')

# 從庫初始化tcpp數據結束後停止複製
mysql -uwxy -p123456 -e "select master_pos_wait('$master_file',$master_pos);stop slave;"

# 取得從庫開始GTID
read start_gtid < <(mysql -uwxy -p123456 -e "show variables like 'gtid_executed';" --skip-column-names | awk '{print $2}' | sed "s/\\\n//g")

# 主庫執行壓測,10個倉庫,32個併發線程,預熱1分鐘,壓測5分鐘
tpcc_start -h172.16.1.125 -d tpcc_test -u wxy -p "123456" -w 10 -c 32 -r 60 -l 300 > tpcc_test.log 2>&1

# 讀取主庫的二進制座標
read master_file master_pos < <(mysql -uwxy -p123456 -h172.16.1.125 -e "show master status;" --skip-column-names | awk '{print $1,$2}')

# 從庫複製開始時間
start_time=`date '+%s'`

# 從庫執行復制
mysql -uwxy -p123456 -e "start slave;select master_pos_wait('$master_file',$master_pos);"

# 從庫複製結束時間
end_time=`date '+%s'`

# 複製執行時長
elapsed=$(($end_time - $start_time))

# 取得從庫結束GTID
read end_gtid < <(mysql -uwxy -p123456 -e "show variables like 'gtid_executed';" --skip-column-names | awk '{print $2}' | sed "s/\\\n//g")

# 取得從庫執行的事務數
read start end < <(mysql -uwxy -p123456 -e "select gtid_subtract('$end_gtid','$start_gtid');" --skip-column-names | awk -F: '{print $2}' | awk -F- '{print $1,$2}')
trx=$(($end - $start + 1))

# 計算從庫、主庫的TPS
Slave_TPS=`expr $trx / $elapsed`
Master_TPS=`expr $trx / 360`

# 打印輸出
echo "TRX: $trx" "Elapsed: $elapsed" "Slave TPS: $Slave_TPS" "Master TPS: $Master_TPS"

2. sync_binlog與innodb_flush_log_at_trx_commit

        sync_binlog控制MySQL服務器將二進制日誌同步到磁盤的頻率,可取值0、1、N,MySQL 8的缺省值爲1。innodb_flush_log_at_trx_commit控制提交時是否將innodb日誌同步到磁盤,可取值0、1、2,MySQL 8的缺省值爲1。關於這兩個參數已經在“MySQL 8 複製(一)——異步複製”中詳細討論,這裏不再贅述。簡單說,對於複製來講,sync_binlog爲0可能造成從庫丟失事務,innodb_flush_log_at_trx_commit爲0可能造成從庫比主庫事務多。而從性能角度看,雙1的性能最差,雙0的性能最好。權衡數據安全與性能,一般建議主庫都設置爲雙1,根據場景從庫可以設置成其它組合來提升性能。

        下表所示爲從庫上sync_binlog、innodb_flush_log_at_trx_commit四種設置的測試結果:

sync_binlog

innodb_flush_log_at_trx_commit

事務數

複製執行時間(秒)

從庫TPS

主庫TPS

0

0

183675

330

556

510

0

1

184177

498

369

511

1

0

183579

603

304

509

1

1

183020

683

267

508

        測試中主庫執行了一共360秒(預熱+壓測),TPS爲510。從表中可以明顯看到這兩個參數的不同組合對複製性能的影響。當從庫僅爲單線程複製時,只有雙0的設置在執行時間和TPS上優於主庫,其它組合會造成複製延遲。

3. 組提交與多線程複製

        MySQL 5.6支持多線程複製(multi-threaded slave,MTS),但太過侷限。它只實現了基於schema的多線程複製,使不同數據庫下的DML操作可以在從庫並行重放,這樣設計的複製效率並不高。如果用戶實例僅有一個庫,那麼就無法實現並行重放,甚至性能會比原來的單線程更差,而單庫多表是比多庫多表更爲常見的一種情形。

        MySQL 5.7的多線程複製基於組提交實現,不再有基於schema的多線程複製限制。

(1)組提交
        從MySQL 5.6開始同時支持Innodb redo log和binlog組提交,並且默認開啓,大大提高了MySQL的事務處理性能。和很多RDBMS一樣,MySQL爲了保證事務處理的一致性和持久性,使用了WAL(Write Ahead Log)機制,即對數據文件進行修改前,必須將修改先記錄日誌。Redo log就是一種WAL的應用,每次事務提交時,不用同步刷新磁盤數據文件,只需要同步刷新redo log就夠了。相比寫數據文件時的隨機I/O,寫Redo log時的順序I/O能夠提高事務提交速度。Redo log的刷盤操作將會是最終影響MySQL TPS的瓶頸所在。爲了緩解這一問題的影響,MySQL使用了redo log組提交,將多個redo log刷盤操作合併成一個。

        爲了保證redo log和binlog的數據一致性,MySQL使用了兩階段提交(prepare階段和commit階段),由binlog作爲事務的協調者。而引入兩階段提交使得binlog又成爲了性能瓶頸,於是MySQL 5.6增加了binlog的組提交,目的同樣是將binlog的多個刷盤操作合併成一個。結合redo log本身已經實現的組提交,將提交過程分成Flush stage、Sync stage、Commit stage三個階段完成組提交,最大化每次刷盤的收益,弱化磁盤瓶頸。每個階段都有各自的隊列,使每個會話的事務進行排隊,提高併發性能。

        Flush階段:

  • 首先獲取隊列中的事務組,將redo log中prepare階段的數據刷盤。
  • 將binlog數據寫入文件系統緩衝,並不能保證數據庫崩潰時binlog不丟失。
  • Flush階段隊列的作用是提供了redo log的組提交。
  • 如果在這一步完成後數據庫崩潰,由於協調者binlog中不保證有該組事務的記錄,所以MySQL可能會在重啓後回滾該組事務。

        Sync階段:

  • 將binlog緩存sync到磁盤,sync_binlog=1時該隊列中所有事務的binlog將永久寫入磁盤。
  • 爲了增加一組事務中的事務數量,提高刷盤收益,MySQL使用兩個參數控制獲取隊列事務組的時機:

            binlog_group_commit_sync_delay=N:在等待N微秒後,開始事務刷盤。
            binlog_group_commit_sync_no_delay_count=N:如果隊列中的事務數達到N個,就忽視binlog_group_commit_sync_delay的設置,直接開始刷盤。

  • Sync階段隊列的作用是支持binlog的組提交。
  • 如果在這一步完成後數據庫崩潰,由於協調者binlog中已經有了事務記錄,MySQL會在重啓後通過Flush階段中Redo log刷盤的數據繼續進行事務的提交。

        Commit階段:

  • 首先獲取隊列中的事務組。
  • 依次將redo log中已經prepare的事務在存儲引擎層提交,清除回滾信息,向redo log中寫入COMMIT標記。
  • Commit階段不用刷盤,如上所述,Flush階段中的redo log刷盤已經足夠保證數據庫崩潰時的數據安全了。
  • Commit階段隊列的作用是承接Sync階段的事務,完成最後的引擎提交,使得Sync可以儘早的處理下一組事務,最大化組提交的效率。

        Commit階段會受到參數binlog_order_commits的影響,當該參數爲OFF時,不保證binlog和事務提交的順序一致,因爲此時允許多個線程發出事務提交指令。也正是基於同樣的原因,可以防止逐個事務提交成爲吞吐量瓶頸,性能會有少許提升。多數情況下,存儲引擎的提交指令與binlog不同序無關緊要,因爲多個單獨事務中執行的操作,無論提交順序如何都應該產生一致的結果。但也不是絕對的,例如會影響XtraBackup工具的備份。XtraBackup會從innodb page中獲取最後提交事務的binlog位置信息,binlog_order_commits=0時事務提交順序和binlog順序可能不一致,這樣此位置前可能存在部分prepare狀態的事務,這些事務在備份恢復後會因回滾而丟失。

        binlog_order_commits的缺省值爲ON,此時存儲引擎的事務提交指令將在單個線程上串行化,以致事務始終以與寫入二進制日誌相同的順序提交。

        這裏有一篇MySQL組提交的圖解說明:[圖解MySQL]MySQL組提交(group commit)

(2)多線程複製
        MySQL 5.6開始出現基於schema的多線程複製,簡單說就是主庫上不同數據庫上的DML可以在從庫上並行重放。因爲大多數生產環境依然習慣於單庫多表的架構,這種情況下MTS依然還是單線程的效果。MySQL 5.7實現了基於組提交的多線程複製,其思想簡單易懂:主庫上同一個組提交的事務可以在從庫並行重放,因爲這些事務之間沒有任何衝突,由存儲引擎的ACID所保證。爲了與5.6版本兼容,5.7引入了新的變量slave_parallel_type,可以配置爲下面兩個值之一:

  • DATABASE:缺省值,基於schema的多線程複製方式。
  • LOGICAL_CLOCK:基於組提交的多線程複製方式。

        那麼從庫如何知道事務是否在一組中呢?

MySQL 5.7的設計方式是將組提交信息存放在二進制日誌的GTID_EVENT中。

[mysql@hdp2/usr/local/mysql/data]$mysqlbinlog binlog.000064 | grep last_committed | awk '{print $11, $12}' | head -10
last_committed=0 sequence_number=1
last_committed=0 sequence_number=2
last_committed=0 sequence_number=3
last_committed=0 sequence_number=4
last_committed=0 sequence_number=5
last_committed=0 sequence_number=6
last_committed=0 sequence_number=7
last_committed=0 sequence_number=8
last_committed=0 sequence_number=9
last_committed=0 sequence_number=10
[mysql@hdp2/usr/local/mysql/data]$

        last_committed表示事務提交的時候,上次事務提交的編號。事務在perpare階段獲取相同的last_committed而且相互不影響,最終會作爲一組進行提交。如果事務具有相同的last_committed,表示這些事務都在一組內,可以進行並行重放。例如上述last_committed爲0的10個事務在從庫是可以進行並行重放的。這個機制是Commit-Parent-Based Scheme的實現方式。

        由於在MySQL中寫入是基於鎖的併發控制,所以所有在主庫同時處於prepare階段且未提交的事務就不會存在鎖衝突,從庫就可以並行執行。Commit-Parent-Based Scheme使用的就是這個原理,簡單描述如下:

  • 主庫上有一個全局計數器(global counter)。每一次存儲引擎提交之前,計數器值就會增加。
  • 主庫上,事務進入prepare階段之前,全局計數器的當前值會被儲存在事務中,這個值稱爲此事務的commit-parent。
  • 主庫上,commit-parent會在事務的開頭被儲存在binlog中。
  • 從庫上,如果兩個事務有同一個commit-parent,它們就可以並行被執行。

        此commit-parent就是在binlog中看到的last_committed。如果commit-parent相同,即last_committed相同,則被視爲同一組,可以並行重放。

        Commit-Parent-Based Scheme的問題在於會降低複製的並行程度,如圖9所示(引自WL#7165)。

圖9

        每一個水平線代表一個事務,時間從左到右。P表示事務在進入prepare階段之前讀到的commit-parent值的那個時間點,可以簡單視爲加鎖時間點。C表示事務增加了全局計數器值的那個時間點,可以簡單視爲釋放鎖的時間點。P對應的commit-parent是取自所有已經執行完的事務的最大的C對應的sequence_number,舉例來說:Trx4的P對應的commit-parent是Trx1的C對應的sequence_number。因爲這個時候Trx1已經執行完,但是Trx2還未執行完。Trx5的P對應的commit-parent是Trx2的C對應的sequence_number。Trx6的P對應的commit-parent是Trx2的C對應的sequence_number。

        Trx5和Trx6具有相同的commit-parent,在進行重放的時候,Trx5和Trx6可以並行執行。Trx4和Trx5不能並行執行,Trx6和Trx7也不能並行執行,因爲它們的commit-parent不同。但注意到,在同一時段,Trx4和Trx5、Trx6和Trx7分別持有它們各自的鎖,事務互不衝突,所以在從庫上並行執行是不會有問題的。針對這種情況,爲了進一步增加並行度,MySQL對並行複製的機制做了改進,提出了一種新的並行複製的方式:Lock-Based Scheme,使同時持有各自鎖的事務可以在從庫並行執行。

        Lock-Based Scheme定義了一個稱爲lock interval的概念,表示一個事務持有鎖的時間間隔。假設有兩個事務Trx1、Trx2,Trx1先於Trx2。那麼,當且僅當Trx1、Trx2的lock interval有重疊,則可以並行執行。換言之,若Trx1結束自己的lock interval早於Trx2開始自己的lock interval,則不能並行執行。如圖10所示,L表示lock interval的開始點,C表示lock interval的結束。

圖10

        對於C(lock interval的結束點),MySQL會給每個事務分配一個邏輯時間戳(logical timestamp),命名爲transaction.sequence_number。此外,MySQL會獲取全局變量global.max_committed_transaction,表示所有已經結束lock interval的事務的最大的sequence_number。對於L(lock interval的開始點),MySQL會把global.max_committed_transaction分配給一個變量,並取名叫transaction.last_committed。transaction.sequence_number和transaction.last_committed這兩個時間戳都會存放在binlog中,就是前面看到的last_committed和sequence_number。

        根據以上分析得出,只要事務和當前執行事務的Lock Interval都存在重疊,就可以在從庫並行執行。圖9中,Trx3、Trx4、Trx5、Trx6四個事務可以並行執行,因爲Trx3的sequence_number大於Trx4、Trx5、Trx6的last_committed,即它們的Lock Interval存在重疊。當Trx3、Trx4、Trx5執行完成之後,Trx6和Trx7可以併發執行,因爲Trx6的sequence_number大於Trx7的last_committed,即兩者的lock interval存在重疊。Trx5和Trx7不能併發執行,因爲Trx5的sequence_number小於Trx7的last_committed,即兩者的lock interval不存在重疊。

        可以通過以下命令粗略查看併發度:

[mysql@hdp2/usr/local/mysql/data]$mysqlbinlog binlog.000064 | grep -o 'last_committed.*' | sed 's/=/ /g' | awk '{print $4-$2-1}' | sort -g | uniq -c
   1693 0
   4795 1
   8174 2
  11378 3
  13879 4
  15407 5
  15979 6
  15300 7
  13762 8
  11471 9
   9061 10
   6625 11
   4533 12
   3006 13
   1778 14
   1021 15
    521 16
    243 17
    135 18
     61 19
     31 20
     23 21
     18 22
      7 23
      5 24
      7 25
      3 26
      3 27
      6 28
      1 29
      1 30
      2 31
      1 32
      3 33
      3 34
      1 37
      1 39
      1 40
      1 42
      1 44
      1 46
      1 49
      1 50
      1 56
      1 120

        第一列爲事務數量,第二列表示這些事務能與它們之前的多少個事務並行執行。例如有1693個事務不能與之前的事務併發,必須等到所有前面的事務完成之後才能開始,但並不表示不能和後面的事務並行執行。當前事務無法判斷能否和後面的事務並行執行,只能與前面事務的sequence_number比較,得出自己是否可以併發執行。

        僅僅設置爲LOGICAL_CLOCK還會存在問題,因爲此時在從庫上應用事務是無序的,和relay log中記錄的事務順序可能不一樣。在這種情況下,從庫的GTID會產生間隙,事務可能在某個時刻主從是不一致的,但是最終會一致,滿足最終一致性。相同記錄的修改,會按照順序執行,這由事務隔離級保證。不同記錄的修改,可以產生並行,並無數據一致性風險。這大概也是slave_preserve_commit_order參數缺省爲0的原因之一。

        如果要保證事務是按照relay log中記錄的順序來重放,需要設置參數slave_preserve_commit_order=1,這要求從庫開啓log_bin和log_slave_updates,並且slave_parallel_type設置爲LOGICAL_CLOCK。

        啓用slave_preserve_commit_order後,正在執行的worker線程將等待,直到所有先前的事務提交後再提交。當複製線程正在等待其它worker線程提交其事務時,它會將其狀態報告爲等待提交前一個事務。使用此模式,多線程複製的重放順序與主庫的提交順序保持一致。
        slave_parallel_workers參數控制並行複製worker線程的數量。若將slave_parallel_workers設置爲0,則退化爲單線程複製。如果slave_parallel_workers=N(N>0),則單線程複製中的SQL線程將轉爲1個coordinator線程和N個worker線程,coordinator線程負責選擇worker線程執行事務的二進制日誌。例如將slave_parallel_workers設置爲1,則SQL線程轉化爲1個coordinator線程和1個worker線程,也是單線程複製。然而,與slave_parallel_workers=0相比,多了一次coordinator線程的轉發,因此slave_parallel_workers=1的性能反而比0還要差。MySQL 8中slave_parallel_workers參數可以動態設置,但需要重啓複製才能生效。

        LOGICAL_CLOCK多線程複製爲了準確性和實現的需要,其lock interval實際獲得的區間比理論值窄,會導致原本一些可以併發行行的事務在從庫上沒有並行執行。當使用級聯複製時,LOGICAL_CLOCK可能會使離主庫越遠的從庫並行度越小。

(3)多線程複製測試
        從庫增加以下配置參數:

sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
slave_preserve_commit_order = 1
slave_parallel_type = LOGICAL_CLOCK

        下表所示爲從庫上slave_parallel_workers分別設置爲2、4、8、16的測試結果:

slave_parallel_workers

事務數

複製執行時間(秒)

從庫TPS

主庫TPS

2

183717

460

399

510

4

183248

396

462

509

8

182580

334

546

507

16

183290

342

535

509

        測試中主庫執行了一共360秒(預熱+壓測),TPS爲509。從表中可以看到,在實驗負載場景下,多線程複製性能明顯高於單線程複製。slave_parallel_workers=8時性能最好,當worker數量增加到16時,性能反而比8時差。太多線程會增加線程間同步的開銷,因此slave_parallel_workers值並非越大越好,需要根據實際負載進行測試來確定其最佳值,通常建議建議4-8個worker線程。

3. 基於WriteSet的多線程複製

        基於組提交LOGICAL_CLOCK多線程複製機制在每組提交事務足夠多,即業務量足夠大時表現較好。但很多實際業務中,雖然事務沒有Lock Interval重疊,但這些事務操作的往往是不同的數據行,也不會有鎖衝突,是可以並行執行的,但LOGICAL_CLOCK的實現無法使這部分事務得到並行重放。爲了解決這個問題,MySQL在5.7.22版本推出了基於WriteSet的並行複製。簡單來說,WriteSet並行複製的思想是:不同事務的記錄不重疊,則都可在從庫上並行重放。可以看到並行的力度從組提交細化爲記錄級。

(1)WriteSet對象
        MySQL中用WriteSet對象來記錄每行記錄,從源碼來看WriteSet就是每條記錄hash後的值(必須開啓ROW格式的二進制日誌),具體算法如下:

WriteSet=hash(index_name | db_name | db_name_length | table_name | table_name_length | value | value_length)

        上述公式中的index_name只記錄唯一索引,主鍵也是唯一索引。如果有多個唯一索引,則每條記錄會產生對應多個WriteSet值。另外,value這裏會分別計算原始值和帶有字符集排序規則(Collation)值的兩種WriteSet。所以一條記錄可能有多個WriteSet對象。

        新產生的WriteSet對象會插入到WriteSet哈希表,哈希表的大小由參數binlog_transaction_dependency_history_size設置,默認25000。內存中保留的哈希行數達到此值後,將清除歷史記錄。

(2)實現原理
        基於WriteSet的複製優化了主庫組提交的實現,主要體現主庫端last_committed的定義變了。原來一組事務是指擁有同一個parent_commit的事務,在二進制日誌中記錄爲同一個last_committed。基於WriteSet的方式中,last_committed的含義是保證衝突事務(更新相同記錄的事務)不能擁有同樣的last_committed值,事務執行的並行度進一步提高。

        當事務每次提交時,會計算修改的每個行記錄的WriteSet值,然後查找哈希表中是否已經存在有同樣的WriteSet,若無,WriteSet插入到哈希表,寫入二進制日誌的last_committed值不變。上一個事務跟當前事務的last_committed相等,意味着它們可以最爲一組提交。若有,更新哈希表對應的WriteSet值爲sequence_number,並且寫入到二進制日誌的last_committed值也更新爲sequnce_number。上一個事務跟當前事務的last_committed必然不同,表示事務衝突,必須等待之前的事務提交後才能執行。

        從庫端的邏輯跟以前一樣沒有變化,last_committed相同的事務可以並行執行。

(3)WriteSet多線程複製測試
        主庫增加以下配置參數:

binlog_transaction_dependency_tracking  = WRITESET
transaction_write_set_extraction        = XXHASH64

        從庫增加以下配置參數:

sync_binlog = 1
innodb_flush_log_at_trx_commit = 1
slave_preserve_commit_order = 1
slave_parallel_type = LOGICAL_CLOCK

        binlog_transaction_dependency_tracking參數指定主庫確定哪些事務可以作爲一組提交的方法,有三個可選值:

  • COMMIT_ORDER:依賴事務提交的邏輯時間戳,是默認值。如果事務更新的表上沒有主鍵和唯一索引,也使用該值。
  • WRITESET:更新不同記錄的事務(不衝突)都可以並行化。
  • WRITESET_SESSION:與WRITESET的區別是WRITESET_SESSION需要保證同一個會話內的事務的先後順序。消除了從庫中某一時刻可能看到主庫從未出現過的數據庫狀態的問題。

        transaction_write_set_extraction參數定義計算WriteSet使用的哈希算法。如果用於多線程複製,必須將此變量設置爲XXHASH64,也是缺省值。如果設置爲OFF,則binlog_transaction_dependency_tracking只能設置爲COMMIT_ORDER。如果binlog_transaction_dependency_tracking的當前值爲WRITESET或WRITESET_SESSION,則無法更改transaction_write_set_extraction的值。

        下表所示爲從庫上slave_parallel_workers分別設置爲2、4、8、16、32的測試結果:

slave_parallel_workers

事務數

複製執行時間(秒)

從庫TPS

主庫TPS

2

209237

515

406

581

4

207083

438

472

575

8

207292

364

569

575

16

205060

331

619

569

32

201488

340

592

559

        測試中主庫執行了一共360秒(預熱+壓測),TPS平均爲572,同等場景下的比COMMIT_ORDER高出12%。當16個複製線程時從庫TPS達到峯值619,比COMMIT_ORDER下性能最好的8複製線程高出13%。

        MySQL的複製延遲是一直被詬病的問題之一,從以上三組測試得出了目前解決延遲最普遍的三種方法:

  • 如果負載和數據一致性要求都不是太高,可以採用單線程複製 + 安全參數雙0。這種模式同樣擁有不錯的表現,一般壓力均可應付。
  • 如果主庫的併發量很高,那麼基於order-commit的模式的多線程複製可以有很好的表現。
  • 基於WriteSet的模式是目前併發度最高的多線程複製,基本可以滿足大部分場景。如果併發量非常高,或是要求從庫與主庫的延遲降至最低,可以採取這種方式。
     
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章