ClickHouse通过副本保证数据的可用,通过分片来实现数据水平扩展和性能提升。高可用HA(High Availability)是设计分布式系统架构时必须考虑的因素之一 。一般指通过设计减少系统不能提供服务的时间,假设系统一直能够提供服务,我们说系统的可用性是100%。如果系统每运行100个时间单位,会有1个时间单位无法提供服务,我们说系统的可用性是99%。很多公司的高可用目标是4个9,即99.99%,换算成时间为系统一年的停机时间为8.76个小时。
1. ClickHouse副本
1.1 定义
既可以将所有节点组成一个单一集群,也可以按照具体业务的诉求,把节点拆分为多个小的集群。在每个小的集群区域之间,它们的节点、分区和副本数量可以各不相同。
同一张表中,数据不同的节点为分片,数据相同为副本。
分片:数据的水平扩展。
副本:数据的存储冗余,防止丢失。
1.2 ReplicatedMergeTree引擎
ClickHouse中只有 MergeTree 系列里的表可支持副本。数据副本必须使用ReplicatedMergeTree复制表系列引擎,其才能应用副本的能力。
在MergeTree引擎中,一个数据分区由开始创建到全部完成,会历经两类存储区域。一是内存,数据首先会被写入内存缓冲区;二是本地磁盘,数据接着会被写入tmp临时目录分区,待全部完成后再将临时目录重命名为正式分区。
ReplicatedMergeTree在上述的基础之上增加了ZooKeeper的部分。其特点为:
(1)依赖ZooKeeper实现同步:执行INSERT和ALTER查询需要zk。但是select查询时并不需要zk。
(2)表级别的副本:在表中可配置副本的数量,以及副本在集群内的分布位置。
(3)多主架构(Multi Master):在任一副本上执行INSERT和ALTER查询,将会分发至每个副本。
(4)Block数据块:insert依据max_insert_block_size的大小(默认1048576行)将数据切分成若干个Block数据块。因此Block数据块是数据写入的基本单元,并且具有写入的原子性和唯一性。
原子性:Block块内的数据要么全部写入成功,要么全部失败。
唯一性:按当前Block数据块的数据顺序、数据行和数据大小等指标,计算Hash信息摘要并记录在案。防止重复写入。
ZooKeeper的配置方式主要是在配置文件中做相应的修改:
(1)创建文件metrika.xml,设置zookeeper-servers标签。
(2)修改config.xml文件,增加标签,包含zk配置。被引用zk的zookeeper-servers标签。
(3)ck系统表提供了zk的代理表,使用sql即可查询zk信息,但必须指定path条件:
select * from system.zookeeper where path='/';
1.3 副本的定义方式
定义副本方式:
engine=ReplicatedMergeTree('zk_path','replica_name')
zk_path用于指定在ZooKeeper中创建的数据表的路径,路径名称是自定义。一般约定俗成为:/clickhouse/tables/{shard}/table_name
/clickhouse/tables/是约定俗成的路径固定前缀,表示存放数据表的根路径。
{shard}表示分片编号,通常用数值替代,例如01、02、03。一张数据表可以有多个分片,而每个分片都拥有自己的副本。
table_name表示数据表的名称,为了方便维护,通常与物理表的名字相同(虽然ClickHouse并不强制要求路径中的表名称和物理表名相同)。
replica_name的作用是定义在ZooKeeper中创建的副本名称,该名称是区分不同副本实例的唯一标识。其中一种约定成俗的命名方式是使用所在服务器的域名称。
* 参数使用
对于zk_path而言,同一张数据表的同一个分片的不同副本,应该定义相同的路径;
对于replica_name而言,同一张数据表的同一个分片的不同副本,应该定义不同的名称。
1.4 ReplicatedMergeTree数据结构
启用ReplicatedMergeTree分布式表引擎后,将会在ZooKeeper内的生成相应的节点数据,其结构为:
元数据:包含/metadata保存元数据、/columns保存列字段信息、/replicas副本名称
判断标识:包含/leader_election主副本的选举工作、/blocks记录Block数据块的Hash信息摘要、/block_numbers、 /quorum记录quorum的数量,至少有quorum数量的副本写入成功后,整个写操作才算成功。
操作日志:包含/log常规操作日志节点、/mutations MUTATION操作日志节点、/replicas/{replica_name}/*每个副本各自的节点下的一组监听节点
* 重要的节点:
/queue:任务队列节点,用于执行具体的操作任务
/log_pointer:log日志指针节点,记录了最后一次执行的log日志下标信息
/mutation_pointer:mutations日志指针节点,记录了最后一次执行的mutations日志名称
* Entry日志对象的数据结构:
LogEntry:封装/log的子节点信息
MutationEntry:封装/mutations的子节点信息
1.5 副本协同流程
副本协同的核心操作主要有INSERT、MERGE、MUTATION和ALTER四种,其对应数据写入、分区合并、数据修改和元数据修改。以下分别介绍其核心流程:
INSERT的核心执行流程:写入数据的节点负责发送Log日志,通知其他副本下载写入的数据。
(1)创建第一个副本实例:
根据zk_path初始化所有的ZooKeeper节点,
在/replicas/节点下注册自己的副本实例,
启动监听任务,监听/log日志节点,
参与副本选举,选举出主副本,选举的方式是向/leader_election/插入子节点,第一个插入成功的副本就是主副本。
(2)创建第二个副本实例:
执行流程同上
(3)向第一个副本实例写入数据:
首先会在本地完成分区目录的写入,
接着向/blocks节点写入该数据分区的block_id,
根据block_id去重,重复数据将会被忽略。
如果设置了insert_quorum参数(默认为0),并且insert_quorum>=2,则节点会进一步监控已完成写入操作的副本个数
(4)由第一个副本实例推送Log日志
由执行了INSERT的副本向/log节点推送操作日志,其余所有副本都会基于Log日志以相同的顺序执行命令。
(5)第二个副本实例拉取Log日志
插入副本推送日志后,其余副本会一直监听/log节点变化,会触发日志的拉取任务并更新log_pointer,将其指向最新日志下标。
拉取了LogEntry,放入任务队列。(可能会同时收到多个LogEntry,因此采用队列方式处理)
(6)第二个副本实例向其他副本发起下载请求
基于/queue队列开始执行任务。当看到type类型为get的时候,即意味着其他副本成功写入了数据,需要本地同步数据。
选择一个远端的其他副本作为数据的下载来源,
选择算法:从/replicas节点拿到所有的副本节点,遍历这些副本,选取其中最大的log_pointer下标,并且/queue子节点数量最少。(数据更完整和负载更小)
下载请求默认5次,max_fetch_partition_retries_count参数控制
(7)第一个副本实例响应数据下载
(8)第二个副本实例下载数据并完成本地写入。整个写入流程结束。
MERGE的核心执行流程:无论在哪被触发,都会首先被转交至主副本,再由主副本负责合并计划的制定、消息日志的推送以及对日志接收情况的监控
副本执行OPTIMIZE后,会创建远程连接,尝试与主副本通信
主副本接收通信。
由主副本制定MERGE计划并推送Log日志,并判断哪些分区需要被合并。
各个副本分别拉取Log日志。
MUTATION的核心执行流程:无论MUTATION操作从哪个副本发起,首先都会由主副本进行响应
对ReplicatedMergeTree执行ALTER DELETE或者ALTER UPDATE操作
推送MUTATION日志。
所有副本实例各自监听MUTATION日志。
由主副本实例响应MUTATION日志并推送Log日志。
各个副本实例分别拉取Log日志。
各个副本实例分别在本地执行MUTATION。
ALTER的核心执行流程:谁执行谁负责的原则
执行副本修改zk中共享元数据。
其余副本监听zk共享元数据变更并各自执行本地修改。
执行副本确认所有副本完成修改。
2. ClickHouse分片
2.1 定义
ClickHouse中的每个服务节点都可称为一个shard(分片)。
Distributed表引擎自身不存储任何数据,它能够作为分布式表的一层透明代理,在集群内部自动开展数据的写入、分发、查询、路由等工作。
2.2 集群的配置方式
集群配置用shard代表分片、用replica代表副本。
其语义为:
(1)1分片,0副本
(2)1分片,1副本
1.不包含副本的分片设置
直接使用定义分片节点。
选填参数:
weight分片权重默认为1
user为ClickHouse用户,默认为default
password为ClickHouse的用户密码,默认为空字符串
secure为SSL连接的端口,默认为9440
compression表示是否开启数据压缩功能,默认为true。
2.自定义分片与副本设置
下定义N个,则表示一个分片,n-1个副本。集群中replica数量的上限是由ClickHouse节点的数量决定的。
2.3 分布式DDL
集群可以实现分布式DDL,分布式DDL语句在执行的过程中也需要借助ZooKeeper的协同能力,以实现日志分发。
通过集群配置实现分布式ddl语句执行:
create/drop/rename/alter table on cluster cluster_name
create table new_table_local on cluster funnel(
id UInt64
)ENGINE=ReplicatedMergeTree('/clickhouse/tables/{shard}/new_table_1','{replica}')
order by id
其中用{shard}和{replica}两个动态宏变量代替硬编码方式。
查看节点的宏变量:
select * from system.macros
config.xml配置中预先定义了分区01的宏变量:
01
funnel.com
2.4 分布式DDL流程
分布式DDL的核心执行流程主要为3步。
推送DDL日志,执行语句的副本创建DDLLogEntry日志并将日志推送到zk
节点监听zk并拉取日志并执行
确认执行进度(客户端会阻塞等待180秒,以期望所有host执行完毕。如果等待时间大于180秒,则会转入后台线程继续等待)
2.5 Distributed原理
本地表:通常以_local为后缀进行命名。本地表是承接数据的载体,可以使用非Distributed的任意表引擎,一张本地表对应了一个数据分片。
分布式表:通常以_all为后缀进行命名。分布式表只能使用Distributed表引擎,它与本地表形成一对多的映射关系,日后将通过分布式表代理操作多张本地表。
分布式表与本地表一致性检查:读时检查的机制,不一致将会在查询时报错。
1.定义形式
分布式表定义为:
ENGINE=Distributed(cluster,database,table[,sharding_key])
其中的sharding_key意为分片键,选填参数。在数据写入的过程中,分布式表会依据分片键的规则,将数据分布到各个host节点的本地表。比如round()。
建表时指定集群名,即使用ON CLUSTER分布式DDL,在集群的每个分片节点上,都会建一张Distributed表,可以从任意一端发起对所有分片的读、写请求。
再使用ON CLUSTER分布式DDL建立对应的本地表即可在集群中各节点都创建。
2.查询的分类
分布式查询分为两类:
一类会作用于本地表的查询:对于INSERT和SELECT查询,Distributed将会以分布式的方式作用于local本地表。
另一类只会影响Distributed自身,不会作用于本地表的查询:
Distributed支持部分元数据操作,包括CREATE、DROP、RENAME和ALTER,其中ALTER并不包括分区的操作(ATTACH PARTITION、REPLACE PARTITION等)。这些查询都只会修改Distributed表自身,并不会修改local本地表。
所以要彻底删除一张分布式表,则需要分别删除分布式表和本地表。
* 不支持的查询:
Distributed表不支持任何MUTATION类型的操作,包括ALTERDELETE和ALTER UPDATE。
2.6 分片规则
不声明分片键,分布式表只能包含一个分片,即只能映射一张本地表。分片键要求返回一个整型类型的取值,包括Int系列和UInt系列。
ENGINE=Distributed(cluster,database,table,userId)
或返回整型的表达式:
round()、intHash64(userId)
还有一些影响分片的参数:
分片权重(weight)
集群配置中设置,默认为1,尽量较小值,权重越大写入数据越多。
slot(槽)
slot的数量等于所有分片的权重之和,slot按照权重元素的取值区间,与对应的分片形成映射关系。
选择函数
判断一行数据写入哪一个分片。
因此,整个分片定位的流程为:
找出slot的取值,公式:slot=shard_value % sum_weight(shard_value分片键取值,sum_weight权重和)
基于slot值找到对应的数据分片
2.7 分布式写入流程
分布式写入的核心流程主要有两种思路。一种思路是外部计算好数据写入到那些分片,并行写入。另一种思路为通过Distributed表引擎代理写入分片数据。
1.写入分片核心流程
执行写入的节点计算数据所属分片,本地分片写入;(异步写直接返回,同步写等待其他节点写入完成,insert_distributed_sync参数默认false异步)
其他节点数据写入临时目录,建立远端连接发送数据;
向其他节点发送数据,节点接收并写入;
执行节点确认写入完成。
2.副本复制数据的核心流程(两种方式)
方式一:通过Distributed复制数据
本地表不使用ReplicatedMergeTree表引擎,也能实现数据副本的功能。Distributed会同时负责分片和副本的数据写入工作,而副本数据的写入流程与分片逻辑相同。
缺点:执行节点需要同时负责分片和副本的写入,可能造成写入的单点瓶颈。
方式二:通过ReplicatedMergeTree复制数据
集群的shard配置中增加internal_replication参数并将其设置为true。Distributed表在该shard中只会选择一个合适的replica并对其写入数据。如果使用ReplicatedMergeTree作为本地表的引擎,则在该shard内,多个replica副本之间的数据复制会交由ReplicatedMergeTree自己处理。
shard中选择replica的算法:节点维护一个全局计数器errors_count,服务异常计数加1,拥有多个replica时,选择errors_count错误最少的那个。
2.8 分布式查询流程
分布式查询只能通过Distributed表引擎。
1.多副本的路由规则
如果一个shard拥有多个副本replica。Distributed表多副本查询时会依据负载均衡算法,由load_balancing参数控制,算法有:
random
默认,选择errors_count错误数量最少的replica;相同则随机选一个。
nearest_hostname
选择errors_count错误数量最少的replica;相同则选择集群配置中host名称与当前host最相似的一个。
in_order
选择errors_count错误数量最少的replica;相同则选择集群配置中replica的定义顺序逐个选择。
first_or_random
选择errors_count错误数量最少的replica;相同则选择集群配置中replica的定义顺序逐个选择,如果replica不可用则随机一个。
2.多分片查询的核心流程
即前文所述的谁执行谁负责。执行节点按照分片数量将查询拆分成若干个针对本地表的子查询,然后向各个分片发起查询,最后再汇总各个分片的返回结果。
相当于多个分片的UNION联合查询。先并行的查询各个分片数据,最后再合并返回结果。
本地表查询和分布式查询各有优劣。单独使用本地表可能会导致局部数据不全的问题,而使用分布式表则可能会产生查询被放大的问题,即没有目标数据存放的节点也会被发起查询请求。
3.使用Global优化分布式子查询
在IN查询子句使用分布式表的时候,查询请求会被放大N的平方倍,N等于分片节点的数量。
可以使用GLOBAL IN或JOIN进行优化:
select uniq(id) from new_table_all where repo=100 and id global in (select id from new_table_all where repo=100);
使用GLOBAL修饰符之后,用内存表临时保存了IN子句查询到的数据,并将其发送到远端分片节点(满足最外层的select分布式查询),到达数据共享的目的。
缺点:内部网络的数据分发要求临时表不能太大,如果存在重复数据可以使用distinct去重。
* 系列完结。
参考资料:
[1] Yandex.clickhouse官方文档[EB/OL]:https://clickhouse.tech/docs/en/
[2] 朱凯.ClickHouse原理解析与应用实践[M]