1. 程式人生 > >MySQL 5.7半同步復制技術

MySQL 5.7半同步復制技術

分布 整體 並發 直接 cli 添加 title poll機制 不同步

一、復制架構衍生史

在談這個特性之前,我們先來看看MySQL的復制架構衍生史。

在2000年,MySQL 3.23.15版本引入了Replication。Replication作為一種準實時同步方式,得到廣泛應用。這個時候的Replicaton的實現涉及到兩個線程,一個在Master,一個在Slave。Slave的I/O和SQL功能是作為一個線程,從Master獲取到event後直接apply,沒有relay log。這種方式使得讀取event的速度會被Slave replay速度拖慢,當主備存在較大延遲時候,會導致大量binary log沒有備份到Slave端。

在2002年,MySQL 4.0.2版本將Slave端event讀取和執行獨立成兩個線程(IO線程和SQL線程),同時引入了relay log。IO線程讀取event後寫入relay log,SQL線程從relay log中讀取event然後執行。這樣即使SQL線程執行慢,Master的binary log也會盡可能的同步到Slave。當Master宕機,切換到Slave,不會出現大量數據丟失。

在2010年MySQL 5.5版本之前,一直采用的是這種異步復制的方式。主庫的事務執行不會管備庫的同步進度,如果備庫落後,主庫不幸crash,那麽就會導致數據丟失。於是在MySQL在5.5中就順其自然地引入了半同步復制,主庫在應答客戶端提交的事務前需要保證至少一個從庫接收並寫到relay log中。那麽半同步復制是否可以做到不丟失數據呢?下面分析。

在2016年,MySQL在5.7.17中引入了一個全新的技術,稱之為InnoDB Group Replication。目前官方MySQL 5.7.17基於Group replication的全同步技術已經問世,全同步技術帶來了更多的數據一致性保障。相信是未來同步技術一個重要方向,值得期待。MySQL 5.7 Group Replication

根據上面提到的這幾種復制協議,分別對應MySQL幾種復制類型,分別是異步、半同步、全同步。

技術分享圖片

  • 對於異步復制,主庫將事務Binlog事件寫入到Binlog文件中,此時主庫只會通知一下Dump線程發送這些新的Binlog,然後主庫就會繼續處理提交操作,而此時不會保證這些Binlog傳到任何一個從庫節點上。
  • 對於全同步復制,當主庫提交事務之後,所有的從庫節點必須收到,APPLY並且提交這些事務,然後主庫線程才能繼續做後續操作。這裏面有一個很明顯的缺點就是,主庫完成一個事務的時間被拉長,性能降低。
  • 對於半同步復制,是介於全同步復制和異步復制之間的一種,主庫只需要等待至少一個從庫節點收到並且Flush Binlog到Relay Log文件即可,主庫不需要等待所有從庫給主庫反饋。同時,這裏只是一個收到的反饋,而不是已經完全執行並且提交的反饋,這樣就節省了很多時間。

二、半同步復制技術

我們今天談論第二種架構。我們知道,普通的replication,即MySQL的異步復制,依靠MySQL二進制日誌也即binary log進行數據復制。比如兩臺機器,一臺主機(master),另外一臺是從機(slave)。

1)正常的復制為:事務一(t1)寫入binlog buffer;dumper線程通知slave有新的事務t1;binlog buffer進行checkpoint;slave的io線程接收到t1並寫入到自己的的relay log;slave的sql線程寫入到本地數據庫。 這時,master和slave都能看到這條新的事務,即使master掛了,slave可以提升為新的master。

2)異常的復制為:事務一(t1)寫入binlog buffer;dumper線程通知slave有新的事務t1;binlog buffer進行checkpoint;slave因為網絡不穩定,一直沒有收到t1;master掛掉,slave提升為新的master,t1丟失。

3)很大的問題是:主機和從機事務更新的不同步,就算是沒有網絡或者其他系統的異常,當業務並發上來時,slave因為要順序執行master批量事務,導致很大的延遲。

