clickhouse原理解析与应用实践_【腾讯看点】ClickHouse最优实践与原理剖析

0f47e619d89672208967af63304684f9.png

2016年开源的MPP数据库ClickHouse,被称为是世界上最快的分析型数据库。我们在腾讯看点千万/秒实时数据的业务场景中,利用ClickHouse实现了亚秒级的多维实时数据分析系统。本文会结合腾讯看点实际业务场景和ClickHouse内核实现细节,进行原理剖析,介绍ClickHouse为什么快,怎么样用能更快。

“腾讯看 点”实时数据分析系统的 实时存储 部分利用了 ClickHouse 来实现。 本文分为 三个部分 来介 绍:
  • 第一是,分布式-高可用
  • 第二是,海量数据-写入
  • 第三是,高性能-查询
ClickHouse有很多表引擎,表引擎决定了数据以什么方式存储,以什么方式加载,以及数据表拥有什么样的特性。 目前ClickHouse拥有MergeTree、外存、内存、网络接口等20多种表引擎,其中最体现ClickHouse性能特点的是MergeTree及其家族表引擎。 并且当前ClickHouse也只有MergeTree及其家族表引擎支持主键索引、数据分区、数据副本等优秀特性。 掌握了MergeTree表引擎的底层原理,才能够掌握ClickHouse的精髓。 我们当前使用的也是ClickHouse的MergeTree系列表引擎,接下来的介绍都是基于MergeTree系列表引擎展开的。

01

分布式-高可用

先看分布式、高可用,不管单节点性能多强,随着业务的增长早晚会有遇到瓶颈的一天;而且意外的宕机在计算机运行中是无法避免的。

bd1175d08f95aa780fcc495d023ce78d.png

ClickHouse通过分片(Shard)来水平扩展集群,将总数据水平分成M份,每个分片保存其中的一份数据避开单节点的性能瓶颈。

通过副本(Replication)来保障集群的高可用,即每个分片拥有若干个数据一样的副本。

而且ClickHouse可以设置,不同的分片配置不同数量的副本。即分片0可能有1个副本,分片1可能有3个副本。

6caec5cce6ad57a123145376d8112679.png

先看ClickHouse默认的高可用方案:

写入数据通过分布式表写入,分布式表会将数据同时写入同一个分片的所有副本里面。

这里会有一个问题,如果副本0写入成功,副本1写入失败,就会造成同一个分片的不同副本数据不一致的问题。

所以默认的高可用方案,是不能用于生产环境的。

我们这里听取的是Clickhouse官方的建议,借助Zookeeper实现高可用的方案。数据写入一个分片的时候仅写入一个副本,然后再写ZooKeeper,通过ZooKeeper告诉同一个分片的其他副本,其他副本再过来拉取数据保证数据的一致性。

接下来看一下,ClickHouse实现这种高可用方案的底层原理。

3a5db859a2889a242523b5eb48fbbd28.png

这种方式的高可用,需要通过ClickHouse的ReplicatedMergeTree表引擎实现,其中在ReplicatedMergeTree表引擎的核心代码中有大量和Zookeeper进行交互的逻辑。

通过这部分与Zookeeper进行交互的逻辑,实现同一个分片内多个副本的协同,包括主副本选举、写入任务队列变更和副本状态变化等等。

可以看到,外部数据写入ClickHouse的一个分片,会先写入一个副本的内存中,在内存中按照指定条件排好序,再写入磁盘的一个临时目录。等写入磁盘临时目录完成,将临时目录重命名为最终目录的名字。

写完之后,通过跟Zookeeper进行一系列交互实现数据的复制。要了解这一部分,首先需要先了解ClickHouse的ReplicatedMergeTree表引擎在Zookeeper内的节点结构。

23c707bba17ecb68ad57faba9ada8774.png

ClickHouse的ReplicatedMergeTree表,是通过监听ZooKeeper的各个节点实现副本之间的协同。这里我简单介绍Zookeeper上部分节点的作用,比如:

第一类,元数据相关的,

  • /metadata节点,是用来保存元数据信息的,包括主键、分区键等。

  • /columns节点,是用来保存列字段信息的,包括列名称和数据类型。

  • /replicas节点,是用来保存副本相关信息的。

