Clickhouse数据副本和分片
Clickhouse数据副本与分区
为什么会存在集群呢?
随着业务量的增长,单节点的压力会随着增大,如果单节点挂掉,那么该CK就无法使用,存在单点故障的问题,所以引申出集群的概念
分片和副本:
简而言之,分片之间的数据完全不同,副本之间的数据完全相同,副本是防止数据丢失,增加数据冗余;分片的主要目的是数据的水平切分。

演进的过程:
单表单副本->存在单点压力,数据无法保障高可用
单表多副本->解决单点压力和数据高可用问题,但数据存储容量有限,无法做到横向扩展
单表多副本多分片->解决数据横向扩展问题,数据根据某个规则划分存储到不同的实例上

一、数据副本
简而言之,对于MergeTree系列的引擎在前面加上Replicated建表,说明该表是一个具备数据副本能力的。

只有使用了ReplicatedMergeTree复制表系列引擎,才能应用副本的能力。ReplicatedMergeTree是MergeTree的派生引擎,它在MergeTree的基础上加入了分布式协同的能力

示意图:
在MergeTree中,一个数据分区由开始创建到创建完成,会经历两个不同的存储。
- 内存:数据首先会被写入到内存的缓冲区中
- 本地磁盘:数据接着会被写入tmp临时目录分区,待全部完成后再将临时目录重命名为正式分区。
ReplicatedMergeTree在上述基础之上增加了ZooKeeper的部分,它会进一步在ZooKeeper内创建一系列的监听节点,并以此实现多个实例之间的通信。在整个通信过程中,ZooKeeper并不会涉及表数据的传输。
副本的特点(Replicated*)
- 依赖zookeeper,在执行Insert、Alter时,ReplicatedMergeTree需要依托zookeeper的分布式协同能力,以实现多个副本之间的同步
- 表级别的副本,副本是在表级别的,针对每张表的副本配置,可以设置不同的副本数以及副本其他信息,包括副本的数量,以及副本在集群内的分布位置等
- 多主架构,不管操作哪个副本,效果都是相同的,借助ZooKeeper的协同能力被分发至每个副本以本地形式执行。
- Block数据块,在执行INSERT命令写入数据时,会依据max_insert_block_size的大小(默认1048576行)将数据切分成若干个Block数据块。所以Block数据块是数据写入的基本单元,并且具有写入的原子性和唯一性。
- 原子性,在数据写入时,一个Block块内的数据要么全部写入成功,要么全部失败
- 唯一性,在写入一个Block数据块的时候,会按照当前Block数据块的数据顺序、数据行和数据大小等指标,计算Hash信息摘要并记录在案。在此之后,如果某个待写入的Block数据块与先前已被写入的Block数据块拥有相同的Hash摘要(Block数据块内数据顺序、数据大小和数据行均相同),则该Block数据块会被忽略,预防由异常原因引起的Block数据块重复写入的问题。
zookeeper的配置方式
zookeeper或者keeper配置
1.在config.d目录下面创建metrika.xml文件
<yandex><zookeeper-servers><!--节点配置,可以配置多个地址--><node index="1"><host>hb1</host><port>2181</port></node><node index="2"><host>hb2</host><port>2181</port></node><node index="3"><host>hb3</host><port>2181</port></node></zookeeper-servers>
</yandex>
2.在config.xml配置文件引入即可
<include_from>/etc/clickhouse-server/config.d/metrika.xml</include_from>
<zookeeper incl="zookeeper-servers" optional="false" /> 该名称与metrika.xml定义的一致。
clickhouse的系统表提供了访问keeper路径的方式,system.zookeeper。
二、ReplicatedMergeTree原理解析
ReplicatedMergeTree是复制表最为基础表引擎。
数据结构
在ReplicatedMergeTree的核心逻辑中,大量运用keeper的能力,实现多个ReplicatedMergeTree副本实例之间的协同,包括主副本的选举、副本状态感知、操作日志分发、任务队列和BlockId去重等。
在执行INSERT数据写入、MERGE分区和MUTATION操作的时候,都会涉及与ZooKeeper的通信。但是在通信的过程中,并不会涉及任何表数据的传输,在查询数据的时候也不会访问ZooKeeper。
keeper节点的结构
ReplicatedMergeTree需要依靠ZooKeeper的事件监听机制以实现各个副本之间的协同。所以,在每张ReplicatedMergeTree表的创建过程中,它会以zk_path为根路径,在Zoo-Keeper中为这张表创建一组监听节点。
测试表建表语句,注意使用on cluster创建表是一次性动作,如果因为网络或者节点异常原因导致某个节点创建失败,那么再次使用默认指定path的方式创建表的uuid时,两次操作的uuid不一致,可能导致数据同步异常。
create table test.rep_tab_local on cluster perftest_2shards_1replicas
(uid Int32,uname String)
engine=ReplicatedMergeTree() order by uid#查询元数据
select * from system.zookeeper where
path='/clickhouse/tables/01ed8af1-b5d6-4a88-8960-42b5946fbaec/02' \G
- 元数据
- metadata:保存元数据信息,包括主键、分区键、采样表达式等