為了彌補以上幾種場景的不足,MySQL從5.5開始推出了半同步復制。相比異步復制,半同步復制提高了數據完整性,因為很明確知道,在一個事務提交成功之後,這個事務就至少會存在於兩個地方。即在master的dumper線程通知slave後,增加了一個ack(消息確認),即是否成功收到t1的標誌碼,也就是dumper線程除了發送t1到slave,還承擔了接收slave的ack工作。如果出現異常,沒有收到ack,那麽將自動降級為普通的復制,直到異常修復後又會自動變為半同步復制。

半同步復制具體特性:

  • 從庫會在連接到主庫時告訴主庫,它是不是配置了半同步。
  • 如果半同步復制在主庫端是開啟了的,並且至少有一個半同步復制的從庫節點,那麽此時主庫的事務線程在提交時會被阻塞並等待,結果有兩種可能,要麽至少一個從庫節點通知它已經收到了所有這個事務的Binlog事件,要麽一直等待直到超過配置的某一個時間點為止,而此時,半同步復制將自動關閉,轉換為異步復制。
  • 從庫節點只有在接收到某一個事務的所有Binlog,將其寫入並Flush到Relay Log文件之後,才會通知對應主庫上面的等待線程。
  • 如果在等待過程中,等待時間已經超過了配置的超時時間,沒有任何一個從節點通知當前事務,那麽此時主庫會自動轉換為異步復制,當至少一個半同步從節點趕上來時,主庫便會自動轉換為半同步方式的復制。
  • 半同步復制必須是在主庫和從庫兩端都開啟時才行,如果在主庫上沒打開,或者在主庫上開啟了而在從庫上沒有開啟,主庫都會使用異步方式復制。

半同步復制潛在問題:

先看一下半同步復制原理圖,如下:

技術分享圖片

master將每個事務寫入binlog(sync_binlog=1),傳遞到slave刷新到磁盤(sync_relay=1),同時主庫提交事務(commit)。master等待slave反饋收到relay log,只有收到ACK後master才將commit OK結果反饋給客戶端。

在MySQL 5.5~5.6使用after_commit的模式下,客戶端事務在存儲引擎層提交後,在得到從庫確認的過程中,主庫宕機了。此時,即主庫在等待Slave ACK的時候,雖然沒有返回當前客戶端,但事務已經提交,其他客戶端會讀取到已提交事務。如果Slave端還沒有讀到該事務的events,同時主庫發生了crash,然後切換到備庫。那麽之前讀到的事務就不見了,出現了幻讀。如下圖所示,圖片引自Loss-less Semi-Synchronous Replication on MySQL 5.7.2。

技術分享圖片

如果主庫永遠啟動不了,那麽實際上在主庫已經成功提交的事務,在從庫上是找不到的,也就是數據丟失了,這是MySQL不願意看到的。所以在MySQL 5.7版本中增加了after_sync(無損復制)參數,並將其設置為默認半同步方式,解決了數據丟失的問題。

三、MySQL 5.6半同步復制配置

具體完整配置可參考:MySQL基於日誌點做主從復制(二)

Master配置

1)安裝半同步模塊並啟動(此模塊就在/usr/local/mysql/lib/plugin/semisync_master.so)

mysql> install plugin rpl_semi_sync_master soname ‘semisync_master.so‘;

技術分享圖片

mysql> set global rpl_semi_sync_master_enabled = 1;
mysql> set global rpl_semi_sync_master_timeout = 2000;

安裝後啟動和定制主從連接錯誤的超時時間默認是10s可改為2s,一旦有一次超時自動降級為異步。(以上內容要想永久有效需要寫到配置文件中)

[root@localhost ~]# cat /etc/my.cnf
[mysqld]
rpl_semi_sync_master_enabled = 1;
rpl_semi_sync_master_timeout = 2000;

Slave配置

1)安裝半同步模塊並啟動

mysql> install plugin rpl_semi_sync_slave soname ‘semisync_slave.so‘;
mysql> set global rpl_semi_sync_slave_enabled = 1;
mysql> show global variables like ‘%semi%‘;
+---------------------------------+-------+
| Variable_name                   | Value |
+---------------------------------+-------+
| rpl_semi_sync_slave_enabled     | ON    |
| rpl_semi_sync_slave_trace_level | 32    |
+---------------------------------+-------+
2 rows in set (0.00 sec)

  

2)從節點需要重新連接主服務器半同步才會生效

mysql> stop slave io_thread;
mysql> start slave io_thread;

