6.ClickHouse的副本与分片

十、副本与分片

10.1 副本和分片

​ 1.数据层面区分:

​ 假设CK集群两个节点,host1,host2,两个节点都有结构相同的一张表table。

​ 此时如果host1的table中的数据和host2的table中的数据不同,那就是分片;

​ 此时如果host1的table中的数据和host2的table中的数据相同,那就是副本。

​ 所以抛开表引擎的不同,单纯从数据层面来看,副本和分片有时候只有一线之隔。

​ 2.功能作用层面区分:

​ 副本:防止数据丢失,增加数据存储的冗余;

​ 分片:实现数据的水平切分,提高数据写入读取的性能。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-npRtuHnD-1610932819138)(C:\Users\12942\AppData\Roaming\Typora\typora-user-images\image-20210112074852130.png)]

10.2 数据副本

​ MergeTree的前面增加Replicated的前缀,则能够组合成一个新的变种引擎,即Replicated-MergeTree复制表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kKDM0L1X-1610932819138)(C:\Users\12942\AppData\Roaming\Typora\typora-user-images\image-20210112075924142.png)]

扫描二维码关注公众号,回复: 13034656 查看本文章

​ 只有使用了ReplicatedMergeTree复制表系列引擎,才能应用副本的能力。即使用ReplicatedMergeTree的数据表就是副本。

​ ReplicatedMergeTree是MergeTree的派生引擎,它在MergeTree的基础上加入了分布式协同的能力。

preview

​ 在MergeTree中,一个数据分区由开始创建到全部完成,会历经两类存储区域。
​ (1)内存:数据首先会被写入内存缓冲区。
​ (2)本地磁盘:数据接着会被写入tmp临时目录分区,待全部完成后再将临时目录重命名为正式分区。ReplicatedMergeTree在上述基础之上增加了ZooKeeper的部分,它会进一步在ZooKeeper内创建一系列的监听节点,并以此实现多个实例之间的通信。在整个通信过程中,ZooKeeper并不会涉及表数据的传输。

副本的特点

​ 作为数据副本的主要实现载体,ReplicatedMergeTree在设计上有一些显著特点。
​ ❑ 依赖ZooKeeper:在执行INSERT和ALTER查询的时候,ReplicatedMergeTree需要借助ZooKeeper的分布式协同能力,以实现多个副本之间的同步。但是在查询副本的时候,并不需要使用ZooKeeper。关于这方面的更多信息,会在稍后详细介绍。
​ ❑ 表级别的副本:副本是在表级别定义的,所以每张表的副本配置都可以按照它的实际需求进行个性化定义,包括副本的数量,以及副本在集群内的分布位置等。
​ ❑ 多主架构(Multi Master):可以在任意一个副本上执行INSERT和ALTER查询,它们的效果是相同的。这些操作会借助ZooKeeper的协同能力被分发至每个副本以本地形式执行。
​ ❑ Block数据块:在执行INSERT命令写入数据时,会依据max_insert_block_size的大小(默认1048576行)将数据切分成若干个Block数据块。所以Block数据块是数据写入的基本单元,并且具有写入的原子性和唯一性。
​ ❑ 原子性:在数据写入时,一个Block块内的数据要么全部写入成功,要么全部失败。
​ ❑ 唯一性:在写入一个Block数据块的时候,会按照当前Block数据块的数据顺序、数据行和数据大小等指标,计算Hash信息摘要并记录在案。在此之后,如果某个待写入的Block数据块与先前已被写入的Block数据块拥有相同的Hash摘要(Block数据块内数据顺序、数据大小和数据行均相同),则该Block数据块会被忽略。这项设计可以预防由异常原因引起的Block数据块重复写入的问题。如果只是单纯地看这些特点的说明,可能不够直观。没关系,接下来会逐步展开,并附带一系列具体的示例。

ZooKeeper的配置方式