- /columns:保存列字段信息,包括列名称和数据类型。

- /replicas:保存副本名称,对应设置参数中的replica_name。,不同副本名称的value是不一样的。

- 判断标识
- /leader_election:用于主副本的选举工作,主副本会主导MERGE和MUTATION操作(ALTER DELETE和ALTER UPDATE)。这些任务在主副本完成之后再借助ZooKeeper将消息事件分发至其他副本。

- /blocks:记录Block数据块的Hash信息摘要,以及对应的partition_id。通过Hash摘要能够判断Block数据块是否重复;通过partition_id,则能够找到需要同步的数据分区

- /block_numbers:按照分区的写入顺序,以相同的顺序记录partition_id。各个副本在本地进行MERGE时,都会依照相同的block_numbers顺序进行。

- /quorum:记录quorum的数量,当至少有quorum数量的副本写入成功后,整个写操作才算成功。quorum的数量由insert_quorum参数控制,默认值为0
- 操作日志
-
log:常规操作日志节点(INSERT、MERGE和DROP PARTITION),它是整个工作机制中最为重要的一环,保存了副本需要执行的任务指令。log使用了ZooKeeper的持久顺序型节点,每条指令的名称以log-为前缀递增,例如log-0000000000、log-0000000001等。每一个副本实例都会监听/log节点,当有新的指令加入时,它们会把指令加入副本各自的任务队列,并执行任务。

-
/mutations:MUTATION操作日志节点,作用与log日志类似,当执行ALERT DELETE和ALERT UPDATE查询时,操作指令会被添加到这个节点。mutations同样使用了ZooKeeper的持久顺序型节点,但是它的命名没有前缀,每条指令直接以递增数字的形式保存,例如0000000000、0000000001等