PS:如果想卸載異步模塊就使用uninstall即可。

Master上查看是否啟用了半同步

技術分享圖片

現在半同步已經正常工作了,主要看Rpl_semi_sync_master_clients是否不為0,Rpl_semi_sync_master_status是否為ON。如果Rpl_semi_sync_master_status為OFF,說明出現了網絡延遲或Slave IO線程延遲。

那麽可以驗證一下半同步超時,是否會自動降為異步工作。可以在Slave上停掉半同步協議,然後在Master上創建數據庫看一下能不能復制到Slave上。

Slave

# 關閉半同步;
mysql> set global rpl_semi_sync_slave_enabled = 0 ;
mysql> stop slave io_thread;
mysql> start slave io_thread;

Master

mysql> create database dbtest;
Query OK, 1 row affected (2.01 sec)
mysql> create database dbtest01; Query OK, 1 row affected (0.01 sec)

  

創建第一個數據庫花了2.01秒,而我們前面設置的超時時間是2秒,而創建第二個數據庫花了0.01秒,由此得出結論是超時轉換為異步傳送。可以在Master上查看半同步相關的參數值Rpl_semi_sync_master_clients和Rpl_semi_sync_master_status是否正常。

mysql> show global status like ‘%semi%‘;  
+--------------------------------------------+-----------+
| Variable_name                              | Value     |
+--------------------------------------------+-----------+
| Rpl_semi_sync_master_clients               | 0         |
| Rpl_semi_sync_master_net_avg_wait_time     | 0         |
| Rpl_semi_sync_master_net_wait_time         | 0         |
| Rpl_semi_sync_master_net_waits             | 37490     |
| Rpl_semi_sync_master_no_times              | 3         |
| Rpl_semi_sync_master_no_tx                 | 197542    |
| Rpl_semi_sync_master_status                | OFF       |
| Rpl_semi_sync_master_timefunc_failures     | 0         |
| Rpl_semi_sync_master_tx_avg_wait_time      | 51351     |
| Rpl_semi_sync_master_tx_wait_time          | 362437445 |
| Rpl_semi_sync_master_tx_waits              | 7058      |
| Rpl_semi_sync_master_wait_pos_backtraverse | 0         |
| Rpl_semi_sync_master_wait_sessions         | 0         |
| Rpl_semi_sync_master_yes_tx                | 7472      |
+--------------------------------------------+-----------+
14 rows in set (0.00 sec)

  

可以看到都自動關閉了,需要註意一點的是,當Slave開啟半同步後,或者當主從之間網絡延遲恢復正常的時候,半同步復制會自動從異步復制又轉為半同步復制,還是相當智能的。

另外個人在實際使用中還碰到一種情況從庫IO線程有延遲時,主庫會自動把半同步復制降為異步復制;當從庫IO延遲沒有時,主庫又會把異步復制升級為半同步復制。可以進行壓測模擬,但是此時查看Master的狀態跟上面直接關閉Slave半同步有些不同,會發現Rpl_semi_sync_master_clients仍然等於1,而Rpl_semi_sync_master_status等於OFF。

隨著MySQL 5.7版本的發布,半同步復制技術升級為全新的Loss-less Semi-Synchronous Replication架構,其成熟度、數據一致性與執行效率得到顯著的提升。

四、MySQL 5.7半同步復制的改進

現在我們已經知道,在半同步環境下,主庫是在事務提交之後等待Slave ACK,所以才會有數據不一致問題。所以這個Slave ACK在什麽時間去等待,也是一個很關鍵的問題了。因此MySQL針對半同步復制的問題,在5.7.2引入了Loss-less Semi-Synchronous,在調用binlog sync之後,engine層commit之前等待Slave ACK。這樣只有在確認Slave收到事務events後,事務才會提交。在commit之前等待Slave ACK,同時可以堆積事務,利於group commit,有利於提升性能。

MySQL 5.7安裝半同步模塊,命令如下:

mysql> install plugin rpl_semi_sync_master soname ‘semisync_master.so‘;
Query OK, 0 rows affected (0.00 sec)

  

看一下相關狀態信息