​ ClickHouse使用一组zookeeper标签定义相关配置,默认情况下,在全局配置config. xml中定义即可。但是各个副本所使用的Zookeeper配置通常是相同的,为了便于在多个节点之间复制配置文件,更常见的做法是将这一部分配置抽离出来,独立使用一个文件保存。

​ 首先,在服务器的/etc/clickhouse-server/config.d目录下创建一个名为metrika.xml的配置文件:

<zookeeper-servers>
  <node index="1">
    <host>10.0.0.31</host>
    <port>2181</port>
  </node>
  <node index="2">
    <host>10.0.0.32</host>
    <port>2181</port>
  </node>
  <node index="3">
    <host>10.0.0.33</host>
    <port>2181</port>
  </node>
</zookeeper-servers>

​ 接着,在全局配置config.xml中使用<include_from>标签导入刚才定义的配置:
​ 并引用ZooKeeper配置的定义:

<!-- If element has 'incl' attribute, then for it's value will be used corresponding substitution from another file.
     By default, path to file with substitutions is /etc/metrika.xml. It could be changed in config in 'include_from' element.
     Values for substitutions are specified in /yandex/name_of_substitution elements in that file.
  -->
  
  <include_from>/etc/clikhouse-server/config.d/metrika.xml</include_from>

​ 其中,incl与metrika.xml配置文件内的节点名称要彼此对应。至此,整个配置过程就完成了。

​ ClickHouse在它的系统表中,颇为贴心地提供了一张名为zookeeper的代理表。通过这张表,可以使用SQL查询的方式读取远端ZooKeeper内的数据。有一点需要注意,在用于查询的SQL语句中,必须指定path条件,例如查询根路径:

SELECT * FROM system.zookeeper where path = '/';
SELECT name,value,czxid,mzxid FROM system.zookeeper where path = '/clickhouse';
副本的定义形式

​ 使用副本增加了数据的冗余存储,所以降低了数据丢失的风险;其次,由于副本采用了多主架构,所以每个副本实例都可以作为数据读、写的入口,这无疑分摊了节点的负载。

​ 在使用副本时,不需要依赖任何集群配置, ReplicatedMergeTree结合ZooKeeper就能完成全部工作。

ReplicatedMergeTree的定义方式如下:

ENGINE =ReplicatedMergeTree('zk_path','replica_name')

​ zk_path用于指定在ZooKeeper中创建的数据表的路径,路径名称是自定义的,并没有固定规则,用户可以设置成自己希望的任何路径。即便如此,ClickHouse还是提供了一些约定俗成的配置模板以供参考,例如:

/clickhouse/tables/{shard}/table_name

​ 其中:
​ ❑ /clickhouse/tables/是约定俗成的路径固定前缀,表示存放数据表的根路径。
​ ❑ {shard}表示分片编号,通常用数值替代,例如01、02、03。一张数据表可以有多个分片,而每个分片都拥有自己的副本。
​ ❑ table_name表示数据表的名称,为了方便维护,通常与物理表的名字相同(虽然ClickHouse并不强制要求路径中的表名称和物理表名相同);而replica_name的作用是定义在ZooKeeper中创建的副本名称,该名称是区分不同副本实例的唯一标识。一种约定成俗的命名方式是使用所在服务器的域名称。

​ 对于zk_path而言,同一张数据表的同一个分片的不同副本,应该定义相同的路径;而对于replica_name而言,同一张数据表的同一个分片的不同副本,应该定义不同的名称。

ZooKeeper内的节点结构

​ ReplicatedMergeTree需要依靠ZooKeeper的事件监听机制以实现各个副本之间的协同。所以,在每张ReplicatedMergeTree表的创建过程中,它会以zk_path为根路径,在Zoo-Keeper中为这张表创建一组监听节点。按照作用的不同,监听节点可以大致分成如下几类:

​ (1)元数据:
​ ❑ /metadata:保存元数据信息,包括主键、分区键、采样表达式等。
​ ❑ /columns:保存列字段信息,包括列名称和数据类型。
​ ❑ /replicas:保存副本名称,对应设置参数中的replica_name。

