Clickhouse — 副本与分片
1. 概述
集群作为副本与分片的基础将Clickhouse的服务从单个节点扩展到多个节点。
Clickhouse的配置相当灵活,可以把所有节点放在一个单一的集群,也可以划分为多个集群。
下面的图片和公式描述了分片和副本的关系:
S
o
u
r
c
e
1.
d
a
t
a
=
=
S
o
u
r
c
e
2.
d
a
t
a
S
o
u
r
c
e
3.
d
a
t
a
≠
S
o
u
r
c
e
4.
d
a
t
a
Source1.data == Source2.data \\ Source3.data \neq Source4.data
Source1.data==Source2.dataSource3.data?=Source4.data 
下面具体讲一下数据副本。
2. 数据副本
说到数据副本,那么就要说到Replicated-MergeTree ,只有这种表引擎才有副本的概念与使用的能力。Replicated-MergeTree 作为MergeTree 的派生引擎,在其基础上添加了分布式协同的功能。如下图: 
上述过程可以分为三步:
-
数据首先被写入内存缓冲区。 -
数据接着被写入tmp 临时分区,待全部完成后写入正式分区。 -
添加了ZooKeeper 部分,建立一系列监听节点。
在整个通讯过程中,zookeeper 不涉及表数据的传输。
2.1 特点
- 依赖
Zookeeper :在INSERT 以及ALTER 查询时,ReplicatedMergedTree 会借助ZooKeeper 实现分布式协同的能力。 - 多主架构:可以在任意的副本进行
INSERT 以及ALTER ,效果一致。 - BLOCK数据块:在
INSERT 时,会根据max_insert_block_size 的大小将数据进行切分。BLOCK的读写操作满足原子性和唯一性
- 原子性:跟mysql一样的理解,要么全部成功,要么全部失败。
- 唯一性:BLOCK会根据数据顺序、数据行、数据大小等计算
HASH 值。写入时发现存在相同HASH 的BLOCK时,会自动忽略当前写入BLOCK块。这样预防了重复写入的问题。 - 表级别副本:副本是在表级别定义的,可以进行个性化设置。
2.2 定义形式
首先我们看一下zookeeper的配置:
下面是在/etc/clickhouse-server/config.d/zookeeper.xml 的配置
<yandex>
<zookeeper-servers>
<node>
<host>zookeeper-clickhouse</host>
<port>2181</port>
</node>
</zookeeper-servers>
</yandex>
接着就是在全局配置/etc/clickhouse-server/config.xml 中配置
<zookeeper incl="zookeeper-servers" optinal="true"/>
<include_from>/etc/clickhouse-server/config.d/zookeeper.xml</include_from>
<zookeeper incl="zookeeper-servers" optinal="false"/>
可以通过sql命令来查看zookeeper里面的信息。
SELECT * FROM system.zookeeper WHERE path='/'
下面进入正题,介绍一下副本的定义形式:
ENGINE = ReplicatedMergeTree('zookeeper_path','replica_name')
Zookeeper_path :用户指定在zookeeper 中创建的数据表的路径(可自定义)。例如
/clickhouse/tables/{shard}/table_name
replica_name :副本位置,同一分片的不同副本需要不同的名字
比如:
ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/test1','alvin1.icloud.com')
ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/test1','alvin2.icloud.com')
ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/test1','alvin1.icloud.com')
ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/test1','alvin2.icloud.com')
ENGINE = ReplicatedMergeTree('/clickhouse/tables/02/test1','alvin3.icloud.com')
ENGINE = ReplicatedMergeTree('/clickhouse/tables/02/test1','alvin4.icloud.com')
3. RelicatedMergeTree原理解析
在RelicatedMergeTree 中大量使用了zookeeper 来实现副本之间的协同,包括主副本选举、副本状态感知、操作日志、任务队列等。下面讲一下zookeeper 内部的结构。
3.1 Zookeeper内部结构
监听节点可以大致分成如下几类:
- 元数据:
- /metadata:保存元数据信息,包括主键、分区键等
- /columns:列名称和数据类型
- /replicas:设置参数中的replica_name
- 判断标识:
- /leader_election:用于主副本选举
- /blocks:记录Block数据块的Hash信息,比如
partition_id - /block_numbers:分区的写入顺序。各个副本在本地
MERGE 时,都会按照相同的顺序进行。 - /quornum:记录quornum的数量。
- 操作日志:
- /log:保存副本需要执行的任务指令。
- /mutation:当执行
ALTER DELETE 和 ALTER UPDATE 查询时,操作指令会被添加到这个节点。 - /replicas/{replica_name}/*:每个副本各自节点下的一组监听节点,指导副本在本地的执行。这里面有一些重要的节点:
- /queue:任务队列节点
- /log_pointer:log日志指针节点。
3.2 副本协同的核心流程
这里分成四个部分进行讲解:分别为INSERT、MERGE、MUTATION、ALTER这四个部分。
每一个部分先放上流程图,然后加上文字的描述。
3.2.1 INSERT
流程:
-
创建第一个副本实例 首先从Alvin1 开始,对该节点执行CREATE CREATE TABLE replicated_sales_1(
id String,
price Float64,
date DateTime
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/replicated_sales_1','alvin1.icloud.com')
PARTITION BY toYYYYMM(create_time)
ORDER BY id
根据zookeeper_path 初始化所有的节点; 在/replica 下注册自己的副本实例;启动监听任务,监听/log ; 参与副本选举,方式就是插入/leader_election 节点,第一个插入成功的副本作为主副本。 -
创建第二个副本实例,在alvin2 执行相同的语句 CREATE TABLE replicated_sales_1(
id String,
price Float64,
date DateTime
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/replicated_sales_1','alvin2.icloud.com')
PARTITION BY toYYYYMM(create_time)
ORDER BY id
-
向第一个副本实例写入数据 INSERT INTO TABLE replicated_sales_1 VALUES(1,99.9,"2021-07-07 00:00:00")
首先会向本地分区写入数据,接着向/block 节点写入分区的block_id -
由第一个副本推送日志logEntry 可以看出操作为GET,需要下载的分区为202107_0_0_0 -
第二个副本拉取日志 注意:拉取以后**不会直接执行!**而是放到/replicas/alvin2.icloud.com/queue 中。 -
第二个副本实例向其他副本发送下载请求 alvin2 基于/queue 开始执行任务,当看到get请求时,会选择一个远端作为数据下载的来源。 副本选择算法:
- 从
/replicas 节点拿到所有的副本节点 - 遍历副本选择/queue自节点最少的且log_pointer下标最大的节点进行拉取。
-
副本实例响应下载 -
第二个实例下载数据并完成本地写入 Tmp_fetch_202107_0_0_0->202107_0_0_0

3.2.2 MERGE
由于细节在图中展现出来,流程仅简要概述
流程:
-
创建远程连接,尝试连接主副本 -
主副本接受通讯 -
由主副本指定MERGE策略并推送Log 从日志里面可以看出,操作类型为MERGE类型,需要合并的分区为202107_0_0_0 与202107_1_1_0 同时,主副本会锁住执行线程,对日志的接收情况进行监听,其由replication_alter_partitions_sync 控制:
- 0: 不做任何等待
- 1:只等待主副本完成
- 2:会等待所有的副本拉取完成
-
各个副本拉取Log -
各个副本在本地执行MERGE 
3.2.3 MUTATION
流程:
- 推送MUTATION日志
- 所有副本监听mutation日志
- 由主副本实例响应mutation日志并推送Log日志
- 各个副本拉取log日志
- 各个副本本地执行mutation

3.2.4 ALTER
过程:
-
修改共享元数据 ALTER TABLE replicated_sales_1 ADD COLUMN comment String
执行后会修改Zookeeper内的共享节点:/metadata,/columns -
监听共享元数据变更并各自执行本地修改 -
确认所有的副本更改完毕

4. 数据分片
为了解决单机情况下,存储量的限制问题。clickhouse引入了分片的概念,对于clickhouse来说,每一个服务节点都可以称作分片(shard)。
为了解决分片的负载问题,click house引入了Distributed表引擎来进行代理,解决路由问题。
为什么叫做代理?
因为Distributed表本身不存放数据,仅作为分布式表的一层透明代理,在集群内自动开展数据的写入、分发、查询、路由工作。
从集群配置来进行讲解,首先给出Distributed表引擎与分片的关系。

4.1 自定义分片与副本
-
不包含副本的分片
<sharding_simple>
<shard>
<replica>
<host>alvin1.icloud.com</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>alvin2.icloud.com</host>
<port>9000</port>
</replica>
</shard>
</sharding_simple>
其中,两分片0副本! -
N个分片N个副本
-
一分片一副本
<sharding_simple>
<shard>
<replica>
<host>alvin1.icloud.com</host>
<port>9000</port>
</replica>
<replica>
<host>alvin2.icloud.com</host>
<port>9000</port>
</replica>
</shard>
</sharding_simple>
-
两分片,每个分片一个副本
<sharding_simple>
<shard>
<replica>
<host>alvin1.icloud.com</host>
<port>9000</port>
</replica>
<replica>
<host>alvin2.icloud.com</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>alvin3.icloud.com</host>
<port>9000</port>
</replica>
<replica>
<host>alvin4.icloud.com</host>
<port>9000</port>
</replica>
</shard>
</sharding_simple>
|