源碼分析 | ClickHouse和他的朋友們(9)MySQL實時復制與實現


本文轉自我司大神 BohuTANG的博客

很多人看到標題還以為自己走錯了夜場,其實沒有。

ClickHouse 可以掛載為 MySQL 的一個從庫 ,先全量再增量的實時同步 MySQL 數據,這個功能可以說是今年最亮眼、最剛需的功能,基於它我們可以輕松的打造一套企業級解決方案,讓 OLTP 和 OLAP 的融合從此不再頭疼。

目前支持 MySQL 5.6/5.7/8.0 版本,兼容 Delete/Update 語句,及大部分常用的 DDL 操作。
代碼已經合並到 upstream master 分支,預計在20.8版本作為experimental 功能發布。

畢竟是兩個異構生態的融合,仍然有不少的工作要做,同時也期待着社區用戶的反饋,以加速迭代。

代碼獲取

獲取 clickhouse/master 代碼編譯即可,方法見 ClickHouse和他的朋友們(1)編譯、開發、測試

MySQL Master

我們需要一個開啟 binlog 的 MySQL 作為 master:

docker run -d -e MYSQL_ROOT_PASSWORD=123 mysql:5.7 mysqld --datadir=/var/lib/mysql --server-id=1 --log-bin=/var/lib/mysql/mysql-bin.log --gtid-mode=ON --enforce-gtid-consistency

創建數據庫和表,並寫入數據:

mysql> create database ckdb;
mysql> use ckdb;
mysql> create table t1(a int not null primary key, b int);
mysql> insert into t1 values(1,1),(2,2);
mysql> select * from t1;
+---+------+
| a | b    |
+---+------+
| 1 |    1 |
| 2 |    2 |
+---+------+
2 rows in set (0.00 sec)

ClickHouse Slave

目前以 database 為單位進行復制,不同的 database 可以來自不同的 MySQL master,這樣就可以實現多個 MySQL 源數據同步到一個 ClickHouse 做 OLAP 分析功能。

首先開啟體驗開關:

clickhouse :) SET allow_experimental_database_materialize_mysql=1;

創建一個復制通道:

clickhouse :) CREATE DATABASE ckdb ENGINE = MaterializeMySQL('172.17.0.2:3306', 'ckdb', 'root', '123');
clickhouse :) use ckdb;
clickhouse :) show tables;
┌─name─┐
│ t1   │
└──────┘
clickhouse :) select * from t1;
┌─a─┬─b─┐
│ 1 │ 1 │
└───┴───┘
┌─a─┬─b─┐
│ 2 │ 2 │
└───┴───┘

2 rows in set. Elapsed: 0.017 sec.

看下 ClickHouse 的同步位點:
cat ckdatas/metadata/ckdb/.metadata

Version:	1
Binlog File:	mysql-bin.000001
Binlog Position:	913
Data Version:	0

Delete

首先在 MySQL Master 上執行一個刪除操作:

mysql> delete from t1 where a=1;
Query OK, 1 row affected (0.01 sec)

然后在 ClickHouse Slave 側查看記錄:

clickhouse :) select * from t1;

SELECT *
FROM t1

┌─a─┬─b─┐
│ 2 │ 2 │
└───┴───┘

1 rows in set. Elapsed: 0.032 sec.

此時的 metadata 里 Data Version 已經遞增到 2:

cat ckdatas/metadata/ckdb/.metadata 
Version:	1
Binlog File:	mysql-bin.000001
Binlog Position:	1171
Data Version:	2

Update

MySQL Master:

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

mysql> update t1 set b=b+1;

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

ClickHouse Slave:

clickhouse :) select * from t1;

SELECT *
FROM t1

┌─a─┬─b─┐
│ 2 │ 3 │
└───┴───┘

1 rows in set. Elapsed: 0.023 sec.

性能測試

測試環境

MySQL          8C16G 雲主機, 192.168.0.3,基礎數據 10188183 條記錄
ClickHouse     8C16G 雲主機, 192.168.0.4
benchyou       8C8G  雲主機,  192.168.0.5, 256並發寫, https://github.com/xelabs/benchyou

性能測試跟硬件環境有較大關系,這里使用的是雲主機模式,數據供參考。

全量性能

8c16G-vm :) create database sbtest engine=MaterializeMySQL('192.168.0.3:3306', 'sbtest', 'test', '123');