​ (2)判断标识:
​ ❑ /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。

​ (3)操作日志:
​ ❑ /log:常规操作日志节点(INSERT、MERGE和DROP PARTITION),它是整个工作机制中最为重要的一环,保存了副本需要执行的任务指令。log使用了ZooKeeper的持久顺序型节点,每条指令的名称以log-为前缀递增,例如log-0000000000、log-0000000001等。每一个副本实例都会监听/log节点,当有新的指令加入时,它们会把指令加入副本各自的任务队列,并执行任务。关于这方面的执行逻辑,稍后会进一步展开。

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

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

INSERT的核心执行流程

​ 当需要在ReplicatedMergeTree中执行INSERT查询以写入数据时,即会进入INSERT核心流程

preview

preview

​ 创建第一个副本实例
​ 假设首先从CH5节点开始,对CH5节点执行下面的语句后,会创建第一个副本实例:

CREATE TABLE replicated_sales_1 (
    id String,
    price Float64,
    create_time DateTime
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/replicated_sales_1','ch5.nauu.com')
partition by toYYYYMM(create_time)
ORDER BY id ;

​ 在创建的过程中,ReplicatedMergeTree会进行一些初始化操作,例如:

​ ❑ 根据zk_path初始化所有的ZooKeeper节点。
​ ❑ 在/replicas/节点下注册自己的副本实例ch5.nauu.com。
​ ❑ 启动监听任务,监听/log日志节点。
​ ❑ 参与副本选举,选举出主副本,选举的方式是向/leader_election/插入子节点,第一个插入成功的副本就是主副本。

​ 接着,在CH6节点执行下面的语句,创建第二个副本实例。表结构和zk_path需要与第一个副本相同,而replica_name则需要设置成CH6的域名:

CREATE TABLE replicated_sales_1 (
    id String,
    price Float64,
    create_time DateTime
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/01/replicated_sales_1','ch6.nauu.com')
partition by toYYYYMM(create_time)
ORDER BY id ;

​ 在创建过程中,第二个ReplicatedMergeTree同样会进行一些初始化操作,例如:
​ ❑ 在/replicas/节点下注册自己的副本实例ch6.nauu.com。
​ ❑ 启动监听任务,监听/log日志节点。
​ ❑ 参与副本选举,选举出主副本。在这个例子中,CH5副本成为主副本。

​ 现在尝试向第一个副本CH5写入数据。执行如下命令:

INSERT INTO TABLE replicated_sales_1 VALUES ('A001',100,'2019-05-10 00:00:00');

​ 上述命令执行之后,首先会在本地完成分区目录的写入:

preview

​ 接着向/blocks节点写入该数据分区的block_id:

preview

​ 该block_id将作为后续去重操作的判断依据。如果此时再次执行刚才的INSERT语句,试图写入重复数据,则会出现如下提示:

preview

​ 即副本会自动忽略block_id重复的待写入数据。
​ 此外,如果设置了insert_quorum参数(默认为0),并且insert_quorum>=2,则CH5会进一步监控已完成写入操作的副本个数,只有当写入副本个数大于或等于insert_quorum时,整个写入操作才算成功。

​ 由第一个副本实例推送Log日志

​ 在3步骤完成之后,会继续由执行了INSERT的副本向/log节点推送操作日志。在这个例子中,会由第一个副本CH5担此重任。日志的编号是/log/log-0000000000,而LogEntry的核心属性如下:

preview

​ 从日志内容中可以看出,操作类型为get下载,而需要下载的分区是201905_0_0_0。其余所有副本都会基于Log日志以相同的顺序执行命令。

​ 第二个副本实例拉取Log日志
​ CH6副本会一直监听/log节点变化,当CH5推送了/log/log-0000000000之后,CH6便会触发日志的拉取任务并更新log_pointer,将其指向最新日志下标:

preview

在拉取了LogEntry之后,它并不会直接执行,而是将其转为任务对象放至队列:

preview

​ 这是因为在复杂的情况下,考虑到在同一时段内,会连续收到许多个LogEntry,所以使用队列的形式消化任务是一种更为合理的设计。注意,拉取的LogEntry是一个区间,这同样也是因为可能会连续收到多个LogEntry。

​ 第二个副本实例向其他副本发起下载请求

​ CH6基于/queue队列开始执行任务。当看到type类型为get的时候,ReplicatedMerge-Tree会明白此时在远端的其他副本中已经成功写入了数据分区,而自己需要同步这些数据。

​ CH6上的第二个副本实例会开始选择一个远端的其他副本作为数据的下载来源。远端副本的选择算法大致是这样的:

​ (1)从/replicas节点拿到所有的副本节点。

​ (2)遍历这些副本,选取其中一个。选取的副本需要拥有最大的log_pointer下标,并且/queue子节点数量最少。log_pointer下标最大,意味着该副本执行的日志最多,数据应该更加完整;而/queue最小,则意味着该副本目前的任务执行负担较小。

​ 在这个例子中,算法选择的远端副本是CH5。于是,CH6副本向CH5发起了HTTP请求,希望下载分区201905_0_0_0:

preview

​ 如果第一次下载请求失败,在默认情况下,CH6再尝试请求4次,一共会尝试5次(由max_fetch_partition_retries_count参数控制,默认为5)。

​ CH5的DataPartsExchange端口服务接收到调用请求,在得知对方来意之后,根据参数做出响应,将本地分区201905_0_0_0基于DataPartsExchang的服务响应发送回CH6:

​ Sending part 201905_0_0_0

CH6副本在收到CH5的分区数据后,首先将其写至临时目录:

​ tmp_fetch_201905_0_0_0

待全部数据接收完成之后,重命名该目录:

preview

​ 至此,整个写入流程结束。

​ 可以看到,在INSERT的写入过程中,ZooKeeper不会进行任何实质性的数据传输。本着谁执行谁负责的原则,在这个案例中由CH5首先在本地写入了分区数据。之后,也由这个副本负责发送Log日志,通知其他副本下载数据。如果设置了insert_quorum并且insert_quorum>=2,则还会由该副本监控完成写入的副本数量。其他副本在接收到Log日志之后,会选择一个最合适的远端副本,点对点地下载分区数据。

ALTER的核心执行流程

​ 当对ReplicatedMergeTree执行ALTER操作进行元数据修改的时候,即会进入ALTER部分的逻辑,例如增加、删除表字段等。而ALTER的核心流程如图所示

preview

​ 与之前的几个流程相比,ALTET的流程会简单很多,其执行过程中并不会涉及/log日志的分发。整个流程从上至下按照时间顺序进行,其大致分成3个步骤。现在根据图所示编号讲解整个过程。

​ 修改共享元数据
​ 在CH6节点尝试增加一个列字段,执行如下语句:

preview

​ 执行之后,CH6会修改ZooKeeper内的共享元数据节点:

preview

​ 数据修改后,节点的版本号也会同时提升:

preview

​ 与此同时,CH6还会负责监听所有副本的修改完成情况:

preview

​ 监听共享元数据变更并各自执行本地修改

​ CH5和CH6两个副本分别监听共享元数据的变更。之后,它们会分别对本地的元数据版本号与共享版本号进行对比。在这个案例中,它们会发现本地版本号低于共享版本号,于是它们开始在各自的本地执行更新操作:

preview

​ 确认所有副本完成修改
​ CH6确认所有副本均已完成修改:

preview

​ 至此,整个ALTER流程结束。
​ 可以看到,在ALTER整个的执行过程中,ZooKeeper不会进行任何实质性的数据传输。所有的ALTER操作,最终都是由各个副本在本地完成的。本着谁执行谁负责的原则,在这个案例中由CH6负责对共享元数据的修改以及对各个副本修改进度的监控。

更多精彩内容,请关注微信公众号获取

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_45320660/article/details/112761872