第二类,校验相关的,

  • /leader_election节点,用于主副本的选举工作等。

  • /blocks节点,存放数据块的block_id,用来防止数据块重复以及数据同步的。

第三类,执行相关的,

  • /log节点,记录了副本协同需要执行的Log记录,包含了数据同步任务执行所需要的源信息。

  • /queue节点,将/log节点的Log记录,转化成具体执行任务之后,记录在queue节点下,ClickHouse的ReplicatedMergeTree表最终执行是基于queue节点的任务队列,执行任务的。

ede25c0ae8aa82405ac069b444c11286.png

这里举个例子,包含了数据副本高可用同步的主要步骤。

第一步,副本R0建立ReplicatedMergeTree表,建表之后会先去Zookeeper的/replicas节点下,注册新建一个子节点R0,然后监听Zookeeper上的/log节点,最后去/leader_election节点下注册一个子节点,参与选主。 第二步,副本R1同样建立ReplicatedMergeTree表,进行同样注册、监听流程,唯一不同的结果是,副本R0选举成为了leader,R1选举成为了Follower。

ClickHouse借助Zookeeper选主,采用的是公平模式。即每一个副本都会在Zookeeper的/leader_election节点下创建一个子节点,先创建的序号小后创建的序号大,序号最小的节点为Leader即主副本。所以这个例子中,R0为主副本R1为从副本。

第三步,向副本R0中插入数据。数据先写入R0的内存中,然后将内存中的数据进行排序后,写入磁盘的临时目录,写完之后将临时目录改名为最终数据目录的名称。

接下来向Zookeeper的/blocks节点写入该数据分区的block_id,作用是防止数据重复及数据同步。

最后会向Zookeeper上的/log节点注册一条Log日志记录,这条记录的内容信息包含了数据来源、block_id、日志操作类型(这里是获取远程数据)等等。

第四步,由于副本R1启动后已经监听了Zookeeper上的/log节点,所以刚才R0写入的Log日志会触发R1的任务去拉取Log日志记录,将R0刚写的Log记录拉取到R1本地。 接下来,R1拿到Log日志记录后并不会直接去R0拉取数据,而是会根据Log记录的内容建立一个对象,这个对象包含被拉取数据的信息,然后R1会将这个对象注册到Zookeeper上的/queue节点中。 最后是根据/queue节点的任务队列,执行拉取数据的任务。 第五步,R1根据副本选择算法,选择一个最合适的远程副本发起Fetch数据的请求。 第六步,R0接收到数据拉取的请求,返回指定分区的数据。 第七步,R1接到数据,先写入内存,然后写入磁盘的临时目录,最终把临时目录改名为正式目录。

这里没有选用消息队列进行数据同步,是因为Zookeeper更加轻量级。而且写的时候,任意写一个副本,其它副本都能够通过Zookeeper获得一致的数据。而且就算其它节点第一次来获取数据失败了,后面只要发现它跟Zookeeper上记录的数据不一致,就会再次尝试获取数据保证一致性。

介绍完分布式高可用,接下来看一下海量数据的写入。

02

海量数据-写入

数据写入,遇到的第一个问题是,海量数据直接写入Clickhouse是会失败的。

可以先看一下,ClickHouse的MergeTree表引擎,底层原理是类似于LSM-tree。数据通过Append的方式写入,后续再启动merge线程将小的数据文件进行合并。

6d260540a54d443e1b0a801d003e113d.png

一次数据写入,会生成一个文件目录。目录结构可以看到,分为四个部分:

  • 第一部分,是分区ID的信息;

  • 第二部分,是这个目录中包含数据的最小BlockNum;

  • 第三部分,是这个目录中包含数据的最大BlockNum;

  • 第四部分,是这个目录进行合并的等级。

举个例子,图中两个黄色的数据目录合并成蓝色的数据目录,数据BlockNum从1_1和2_2合并成了1_2,数据合并level也从0变成了1。

了解了ClickHouse MergeTree家族表的写入过程,这里我们就会发现两个问题。

