作者:陈琦
部门:数据中台
ClickHouse 是一款由俄罗斯 Yandex 公司开发的 C++ 开源高性能 OLAP 组件。在 Yandex 内部, ClickHouse 主要用于在线流量分析产品 Yandex Metrica,类似于 Google Analytics 或者百度统计。
1.1 OLAP 组件分类
简介:
ROLAP: 即关系型 OLAP,通过对原始明细数据实时聚合计算的方式来进行查询。比如 Presto,Impala 之类的 OLAP 引擎。
MOLAP: 即多维型 OLAP,通过摄入时对原始明细数据进行预聚合加工处理,然后通过预聚合数据来进行查询。比如 Kylin,Druid 之类的 OLAP 引擎。
Hybird OLAP: 混合类型 OLAP。
优缺点介绍:
ROLAP: 没有预聚合的生产数据成本,查询方式灵活,因为总是从原始明细数据查询 RT 相对高。
MOLAP: 预聚合造成高昂的生产数据成本,维度爆炸,数据 Schema 变化需要重新生产数据。
这边简单提下 ClickHouse 和 Druid 两款 OLAP 组件的诞生历史:
MySQL -> 预计算 KV 系统 -> ClickHouse。
MySQL -> 预计算 KV 系统 -> Druid。
两者都经历了从 MYSQL -> 预计算 KV 引擎的方式的演变,最终 ClickHouse 选择了 ROLAP(当然也可以通过用户定义物化视图的方式在一些场景中做预聚合计算) 的方式,而 Druid 选择了 MOLAP 的方式,只对最宽的维度的指标计算,而不是像 Kylin 那样把各个维度组合都进行计算(虽然 Kylin 也可以在一些场景下进行维度组合剪枝来减少维度爆炸问题),来平衡数据摄入预聚合,维度爆炸和查询性能。
ClickHouse 基于这种选择的原因是如果 RT 足够低的情况下那么 ROLAP 的方式会更加灵活,同时也提供了物化视图的方式针对一些场景中做预聚合计算来加速计算。因此 ClickHouse 做了大量的性能优化,保证其高性能,在业界 OLAP 领域的 Benchmark 比较中处于领先行列。
1.2 ClickHouse 特性
SQL 支持: 支持大部分 SQL 功能。列式存储,数据压缩: 列式存储能够更加有利于 OLAP 聚合查询,同时也能大大提高数据压缩率。
多核(垂直扩展),分布式处理(水平扩展): 使用多线程和多分片并行处理。
实时数据摄入: 数据可以实时批量摄入立即被查询。
向量化引擎 / 代码编译生成: 传统火山模型的虚函数,分支预测等开销大大降低了整个算子流水线的执行,尤其对于 OLAP 这种聚合计算,CPU 密集的场景下。向量化引擎通过将算子处理从当个 tuple 变成向量的方式分摊了这部分的开销,也更容易使用 SMID 去加速 CPU 计算,尽可能地将计算保持在 CPU Cache 内。而代码编译生成通过改成以数据为中心的方式消除这部分的开销,尽可能地将计算保持在 CPU 寄存器中。当然这两项技术也不是万能的,由于有些情况,比如 Aggregation 或者 Join 时过多的数据,不可避免地只能通过物化到内存中,导致瓶颈产生,无法有效地提高性能。两者在有些场景甚至是可以混合使用的,一些前沿论文中还有使用软件预取的方式去尽可能地优化。
主键索引,二级索引: ClickHouse 主要采用了稀疏索引的方式做主键索引,minmax,set,ngrambf/tokenbf 等 Bloom Filter 去做二级索引。
ClickHouse 不擅长
没有高速,低延迟的更新和删除方法。
稀疏索引使得点查性能不佳。
不支持事务。
1.3 ClickHouse 为何会那么快
业界使用类似技术的产品很多,比如 Presto,Impala,那么 ClickHouse 为啥会那么快呢?ClickHouse 官方说是自底而上的设计,极度关注底层实现性能,通过各种数据结构和算法优化等,代码级优化等。
比如下方的 Aggregator 针对不同的数据类型使用不同的 Hash 表进行优化。
虽说如此,但是笔者并没有从算子级别做过 micro-benchmark。最近笔者也在抽取出相关算子进行这方面的学习和测试。
1.4 ClickHouse 应用场景
用户行为分析,精细化运营分析: 日活,留存率分析,路径分析,有序漏斗转化率分 析,Session 分析等。
实时日志分析,监控分析。
实时数仓。
2.1 ClickHouse 整体流程
当一个 SQL 传入服务器后,ClickHouse 将SQL 解析成 AST 语法树。
通过 Interpreter,将 AST 语法树转化成 QueryPlan,构建 Pipeline。这里 Interpreter 做的事情通常在别的数据库中,比如 Presto,会分成语义解析(元数据校验),执行计划,执行计划优化等阶段。而在 ClickHouse 中这块代码耦合度相对比较高,个人觉得也是可以重构下的。
执行 Pipeline。
Processor: 有 inputs 和 outputs,一般有 Source/Transform/Sink。 QueryPlanStep: 通过将 processor 串联起来形成执行计划中一步。QueryPlan: 添加多个QueryPlanStep,构建 QueryPipeline。QueryPipeline: 通过 QueryPlan 生成,最终调用串联起来的 Processor (Source / Transform / Sink) 的执行函数。
这里再介绍下表达式的调用关系:
expressionAnalyzer:调用 ActionsVisitor。ActionsVisitor:遍历 AST 表达式树,生成 expressionActions。expressionActions:串联起 expressionAction。expressionStep:将 expressionTransform 串联到 Pipeline,执行 Pipeline。expressionTransform:调用 expressionActions 执行表达式树。
2.2 MergeTree
MergeTree 和 LSM 结构类似,不过没有 MemTable 和 WAL (不过最近 Polymorphic Parts 的特性好像添加了 MemTable 和 WAL,笔者还没有测试过)。数据按照主键排序,每次写入根据数据分区形成一个个的 Parts 文件,Parts 内部有序,服务器后台不断地进行合并 Parts 来减少读放大。当 MergeTree Merge 的速度不如插入形成 Parts 的速度,会出现 Too Many Parts 等类似错误。因此官方要求一定要使用批量写入机制。
2.3 主键索引
ClickHouse 的主键索引采用的是稀疏索引,将每列数据按照 index granularity(默认8192行)进行划分。稀疏索引的好处是条目相对稠密索引较少,能够将其加载到内存,而且对插入时建立索引的成本相对较小。
ClickHouse 数据按列进行存储,每一列都有对应的 mrk 标记文件,bin 文件。mrk 文件与主键索引对齐,主要用于记录数据在 bin 文件中的偏移量信息。查询时通过对主键索引进行二分查找,定位到对应的 mrk 标记文件,进而找到对应的 bin 文件的偏移量,最终扫描得到相应的数据,避免了全表扫描,从而加速查询。值得注意的是,ClickHouse 的主键与 MySQL 等数据库不同,主键不是唯一的。
2.3 MergeTree 家族
ReplacingMergeTree: 会根据主键进行去重,但是这是后台合并时才会去重,无法控制合并时机,尽管可以用 语句来强制合并执行,但是由于性能原因一般不会使用。
CollapsingMergeTree: 异步的删除(折叠)这些除了特定列 Sign 有 1 和 -1 的值以外,其余所有字段的值都相等的成对的行。没有成对的行会被保留。
VersionedCollapsingMergeTree: 类似于 CollapsingMergeTree, 多了 Version 列,支持多线程乱序插入的场景,相比之下, CollapsingMergeTree 只允许严格连续插入。
AggregatingMergeTree: 做增量数据的聚合统计,包括物化视图的数据聚。
ReplicatedXXXMergeTree: 使得以上 MergeTree 家族拥有副本机制,保证高可用,用于生产环境。
3.1 Presto
为了使用户能够快速查询 HDFS 上的数据,有赞于 2018 年引入了 Presto。Presto 是一个 SQL on Hadoop 系统,通过 SQL 让用户快速查询 HDFS 上的数据。其全内存 + Pipeline 的设计使得比类似功能的 Hive 和 Spark 更为快速。Presto 在有赞的应用场景主要是临时查询,BI 报表,元数据等。
但是存储在 HDFS 上的数据基本上是离线 T+1,最快也是小时级别产出的原因,导致我们无法使用 Presto 对实时数据进行查询分析。
那个阶段我们大多数的实时需求是通过 Strom/Flink 等实时任务来预计算好相关的指标存储到 Hbase / Redis 等 KV 数据库来满足的。这种方式需要引入大量的人工开发成本,同时也会存在不灵活,维度计算爆炸等痛点。
更多可见:Presto 在有赞的实践之路
3.2 Druid
因为以上 Presto 实时方面的痛点,有赞在 2019 年引入了 Druid。Druid 是一个实时 OLAP 系统。Druid 通过位图索引,预计算最细粒度的聚合 + 实时聚合这种方式来牺牲一点点 RT,来改善维度爆炸的问题。
于是我们建立了一个 Druid 平台,能够让业务方通过配置 Kafka topic,维度和指标,就能轻松创建一个实时计算源,通过SQL来完成基于这些维度和指标的实时计算分析,大大地改善了实时任务的开发成本。
更多可见:Druid在有赞的实践
在使用 Druid 的过程中,我们也发觉了一些痛点,比如
不支持 Join,导致用户需要导入大宽表。
无法查询明细。
当维度多的时候,维度基数大的情况下,预聚合能力就不再有那么好的效果,实时聚合的效率也不那么高。
在一些场景,比如跨天去重,业务方希望做到精确查询,无法做到。
3.3 Kylin
同时我们在离线上面也同样遇到一些问题。在一些离线场景上我们需要通过 OLAP 分析,又需要精确查询的功能(比如精确 bitmap 去重),性能又要求很高的保证的时候,Druid也无法满足。于是引入了 Kylin 作为纯离线 OLAP 的解决方案去应用于这些场景。
3.4 ClickHouse
其实 2018 年的时候我们就有关注过 ClickHouse。但是那时候属于小众技术,成熟度,生态都一般。但是随着这两年的发展,业界很多公司开始采用 ClickHouse 的核心 OLAP 组件。同时,有赞的业务发展过程中对于实时明细类的需求也越来越多。因此我们引入了 ClickHouse 组件,并且将其平台化,来解决这些业务的需求。
因此目前我们拥有了 Presto,Druid,Kylin,ClickHouse 等多款组件来满足有赞。
OLAP 领域不同的业务需求:
目前在 ClickHouse 在有赞刚刚起步,部署了两个集群,共有 15 个分片双副本去提供服务, 每天导入数据量在 400 亿左右,导入速度达到 250-300 MB/s,平均查询时间 400 ms 左右。
4.1 集群部署
LoadBalancer:LVS。Proxy:基于 Openresty 的 Apisix 代理网关,用于熔断,限流,安全,日志等功能。通过这里我们还可以自己开发相关插件,比如 DMP/CDP 使用的自定义插件。Distribute table:CH 中的分布式表,会关联多个节点中相应的本地分片表。一般用来做查询,用来分发查询请求,对每台节点的结果进行合并,排序等。虽然也能使用分布式表来写入数据,但是会造成性能问题,因此业界分享都是推荐读分布式表,写本地表。CH Shard & Replication:CH 中分片和复制集。一般多分片,多副本来保证高可用,可扩展性和性能。Zookeeper:CH 中使用 Zookeeper 来协调分布式方面的操作。业界经验,随着集群节点数目增多,最好是 SSD ZK 专用集群来保证性能。
目前来说因为考虑到机器的成本,分布式表层和分片复制集层是在部署在同一个主机上的。
4.2 写入流程
离线写入: 通过改造 WaterDrop(Spark) 任务,打通内部 DP(Data Platform) 平台,提供界面化的配置一站式地将 Hive 表数据调度导入到 ClickHouse 表中。每次写入的时候会先通过使用分布式锁在分区级别加锁,写入临时表,最后原子覆盖掉相关表。
实时写入: 通过 Flink SQL/Flink Jar 任务去将 Kafka 中的数据实时导入到 ClickHouse 表中。
4.3 写入中的一些技术细节
使用 JDBC Http Client 的方式:虽然业界有公司去开发 TCP 方式的 JDBC Client 来获得高性能,但是对于 Http 的方式对于代理更友好。
批量方式写入:因为每次写入根据数据分区形成一个个的 parts,MergeTree merge 的速度不如插入形成 parts 的速度,会出现 Too Many Parts 等类似错误。因此官方要求一定要使用批量写入机制。
写本地表,读分布式表:性能高,对 ZK 压力小,灵活度高,这点我们也是从业界相关分享中学习到的。
Random,Round-Robin,Hash 等方式数据摄入:这儿特别提下 Hash 方式,它是通过配置 Hash Key 将数据摄入到相应的分片中。在一些场景下会使用到,比如后面会提到的 DMP/CDP 产品的业务场景中。
4.4 查询
客户端会使用 JDBC Client 进行 SQL 查询,发送 HTTP 请求到 代理(Apisix),最终路由请求到 ClickHouse 分片。大多数的应用场景我们会查询 ClickHouse 分片上的分布式表。但是 DMP/CDP 应用场景比较特殊,后面会提到。
4.5 目前对社区的贡献
在使用 clickhouse-copier 工具时发现无法处理非分区表,进行了修复,也被社区采纳。Fix #15235. When clickhouse-copier handle non-partitioned table, throws segfault error.
5.1 应用场景
DMP/CDP 使用了预计算离线标签位图技术结合明细层实时人群圈选来分析。
SCRM 因为灵活度/数据量/业务上的权衡,则直接使用了通过明细层进行实时人群圈选的技术。
5.2 DMP/CDP
DMP 系统,全称为 Data Management Platform, 主要用于对人群进行圈选,画像分析,广告投放。随着精细化自动营销的慢慢发展,也慢慢地不拘泥于广告投放这种营销行为。于是出现了 CDP 系统,全称为 Client Data Platform,主要是通过聚合商家全域数据流量(其中包括用户数据,用户行为数据等)来进行行为分析,画像分析,自动化营销。有赞是提供商家服务的 SaaS 化公司,也需要提供类似的平台软件赋能于商家。因此,DMP/CDP 产品在有赞均在不断迭代,逐步演进。
针对 DMP/CDP 产品中使用到的根据标签进行人群圈选/人群画像功能,我们慢慢由 Presto,ES 等方案过渡到 ClickHouse。这边当初调研的时候也是参考了字节,苏宁,贝壳等公司的分享和人员的帮助,特别感谢他们。
设计要点:
使用 ClickHouse Bitmap 来完成可枚举的标签预先计算,从而加速计算。
通过对不同标签生成不同的用户 Bitmap,然后圈选时不同标签的与或非组合就变成了对不同用户位图的 and,or,xor(保存一个全量所有用户的 Bitmap,然后进行异或)。这边还有个问题是如果用户数太多了怎么办,我们在数据写入的时候通过切分 Bitmap + 通过hash(uid) 映射用户到不同 offset 的 Bitmap,而不同 uid 的 bitmap 也会正交地分散到不同的分片中。这就是我们在数据写入的时候需要去设计 hash 的导入方式去预 sharding 的原因。
于是最后演变成不同分片进行自身本地表的查询处理,就能完成自己数据的用户圈选。人群圈选等于简单合并数据,不需要任何合并。而人群预估等于简单对每台分片上的数据进行求和。这样既能利用多核,又能利用多分片进行并发查询。
这里还有个问题是我们研究了很多方法去使用分布式表 + 一些参数(比如)。使用分布式表的方法因为 Clickhouse 在一些条件下无法做分布式 Join 导致多机反而比单机还慢的结果。这儿后面看到苏宁同学相关的分享使用 with 语句的方法,但是因为我们是 Bitmap 分段的做法,好像也不能用。
最终,我们采取了在 JDBC 客户端改造 + Apisix 上开发一个插件,形成一个特殊的接口。该接口拥有发送一个请求的同时,根据集群名称,并发发送多个请求到该集群的不同的分片上去的功能。
使用 ClickHouse 来进行明细层实时人群画像计算,并且转换为位图,和预计算的位图进行运算。
一般对应此类需求的都是从用户行为表中取得的数据,这其中有一些无法预计算的不可枚举标签,也有使用函数来匹配用户行为序列路径。最终,会转换为位图,并与预计算的位图进行运算,生成用户画像。
5.3 SCRM
SCRM 系统,全称 Social Client Relationship Management,主要是用于对客户数据进行管理,分析,营销。有赞 SCRM 是目前有赞针对商家推出的一款 SaaS 产品。目前这款产品的特点是:
状态回溯
维度可变
动态圈选
跨店,跨天去重
总之,非常灵活。因为灵活度/数据量/业务上的权衡,目前的选择是通过明细层进行实时人群圈选的技术。
6.1 ClickHouse 的痛点
扩容/缩容后数据无法自动平衡,只能通过低效的数据重新导入的方式来进行人工平衡。尽管我们开发了一套工具基于 clickhouse-copier 来帮助运维进行这个操作,从而加速整个过程,降低人工操作的错误率。但是被迁移的表在迁移过程中仍然需要停止写入的。
单表查询性能高,但是 Join 性能不高。通常有两方面的原因:
ClickHouse 分布式 Join 处理方式不进行 Shuffle exchange, 不适合数据量大的情况。
有些 SQL 语法,比如当 Join 的左表是 subquery,而不是表的时候,ClickHouse 无法进行分布式 Join,只能在分布式表的 Initiator 的单节点进行 Join。
无法高效地更新单行/多行数据。这个其实是目前绝大部分 OLAP 系统的痛点,因为这个是性能和业务的权衡。ClickHouse 大部分类型的 MergeTree 都是在 Compaction 的时候才进行更新,可以看成是一种异步更新的方式。虽然在一些情况下可以通过 ReplacingMergeTree + argMax() 的方式去查询最新值,但是其实会有写放大的现象。当然,业界也有像 Kudu 之类的系统,使用 Delta Tree 来平衡了读放大,写放大的方式去完成这个功能。
6.2 ClickHouse 在有赞的计划
ClickHouse 容器化部署:容器化部署更高拥有更好的弹性伸缩能力,也能和其它的服务进行混合部署来节省成本。此外,有些业务的导入数据量还是非常巨大的。但是其实查询量并不大。但是因为读写不分离,这时候导入数据量反而决定了集群的规模。因此我们希望将读写进行分离,写入部分通过 k8s 容器化技术临时构建集群来完成。
ClickHouse 更多的推广,更多业务的接入:比如用户行为分析,实时数仓,实时报表等业务。目前有赞的 BI 平台支持 Presto 离线明细类报表和 Druid 实时预聚合报表,ClickHouse 实时明细类报表的引入,能够使得用户更加实时,方便地进行报表分析。
ClickHouse 更好的平台化以及故障防范:我们会在 ClickHouse 平台化,业务方易用性,多租户隔离,限流,熔断,监控报警,业务治理等方面进行更多地投入,特别是前期业务方还不多的时候,否则未来无论是平台侧还是业务方改造都是非常大的。
核心业务双链路 和 替换 Druid:OLAP 的核心业务使用 Druid + ClickHouse 的双链路去实现,保障稳定性。Druid 经过这两年的使用,感觉确实存在一些痛点,比如组件过多,前期版本 BUG 过多,引擎算子性能一般等。目前业界也有些公司正在尝试使用 ClickHouse 替换 Druid 的一些业务,这方面我个人觉得应该也是可行的。
ClickHouse 痛点解决:目前 ClickHouse的最大痛点就是扩容/缩容后的数据无法自平衡。社区 2021 年的 Roadmap 也规划了存储计算分离来解决这个问题,但是目前还没有具体时间。这方面我们内部也在看怎么更好地解决问题。
6.3 OLAP 领域的新探索
随着软硬件技术的发展,驱动着一代又一代新的系统来满足/创造业务需求,提升用户体验。对于这些新技术,我们应该保持着敏锐的嗅觉,做适当的技术储备和探索。就目前 OLAP 领域中,有以下这些新探索,特别是好多产品都是我们国内的公司开发的,就我个人而言,这是欣喜的,并且值得鼓励的。
Apache Doris: 业界也有公司开始使用百度开源的 Apache Doris 来做一些业务上的探索。从功能上来说,Doris 拥有扩容/缩容后数据自平衡的功能,有一定的行级更新能力,精确 Bitmap 去重等功能。但是目前来看成熟度,还是存在一定风险的。
TiDB + TiFlash 的 HTAP 数据库: TiFlash 可以通过 Raft Learner 来读取 TiDB 上的数据,通过一套系统来直接进行 OLAP 分析,有一定的行级更新能力,但是目前不开源。
Presto/Impala + Iceberg / Hudi 数据湖方式来做近实时计算/增量计算。
基于 GPU 的 OLAP 计算引擎。
云原生的 OLAP 系统: Snowflake。
相关链接:
Why Clickhouse is so fast:
https://clickhouse.tech/docs/en/faq/general/why-clickhouse-is-so-fast
The Secrets of ClickHouse Performance Optimizations:
https://www.youtube.com/watch?v=ZOZQCQEtrz8
基于 OpenResty 的 Apisix 代理网关:
https://github.com/apache/apisix
Fix #15235. When clickhouse-copier handle non-partitioned table, throws segfault error.
https://github.com/ClickHouse/ClickHouse/pull/17248
ClickHouse 分布式 Join 问题:
https://github.com/ClickHouse/ClickHouse/issues/9477
Handling real-time updates in clickhouse:
https://altinity.com/blog/2020/4/14/handling-real-time-updates-in-clickhouse