MySQL到Clickhouse的实时复制


MySQL与Clickhouse是两个完全不一样的数据库,两者均有着自己的优缺点,两者所适合的业务场景也是不一样的,在实际业务中,我们需要根据数据库自身的特性优点选择合适它的业务场景。传统的MySQL数据库虽然很好的支持了OLTP的业务,但是对于OLAP这类需要对大批量数据进行统计分析的业务场景还是存在较大的一定的性能瓶颈。想要对将线上核心的业务数据做较好的BI统计分析查询,我们就必须要面对如何将线上数据同步至对应的统计分析数据库。Clickhouse从2020年开始,推出了MaterializeMySQL引擎,实现了将Clickhouse作为MySQL的从库,对MySQL数据进行实时同步,实现了OLTP到OLAP的跨越。

一、MaterializeMySQL

1.1 MySQL与CK的简单比较

MySQL与Clickhouse的异同主要如下:

MySQL Clickhouse
关系型数据库,支持事物 分布式列数据库,不支持事物
行存储模式,适合尽量少的读取需要的行数据 列存储模式,且数据压缩比高,对大批量数据读取有着天然优势
单进程多线程服务,单条业务请求查询无法有效利用到多个CPU资源 多核并行
面向OLTP业务 面向联机分析处理的OLAP业务

1.2 MaterializeMySQL原理

在2020年下半年,Yandex 公司在 ClickHouse社区发布了MaterializeMySQL引擎,该引擎主要是用来支持从MySQL全量及增量实时数据同步。目前支持 MySQL 5.6/5.7/8.0 版本,兼容 Delete/Update 语句,及大部分常用的 DDL 操作。

众所周知,MySQL的自身复制主要依赖于binlog事务日志,MaterializeMySQL引擎也不例外。但是相对于原生MySQL的binlog复制不同的是,由于两者语法上的差异,MaterializeMySQL并不是将event中的SQL语句转换为CK中具体语句进行执行,而是直接讲Binlog Event转换为底层 Block 结构,然后直接写入底层存储引擎,接近于物理复制。

MaterializeMySQL实现流程:

  • MaterializeMySQL支持数据库级别的复制。
  • 当在Clickhouse中创建库级别复制后,clickhouse通过我们指定的数据库账号通过TCP/IP连接到数据,对数据库执行Flush table with read lock 并获取相关的binlog、表结构元数据信息;元数据复制完毕后释放全局只读锁,并开始通过select * from table_name开始复制表数据信息。
  • 对于后续的增量数据的同步,MaterializeMySQL通过对binlog event的解析来实现的实时同步 (MYSQL_QUERY_EVENT(DDL)、MYSQL_WRITE_ROWS_EVENT(insert)、MYSQL_UPDATE_ROWS_EVENT(update)、MYSQL_DELETE_ROWS_EVENT(delete)
  • 对于DDL操作,MaterializeMySQL默认将MySQL表数据的主键作为CK表的排序键和分区键,但是由于Clickhouse与MySQL的数据定义有区别,DDL语句也会进行相应的转换
  • 对于Update/Delete操作,MaterializeMySQL引入_version的隐藏字段,用来做版本控制,并结合_sign字段标记数据的有效性

MaterializeMySQL创建复制通道时,在全量初始化同步阶段,可通过general_log查看MySQL具体执行操作细节,具体执行日志如下:

2021-03-14T15:40:02.016351+08:00	   26 Connect	[email protected] on ck_test using TCP/IP
2021-03-14T15:40:02.017402+08:00	   26 Query	SET NAMES utf8
2021-03-14T15:40:02.018822+08:00	   26 Query	SHOW VARIABLES WHERE (Variable_name = 'log_bin' AND upper(Value) = 'ON') OR (Variable_name = 'binlog_format' AND upper(Value) = 'ROW') OR (Variable_name = 'binlog_row_image' AND upper(Value) = 'FULL') OR (Variable_name = 'default_authentication_plugin' AND upper(Value) = 'MYSQL_NATIVE_PASSWORD')
2021-03-14T15:40:02.032549+08:00	   26 Query	SELECT version() AS version
2021-03-14T15:40:02.033620+08:00	   26 Query	FLUSH TABLES
2021-03-14T15:40:02.051444+08:00	   26 Query	FLUSH TABLES WITH READ LOCK
2021-03-14T15:40:02.052364+08:00	   26 Query	SHOW MASTER STATUS
2021-03-14T15:40:02.053295+08:00	   26 Query	SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
2021-03-14T15:40:02.054027+08:00	   26 Query	START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */
2021-03-14T15:40:02.055520+08:00	   26 Query	SELECT TABLE_NAME AS table_name FROM INFORMATION_SCHEMA.TABLES  WHERE TABLE_SCHEMA = 'ck_test'
2021-03-14T15:40:02.057527+08:00	   26 Query	SHOW CREATE TABLE ck_test.t1
2021-03-14T15:40:02.060780+08:00	   26 Query	SHOW CREATE TABLE ck_test.t2
2021-03-14T15:40:02.062275+08:00	   26 Query	UNLOCK TABLES
2021-03-14T15:40:02.075348+08:00	   26 Query	SELECT * FROM ck_test.t2
2021-03-14T15:40:02.101797+08:00	   26 Query	SELECT * FROM ck_test.t1
2021-03-14T15:40:02.106329+08:00	   26 Query	COMMIT
2021-03-14T15:40:02.109026+08:00	   27 Connect	[email protected] on  using TCP/IP
2021-03-14T15:40:02.109637+08:00	   27 Query	SET @master_binlog_checksum = 'CRC32'
2021-03-14T15:40:02.110123+08:00	   27 Query	SET @master_heartbeat_period = 1000000000
2021-03-14T15:40:02.111290+08:00	   27 Binlog Dump GTID	Log: '' Pos: 4 GTIDs: '4a2dfc1c-1f50-11eb-a38b-fa057042bc00:1-53,
a4ec8037-1a70-11eb-91ff-fa9f1ef63700:1-1741042'

二、MySQL->CK的实时复制实现

1.1 环境准备

1、MySQL

  • 开启binlog日志,且row_format=row
  • 复制使用gtid模式
gtid_mode=ON
enforce_gtid_consistency=1
binlog_format=ROW

2、Clickhouse

1)环境参数

