大数据时代用户们对数据分析的要求一直都在。早期通过Hadoop的生态圈,用HIVE等语言进行数据分析,虽然很好的解决了数据规模的问题,但是时延却一直不好。
后来大数据领域发展出了Spark,Presto等技术,这些技术虽然从功能上来说,都日趋完善,性能上较上一代基于Hadoop生态圈的技术也有所提升,但是其实时性依然表现一般,用户们普遍感觉不够快。
而在另外一方面,传统意义上的分布式数仓的MPP数据库,在功能和性能上都表现的不错,但是其能够处理的数据量却一直为人诟病。
ClickHouse的横空出世,让大家感觉到耳目一新。ClickHouse能够处理大量的数据,单表查询性能非常的优越,能够满足很多用户的实时分析的需求。当然ClickHouse并非没有缺点。其计算引擎不是一个完备的MPP引擎,任何超越单表查询的SQL语句,要么表现拉胯,要么干脆就没办法执行。而把所有数据都放在一个大宽表里面,也不符合现实情况。 StarRocks是新一代极速全场景MPP数据库。它为用户提供了简单易用的全场景敏捷、实时的数据分析。StarRocks不仅仅单表查询上速度可以媲美ClickHouse,其多表分析也同样展现了极速性能。我听说了StarRocks以后对它非常感兴趣,就研究了一下,和ClickHouse做了一个对比。
StarRocks的架构简洁,核心只有FE(Frontend)、BE(Backend)两类进程,没有任何外部组件的依赖。FE和BE模块都可以水平扩展。FE模块负责管理客户端连接,元数据管理,查询优化和调度等。BE模块负责数据的存储和SQL计算工作。
ClickHouse的架构,并没有Frontend和Backend的区分,每个节点,都可以认为是一个独立的ClickHouse的数据库,拥有数据存储和SQL计算查询所有的功能,从连接用户,到查询优化到查询执行。ClickHouse的集群实现则是通过两个方面来实现,一方面是通过配置clickhouse_remote_servers来管理集群,以便可以在集群节点中进行分片和副本机制,另外一方面,则通过Distributed Table来完成,这些实现机制和通常意义上的MPP数据库不同,也导致了ClickHouse对分布式查询的执行非常的有限。我们将从存储和查询执行两个方面对两个架构做一个对比。
在StarRocks中,表的元数据存在FE节点上。FE节点根据配置会有Follower和Observer两种角色。Follower们会通过类Paxos的BDBJE协议选出一个Leader,由Leader对元数据进行写操作。其他节点会将写操作路由到Leader节点。非Leader的Follower会参与选Leader操作,并且在每次元数据写入时,必须有多数Follower成功才能确认是写入成功。Observer不参与选主操作,只会异步同步并且回放日志,主要用于扩展集群的查询并发能力。每个FE节点都会在内存保留一份完整的元数据,这样每个FE节点都能够提供无差别的服务。
ClickHouse的每个节点都维护自己的元数据,其维护方式是存在system数据库里面,这个数据库里面也包括了很多其他系统相关的信息。它是在ClickHouse节点启动的时候由系统创建,其中大部分的信息存在内存里,一部分信息则存在磁盘上。ClickHouse的每个节点并不知道其他节点上的元数据信息。
在StarRocks里,一张表的数据会被拆分成多个Tablet,每个Tablet都会以多副本的形式存储在BE节点中。一张表首先通过分区机制分区,然后每个分区内的数据可以通过一个或者多个列进行分桶(Sharding),最终将数据切成多个Tablet。用户可以自行制定分桶的大小。每个Tablet则被按照多副本存储,默认三副本。
StarRocks的Tablet数量和BE的物理节点个数没有强制的依赖关系。StarRocks负责管理这些Tablet以及它们的副本信息。这做法有点类似HDFS这样的分布式文件系统,提供了高可用性。
ClickHouse的数据存储就简单很多。物理上,ClickHouse每个节点只管理自己节点上的数据和元数据。其数据的读写是通过IStorage这个接口来实现的。ClickHouse支持各种各样的IStorage接口,但是对本地数据存储最重要的是MergeTree系列的存储方式。ClickHouse通过MergeTree来提供列存。
MergeTree通过主键来索引。MergeTree是典型的分段式列存数据结构。数据在MergeTree里面被按照“parts”存储,每个part里面的数据按照主键排序,每个列被存成单独的压缩的column.bin文件。
和StarRocks不一样的是,ClickHouse并没有全局元数据的概念,所以也不存在着全局分片成Tablet,然后每个Tablet多副本的做法。ClickHouse也提供了Replication的能力。它是通过ZooKeeper和ReplicatedMergeTree来实现的。
当选用ReplicatedMergeTree作为存储方式的时候,需要传递ZooKeeper里面的一个path作为参数。简单来说,如果节点A上的Table1和节点B上的Table2同时选择了ReplicatedMergeTree作为其存储方式,并且它们在ZooKeeper里面的path相同的时候,它们互为备份。
由于ClickHouse只支持插入,所以互相为备份的意思是对一张表的插入会被复制到另外一张表上,反之亦然。当然,我们可以有三个四个乃至更多个表都选择ReplicatedMergeTree作为存储方式,并设置相同的ZooKeeper path,最终这些表互相为备份。
ClickHouse的分布式表的实现,是通过一个叫做Distributed Table的存储方式。用户可以创建Distributed Table。Distributed Table本身并不存任何数据,而是代表了一种系统在远端节点上做分桶的机制。当用户选择去查询Distributed Table的时候,系统根据load balancing的设置,选取远端的节点,把查询发给远端节点,再汇总这些查询结果。
对比ClickHouse和StarRocks的存储方式,我们可以看出来,StarRocks是一个比较健全的综合了传统MPP数据库和Hadoop文件系统优点的存储系统。而ClickHouse更像是一个一开始是单节点数据库,最后被硬生生的扩展成一个分布式系统的产品。
StarRocks有全局的元数据,自顶向下的管理数据库里面每张表。每张表的数据通过逻辑上的分解成为Tablet,然后每个Tablet被备份存储到不同的BE节点去。这种方式有解耦了逻辑分区和物理存储之间的关系,相对传统MPP来说能更好防止单节点失败,对扩容缩容和故障恢复都很有优势,无需停下集群的运行。相对于Hadoop系统来说,StarRocks的元数据管理更像是一个传统意义上的MPP数据库,而非Hadoop那样的文件系统。
ClickHouse则不同。它不具备全局的元数据视角,数据也仅仅限于单节点管理。它在数据的备份上通过ZooKeeper来实现,这种实现怎么看都给人以一种不得不凑数出来的解决方案。而它的分布式查询通过Distributed Table进行的时候,还需要配合集群的配置文件让系统来决定到底要去哪些节点上去查询哪些数据,更是一种很不成熟的解决方案。
从存储层面我们也可以看出来,对ClickHouse来说,实现delete和update是非常困难的事情,即使能实现,也不会有多好的效率,例如采用ReplacingMergeTree引擎来实现upsert语义的时候,select可能会读到没有merge完成的多个版本数据,如果通过select final在读取过程中进行sort merge就会大大降低查询性能。而在StarRocks上,存储引擎不仅能够提供高效的Append操作,也能高效的处理Upsert类操作。使用Delete and insert的实现方式,通过主键索引快速过滤,消除了读取时Sort merge操作,同时还可以充分利用其他二级索引,从而可以在大量更新的场景下保证查询效率。对于删除ClickHouse采用一个异步的mutation机制,跟大家普遍使用的delete语法有较大的差异,而且需要重写所有parts文件导致对小范围删除很不友好,而StarRocks就通过主键索引和delete vector较好的解决了这个问题。
另外StarRocks的存储引擎在数据更新时能够保证每一次操作的ACID。一个批次的导入数据生效是原子性的,要么全部导入成功,要么全部失败。并发进行的各个事务相互之间互不影响,对外提供Snapshot Isolation的事务隔离级别。相对ClickHouse如果一次导入部分失败会有比较多的数据清理工作,难以实现数据导入的不丢不重保证。
聊完存储,我们聊聊查询引擎。ClickHouse,大体上我们可以理解成为一个单机查询引擎。最多是通过Distributed Table允许做一些在不同机器上查询单表的不同分桶(Shard),然后汇总数据,其分布式执行的能力是很弱的,分布式的Join更是不需要谈。
StarRocks则不同,StarRocks采用MPP(Massively Parallel Processing)分布式执行框架。在MPP执行框架中,一条查询请求会被拆分成多个物理计算单元在多机并行执行。每个执行节点拥有独享的资源(CPU、内存),MPP框架能够使得单个查询请求可以充分利用所有执行节点的资源。所以,单个查询的性能可以随着集群的水平扩展而不断提。
就单表查询而言,ClickHouse实现了全面的向量化引擎。这是ClickHouse最为引以为傲的地方。为了利用CPU的向量化能力,ClickHouse采用从磁盘到内存都是列存的方式。为了增加对Aggregate和Join的查询速度,ClickHouse对哈希表的实现针对不同数据类型进行全面的优化。类似的地方还体现在各种函数的实现上,ClickHouse每种类型组合都有自己的实现,等等。总而言之,用ClickHouse自己的话来说,它们比其他的引擎牛逼的地方就在于细节,不仅仅是全面的向量化,而且在任何可以优化的地方都进行深入的优化。这就是ClickHouse快的原因。
但是StarRocks也实现了全面的向量化执行引擎。和ClickHouse一样,StarRocks也是采用列存模式,无论是磁盘还是内存数据都是列存,StarRocks对SQL算子的实现过程中也以按列的方式进行计算。在使用向量化的技术实现所有CPU算子外,StarRocks还实现了其他的优化。比如StarRocks实现了Operation on Encoded Data的技术。对于字符串字段的操作,StarRocks在无需解码情况下可以直接基于编码字段完成算子执行,比如实现关联算子、聚合算子、表达式算子计算等。这可以极大的降低SQL在执行过程中的计算复杂度。通过这个优化手段,可以获得2倍的性能提升。总体来说,StarRocks的单表性能在大部分场景下可以和ClickHouse媲美,在有些场景下能够胜出。
而查询一旦涉及到多表,需要分布式Join的时候,ClickHouse就不是快不快的问题,而是能不能的问题。但是StarRocks依然可以快速的胜任这些查询。
要实现多表查询的极致性能,MPP执行框架是基础。在这个基础上,全面的向量化执行引擎也只是一个必要条件。更重要的是,对多表的Join,如何选择一个合适的执行次序和执行策略。这就需要一个基于代价的查询优化器CBO(Cost Based Optimizer)。
ClickHouse只有一个Planner,不存在任何的基于代价的优化器。而StarRocks则从零设计,实现了一个全新的CBO。StarRocks 优化器以Cascades 和 ORCA 论文为基础,并在设计实现中针对全面向量化执行引擎进行了深度定制,优化和创新。优化器内部实现了公共表达式复用,相关子查询重写,Lateral Join, CTE 复用,JoinRorder,Join 分布式执行策略选择,Runtime Filter 下推,低基数字典优化†等重要功能和优化。当前已完整支持了 TPC-DS 99 条SQL。这不仅仅是ClickHouse所不具备的,也比Presto这样的MPP执行引擎提供了更为极致的性能。
此外,StarRocks实现了物化视图。StarRocks的物化视图能够自动维护。如果原始表有变更发生,StarRocks会自动的完成物化视图的更新,不需要额外的维护操作就可以保证物化视图能够维持与原表一致。ClickHouse也支持用户创建物化视图,但是其并不具备自动化完成视图的更新功能。ClickHouse有一个新的实现功能,能够在一定程度上实现自动更新物化视图的能力,但是并不成熟。
StarRocks在进行查询规划时,如果查询优化器发现有合适的物化视图能够加速查询,查询就会自动改写成用物化视图加上,而ClickHouse是做不到的。
总的来说,通过全面的向量化执行引擎和其他优化,ClickHouse除了在单表性能上进行了极速的优化以外,其在存储和执行方面都有较大的缺陷,无法执行复杂的分布式查询,在集群的节点扩容缩容或者节点故障的时候也会面临诸多的维护困难。StarRocks,通过全面化的向量执行引擎,不仅仅在单表性能上可以和ClickHouse媲美,多表性能也同样的优越,在整个系统架构,数据存储,MPP执行引擎,基于代价的优化器等各方面都有显著优势,是云时代极速全场景MPP数据库。