8c16G-vm :) watch lv1;

WATCH lv1

┌─count()─┬───────────────now()─┬─_version─┐
│       0 │ 2020-07-29 06:36:04 │        1 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│ 1113585 │ 2020-07-29 06:36:05 │        2 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│ 2227170 │ 2020-07-29 06:36:07 │        3 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│ 3340755 │ 2020-07-29 06:36:10 │        4 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│ 4454340 │ 2020-07-29 06:36:13 │        5 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│ 5567925 │ 2020-07-29 06:36:16 │        6 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│ 6681510 │ 2020-07-29 06:36:18 │        7 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│ 7795095 │ 2020-07-29 06:36:22 │        8 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│ 8908680 │ 2020-07-29 06:36:25 │        9 │
└─────────┴─────────────────────┴──────────┘
┌──count()─┬───────────────now()─┬─_version─┐
│ 10022265 │ 2020-07-29 06:36:28 │       10 │
└──────────┴─────────────────────┴──────────┘
┌──count()─┬───────────────now()─┬─_version─┐
│ 10188183 │ 2020-07-29 06:36:28 │       11 │
└──────────┴─────────────────────┴──────────┘
← Progress: 11.00 rows, 220.00 B (0.16 rows/s., 3.17 B/s.)

在這個硬件環境下,全量同步性能大概是 424507/s42w 事務每秒。
因為全量的數據之間沒有依賴關系,可以進一步優化成並行,加速同步。
全量的性能直接決定 ClickHouse slave 壞掉后重建的速度,如果你的 MySQL 有 10 億條數據,大概 40 分鍾就可以重建完成。

增量性能(實時同步)

在當前配置下,ClickHouse slave 單線程回放消費能力大於 MySQL master 256 並發下生產能力,通過測試可以看到它們保持實時同步

benchyou 壓測數據,2.1w 事務/秒(MySQL 在當前環境下TPS上不去):

./bin/benchyou --mysql-host=192.168.0.3 --mysql-user=test --mysql-password=123 --oltp-tables-count=1 --write-threads=256 --read-threads=0

time            thds               tps     wtps    rtps
[13s]        [r:0,w:256,u:0,d:0]  19962    19962   0    

time            thds               tps     wtps    rtps
[14s]        [r:0,w:256,u:0,d:0]  20415    20415   0 

time            thds               tps     wtps    rtps
[15s]        [r:0,w:256,u:0,d:0]  21131    21131   0

time            thds               tps     wtps    rtps
[16s]        [r:0,w:256,u:0,d:0]  21606    21606   0

time            thds               tps     wtps    rtps
[17s]        [r:0,w:256,u:0,d:0]  22505    22505   0

ClickHouse 側單線程回放能力,2.1w 事務/秒,實時同步:

┌─count()─┬───────────────now()─┬─_version─┐
│  150732 │ 2020-07-30 05:17:15 │       17 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│  155477 │ 2020-07-30 05:17:16 │       18 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│  160222 │ 2020-07-30 05:17:16 │       19 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│  164967 │ 2020-07-30 05:17:16 │       20 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│  169712 │ 2020-07-30 05:17:16 │       21 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│  174457 │ 2020-07-30 05:17:16 │       22 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│  179202 │ 2020-07-30 05:17:17 │       23 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│  183947 │ 2020-07-30 05:17:17 │       24 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│  188692 │ 2020-07-30 05:17:17 │       25 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│  193437 │ 2020-07-30 05:17:17 │       26 │
└─────────┴─────────────────────┴──────────┘
┌─count()─┬───────────────now()─┬─_version─┐
│  198182 │ 2020-07-30 05:17:17 │       27 │
└─────────┴─────────────────────┴──────────┘

實現機制

在探討機制之前,首先需要了解下 MySQL 的 binlog event ,主要有以下幾種類型:

1. MYSQL_QUERY_EVENT    -- DDL
2. MYSQL_WRITE_ROWS_EVENT -- insert數據
3. MYSQL_UPDATE_ROWS_EVENT -- update數據
4. MYSQL_DELETE_ROWS_EVENT -- delete數據

當一個事務提交后,MySQL 會把執行的 SQL 處理成相應的 binlog event,並持久化到 binlog 文件。

binlog 是 MySQL 對外輸出的重要途徑,只要你實現 MySQL Replication Protocol,就可以流式的消費MySQL 生產的 binlog event,具體協議見 Replication Protocol

