ClickHouse vs Elasticsearch性能比对

熟悉了解ClickHouse、elasticsearch极其瓶颈

Posted by Zhan on April 29, 2021

ClickHouse vs ElasticSearch

​ 通过朋友介绍了解到ClickHouse可以用来做海量存储,且可以灵活扩展,目前势头较强劲,特总结学习一下;

术语描述

ClickHouse:俄罗斯搜索巨头Yandex开发的完全列式存储计算的分析型数据库,近年在OLAP领域中一直热门,下文简称为C;

Elasticsearch:一个近实时的分布式搜索分析引擎,它底层的存储完全构建在Lucene之上。通过扩展Lucene的机搜索能力,使其具有分布式搜索和分析能力。Elasticsearch通常会和其他两个开源组件Logstash(日志采集)和Kibana(仪表盘)一起提供端到端的日志/搜索分析,简称ELK,下文简称为E;

Logstash:具有实时管道功能的开源数据收集引擎,Logstash可以动态的把来自不同数据源的数据统一起来,并将数据规范化至你选择的目的地,清理和大众化你的所有数据,用于各种高级下游分析和可视化用例;

Kibana:开源的分析与可视化平台,与Elasticsearch一起使用可搜索查看存在Elasticsearch中的数据;

Raft协议:

Meta同步协议:

Zookeeper:

DDL任务:

内核架构分析

分布式架构

​ 分布式架构需要解决的核心问题包括:节点发现、meta同步、副本数据同步;

​ E采用原生的节点发现、Meta同步协议,易用性体验很好,E的Meta同步协议需要解决的问题其实和开源的Raft协议非常相似,只是出现的早于Raft协议,目前比较成熟,也就造成了E有非常易用的多角色划分,auto schema inference等功能。另外E的多副本数据同步,并未复用Meta同步协议,采用传统的主备同步机制,由主节点负责同步到备节点,此种方式更加简单高效;

​ C因为比较年轻,还处于分布式易用性上升期,因此分布式架构相对简单一些。C引入了外置的zk集群,来进行分布式DDL任务(节点变更)、主备同步任务等操作的下发。多副本之间的数据同步(data shipping)的任务下发也依赖与zk,但最终多副本之间的数据传输还是通过http协议来进行点对点的拷贝,同时多副本都可写,数据同步是多向的。节点发现,C目前不具备这方面能力,需要通过手动配置集群节点地址解决。C目前的脚手架式分布式架构,导致它有极强的灵活部署能力和运维介入能力,同时用户易用性略差,门槛较高。能力上限方面C的分布式部署扩展暂时无短板,集群规模上限与E无差异。C架构扁平,无前端节点和后端节点之分,可部署任意规模集群。同时C在多副本功能上有更细粒度的控制能力,可做到表级别副本数配置,同一物理集群可划分多个逻辑集群,每个逻辑机器可任意配置分片数和副本数;

存储架构

写入链路设计

​ 大数据场景下核心指标为写入吞吐,用户层面对大数据产品的要求:存的下、写的快。

​ Elasicsearch的每一个Shard中,写入分为两步,先写入Lucene(首先为防止用户的写入请求包含非法数据,其次写Lucene索引后,并不是可被搜索的,需要通过fresh把内存的对象转换成完整的Segment后,然后再次reopen后才能被搜索,这个refresh时间间隔用户可设定),再写入TransLog。写入请求到达Shard后,先写Lucene内存索引,此时数据还处于内存中。接着去写TransLog,写完后刷新TransLog数据至磁盘,写磁盘成功后,请求返回给用户。

​ 前述可知,Lucene索引并没有写入实时可见的能力,因此E为近实时(NearRealTime)系统。每隔一段比较长的时间,Lucene会把内存中生成的新Segment刷新到磁盘上,刷新后索引文件已经持久化了,历史的TransLog就没用了,才会清空掉旧的TransLog。

avatar

​ 与E的写入链路相比,C的方式更直接、极致,所有数据写入时直接落盘,同时省略了传统的写redo日志阶段。在极高写入吞吐要求场景下,E和C都需要为提升吞吐舍弃部分写入实时可见性。C主推的做法是把数据延迟攒批写入交给客户端来实现。另外在多副本同步上,E要求的是实时同步,即写入请求必须写穿多个副本才能返回,而C依赖于zk做异步的磁盘文件同步(datashipping),实战中C的写入吞吐能力远超同规格的E。

avatar

Segment vs DataPart

​ E的磁盘文件由一个个Segment组成,Segment实际是一份最小的Lucene索引,(关于Segment内部的存储格式移步E篇的详细介绍文章)而Segment又会在后台异步合并,合并时主要解决两个问题:1.让二级索引更加有序;2.完成主键数据变更;

​ 二级索引是一种全局有序的索引,全部数据构建到一个索引里面比构建到多个索引里对查询的加速更明显。E支持主键删除更新,这依托于Lucene索引的删除实现。更新操作会被换为删除+写入操作。当Lucene索引的Segment里存在多条删除记录时,系统需要通过Segment合并来踢出这些记录。在多个Segment进行合并时,Lucene索引中存储数据表现为append-only的合并,此种方式下二级索引的合并就不需要进行重排序;

​ C存储中的最小的单位是DataPart,一次批量写入的数据会落盘为一个dataPart。DataPart内部数据存储是完全有序(按照表定义的order by排序),这种有序存储就是一种默认聚簇索引可以用来加速数据扫描。C会对dataPart进行异步合并,合并主要为解决:1.让数据存储更加有序;2.完成主键数据变更;dataPart在合并存储数据时表现出的是merge-sorted方式,合并后产生的dataPart仍然处于完全有序状态。依赖于dataPart存储完全有序的设定,C实现主键数据更新的方式和E截然不同。E在变更主键时,采用”先查原记录-生成新纪录-删除原记录-写入新纪录”的方式,这种方式完全限制住了主键更新的效率,主键更新写入和append-only写入的效率差异非常大。而C的主键更新完全异步进行,主键相同的多条记录在异步合并的时候会产生最新的记录结果,这种异步批量的主键更新比E更加高效;