第一:如果一次写入的数据量太小,比如一条写一次,那么会产生大量的文件目录。当后台合并线程来不及合并的时候,文件目录数量会越来越多,这会导致ClickHouse抛出Too many parts的异常,写入失败。 第二:根据之前的介绍,每一次写入除了写入数据本身,ClickHouse还需要跟Zookeeper进行十来次的数据交互,而我们知道Zookeeper是不能承受高并发的访问。 可以看到,写入QPS过高导致进一步Zookeeper的QPS过高,从而导致Zookeeper崩溃。

我们采用的解决方案是,改用Batch的方式写入。即一个Batch的数据,产生一个数据目录,与Zookeeper进行一系列交互。那Batch设置多大呢,Batch太小的话缓解不了ZK的压力,Batch也不能太大,不然上游内存的压力和数据延迟会太大。通过实验,最终我们选用了大小几十万的Batch。

这样避免了QPS太高带来的问题。当前方案其实还有优化空间的,比如Zookeeper是无法线性扩展的。我了解到,业内有些团队就把log和data part相关信息不写入Zookeeper,这样减少了Zookeeper的压力。不过这样涉及到了对源代码的修改,对于一般的业务团队实现的成本太高了。

数据写入,遇到的第二个问题是,如果数据写入通过分布式表写入会遇到单点问题。

先介绍一下分布式表。分布式表实际上是一张逻辑表并不存储真实数据,可以理解为一张代理表。

比如用户查询分布式表,分布式表会将查询请求下发到每一个分片的本地表上进行查询,然后收集每个分片本地表的结果,汇总之后再返回给用户。

用户写入分布式表的场景,是用户将一个Batch的数据写入分布式表,分布式表根据一定的规则,将这个Batch的数据分为若干个Mini Batch的数据,存储到不同的分片上。

这里有一个很容易误解的地方,我们开始以为,分布式表是按照一定规则做一个网络转发,那么我们当时想只要万兆网卡的带宽足够,就不会出现单点的性能瓶颈。但是实际上,ClickHouse是这样做的。

4a4c86fd11a786fd8e334270f407cdd8.png

我们看一个例子,有三个分片Shard-1,Shard-2和Shard-3,其中分布式表建在Shard-2的节点上。

第一步,我们给分布式表写入300条数据,分布式表会根据路由规则,把数据进行分组。假设Shard-1分到50条,Shard-2分到150条,Shard-3分到100条。 第二步,因为分布式表跟Shard-2在同一台机器上,所以Shard-2的150条直接写入磁盘。Shard-1的50条和shard-3的100条,并不是直接通过网络转发给他们的,而是在分布式表的机器即shard-2的节点上,先写入磁盘的临时目录。 第三步,分布式表节点Shard-2向Shard-1节点和Shard-3节点,分别发起远程连接请求,将对应临时目录的数据发送给Shard-1和shard-3。

这里可以看到,分布式表所在的节点Shard-2,全量数据都会先落在磁盘上。我们知道磁盘的写入速度不够快,很容易出现单点的磁盘瓶颈。

比如单QQ看点的视频内容,每天可能写入百亿级的数据,如果写一张分布式表就很容易造成单台机器出现磁盘的瓶颈。

尤其是Clickhouse底层运用的是Mergetree,在合并的过程中,会存在写放大的问题加重磁盘的压力。峰值每分钟几千万条数据写完耗时几十秒,如果正在做Merge就会阻塞写入请求,查询也会非常慢。

我们做的两个优化方案:

  • 第一,对磁盘做Raid提升磁盘的IO。

  • 第二,在写入之前,上游进行数据划分分表操作,直接分开写入到不同的分片上,磁盘压力直接变为了原来的1/N。

这样很好的避免了磁盘的单点瓶颈。

数据写入,遇到的第三个问题是。

虽然我们的写入按照分片进行了划分,但是这里引入了一个分布式系统常见的问题,就是局部的Top并非全局Top的问题。

比如同一个内容X的数据落在了不同的分片上,计算全局Top100点击的内容。之前说到,分布式表会将查询请求下发到各个分片上,分别计算局部的Top100点击的内容,然后将结果汇总。

ce5a77d70399f9b4867d840b51e90259.png

可以看到例子中,内容X在分片1和分片2上不是Top100,所以在汇总数据的时候,会丢失内容X在分片1和分片2上的点击数据,造成数据错误的问题。

我们做的优化,是在写入之前加上一层路由,将同一个内容ID的记录,全部路由到同一个分片上解决了该问题。

这里需要多说一句,现在最新版的ClickHouse已经不存在这个问题了。ClickHouse是这样优化这个问题的,对于有Group by 和 Limit的SQL命令,只把Group by语句下发到本地表执行,然后将各个本地表执行的全量结果都回传到分布式表,再进行一次全局的group by,最后再做limit操作。这样虽然解决了全局TopN的问题,代价是牺牲了一部分执行的性能。

03

高性能存储-查询

介绍完写入,下一步介绍Clickhouse的高性能存储和查询。

Clickhouse高性能查询,一个关键点是稀疏索引。稀疏索引这个设计就很有讲究,设计得好可以加速查询,设计不好反而会影响查询效率。

be025e1c57ec3a24dc757590148955f6.png

介绍稀疏索引,得对比稠密索引来介绍。稠密索引,每一条数据都建立了索引,对应图中即是Whole data里面的每一条数据都有索引。

而稀疏索引,可以看到图中以CounterID和Date建立了稀疏索引,稀疏间隔是8,那么相当于每隔8行会记录一下CounterID-Date的值,建立索引。

可以看到,如果查询的过滤条件中包含CounterID 等于 (‘a’, ‘h’), 服务器可以通过索引,仅仅读取Marks范围在[0,3]和[6,8]之间的数据。

ClickHouse中主键索引字段通常也是排序字段,将相同的字段排序到一起可以进一步提升压缩比,从而提高性能。

所以,稀疏索引这种方式适合在大数据场景下进行OLAP查询分析,同时稀疏索引的大小也比稠密索引小了很多,可以常驻内存。

ClickHouse默认的稀疏间隔是8192行,我根据我们的业务场景,因为我们的查询,大部分都是和时间、内容ID相关的,比如说,某个内容,过去N分钟,在各个人群表现如何?

我按照日期,分钟粒度时间和内容ID建立了稀疏索引。针对某个内容的查询,建立稀疏索引之后可以减少99%的文件扫描。

4822f539264d0a27a72a219e638122c0.png

Clickhouse高性能查询,面临的第二个问题是,我们现在数据量太大,维度太多。

拿QQ看点的视频内容来说,一天入库流水有上百亿条,有些维度有几百个类别。如果一次性把所有维度进行预聚合,数据量会指数膨胀,查询反而变慢,并且会占用大量内存空间。

我们的优化是,针对不同的维度建立对应的预聚合物化视图,用空间换时间,这样可以缩短查询的时间。

通过一个例子可以看到,通过SummingMergeTree建立一个,内容Id粒度聚合的累加pv的物化视图,相当于提前进行了group by的计算。

等需要查询聚合结果的时候,直接查询物化视图,数据都是已经聚合计算过的,数据扫描量只是流水表的千分之一。

b60455852c8644824fae1ddb3f40ff88.png

分布式表查询第三个问题是,查询单个内容ID,分布式表会将查询请求下发到所有的分片上,然后再返回查询结果进行汇总。

实际上因为做过路由,一个内容ID只存在于一个分片上,剩下的分片都在空跑。

针对这类查询,我们的优化是,后台按照同样的规则先进行路由,直接查询目标分片,这样减少了N-1/N的负载,可以大量缩短查询时间。

而且由于我们提供的是OLAP的查询,数据满足最终一致性即可,通过主从副本读写分离,可以进一步提升性能。

我们在后台还做了一个1分钟的数据缓存,针对相同条件查询,后台就直接返回了。

这里再介绍一下我们的扩容的方案,调研了业内的一些常见方案。

比如Hbase原始数据都存放在HDFS上,扩容只是region server扩容,不涉及原始数据的迁移。但是clickhouse的每个分片数据都是在本地,是一个类似rocksdb的底层存储引擎,不能像Hbase那样方便扩容。