由於歷史原因,協議繁瑣而詭異,這不是本文重點。

對於 ClickHouse 消費 MySQL binlog 來說,主要有以下3個難點:

  • DDL 兼容
  • Delete/Update 支持
  • Query 過濾

DDL

DDL 兼容花費了大量的代碼去實現。

首先,我們看看 MySQL 的表復制到 ClickHouse 后會變成什么樣子。

MySQL master:

mysql> show create table t1\G;
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `a` int(11) NOT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

ClickHouse slave:

ATTACH TABLE t1
(
    `a` Int32,
    `b` Nullable(Int32),
    `_sign` Int8,
    `_version` UInt64
)
ENGINE = ReplacingMergeTree(_version)
PARTITION BY intDiv(a, 4294967)
ORDER BY tuple(a)
SETTINGS index_granularity = 8192

可以看到:

  • 默認增加了 2 個隱藏字段:_sign(-1刪除, 1寫入) 和 _version(數據版本)
  • 引擎轉換成了 ReplacingMergeTree,以 _version 作為 column version
  • 原主鍵字段 a 作為排序和分區鍵

這只是一個表的復制,其他還有非常多的DDL處理,比如增加列、索引等,感興趣可以觀摩 Parsers/MySQL 下代碼。

Update和Delete

當我們在 MySQL master 執行:

mysql> delete from t1 where a=1;
mysql> update t1 set b=b+1;

ClickHouse t1數據(把 _sign 和 _version 一並查詢):

clickhouse :) select a,b,_sign, _version from t1;

SELECT 
    a,
    b,
    _sign,
    _version
FROM t1

┌─a─┬─b─┬─_sign─┬─_version─┐
│ 1 │ 1 │     1 │        1 │
│ 2 │ 2 │     1 │        1 │
└───┴───┴───────┴──────────┘
┌─a─┬─b─┬─_sign─┬─_version─┐
│ 1 │ 1 │    -1 │        2 │
└───┴───┴───────┴──────────┘
┌─a─┬─b─┬─_sign─┬─_version─┐
│ 2 │ 3 │     1 │        3 │
└───┴───┴───────┴──────────┘

根據返回結果,可以看到是由 3 個 part 組成。

part1 由 mysql> insert into t1 values(1,1),(2,2) 生成:

┌─a─┬─b─┬─_sign─┬─_version─┐
│ 1 │ 1 │     1 │        1 │
│ 2 │ 2 │     1 │        1 │
└───┴───┴───────┴──────────┘

part2 由 mysql> delete from t1 where a=1 生成:

┌─a─┬─b─┬─_sign─┬─_version─┐
│ 1 │ 1 │    -1 │        2 │
└───┴───┴───────┴──────────┘
說明:
_sign = -1表明處於刪除狀態

part3 由 update t1 set b=b+1 生成:

┌─a─┬─b─┬─_sign─┬─_version─┐
│ 2 │ 3 │     1 │        3 │
└───┴───┴───────┴──────────┘

使用 final 查詢:

clickhouse :) select a,b,_sign,_version from t1 final;

SELECT 
    a,
    b,
    _sign,
    _version
FROM t1
FINAL

┌─a─┬─b─┬─_sign─┬─_version─┐
│ 1 │ 1 │    -1 │        2 │
└───┴───┴───────┴──────────┘
┌─a─┬─b─┬─_sign─┬─_version─┐
│ 2 │ 3 │     1 │        3 │
└───┴───┴───────┴──────────┘

2 rows in set. Elapsed: 0.016 sec.

可以看到 ReplacingMergeTree 已經根據 _version 和 OrderBy 對記錄進行去重。

Query

MySQL master:

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

ClickHouse slave:

clickhouse :) select * from t1;

SELECT *
FROM t1

┌─a─┬─b─┐
│ 2 │ 3 │
└───┴───┘

clickhouse :) select *,_sign,_version from t1;

SELECT 
    *,
    _sign,
    _version
FROM t1

┌─a─┬─b─┬─_sign─┬─_version─┐
│ 1 │ 1 │    -1 │        2 │
│ 2 │ 3 │     1 │        3 │
└───┴───┴───────┴──────────┘
說明:這里還有一條刪除記錄,_sign為-1

MaterializeMySQL 被定義成一種存儲引擎,所以在讀取的時候,會根據 _sign 狀態進行判斷,如果是-1則是已經刪除,進行過濾。