-- 该参数默认关闭,若需要使用MaterializeMySQL引擎,必须打开该参数
mdw :) set allow_experimental_database_materialize_mysql=1;             

2)创建复制通道

-- 语法
CREATE DATABASE ${dbname} ENGINE = MaterializeMySQL('${mysql_ip}:${mysql_port}', '${mysql_dbname}', '${mysql_user}', '${mysql_passoword}');

-- 执行SQL
mdw :) CREATE DATABASE ck_test ENGINE = MaterializeMySQL('172.16.104.13:3306', 'ck_test', 'root', '123');

3)复制信息

对于MySQL的实时复制信息,存储在datadir下的metadata目录下。

-- MySQL的binlog位点信息
root@mysql 15:05:  [ck_test]> show master status\G
*************************** 1. row ***************************
             File: mysql-bin.000005
         Position: 4048
     Binlog_Do_DB:
 Binlog_Ignore_DB:
Executed_Gtid_Set: 4a2dfc1c-1f50-11eb-a38b-fa057042bc00:1-37,
a4ec8037-1a70-11eb-91ff-fa9f1ef63700:1-1741042
1 row in set (0.00 sec)

-- Clickhouse复制位点信息
[root@mdw ck_test]# pwd
/data/clickhouse-server/data/metadata/ck_test
[root@mdw ck_test]# cat .metadata
Version:	2
Binlog File:	mysql-bin.000005                                            //binlog文件
Executed GTID:	4a2dfc1c-1f50-11eb-a38b-fa057042bc00:1-37,a4ec8037-1a70-11eb-91ff-fa9f1ef63700:1-1741042        //GTID信息
Binlog Position:	4048                                                    //binlog位点
Data Version:	9                                                           //数据版本信息,全局递增

1.2 基本功能测试

1、数据写入的同步

对于MySQL的所有数据,在CK中有会有对应的_sign,_version隐藏字段,用于进行版本控制的标记和查询。、

-- MySQL
root@mysql 14:44:  [ck_test]> create table t2(id int primary key not null auto_increment,name varchar(2));
Query OK, 0 rows affected (0.03 sec)

root@mysql 14:45:  [ck_test]> insert into t2 values(null,'aa'),(null,'bb');
Query OK, 2 rows affected (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 0

root@mysql 14:45:  [ck_test]> select * from t2;
+----+------+
| id | name |
+----+------+
|  1 | aa   |
|  2 | bb   |
+----+------+
2 rows in set (0.01 sec)

-- Clickhouse
mdw :) select * from ck_test.t2 order by id ;

SELECT *
FROM ck_test.t2
ORDER BY id ASC

