Clickhouse具有丰富的表引擎,而与副本相关的表引擎则有Replicated+*MergeTree来构成,如下图所示
换言之,只有使用了ReplicatedMergeTree复制表系列引擎,才能应用副本的能力。**ReplicatedMergeTree时MergeTree的派生引擎,它在MergeTree的基础上加入了分布式协同的能力。**如下图所示:
上图展示了两个节点利用Zookeepr进行副本协同复制的原理图。在MergeTree中,一个数据分区由开始创建到全部完成,会经历两类存储区域:
ReplicatedMergeTree在上述基础上增加了Zookeeper的部分,它会进一步在Zookeeper内创建一系列的监听节点,并以此实现多个实例之间的通信。在整个通信中,Zookeeper并不会涉及表数据的传输。
作为数据副本的主要实现载体,ReplicatedMergeTree在设计上有一些显著特点:
ReplicatedMergeTree作为复制表系列的基础表引擎,涵盖了数据副本最为核心的逻辑。
在ReplicatedMergeTree的核心逻辑中,大量运用了Zookeeper的能力,以实现多个ReplicatedMergeTree副本实例之间的协同,包括主副本选举、副本状态感知、操作日志分发、任务队列和BlockID去重判断等。在执行Insert数据写入、Merge分区和MUTATION操作的时候,都会涉及与Zookeeper的通信。但是在通信的过程中,并不会涉及任何表数据的传输,在查询数据的时候也不会访问Zookeeper。
ReplicatedMergeTree需要依靠Zookeeper的事件监听机制以实现各个副本之间的协同。所以,**在每张ReplicatedMergeTree表的创建过程中,它会以zk_path为根路径,在Zookeeper中为这张表创建一组监听节点。**按照作用的不同,监听节点可以大致分成如下几类:
从上一小节的介绍中能够得知,ReplicatedMergeTree在Zookeeper中有两组非常重要的父节点,那就是/log和/mutations。它们是分发操作指令的信息通道,而发送指令的方式,则是为这些父节点添加子节点。所有的副本实例,都会监听父节点的变化,当有子节点被添加时,它们能够实时感知。
这些被添加的子节点在Clickhouse中被统一抽样为Entry对象,而具体实现则由LogEntry和MutationEntry对象承载,分别对应/log和/mutations节点。
LogEntry用于封装/log的子节点信息,它拥有如下几个核心属性:
MutationEntry用于封装/mutations的子节点信息,它同样拥有如下几个核心属性:
以上就是Entry日志对象的数据结构信息,在接下来将要介绍的核心流程中,将会看到它们的身影。
副本协同的核心流程主要有INSERT、MERGE、MUTATION和ALTER四种,分别对应了数据写入,分区合并,数据修改和元数据修改。INSERT和ALTER查询是分布式执行的。 借助Zookeeper的事件通知机制,多个副本之间会自动进行有效协同,但是它们不会使用Zookeeper存储任何分区数据。
当需要在ReplicatedMergeTree中执行INSERT查询以写入数据时,即会进入INSERT核心流程。整体流程如下图所示:
假设首先从Linux121节点开始,对Linux121节点执行下面的语句后,会创建第一个副本实例
CREATE TABLE test.replicated_test_1
(
`id` String,
`price` Float64,
`create_time` DateTime
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/replicated_test_1', 'linux121')
PARTITION BY toYYYYMM(create_time)
ORDER BY id;
在创建的过程中,ReplicatedMergeTree会进行一些初始化操作,例如:
这里我因为已经在Linux122节点下创建了对应表副本,所以注册的replicas包括linux121和linux122
启动监听任务,监听/log日志节点
参与副本选举,选举出主副本,选举的方式是向/leader_election/插入子节点,第一个插入成功的副本就是主副本
接着,在Linux122节点执行下面的语句,创建第二个副本实例。表结构和zk_path需要与第一个副本相同,而replica_name则需要设置成Linux122的域名
CREATE TABLE test.replicated_test_1
(
`id` String,
`price` Float64,
`create_time` DateTime
)
ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/replicated_test_1', 'linux122')
PARTITION BY toYYYYMM(create_time)
ORDER BY id;
在创建过程中,第二个ReplicatedMergeTree同样会进行一些初始化操作,例如:
现在尝试向第一个副本Linux121写入数据。执行如下命令:
insert into replicated_test_1 values ('A001', 100.0, '2023-04-01 12:00:00');
上述命令执行之后,首先会在本地完成分区目录的写入,在clickhouse-server.log日志下可以找到对应记录:
2023.04.02 16:37:54.521226 [ 9683 ] {} system.metric_log: Renaming temporary part tmp_insert_202304_3005_3005_0 to 202304_8903_8903_0.
此外,如果设置了insert_quorum参数(默认为0),并且insert_quorum >= 2,则Linux121会进一步监控已完成写入操作的副本个数,只有当写入副本个数大于或等于insert_quorum时,整个写入操作才算成功。
在第(3)步骤完成之后,会继续由执行了INSERT的副本向/log节点推送操作日志。在这个例子中,会由第一个副本Linux121担此重任。日志的编号为:/log/log-0000000000,而LogEntry的核心属性如下:
从日志内容可以看出,操作类型为get下载,而需要下载的分区是202304_0_0_0。其余所有副本都会基于Log日志以相同的顺序执行命令。
Linux122副本会一直监听/log节点变化,当Linux121推送了/log/log-0000000000之后,Linux122便会触发日志的拉取任务并更新log_pointer,将其指向最新日志下标:
/replicas/linux122/log_pointer: 0
在拉取了LogEntry之后,它并不会直接执行,而是将其转为任务对象放至队列:
/replicas/linux122/queue
Pulling 1 entries to queue: log-0000000000 - log-0000000000
这是因为在复杂的情况下,考虑到在同一时段内,会连续收到许多个LogEntry,所以使用队列的形式消化任务是一个更为合理的设计。注意,拉取的LogEntry是一个区间,这同样也是因为可能会连续收到多个LogEntry。
Linux122基于/queue队列开始执行任务。当看到type类型为get的时候,ReplicatedMergeTree会明白此时在远端的其他副本中已经成功写入了数据分区,而自己需要同步这些数据。
Linux122上的第二个副本实例会开始选择一个远端的其他副本作为数据的下载来源。远端副本的选择算法大致是这样的:
在这个例子中,算法选择的远端副本是Linux121。于是Linux122副本向Linux121节点发起了HTTP请求,希望下载分区202304_0_0_0:
2023.04.02 16:10:28.848950 [ 7092 ] {} test.replicated_test_1 (b07cd471-2267-4e0b-ba15-eafd33b020f3): Fetching part 202304_0_0_0 from /clickhouse/tables/01/replicated_test_1/replicas/linux121
如果第一次下载请求失败,在默认情况下,Linux122再尝试请求4次,一共会尝试5次(由max_fetch_partition_retries_count参数控制,默认为5)。
Linux121的DataPartsExchange端口服务接收到调用请求,在得知对方来意之后,根据参数做出响应,将本地分区202304_0_0_0基于DataPartsExchange的服务响应返回Linux121:
Sending part 202404_0_0_0
Linux122副本在收到Linux121的分区数据后,首先将其写至临时目录:
tmp_fetch_202304_0_0_0
待全部数据接收完成之后,重命名该目录:
Renaming temporary part tmp_fetch_202304_0_0_0 to 202304_0_0_0
至此,整个写入流程结束。
可以看到,在INSERT的写入过程中,Zookeeper不会进行任何实质性的数据传输。本着谁执行谁负责的原则,在这个案例中由Linux121首先在本地写入了分区数据。之后,也由这个副本负责发送Log日志,通知其他副本下载数据。如果设置了insert_quorum并且insert_quorum>=2 ,则还会由该副本监控完成写入的副本数量。其他副本在接收到Log日志之后,会选择一个最合适的远端副本,点对点地下载分区数据。
当对ReplicatedMergeTree执行ALTER操作进行元数据修改的时候,即会进入ALTER部分的逻辑,例如增加、删除表字段等。而ALTER的核心流程如下图所示:
与INSERT的流程相比,ALTER的流程会简单很多,其执行过程并不会涉及/log日志的分发。整个流程从上至下按照时间顺序进行,其大致分成3个步骤。现在根据上图讲解整个过程:
在Linux122节点尝试增加一个列字段,执行如下语句:
ALTER TABLE replicated_test_1 add column place String after id
执行之后,Linux122会修改Zookeeper内的共享元数据节点:
/metadata,/columns
Update shared metadata nodes in ZooKeeper. Waiting for replicas to apply changes
数据修改后,节点的版本号也会同时提升:
Version of metadata nodes in Zookeeper changed. Waiting for structure write lock.
与此同时,linux122还会负责监听所有副本的修改完成情况:
Waiting for linux121 to apply changes
Waiting for linux122 to apply changes
Linux121和Linux122两个副本分别监听共享元数据的变更。之后,它们会分别对本地的元数据版本号与共享版本号进行对比。在这个案例中,它们会发现本地版本号低于共享版本号,于是它们开始在各自的本地执行更新操作:
Metadata changed in Zookeeper. Applying changes locally
Applied changes to the metadata of the table
Linux122确认所有副本均已完成修改。
ALTER finished
Done processing query
至此,整个ALTER流程结束
可以看到,在ALTER整个的执行过程中,Zookeeper不会进行任何实质性的数据传输。所有的ALTER操作,最终都是由各个副本在本地完成的。本着谁执行谁负责的原则,在这个案例中,由linux122负责对共享元数据的修改以及对各个副本修改进度的监控。