​ Segment和DataPart内部文件存储的能力差异,Segment完全就是Lucene索引的存储格式,Lucene索引在倒排文件上的存储效果是极致的,Lucene索引同时提供了行存、列存等不同格式的原始数据存储。E默认把原数据村两份,一份在行存一份在列存,E根据查询的pattern选择扫描合适的存储文件。原生C的dataPart中并没有任何二级索引文件,数据完全按列存储。C实现的列存在压缩率、扫描吞吐上都做到了极致,相对而言,E的存储比较中庸,成本至少翻倍。

Sechmaless

​ 提到schemaless(E应该叫auto schema inference),E的处理是可以自动推断写入数据的json-schema,根据写入数据的json-schema调整存储表的meta结构,可以节省很多建表、加列的麻烦,得益于E的分布式meta同步能力。E的存储需要甚至强绑定schema,因为schema是以二级索引为核心的存储,没有类型的字段是无法建索引的。

​ 真正的schemaless应是可以灵活高效变更字段类型,同时保证查询性能不会大幅下降。目前想变更E index中的某个字段类型,只能把整份数据数据reindex。相对比而言,C的存储反而不强绑定schema,C的分析能力是以存储扫描为核心,可以在数据扫描进行动态类型转换,也可在dataPart合并异步调整字段类型,在查询时字段类型变更引起的代价也就是运行时增加cast算子的开销,用户不会感受到急剧的性能下降。E的auto schema inference对小规模用户比较友好,但无法创建租价性能的schema,大数据场景下还是需要根据具体的查询需求来创建shcema,所有便利都需成本代价。

查询架构

计算引擎

​ E的搜索引擎完全不具备数据库计算引擎的流式处理能力,完全回合制的request-response数据处理,当用户需要返回大量数据时,容易出现查询失败或触发GC。E的搜索引擎能力上限就是两阶段查询,多表关联查询是完全超出能力上限的,E不支持数据分析行为,E的搜索引擎支持三中不同模式的搜索:

​ ·query_and_fetch:每个分布式节点独立搜索,然后把所得结果返回客户端;

​ ·query_then_fetch:每个分布式存储节点先搜索到各自TopN的记录id和对应的score,汇聚到查询请求节点后做重排得到最终的TopN,最后请求存储节点去拉去明细数据(此处设计为两轮请求为尽量减少拉去明细的数量,即磁盘扫描次数);

​ ·dfs_query_then_fetch:为均衡各个存储节点打分标准,先统计全局的TF(Term Frequency)和DF(Document Frequency),在进行query_then_fetch;

​ C的计算引擎特点是极致的向量化,完全用C++模板手写的向量化函数和aggregator算子使得C在聚合查询上的处理性能达到极致,配合存储极致的并行扫描能力,轻松把机器资源跑满。C的计算引擎能力在分析查询支持上完全覆盖住E的搜索引擎,有完备SQL能力的计算引擎可以让用户在处理数据分析时更加灵活、自由;

数据扫描

​ C是完全列式的存储计算引擎,且以有序存储为核心,在查询扫描过程中,先根据存储的有序性、列存块统计信息、分区键等信息推断出需要扫描的列存块,然后并行扫描数据,如表达式、聚合算子都是在正规的计算引擎中处理。

​ E的数据扫描主要发生在query和fetch阶段,query为扫描Lucene的索引文件获取查询命中的DocId,也包括扫描列文件进行聚合计算。fetch为点查Lucene缩阴中的行存文件读取明细。表达式计算和聚合计算在两个阶段都可能发生,其计算逻辑都是以行为单位进行运算。E的数据扫描和计算都没有向量化能力,以二级索引为基础,当二级索引返回的命中行数特别大(涉及大量数据的分析查询)时,其搜索引擎就会暴露出数据处理能力不足短板;

高并发

​ C的查询跑得快,并发不行,这个是错误观点。因为C的并行太好,查询就可以把磁盘吞吐打满,查询并行不依赖shard,可任意调整。处理并发请求的吞吐能力是衡量一个数据系统效率的最终指标,C的架构没有天然并发缺陷,硬件能力决定其并发上限。

​ 默认情况下,C的目标是保证单个query的latency足够低,部分场景下用户可通过设置合适的系统参数来提升并发能力,比如max_threads。

​ 为何有些场景E的并发能力会很好:E的cache分为Query cache、request cache、data cache、Index cache,以场景下存在热点数据为前提,从查询结果到索引扫描结果层层cache,结果被反复利用。C的只有一个面向IO的UncompressedBlockCahe和系统的PageCache,立足于分析查询场景,围绕磁盘数据,提供IOcache能力;数据扫描粒度,E具备全列的二级索引能力,这些索引能力一般都为预热加载至内存,即使查询条件多变查询到结果的代价也很低,拿到结果就可以按行读取计算。原生C并无二级索引,只能在多变查询下大批量扫描数据过滤结果(阿里云C已经具备二级索引能力)。

​ 结论,E只有在完全搜索场景下(where过滤后记录数较少),且内存足够时才能提现并发优势。分析场景下(where过滤后记录数较多),C凭借极致列存和向量化计算会有更加出色的并发表现。二者侧重不同,C并发处理立足于磁盘吞吐,E的并发能力立足于内存Cache。C适合成本低、大数据量的分析场景,能够充分利用磁盘的带宽能力;

性能测试

​ LATER TO PATCH;