┌─id─┬─name─┐
│  1 │ aa   │
└────┴──────┘
┌─id─┬─name─┐
│  2 │ bb   │
└────┴──────┘

2 rows in set. Elapsed: 0.009 sec.

-- Clickhouse隐藏字段查询
mdw :) select *,_sign,_version from ck_test.t2 order by id;

SELECT
    *,
    _sign,
    _version
FROM ck_test.t2
ORDER BY id ASC

┌─id─┬─name─┬─_sign─┬─_version─┐
│  1 │ aa   │     1 │        7 │                //一次性写入的_version=7一致,由于是insert操作,_sign=1。
│  2 │ bb   │     1 │        7 │
└────┴──────┴───────┴──────────┘

2 rows in set. Elapsed: 0.003 sec.


2、数据更新

对于MySQL的Update操作,当我们直接查询CK表数据时,可以看到表数据已经正常“更新”,但是我们额外去查询_sign,_version的信息时,可以发现更新前数据其实并没有进行物理删除。所以,对于Update操作Clickhouse的做法其实是变更后的记录进行写入,并标记_sign=1,_version=${当前版本}+1,我们执行query查询时,CK根据版本控制帮我们最终返回为正常的结果集信息。

-- MySQL
root@mysql 14:45:  [ck_test]> select * from t2;
+----+------+
| id | name |
+----+------+
|  1 | aa   |
|  2 | bb   |
+----+------+
2 rows in set (0.01 sec)

root@mysql 14:47:  [ck_test]> update t2 set name='aaa' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

root@mysql 14:47:  [ck_test]> select * from t2;
+----+------+
| id | name |
+----+------+
|  1 | aaa  |
|  2 | bb   |
+----+------+
2 rows in set (0.01 sec)

-- Clickhouse
mdw :) select * from ck_test.t2 order by id ;                   //可以看到,正常的查询,CK已经帮我们做了处理

SELECT *
FROM ck_test.t2
ORDER BY id ASC

┌─id─┬─name─┐
│  1 │ aaa  │
└────┴──────┘
┌─id─┬─name─┐
│  2 │ bb   │
└────┴──────┘

2 rows in set. Elapsed: 0.014 sec.

mdw :) select *,_sign,_version from ck_test.t2 order by id;

SELECT
    *,
    _sign,
    _version
FROM ck_test.t2
ORDER BY id ASC

┌─id─┬─name─┬─_sign─┬─_version─┐
│  1 │ aaa  │     1 │        8 │                                //update更新后的数据被写入ck表,_version+1
└────┴──────┴───────┴──────────┘
┌─id─┬─name─┬─_sign─┬─_version─┐
│  1 │ aa   │     1 │        7 │
│  2 │ bb   │     1 │        7 │
└────┴──────┴───────┴──────────┘

3 rows in set. Elapsed: 0.008 sec.

3、数据删除

对于Delete操作,当我们直接查询CK表数据时,可以看到表数据已经正常“删除”,但是我们额外去查询_sign,_version的信息时,可以发现更新前数据其实并没有进行物理删除,而是新增一行需要删除的行记录数据,并标记_sign=-1、_version=当前版本+1,所以对于ck的delete操作,其实也不会直接对记录进行物理删除,而是依靠sign进行删除标记。当我们执行query查询时,CK根据版本控制帮我们最终返回为正常的结果集信息。

-- MySQL
root@mysql 14:47:  [ck_test]> select * from t2;
+----+------+
| id | name |
+----+------+
|  1 | aaa  |
|  2 | bb   |
+----+------+
2 rows in set (0.00 sec)

root@mysql 14:48:  [ck_test]> delete from t2 where id=2;
Query OK, 1 row affected (0.01 sec)

root@mysql 14:48:  [ck_test]> select * from t2;
+----+------+
| id | name |
+----+------+
|  1 | aaa  |
+----+------+
1 row in set (0.01 sec)

-- Clickhouse
mdw :) select * from ck_test.t2 order by id ;                           //query操作正常显示结果集信息

SELECT *
FROM ck_test.t2
ORDER BY id ASC

┌─id─┬─name─┐
│  1 │ aaa  │
└────┴──────┘

1 rows in set. Elapsed: 0.009 sec.

mdw :) select *,_sign,_version from ck_test.t2 order by id;

SELECT
    *,
    _sign,
    _version
FROM ck_test.t2
ORDER BY id ASC

┌─id─┬─name─┬─_sign─┬─_version─┐
│  1 │ aaa  │     1 │        8 │
└────┴──────┴───────┴──────────┘
┌─id─┬─name─┬─_sign─┬─_version─┐
│  1 │ aa   │     1 │        7 │
│  2 │ bb   │    -1 │        9 │                                            //新增删除记录行,并标记_sign=-1表示删除操作
│  2 │ bb   │     1 │        7 │
└────┴──────┴───────┴──────────┘

4 rows in set. Elapsed: 0.008 sec.


4、DDL

1)对于ck表数据结构信息,由于MaterializeMySQL暂时不支持show create xx的语法,所以我们可以通过对应的物理文件查看CK创建的表结构信息。

[root@mdw metadata]# cat ck_test
cat: ck_test: 是一个目录
[root@mdw metadata]# cat ck_test.sql
ATTACH DATABASE ck_test
ENGINE = MaterializeMySQL('172.16.104.13:3306', 'ck_test', 'root', '123')
[root@mdw metadata]# cat ck_test/t2.sql
ATTACH TABLE t2
(
    `id` Int32,
    `name` Nullable(String),
    `_sign` Int8 MATERIALIZED 1,
    `_version` UInt64 MATERIALIZED 1
)
ENGINE = ReplacingMergeTree(_version)
PARTITION BY intDiv(id, 4294967)                                    //分区键、排序键均由MySQL表数据主键继承
ORDER BY tuple(id)
SETTINGS index_granularity = 8192

2)新增字段DDL操作

-- MySQL新增表字段
root@mysql 14:48:  [ck_test]> alter table t2 add age int;
Query OK, 0 rows affected (0.07 sec)
Records: 0  Duplicates: 0  Warnings: 0

root@mysql 14:52:  [ck_test]> select * from t2;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | aaa  | NULL |
+----+------+------+
1 row in set (0.00 sec)


-- Clickhouse表字段信息
[root@mdw metadata]# cat ck_test/t2.sql
ATTACH TABLE t2
(
    `id` Int32,
    `name` Nullable(String),
    `age` Nullable(Int32),                                          //CK表同步MySQL新增字段,对于ck这种列存储来讲,新增字段的操作还是比较简单的
    `_sign` Int8 MATERIALIZED 1,
    `_version` UInt64 MATERIALIZED 1
)
ENGINE = ReplacingMergeTree(_version)
PARTITION BY intDiv(id, 4294967)
ORDER BY tuple(id)

mdw :) select * from ck_test.t2 order by id ;

SELECT *
FROM ck_test.t2
ORDER BY id ASC

┌─id─┬─name─┬──age─┐
│  1 │ aaa  │ ᴺᵁᴸᴸ │
└────┴──────┴──────┘

1 rows in set. Elapsed: 0.010 sec.

mdw :) select *,_sign,_version from ck_test.t2 order by id;

SELECT
    *,
    _sign,
    _version
FROM ck_test.t2
ORDER BY id ASC

┌─id─┬─name─┬──age─┬─_sign─┬─_version─┐
│  1 │ aa   │ ᴺᵁᴸᴸ │     1 │        7 │
│  1 │ aaa  │ ᴺᵁᴸᴸ │     1 │        8 │
│  2 │ bb   │ ᴺᵁᴸᴸ │    -1 │        9 │
│  2 │ bb   │ ᴺᵁᴸᴸ │     1 │        7 │
└────┴──────┴──────┴───────┴──────────┘

4 rows in set. Elapsed: 0.023 sec.

三、后续一些疑问以及思考

  • 对于Clickhouse创建复制,是需要获取全局读锁、并且进行增长表的全表扫描查询,这都是一些资源消耗巨大的操作。所以还是建议将对ck复制选择开启binlog以及log_slave_updates的从库作为源进行复制
  • ck创建复制通道时,需要数据库用户权限最小为:SELECT, REPLICATION SLAVE ON .
  • 对于正常的OLTP业务,我们的表数据变更还是有频繁的更新操作的,而ck全部是做一种冗余插入的操作。我们关心的第一点是性能的问题,频繁的数据更新是否会导致ck有较大的资源消耗、第二点是磁盘空间消耗,ck是否会后台急性一些merge操作删除历史无效数据?

文章参考:

MySQL到ClickHouse的高速公路-MaterializeMySQL引擎
ClickHouse和他的朋友们(9)MySQL实时复制与实现
CK官方文档
Clickhouse for Github

猜你喜欢

转载自blog.csdn.net/weixin_37692493/article/details/114791399