然后redis是哈希槽这种类似一致性哈希的方式,是比较经典的分布式缓存方案。redis slot在rehash的过程中,虽然存在短暂的ask不可用,但是总体来说迁移是比较方便的,从原h[0]迁移到h[1]最后再删除h[0]。但是Clickhouse大部分都是OLAP批量查询,而且由于列式存储不支持删除的特性,一致性哈希的方案也不是很适合。

我们目前扩容的方案是另外消费一份数据写入新Clickhouse集群,两个集群一起跑一段时间,因为实时数据就保存3天,等3天之后后台服务直接访问新集群。

a3faf7eac9585dc3f3a19489f23c269e.png

一直讲到这里,主要是介绍我们对ClickHouse的使用优化,其实ClickHouse高性能的查询还得益于其底层的各种优化。

为了更好的理解ClickHouse高性能的查询,我简单介绍一下ClickHouse的底层数据存储。

ClickHouse是列式存储的,每一列的字段数据都是独立存储的。每一个字段都有两个文件,作为这一列的存储文件。这两个文件,一个是承载数据物理存储的.bin文件,另一个是存放一些源信息的.mrk标记文件。

.bin文件中,ClickHouse先按照建表语句中的Order by排序键,将列数据进行排序,然后按照一定大小的数据块进行切割,对切割后的数据块进行压缩,最后将压缩过的数据块写入.bin文件存储。

.mrk标记文件中,主要是存放源信息。比如跟稀疏索引那块稀疏间隔一样的Marks标记;还有压缩块在.bin文件中的偏移量;以及压缩块解压后Marks标记对应的偏移量。

可以看到通过稀疏索引,可以获得Marks标记的范围即mrk标记文件中,marks标记序号范围。通过marks标记序号范围,可以不用读取整个.bin文件,仅读取命中的压缩块即可。同时我们有解压后的marks标记对应的偏移量,可以进一步过滤掉压缩块中不需要遍历的数据。

15e0148554ebdde89d712a256ddd8e89.png

在了解ClickHouse文件存储之后,我们可以更好地了解ClickHouse查询性能彪悍的原因。

1、多核CPU并行计算,看到刚才的文件存储方式,文件是按照压缩块的方式存在.bin文件中。ClickHouse可以通过大量CPU,并行读取不同的压缩块并行解压计算。

2、SIMD指令集加速,充分利用了CPU寄存器的并行计算能力。如果是传统的CPU指令集,寄存器就算有128位,如果要进行N次8bit的计算,每次都只能利用寄存器的低8位重复计算N次;而SIMD的话,可以将16个8bit的运算并行放到128位的寄存器中,仅通过1次计算就可以并行计算16个8bit的运算。ClickHouse是通过SSE2指令集实现的,所以最好选用Intel的机器。

3、之前介绍的,ClickHouse分布式水平扩展集群的能力,也可以很好的提升性能。

4、还有稀疏索引,列式存储和极致的数据压缩,也都是大数据场景下高性能查询的关键点。

5、实际上,ClickHouse还有很多优化性能的细节。比如聚合分析,通常的实现是通过HashMap实现,HashMap的key是group by的key,HashMap的value是聚合的值。ClickHouse通过对长字符串的Key,进行Hash实现的HashMapWithSavedHash,以及对Uint8这类范围小的数据字段通过数组实现的FixedHashMap,以及大量新增类别字段和内存限制的场景,也有TwoLevelHashMap和Split to disk的实现方案。

实际上,ClickHouse的内核实现中,并没有使用什么神秘的算法,但是正是这所有的优化组合在一起,才有了ClickHouse彪悍的查询性能。

c96d2aef4428a5d04c9ddefbedc4f90a.png

最后,给大家介绍一下我们系统的成果。

腾讯看点实时数据分析系统,能够亚秒级响应多维条件查询请求,

在未命中缓存情况下,

  • 过去30分钟内容的查询,99%的请求耗时在1秒内;

  • 过去24小时内容的查询,90%的请求耗时在5秒内,99%的请求耗时在10秒内。

参考文献:

https://clickhouse.tech/

《clickhouse原理解析与应用实践》
        腾讯看点多维实时分析系统技术架构

e68d85722f9f5fdd33770ae9eb8915c3.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值