-
/replicas/{replica_name}/*:每个副本各自的节点下的一组监听节点,用于指导副本在本地执行具体的任务指令,其中较为重要的节点有如下几个:
(1)/queue:任务队列节点,用于执行具体的操作任务。当副本从/log或/mutations节点监听到操作指令时,会将执行任务添加至该节点下,并基于队列执行。
(2)log_pointer:log日志指针节点,记录了最后一次执行的log日志下标信息,例如log_pointer:4对应了/log/log-0000000003(从0开始计数)
(3)/mutation_pointer:mutations日志指针节点,记录了最后一次执行的mutations日志名称,例如mutation_pointer:0000000000对应了/mutations/000000000。

LogEntry结构:
LogEntry用于封装/log的子节点信息,它拥有如下几个核心属性:
- source replica:发送这条Log指令的副本来源,对应replica_name。
- type:操作指令类型,主要有get、merge和mutate三种,分别对应从远程副本下载分区、合并分区和MUTATION操作。
- block_id:当前分区的BlockID,对应/blocks路径下子节点的名称。
- partition_name:当前分区目录的名称。

合并的log子节点信息

MutationEntry结构:
MutationEntry用于封装/mutations的子节点信息,它同样拥有如下几个核心属性: - source replica:发送这条MUTATION指令的副本来源,对应replica_name。
- commands:操作指令,主要有ALTER DELETE和ALTER UPDATE。
- mutation_id:MUTATION操作的版本号。
- partition_id:当前分区目录的ID。

三、副本协调的流程
副本协同的核心流程主要有INSERT、MERGE、MUTATION和ALTER四种,分别对应了数据写入、分区合并、数据修改和元数据修改。INSERT和ALTER查询是分布式执行的。借助ZooKeeper的事件通知机制,多个副本之间会自动进行有效协同,但是它们不会使用ZooKeeper存储任何分区数据。
而其他查询并不支持分布式执行,包括SELECT、CREATE、DROP、RENAME和ATTACH。
注意:alter增加表字段和删除表字段,可以进行副本之间同步
1、Insert执行流程
示意图如下:
步骤1:创建表create
由于该CREATE语句没有分布式协同,需要每个实例节点执行
a.初始化表节点,根据创建表时指定的zk_path初始化,默认是/clickhouse/tables/{uuid}/{shard} {副本名称replicas}
b.注册副本节点/replicas/副本名称
c.监听/log,实时操作日志,持续监听
d.选举主副本,默认第第一个将会是主副本/leader_election
e.第二个节点创建,注册副本节点/replicas、监听/log、选举/leader_election
步骤2:数据写入
a.客户端执行insert
b.本地创建新分区(2025_0_0_0),写入/block_id(/block_id/2025_0_0_0)
c.由执行insert的副本发生日志,推送logEntry(/log/log-0000000)
d.其他副本监听/log有新操作日志,拉取logEntry,创建task任务放进副本执行队列/replicas/副本名称/queue
e.基于/queue队列,执行task任务
f.选择远端副本下载数据,
选择算法:
从/replicas所有副本列表查找
规则(/log_pointer最大,/queue队列最小副本)log_pointer下标最大,意味着该副本执行的日志最多,数据应该更加完整;而/queue最小,则意味着该副本目前的任务执行负担较小
g.选择完成后发起下载请求,默认请求5次。如果失败就会更换副本发起请求,max_fetch_partition_retries_count参数控制
h.由目标副本接到下载请求后主动send数据到该副本进行数据持久化。
i.(首先先写到临时目录,数据接收完成后,将临时路径改成正式路径)完成数据持久化后,发生ack应答确保数据一致性。

副本监听到/log最新的操作日志后并不会立马执行,而是将其转换成task写入/queue队列中,因为在某些时刻会同时多个操作日志,这时候用队列的作用就是缓存操作日志异步执行。
可以看到,在INSERT的写入过程中,ZooKeeper不会进行任何实质性的数据传输。本着谁执行谁负责的原则,在这个案例中由A节点首先在本地写入了分区数据。之后,也由这个副本负责发送Log日志,通知其他副本下载数据。如果设置了insert_quorum并且insert_quorum>=2,则还会由该副本监控完成写入的副本数量。其他副本在接收到Log日志之后,会选择一个最合适的远端副本,点对点地下载分区数据。
2、Merge执行流程
当ReplicatedMergeTree触发分区合并动作时,也会通过keeper分布式协调组件进行状态同步。
无论哪个副本发起merge操作,其合并的计划都会交给主副本来定制,在创建副本表时就已经选举出主副本节点。
1.从副本创建远程连接
从副本执行OPTIMIZE,强制触发MERGE合并,从副本通过/replicas找到主副本,并尝试与之建立连接。
2.主副本接收连接
主副本接收来着从副本的连接,并由主副本制定merge合并计划,判断哪些分区需要合并,确认后会生产LogEntry推送至/log下,用于通知/replicas副本集下面所有副本进行merge操作。
此时主副本会监控所有副本执行情况。
监听行为由replication_alter_partitions_sync参数控制 默认是1:
1:等待主副本合并操作完成
0:不等待
2:等待所有副本合并操作完成
3.各个副本监听/log
当副本监听到/log有操作日志后,分别拉取日志到本地并转换成task推送到/queue任务队列。
4.各个副本本地执行merge操作
副本在本地执行merge操作。
在MERGE的合并过程中,ZooKeeper也不会进行任何实质性的数据传输,所有的合并操作,最终都是由各个副本在本地完成的。而无论合并动作在哪个副本被触发,都会首先被转交至主副本,再由主副本负责合并计划的制定、消息日志的推送以及对日志接收情况的监控。
注意新版本所有副本都可以设置成主副本,目的在于谁负责谁执行,避免过多的远程建立连接的动作。

3、Mutation执行流程
当对ReplicatedMergeTree表执行ALTER UPDATE、ALTER DELETE时会执行mutation流程
mutation执行流程与merge类型,无论是哪个副本发起mutation操作,都会将请求转发到主副本,由主副本制定执行计划、推送log日志、监控log日志拉取情况。

4、Alter执行流程
Alter针对表字段的增加和删除。该执行流程不会远程连接主副本节点生成执行计划,推送/log进行分发。遵循谁负责谁执行
执行流程:
- 在某个节点执行修复表字段类型操作,执行之后,该节点会修改keeper共享元数据,/metadata、/columns,节点版本信息会增加,
- 其他副本监听到/metadata的版本变化后,会各自执行本地元数据变更(本地的元数据版本号与共享版本号进行对比,会发现本地版本号低于共享版本号,于是它们开始在各自的本地执行更新操作)
- 发起执行的副本会监听所有副本执行完成后返回响应

在ALTER整个的执行过程中,ZooKeeper不会进行任何实质性的数据传输。所有的ALTER操作,最终都是由各个副本在本地完成的。本着谁执行谁负责的原则,在这个案例中由CH6负责对共享元数据的修改以及对各个副本修改进度的监控。
四、分布式表查询的核心流程
面向集群数据查询时,只能通过Distributed表引擎实现,当Distributed表接收到SELECT查询的时候,它会依次查询每个分片的数据,再合并汇总返回。
多副本路由规则
在查询数据的时候,如果集群中的一个shard,拥有多个replica,那么Distributed表引擎需要面临副本选择的问题。它会使用负载均衡算法从众多replica中选择一个,而具体使用何种负载均衡算法,则由load_balancing参数控制。
- random:random是默认的负载均衡算法,CK的服务节点,拥有一个全局计数器errors_count,当服务发生异常时,该计数器累积+1,而random算法会选择errors_count错误数最少的replica。如果errors_count错误数一样,将随机选择一个。
- nearest_hostname:nearest_hostname可以看作random算法的变种,首先它会选择errors_count错误数量最少的replica,如果多个replica的errors_count计数相同,则选择集群配置中host名称与当前host最相似的一个。而相似的规则是以当前host名称为基准按字节逐位比较,找出不同字节数最少的一个。
- in_order:策略同样与random类似,维护每个replica的errors_count值,选择错误数量最少的一个副本,如果错误数相同,按照集群配置定义的顺序进行选择。
- first_or_random:first_or_random可以看作in_order算法的变种,首先它会选择errors_count错误数量最少的replica,如果多个replica的errors_count计数相同,它首先会选择集群配置中第一个定义的replica,如果该replica不可用,则进一步随机选择一个其他的replica。
多分片查询的核心流程
分布式查询与分布式写入类似,同样本着谁执行谁负责的原则,它会由接收SELECT查询的Distributed表,并负责串联起整个过程。首先它会将针对分布式表的SQL语句,按照分片数量将查询拆分成若干个针对本地表的子查询,然后向各个分片发起查询,最后再汇总各个分片的返回结果。
后续将持续更新,文中的流程图借鉴《ClickHouse原理解析与应用实践》电子书,如有问题可交流沟通
