1. 程式人生 > >mysql主從複製,資料量大, 高併發時,出現數據不一致

mysql主從複製,資料量大, 高併發時,出現數據不一致

mysql5.7的並行複製就可以解決資料延遲的問題。

MySQL 5.7並行複製時代

眾所周知,MySQL的複製延遲是一直被詬病的問題之一,然而在Inside君之前的兩篇部落格中(1,2)中都已經提到了MySQL 5.7版本已經支援“真正”的並行複製功能,官方稱為為enhanced multi-threaded slave(簡稱MTS),因此複製延遲問題已經得到了極大的改進,甚至在Inside君所在的網易電商應用中已經完全消除了之前延遲長達幾小時的問題。然而,Inside君發現還是有很部分小夥伴不瞭解這個足以載入史冊的“偉大”的特性,故作分享。總之, 5.7版本後,複製延遲問題永不存在

 。

MySQL 5.6並行複製架構

誠然,MySQL 5.6版本也支援所謂的並行複製,但是其並行只是基於schema的,也就是基於庫的。如果使用者的MySQL資料庫例項中存在多個schema,對於從機複製的速度的確可以有比較大的幫助。MySQL 5.6並行複製的架構如下所示:

在上圖的紅色框框部分就是實現並行複製的關鍵所在。在MySQL 5.6版本之前,Slave伺服器上有兩個執行緒I/O執行緒和SQL執行緒。I/O執行緒負責接收二進位制日誌(更準確的說是二進位制日誌的event),SQL執行緒進行回放二進位制日誌。如果在MySQL 5.6版本開啟並行複製功能,那麼SQL執行緒就變為了coordinator執行緒,coordinator執行緒主要負責以前兩部分的內容:

  • 若判斷可以並行執行,那麼選擇worker執行緒執行事務的二進位制日誌
  • 若判斷不可以並行執行,如該操作是DDL,亦或者是事務跨schema操作,則等待所有的worker執行緒執行完成之後,再執行當前的日誌

這意味著 coordinator執行緒並不是僅將日誌傳送給worker執行緒,自己也可以回放日誌 ,但是所有可以並行的操作交付由worker執行緒完成。coordinator執行緒與worker是典型的生產者與消費者模型。

上述機制實現了基於schema的並行複製存在兩個問題,首先是crash safe功能不好做,因為可能之後執行的事務由於並行複製的關係先完成執行,那麼當發生crash的時候,這部分的處理邏輯是比較複雜的。從程式碼上看,5.6這裡引入了Low-Water-Mark標記來解決該問題,從設計上看( 

WL#5569 ),其是希望藉助於日誌的冪等性來解決該問題,不過5.6的二進位制日誌回放還不能實現冪等性。另一個最為關鍵的問題是這樣設計的並行複製效果並不高,如果使用者例項僅有一個庫,那麼就無法實現並行回放,甚至效能會比原來的單執行緒更差。而 單庫多表是比多庫多表更為常見的一種情形 。

MySQL 5.7並行複製原理

MySQL 5.7基於組提交的並行複製

MySQL 5.7才可稱為真正的並行複製,這其中最為主要的原因就是slave伺服器的回放與主機是一致的即master伺服器上是怎麼並行執行的slave上就怎樣進行並行回放。不再有庫的並行複製限制,對於二進位制日誌格式也無特殊的要求(基於庫的並行複製也沒有要求)。

從MySQL官方來看,其並行複製的原本計劃是支援表級的並行複製和行級的並行複製,行級的並行複製通過解析ROW格式的二進位制日誌的方式來完成, WL#4648 。但是最終出現給小夥伴的確是在開發計劃中稱為:MTS: Prepared transactions slave parallel applier,可見: WL#6314 。該並行複製的思想最早是由MariaDB的Kristain提出,並已在MariaDB 10中出現,相信很多選擇MariaDB的小夥伴最為看重的功能之一就是並行複製。

MySQL 5.7並行複製的思想簡單易懂,一言以蔽之: 一個組提交的事務都是可以並行回放 ,因為這些事務都已進入到事務的prepare階段,則說明事務之間沒有任何衝突(否則就不可能提交)。

為了相容MySQL 5.6基於庫的並行複製,5.7引入了新的變數slave-parallel-type,其可以配置的值有:

  • DATABASE:預設值,基於庫的並行複製方式
  • LOGICAL_CLOCK:基於組提交的並行複製方式

支援並行複製的GTID

如何知道事務是否在一組中,又是一個問題,因為原版的MySQL並沒有提供這樣的資訊。在MySQL 5.7版本中,其設計方式是將組提交的資訊存放在GTID中。那麼如果使用者沒有開啟GTID功能,即將引數gtid_mode設定為OFF呢?故MySQL 5.7又引入了稱之為Anonymous_Gtid的二進位制日誌event型別,如:

mysql> SHOW BINLOG EVENTS in 'mysql-bin.000006';
+------------------+-----+----------------+-----------+-------------+-----------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+----------------+-----------+-------------+-----------------------------------------------+
| mysql-bin.000006 | 4 | Format_desc | 88 | 123 | Server ver: 5.7.7-rc-debug-log, Binlog ver: 4 |
| mysql-bin.000006 | 123 | Previous_gtids | 88 | 194 | f11232f7-ff07-11e4-8fbb-00ff55e152c6:1-2 |
| mysql-bin.000006 | 194 | Anonymous_Gtid | 88 | 259 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| mysql-bin.000006 | 259 | Query | 88 | 330 | BEGIN |
| mysql-bin.000006 | 330 | Table_map | 88 | 373 | table_id: 108 (aaa.t) |
| mysql-bin.000006 | 373 | Write_rows | 88 | 413 | table_id: 108 flags: STMT_END_F |
......

這意味著在 MySQL 5.7版本中即使不開啟GTID,每個事務開始前也是會存在一個Anonymous_Gtid ,而這GTID中就存在著組提交的資訊。

LOGICAL_CLOCK

然而,通過上述的SHOW BINLOG EVENTS,我們並沒有發現有關組提交的任何資訊。但是通過mysqlbinlog工具,使用者就能發現組提交的內部資訊:

[email protected]:~# mysqlbinlog mysql-bin.0000006 | grep last_committed
#150520 14:23:11 server id 88 end_log_pos 259 CRC32 0x4ead9ad6 GTID last_committed=0 sequence_number=1
#150520 14:23:11 server id 88 end_log_pos 1483 CRC32 0xdf94bc85 GTID last_committed=0 sequence_number=2
#150520 14:23:11 server id 88 end_log_pos 2708 CRC32 0x0914697b GTID last_committed=0 sequence_number=3
#150520 14:23:11 server id 88 end_log_pos 3934 CRC32 0xd9cb4a43 GTID last_committed=0 sequence_number=4
#150520 14:23:11 server id 88 end_log_pos 5159 CRC32 0x06a6f531 GTID last_committed=0 sequence_number=5
#150520 14:23:11 server id 88 end_log_pos 6386 CRC32 0xd6cae930 GTID last_committed=0 sequence_number=6
#150520 14:23:11 server id 88 end_log_pos 7610 CRC32 0xa1ea531c GTID last_committed=6 sequence_number=7
#150520 14:23:11 server id 88 end_log_pos 8834 CRC32 0x96864e6b GTID last_committed=6 sequence_number=8
#150520 14:23:11 server id 88 end_log_pos 10057 CRC32 0x2de1ae55 GTID last_committed=6 sequence_number=9
#150520 14:23:11 server id 88 end_log_pos 11280 CRC32 0x5eb13091 GTID last_committed=6 sequence_number=10
#150520 14:23:11 server id 88 end_log_pos 12504 CRC32 0x16721011 GTID last_committed=6 sequence_number=11
#150520 14:23:11 server id 88 end_log_pos 13727 CRC32 0xe2210ab6 GTID last_committed=6 sequence_number=12
#150520 14:23:11 server id 88 end_log_pos 14952 CRC32 0xf41181d3 GTID last_committed=12 sequence_number=13
...

可以發現較之原來的二進位制日誌內容多了last_committed和sequence_number,last_committed表示事務提交的時候,上次事務提交的編號,如果事務具有相同的last_committed,表示這些事務都在一組內,可以進行並行的回放。例如上述last_committed為0的事務有6個,表示組提交時提交了6個事務,而這6個事務在從機是可以進行並行回放的。

上述的last_committed和sequence_number代表的就是所謂的LOGICAL_CLOCK。先來看原始碼中對於LOGICAL_CLOCK的定義:

class Logical_clock
{
  private:
  int64 state;
  /*
  Offset is subtracted from the actual "absolute time" value at
  logging a replication event. That is the event holds logical
  timestamps in the "relative" format. They are meaningful only in
  the context of the current binlog.
  The member is updated (incremented) per binary log rotation.
  */
  int64 offset;
  ......

state是一個自增的值,offset在每次二進位制日誌發生rotate時更新,記錄發生rotate時的state值。其實state和offset記錄的是全域性的計數值,而存在二進位制日誌中的僅是當前檔案的相對值。使用LOGICAL_CLOCK的場景如下:

class MYSQL_BIN_LOG: public TC_LOG
{
  ...
  public:
  /* Committed transactions timestamp */
  Logical_clock max_committed_transaction;
  /* "Prepared" transactions timestamp */
  Logical_clock transaction_counter;
  ...

可以看到在類MYSQL_BIN_LOG中定義了兩個Logical_clock的變數:

  • max_c ommitted_transaction:記錄上次組提交時的logical_clock,代表上述mysqlbinlog中的last_committed
  • transaction_counter:記錄當前組提交中各事務的logcial_clock,代表上述mysqlbinlog中的sequence_number

並行複製測試

下圖顯示了開啟MTS後,slave伺服器的QPS。測試的工具是sysbench的單表全update測試,測試結果顯示在16個執行緒下的效能最好,從機的QPS可以達到25000以上,進一步增加並行執行的執行緒至32並沒有帶來更高的提升。而原單執行緒回放的QPS僅在4000左右,可見MySQL 5.7 MTS帶來的效能提升,而由於測試的是單表,所以MySQL 5.6的MTS機制則完全無能為力了。

並行複製配置與調優

master_info_repository

開啟MTS功能後,務必將引數master_info_repostitory設定為TABLE,這樣效能可以有50%~80%的提升。這是因為並行複製開啟後對於元master.info這個檔案的更新將會大幅提升,資源的競爭也會變大。在之前 InnoSQL 的版本中,添加了引數來控制重新整理master.info這個檔案的頻率,甚至可以不重新整理這個檔案。因為重新整理這個檔案是沒有必要的,即根據master-info.log這個檔案恢復本身就是不可靠的。在MySQL 5.7中,Inside君推薦將master_info_repository設定為TABLE,來減小這部分的開銷。

slave_parallel_workers

若將slave_parallel_workers設定為0,則MySQL 5.7退化為原單執行緒複製,但將slave_parallel_workers設定為1,則SQL執行緒功能轉化為coordinator執行緒,但是隻有1個worker執行緒進行回放,也是單執行緒複製。然而,這兩種效能卻又有一些的區別,因為多了一次coordinator執行緒的轉發,因此slave_parallel_workers=1的效能反而比0還要差,在Inside君的測試下還有20%左右的效能下降,如下圖所示:

這裡其中引入了另一個問題,如果主機上的負載不大,那麼組提交的效率就不高,很有可能發生每組提交的事務數量僅有1個,那麼在從機的回放時, 雖然開啟了並行複製,但會出現效能反而比原先的單執行緒還要差的現象,即延遲反而增大了 。聰明的小夥伴們,有想過對這個進行優化嗎?

Enhanced Multi-Threaded Slave配置

說了這麼多,要開啟enhanced multi-threaded slave其實很簡單,只需根據如下設定:

# slave
slave-parallel-type=LOGICAL_CLOCK
slave-parallel-workers=16
master_info_repository=TABLE
relay_log_info_repository=TABLE
relay_log_recovery=ON

並行複製監控

複製的監控依舊可以通過SHOW SLAVE STATUS\G,但是MySQL 5.7在performance_schema架構下多了這些表,使用者可以更細力度的進行監控:

mysql> show tables like 'replication%';
+---------------------------------------------+
| Tables_in_performance_schema (replication%) |
+---------------------------------------------+
| replication_applier_configuration           |
| replication_applier_status                  |
| replication_applier_status_by_coordinator   |
| replication_applier_status_by_worker        |
| replication_connection_configuration        |
| replication_connection_status               |
| replication_group_member_stats              |
| replication_group_members                   |
+---------------------------------------------+
8 rows in set (0.00 sec)

總結

MySQL 5.7推出的Enhanced Multi-Threaded Slave解決了困擾MySQL長達數十年的複製延遲問題,再次提醒一些無知的PostgreSQL使用者,不要再停留在之前對於MySQL的印象,物理複製也不一定肯定比邏輯複製有優勢,而MySQL 5.7的MTS已經完全可以解決延遲問題。anyway,和Inside君一起見證劃時代MySQL 5.7 GA版本的降臨吧。