mysql> show global variables like ‘%semi%‘;
+-------------------------------------------+------------+
| Variable_name                             | Value      |
+-------------------------------------------+------------+
| rpl_semi_sync_master_enabled              | OFF        |
| rpl_semi_sync_master_timeout              | 10000      |
| rpl_semi_sync_master_trace_level          | 32         |
| rpl_semi_sync_master_wait_for_slave_count | 1          |
| rpl_semi_sync_master_wait_no_slave        | ON         |
| rpl_semi_sync_master_wait_point           | AFTER_SYNC |
+-------------------------------------------+------------+
6 rows in set (0.00 sec)

  

  • 支持無損復制(Loss-less Semi-Synchronous)

在Loss-less Semi-Synchronous模式下,master在調用binlog sync之後,engine層commit之前等待Slave ACK(需要收到至少一個Slave節點回復的ACK後)。這樣只有在確認Slave收到事務events後,master事務才會提交,然後把結果返回給客戶端。此時此事務才對其他事務可見。在這種模式下解決了after_commit模式帶來的幻讀和數據丟失問題,因為主庫沒有提交事務。但也會有個問題,假設主庫在存儲引擎提交之前掛了,那麽很明顯這個事務是不成功的,但由於對應的Binlog已經做了Sync操作,從庫已經收到了這些Binlog,並且執行成功,相當於在從庫上多了數據,也算是有問題的,但多了數據,問題一般不算嚴重。這個問題可以這樣理解,作為MySQL,在沒辦法解決分布式數據一致性問題的情況下,它能保證的是不丟數據,多了數據總比丟數據要好。

無損復制其實就是對semi sync增加了rpl_semi_sync_master_wait_point參數,來控制半同步模式下主庫在返回給會話事務成功之前提交事務的方式。rpl_semi_sync_master_wait_point該參數有兩個值:AFTER_COMMIT和AFTER_SYNC

第一個值:AFTER_COMMIT(5.6默認值)

master將每個事務寫入binlog(sync_binlog=1),傳遞到slave刷新到磁盤(sync_relay=1),同時主庫提交事務。master等待slave反饋收到relay log,只有收到ACK後master才將commit OK結果反饋給客戶端。

技術分享圖片

第二個值:AFTER_SYNC(5.7默認值,但5.6中無此模式)

master將每個事務寫入binlog , 傳遞到slave刷新到磁盤(relay log)。master等待slave反饋接收到relay log的ack之後,再提交事務並且返回commit OK結果給客戶端。 即使主庫crash,所有在主庫上已經提交的事務都能保證已經同步到slave的relay log中。

技術分享圖片

半同步復制與無損復制的對比

1.1 ACK的時間點不同

  • 半同步復制在InnoDB層的Commit Log後等待ACK,主從切換會有數據丟失風險。
  • 無損復制在MySQL Server層的Write binlog後等待ACK,主從切換會有數據變多風險。

1.2 主從數據一致性

  • 半同步復制意味著在Master節點上,這個剛剛提交的事物對數據庫的修改,對其他事物是可見的。因此,如果在等待Slave ACK的時候crash了,那麽會對其他事務出現幻讀,數據丟失。
  • 無損復制在write binlog完成後,就傳輸binlog,但還沒有去寫commit log,意味著當前這個事物對數據庫的修改,其他事物也是不可見的。因此,不會出現幻讀,數據丟失風險。

因此5.7引入了無損復制(after_sync)模式,帶來的主要收益是解決after_commit導致的master crash後數據丟失問題,因此在引入after_sync模式後,所有提交的數據已經都被復制,故障切換時數據一致性將得到提升。

  • 性能提升,支持發送binlog和接受ack的異步化

舊版本的semi sync受限於dump thread ,原因是dump thread承擔了兩份不同且又十分頻繁的任務:傳送binlog給slave ,還需要等待slave反饋信息,而且這兩個任務是串行的,dump thread必須等待slave返回之後才會傳送下一個events事務。dump thread已然成為整個半同步提高性能的瓶頸。在高並發業務場景下,這樣的機制會影響數據庫整體的TPS 。

技術分享圖片

為了解決上述問題,在5.7版本的semi sync框架中,獨立出一個Ack Receiver線程 ,專門用於接收slave返回的ack請求,這將之前dump線程的發送和接受工作分為了兩個線程來處理。這樣master上有兩個線程獨立工作,可以同時發送binlog到slave,和接收slave的ack信息。因此半同步復制得到了極大的性能提升。這也是MySQL 5.7發布時號稱的Faster semi-sync replication。

技術分享圖片

但是在MySQL 5.7.17之前,這個Ack Receiver線程采用了select機制來監聽slave返回的結果,然而select機制監控的文件句柄只能是0-1024,當超過1024時,用戶在MySQL的錯誤日誌中或許會收到類似如下的報錯,更有甚者會導致MySQL發生宕機。

semi-sync master failed on net_flush() before waiting for slave reply.

MySQL 5.7.17版本開始,官方修復了這個bug,開始使用poll機制來替換原來的select機制,從而可以避免上面的問題。其實poll調用本質上和select沒有區別,只是在I/O句柄數理論上沒有上限了,原因是它是基於鏈表來存儲的。但是同樣有缺點:比如大量的fd的數組被整體復制於用戶態和內核地址空間之間,而不管這樣的復制是不是有意義。poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麽下次poll時會再次報告該fd。

其實在高性能軟件中都是用另外一種調用機制,名為epoll,高性能的代表,比如Nginx,haproxy等都是使用epoll。可能poll的復雜性比epoll低,另外對於ack receiver線程來說可能poll足矣。

  • 性能提升,控制主庫接收slave寫事務成功反饋數量

MySQL 5.7新增了rpl_semi_sync_master_wait_slave_count參數,可以用來控制主庫接受多少個slave寫事務成功反饋,給高可用架構切換提供了靈活性。如圖所示,當count值為2時,master需等待兩個slave的ack。

技術分享圖片

  • 性能提升, Binlog互斥鎖改進

舊版本半同步復制在主提交binlog的寫會話和dump thread讀binlog的操作都會對binlog添加互斥鎖,導致binlog文件的讀寫是串行化的,存在並發度的問題。

技術分享圖片

MySQL 5.7對binlog lock進行了以下兩方面優化:

1. 移除了dump thread對binlog的互斥鎖。

2. 加入了安全邊際保證binlog的讀安全。

技術分享圖片

可以看到從replication功能引入後,官方MySQL一直在不停的完善,前進。同時我們可以發現當前原生的MySQL主備復制實現實際上很難在滿足數據一致性的前提下做到高可用、高性能。

五、參數sync_binlog/sync_relay與半同步復制

sync_binlog的配置

其實無損復制流程中也會存在著會導致主備數據不一致的情況,使主備同步失敗的情形。見下面sync_binlog配置的分析。

源碼剖析

sql/binlog.cc ordered_commit
9002   update_binlog_end_pos_after_sync= (get_sync_period() == 1);
       ...
         //當sync_period(sync_binlog)為1時,在sync之後update binlog end pos
9021     if (!update_binlog_end_pos_after_sync)
           //更新binlog end position,dump線程會發送更新後的events
9022       update_binlog_end_pos();
       ...
         //
9057     std::pair<bool, bool> result= sync_binlog_file(false);
       ...
         //
9061     if (update_binlog_end_pos_after_sync)
9062   {
       ...
9068       update_binlog_end_pos(tmp_thd->get_trans_pos());
9069   }
 
 
sql/binlog.cc sync_binlog_file
8618 std::pair<bool, bool>
8619 MYSQL_BIN_LOG::sync_binlog_file(bool force)
8620 {
8621   bool synced= false;
8622   unsigned int sync_period= get_sync_period();  //sync_binlog值
       //sync_period為0不做sync操作,其他值為達到sync調用次數後sync
8623   if (force || (sync_period && ++sync_counter >= sync_period))
8624   {

  

配置分析

當sync_binlog為0的時候,binlog sync磁盤由操作系統負責。當不為0的時候,其數值為定期sync磁盤的binlog commit group數。通過源碼我們知道,sync_binlog值不等於1的時候事務在FLUSH階段就傳輸binlog到從庫了,而值為1時,binlog同步操作是在SYNC階段後。當sync_binlog值大於1的時候,sync binlog操作可能並沒有使binlog落盤。如果沒有落盤,事務在提交前,Master掉電,然後恢復,那麽這個時候該事務被回滾。但是Slave上可能已經收到了該事務的events並且執行,這個時候就會出現Slave事務比Master多的情況,主備同步會失敗。所以如果要保持主備一致,需要設置sync_binlog為1。

WAIT_AFTER_SYNC和WAIT_AFTER_COMMIT兩圖中Send Events的位置,也可能導致主備數據不一致,出現同步失敗的情形。實際在rpl_semi_sync_master_wait_point分析的圖中是sync binlog大於1的情況。根據上面源碼,流程如下圖所示。Master依次執行flush binlog, update binlog position, sync binlog。如果Master在update binlog position後,sync binlog前掉電,Master再次啟動後原事務就會被回滾。但可能出現Slave獲取到Events,這也會導致Slave數據比Master多,主備同步失敗。

技術分享圖片

由於上面的原因,sync_binlog設置為1的時候,MySQL會update binlog end pos after sync。流程如下圖所示。這時候,對於每一個事務都需要sync binlog,同時sync binlog和網絡發送events會是一個串行的過程,性能下降明顯。

技術分享圖片

sync_relay_log的配置

源碼剖析

sql/rpl_slave.cc handle_slave_io
 
5764       if (queue_event(mi, event_buf, event_len))
           ...
5771       if (RUN_HOOK(binlog_relay_io, after_queue_event,
5772                    (thd, mi, event_buf, event_len, synced)))
 
after_queue_event
->plugin/semisync/semisync_slave_plugin.cc repl_semi_slave_queue_event
->plugin/semisync/semisync_slave.cc ReplSemiSyncSlave::slaveReply
 
queue_event
->sql/binlog.cc MYSQL_BIN_LOG::append_buffer(const char* buf, uint len, Master_info *mi)
->sql/binlog.cc after_append_to_relay_log(mi);
->sql/binlog.cc flush_and_sync(0)
->sql/binlog.cc sync_binlog_file(force)

  

配置分析

在Slave的IO線程中get_sync_period獲得的是sync_relay_log的值,與sync_binlog對sync控制一樣。當sync_relay_log不是1的時候,semisync返回給Master的position可能沒有sync到磁盤。在gtid_mode下,在保證前面兩個配置正確的情況下,sync_relay_log不是1的時候,僅發生Master或Slave的一次Crash並不會發生數據丟失或者主備同步失敗情況。如果發生Slave沒有sync relay log,Master端事務提交,客戶端觀察到事務提交,然後Slave端Crash。這樣Slave端就會丟失掉已經回復Master ACK的事務events。

技術分享圖片

但當Slave再次啟動,如果沒有來得及從Master端同步丟失的事務Events,Master就Crash。這個時候,用戶訪問Slave就會發現數據丟失。

技術分享圖片

通過上面這個Case,MySQL semisync如果要保證任意時刻發生一臺機器宕機都不丟失數據,需要同時設置sync_relay_log為1。對relay log的sync操作是在queue_event中,對每個event都要sync,所以sync_relay_log設置為1的時候,事務響應時間會受到影響,對於涉及數據比較多的事務延遲會增加很多。

MySQL三節點

在一主一從的主備semisync的數據一致性分析中放棄了高可用,當主備之間網絡抖動或者一臺宕機的情況下停止提供服務。要做到高可用,很自然我們可以想到一主兩從,這樣解決某一網絡抖動或一臺宕機時候的可用性問題。但是,前文敘述要保證數據一致性配置要求依然存在,即正常情況下的性能不會有改善。同時需要解決Master宕機時候,如何選取新主機的問題,如何避免多主的情形。

技術分享圖片

選取新主機時一定要讀取兩個從機,看哪一個從機有最新的日誌,否則可能導致數據丟失。這樣的三節點方案就類似分布式Quorum機制,寫的時候需要保證寫成功三節點中的法定集合,確定新主的時候需要讀取法定集合。利用分布式一致性協議Paxos/Raft可以解決數據一致性問題,選主問題和多主問題,因此近些年,國內數據庫團隊大多實現了基於Paxos/Raft的三節點方案。近來MySQL官方也以插件形式引入了支持多主集群的Group Replication方案。

轉自:http://www.ywnds.com/?p=7023

MySQL 5.7半同步復制技術