並行回放

為什么 MySQL 需要並行回放?

假設 MySQL master 有 1024 個並發同時寫入、更新數據,瞬間產生大量的 binlog event ,MySQL slave 上只有一個線程一個 event 接着一個 event 式回放,於是 MySQL 實現了並行回放功能!

那么,MySQL slave 回放時能否完全(或接近)模擬出 master 當時的 1024 並發行為呢?

要想並行首先要解決的就是依賴問題:我們需要 master 標記出哪些 event 可以並行,哪些 event 有先后關系,因為它是第一現場。

MySQL 通過在 binlog 里增加:

  • last_committed,相同則可以並行
  • sequece_number,較小先執行,描述先后依賴
last_committed=3   sequece_number=4   -- event1
last_committed=4   sequece_number=5   -- event2
last_committed=4   sequece_number=6   -- event3
last_committed=5   sequece_number=7   -- event4

event2 和 event3 則可以並行,event4 需要等待前面 event 完成才可以回放。
以上只是一個大體原理,目前 MySQL 有3種並行模式可以選擇:

  1. 基於 database 並行
  2. 基於 group commit 並行
  3. 基於主鍵不沖突的 write set 並行

最大程度上讓 MySQL slave加速回放,整套機制還是異常復雜的。

回到 ClickHouse slave 問題,我們采用的單線程回放,延遲已經不是主要問題,這是由它們的機制決定的:
MySQL slave 回放時,需要把 binlog event 轉換成 SQL,然后模擬 master 的寫入,這種邏輯復制是導致性能低下的最重要原因。
而 ClickHouse 在回放上,直接把 binlog event 轉換成 底層 block 結構,然后直接寫入底層的存儲引擎,接近於物理復制,可以理解為把 binlog event 直接回放到 InnoDB 的 page。

讀取最新

雖然 ClickHouse slave 回放非常快,接近於實時,如何在ClickHouse slave上總是讀取到最新的數據呢?

其實非常簡單,借助 MySQL binlog GTID 特性,每次讀的時候,我們跟 master 做一次 executed_gtid 同步,然后等待這些 executed_gtid 回放完畢即可。

數據一致性

對一致性要求較高的場景,我們怎么驗證 MySQL master 的數據和 ClickHouse slave 的數據一致性呢?

這塊初步想法是提供一個兼容 MySQL checksum 算法的函數,我們只需對比兩邊的 checksum 值即可。

總結

ClickHouse 實時復制同步 MySQL 數據是 upstream 2020 的一個 roadmap,在整體構架上比較有挑戰一直無人接單,挑戰主要來自兩方面:

  • 對 MySQL 復制通道與協議非常熟悉
  • 對 ClickHouse 整體機制非常熟悉

這樣,在兩個本來有點遙遠的山頭中間架起了一座高速,這條 10851號 高速由 zhang1024(ClickHouse側) 和 BohuTANG(MySQL復制) 兩個修路工聯合承建,目前已經合並到 upstream 分支。

關於同步 MySQL 的數據,目前大家的方案基本都是在中間安置一個 binlog 消費工具,這個工具對 event 進行解析,然后再轉換成 ClickHouse 的 SQL 語句,寫到 ClickHouse server,鏈路較長,性能損耗較大。

10851號 高速是在 ClickHouse 內部實現一套 binlog 消費方案,然后根據 event 解析成 ClickHouse 內部的 block 結構,再直接回寫到底層存儲引擎,幾乎是最高效的一種實現方式,實現與 MySQL 實時同步的能力,讓分析更接近現實。

基於 database 級的復制,實現了多源復制的功能,如果復制通道壞掉,我們只需在 ClickHouse 側刪掉 database 再重建一次即可,非常快速、方便,OLTP+OLAP 就是這么簡單!

要想富,先修路!


歡迎關注我的微信公眾號【數據庫內核】:分享主流開源數據庫和存儲引擎相關技術。

歡迎關注公眾號數據庫內核
標題 網址
GitHub https://dbkernel.github.io
知乎 https://www.zhihu.com/people/dbkernel/posts
思否(SegmentFault) https://segmentfault.com/u/dbkernel
掘金 https://juejin.im/user/5e9d3ed251882538083fed1f/posts
開源中國(oschina) https://my.oschina.net/dbkernel
博客園(cnblogs) https://www.cnblogs.com/dbkernel


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM