写点什么

趣头条基于 ClickHouse 玩转每天 1000 亿数据量

2020 年 1 月 16 日

趣头条基于ClickHouse玩转每天1000亿数据量

本文由 dbaplus 社群授权转载。


业务背景

随着公司规模越来越大,业务线越来越多,公司的指标规模也在急速增长,现有的基于 storm 实时计算的指标计算架构的缺点越来越凸显,所以我们急需对现有的架构进行调整。


1、基于 storm 的指标平台存在的问题

  • 指标口径不够直观

  • 数据无法回溯

  • 稳定性不够


2、什么是我们需要的?

我们需要一个稳定的、基于 SQL、方便进行数据回溯、并且要足够快速的引擎,支持我们的实时指标平台。


1)稳定性 是最主要的,基于 storm 的架构数据都是存储在内存中的,如果指标配置有问题,很容易导致 OOM,需要清理全部的数据才能够恢复。


2)基于 SQL 是避免像 storm 架构下离线 SQL 到 storm topology 转换的尴尬经历。


3)方便回溯 是数据出现问题以后,我们可以简单的从新刷一下就可以恢复正常,在 storm 架构下有些场景无法完成。


4)快速 那是必须的,指标数越来越多,如果不能再 5 分钟周期内完成所有的指标计算是不能接受的。


clickhouse

1、为什么选择 clickhouse?

足够快,在选择 clickhouse 以前我们也有调研过 presto、druid 等方案,presto 的速度不够快,无法在 5 分钟内完成这么多次的查询。


druid 的预计算挺好的,但是维度固定,我们的指标的维度下钻都是很灵活的,并且 druid 的角色太多维护成本也太高,所以也被 pass 了。


最终我们选择了 clickhouse,在我们使用之前,部门内部其实已经有使用单机版对离线数据的查询进行加速了,所以选择 clickhouse 也算是顺理成章。


2、clickhouse 和 presto 查询速度比较

clickhouse 集群现状:32 核 128G 内存机器 60 台,使用 ReplicatedMergeTree 引擎,每个 shard 有两个 replica。


presto 集群的现状:32 核 128G 内存机器 100 台。


1)最简单的 count()的 case




从上图可以看到 clickhouse 在 count 一个 1100 亿数据表只需要 2s 不到的时间, 由于数据冗余存储的关系,clickhouse 实际响应该次查询的机器数只有 30 台(60 / 2),presto 在 count 一个 400 亿的数据表耗时 80 秒左右的时候,100 台机器同时在处理这个 count 的查询。


2)常规指标维度下钻计算 count() + group by + order by + limit




同样在 1100 亿数据表中 clickhouse 在该 case 上面的执行时间也是非常不错的耗时 5s 左右,presto 在 400 亿的数据集上完成该查询需要 100s 左右的时间。


从上面两个常规的 case 的执行时间我们可以看出,clickhouse 的查询速度比 presto 的查询速度还是要快非常多的。


3、clickhouse 为什么如此快

1)优秀的代码,对性能的极致追求


clickhouse 是 CPP 编写的,代码中大量使用了 CPP 最新的特性来对查询进行加速。


2)优秀的执行引擎以及存储引擎


clickhouse 是基于列式存储的,使用了向量化的执行引擎,利用 SIMD 指令进行处理加速,同时使用 LLVM 加快函数编译执行,当然了 Presto 也大量的使用了这样的特性。


3)稀疏索引


相比于传统基于 HDFS 的 OLAP 引擎,clickhouse 不仅有基于分区的过滤,还有基于列级别的稀疏索引,这样在进行条件查询的时候可以过滤到很多不需要扫描的块,这样对提升查询速度是很有帮助的。


4)存储执行耦合


存储和执行分离是一个趋势,但是存储和执行耦合也是有优势的,避免了网络的开销,CPU 的极致压榨加上 SSD 的加持,每秒的数据传输对于网络带宽的压力是非常大的,耦合部署可以避免该问题。


5)数据存储在 SSD,极高的 iops。


4、clickhouse 的 insert 和 select

1)clickhouse 如何完成一次完整的 select


这里有个概念需要澄清一下,clickhouse 的表分为两种,一种是本地表另一种是分布式表。本地表是实际存储数据的而分布式表是一个逻辑上的表,不存储数据的只是做一个路由使用,一般在查询的时候都是直接使用分布式表,分布式表引擎会将我们的查询请求路由本地表进行查询,然后进行汇总最终返回给用户。


2)索引在查询中的使用


索引是 clickhouse 查询速度比较快的一个重要原因,正是因为有索引可以避免不必要的数据的扫描和处理。传统基于 hdfs 的 olap 引擎都是不支持索引的,基本的数据过滤只能支持分区进行过滤,这样会扫描处理很多不必要的数据。


clickhouse 不仅支持分区的过滤也支持列级别的稀疏索引。clickhouse 的基础索引是使用了和 kafka 一样的稀疏索引,索引粒度默认是 8192,即每 8192 条数据进行一次记录,这样对于 1 亿的数据只需要记录 12207 条记录,这样可以很好的节约空间。


二分查找+遍历也可以快速的索引到指定的数据,当然相对于稠密索引,肯定会有一定的性能损失,但是在大数据量的场景下,使用稠密索引对存储也是有压力的。



下面我们通过举例看下索引在 clickhouse 的一次 select 中的应用,该表的排序情况为 order by CounterID, Date 第一排序字段为 CounterID,第二排序字段为 Date,即先按照 CounterID 进行排序,如果 CounterID 相同再按照 Date 进行排序。


  • 场景 1 where CounterId=’a’


CounterID 是第一索引列,可以直接定位到 CounterId=’a’的数据是在[0,3]数据块中。


  • 场景 2 where Date=’3’


Date 为第二索引列,索引起来有点费劲,过滤效果还不是特别的好,Date=’3’的数据定位在[2,10]数据块中。


  • 场景 3 where CounterId=’a’ and Date=’3’


第一索引 + 第二索引同时过滤,[0,3] 和 [2,10]的交集,所以为[2,3]数据块中。


  • 场景 4 where noIndexColumn=’xxx’


对于这样没有索引字段的查询就需要直接扫描全部的数据块[0,10]。


3)clickhouse 如何完成一次插入


clickhouse 的插入是基于 Batch 的,它不能够像传统的 mysql 那样频繁的单条记录插入,批次的大小从几千到几十万不等,需要和列的数量以及数据的特性一起考虑,clickhouse 的写入和 Hbase 的写入有点”像”(类 LSM-Tree),主要区别有:


  • 没有内存表;

  • 不进行日志的记录。


clickhouse 写入的时候是直接落盘的, 在落盘之前会对数据进行排序以及必要的拆分(如不同分区的数据会拆分成多个文件夹),如果使用的是 ReplicatedMergeTree 引擎还需要与 zookeeper 进行交互,最终会有线程在后台把数据(文件夹)进行合并(merge),将小文件夹合并生成大文件夹方便查询的时候进行读取(小文件会影响查询性能)。


5、关于集群的搭建

1)单副本


缺点:


  • 集群中任何一台机器出现故障集群不可用;

  • 如果磁盘出现问题不可恢复数据永久丢失;

  • 集群升级期间不可用(clickhouse 版本更新快)。


2)多副本


多副本可以完美的解决单副本的所有的问题,多副本有 2 个解决方案:


  • RAID 磁盘阵列;

  • 使用 ReplicatedMergeTree 引擎,clickhouse 原生支持同步的引擎(基于 zookeeper)。


两种方案的优缺点:


  • 基于 RAID 磁盘阵列的解决方案,在版本升级,机器 down 机的情况下无法解决单副本的缺陷;

  • 基于 zookeeper 的同步,需要双倍的机器(费钱),同时对 zookeeper 依赖太重,zookeeper 会成为集群的瓶颈,当 zookeeper 有问题的时候集群不可写入(ready only mode);

  • 副本不仅仅让数据更安全,查询的请求也可以路由到副本所在的机器,这样对查询并发度的提升也是有帮助的,如果查询性能跟不上添加副本的数量也是一个解决方案。


6、常见的引擎(MergeTree 家族)

1)(Replicated)MergeTree


该引擎为最简单的引擎,存储最原始数据不做任何的预计算,任何在该引擎上的 select 语句都是在原始数据上进行操作的,常规场景使用最为广泛,其他引擎都是该引擎的一个变种。


2)(Replicated)SummingMergeTree


该引擎拥有“预计算(加法)”的功能。


实现原理:在 merge 阶段把数据加起来(对于需要加的列需要在建表的时候进行指定),对于不可加的列,会取一个最先出现的值。


3)(Replicated)ReplacingMergeTree


该引擎拥有“处理重复数据”的功能。


使用场景:“最新值”,“实时数据”。


4)(Replicated)AggregatingMergeTree


该引擎拥有“预聚合”的功能。


使用场景:配合”物化视图”来一起使用,拥有毫秒级计算 UV 和 PV 的能力。


5)(Replicated)CollapsingMergeTree


该引擎和 ReplacingMergeTree 的功能有点类似,就是通过一个 sign 位去除重复数据的。


需要注意的是,上述所有拥有"预聚合"能力的引擎都在"Merge"过程中实现的,所以在表上进行查询的时候 SQL 是需要进行特殊处理的。


如 SummingMergeTree 引擎需要自己 sum(), ReplacingMergeTree 引擎需要使用时间+版本进行 order by + limit 来取到最新的值,由于数据做了预处理,数据量已经减少了很多,所以查询速度相对会快非常多。


7、最佳实践

1)实时写入使用本地表,不要使用分布式表


分布式表引擎会帮我们将数据自动路由到健康的数据表进行数据的存储,所以使用分布式表相对来说比较简单,对于 Producer 不需要有太多的考虑,但是分布式表有些致命的缺点。


  • 数据的一致性问题,先在分布式表所在的机器进行落盘,然后异步的发送到本地表所在机器进行存储,中间没有一致性的校验,而且在分布式表所在机器时如果机器出现 down 机,会存在数据丢失风险;

  • 据说对 zookeeper 的压力比较大(待验证)。


2)推荐使用(*)MergeTree 引擎,该引擎是 clickhouse 最核心的组件,也是社区优化的重点


数据有保障,查询有保障,升级无感知。


3)谨慎使用 on cluster 的 SQL


使用该类型 SQL hang 住的案例不少,我们也有遇到,可以直接写个脚本直接操作集群的每台进行处理。


8、常见参数配置推荐

1)max_concurrent_queries


最大并发处理的请求数(包含 select,insert 等),默认值 100,推荐 150(不够再加),在我们的集群中出现过”max concurrent queries”的问题。


2)max_bytes_before_external_sort


当 order by 已使用 max_bytes_before_external_sort 内存就进行溢写磁盘(基于磁盘排序),如果不设置该值,那么当内存不够时直接抛错,设置了该值 order by 可以正常完成,但是速度相对存内存来说肯定要慢点(实测慢的非常多,无法接受)。


3)background_pool_size


后台线程池的大小,merge 线程就是在该线程池中执行,当然该线程池不仅仅是给 merge 线程用的,默认值 16,推荐 32 提升 merge 的速度(CPU 允许的前提下)。


4)max_memory_usage


单个 SQL 在单台机器最大内存使用量,该值可以设置的比较大,这样可以提升集群查询的上限。


5)max_memory_usage_for_all_queries


单机最大的内存使用量可以设置略小于机器的物理内存(留一点内操作系统)。


6)max_bytes_before_external_group_by


在进行 group by 的时候,内存使用量已经达到了 max_bytes_before_external_group_by 的时候就进行写磁盘(基于磁盘的 group by 相对于基于磁盘的 order by 性能损耗要好很多的),一般 max_bytes_before_external_group_by 设置为 max_memory_usage / 2,原因是在 clickhouse 中聚合分两个阶段:


  • 查询并且建立中间数据;

  • 合并中间数据 写磁盘在第一个阶段,如果无须写磁盘,clickhouse 在第一个和第二个阶段需要使用相同的内存。


这些内存参数强烈推荐配置上,增强集群的稳定性避免在使用过程中出现莫名其妙的异常。


9、那些年我们遇到过的问题

1)Too many parts(304). Merges are processing significantly slower than inserts


相信很多同学在刚开始使用 clickhouse 的时候都有遇到过该异常,出现异常的原因是因为 MergeTree 的 merge 的速度跟不上目录生成的速度, 数据目录越来越多就会抛出这个异常, 所以一般情况下遇到这个异常,降低一下插入频次就 ok 了,单纯调整 background_pool_size 的大小是治标不治本的。


我们的场景


我们的插入速度是严格按照官方文档上面的推荐”每秒不超过 1 次的 insert request”,但是有个插入程序在运行一段时间以后抛出了该异常,很奇怪。


问题排查


排查发现失败的这个表的数据有一个特性,它虽然是实时数据但是数据的 eventTime 是最近一周内的任何时间点,我们的表又是按照 day + hour 组合分区的那么在极限情况下,我们的一个插入请求会涉及 7*24 分区的数据,也就是我们一次插入会在磁盘上生成 168 个数据目录(文件夹),文件夹的生成速度太快,merge 速度跟不上了,所以官方文档的上每秒不超过 1 个插入请求,更准确的说是每秒不超过 1 个数据目录。


case study


分区字段的设置要慎重考虑,如果每次插入涉及的分区太多,那么不仅容易出现上面的异常,同时在插入的时候也比较耗时,原因是每个数据目录都需要和 zookeeper 进行交互。


2)DB::NetException: Connection reset by peer, while reading from socket xxx


查询过程中 clickhouse-server 进程挂掉。


问题排查


排查发现在这个异常抛出的时间点有出现 clickhouse-server 的重启,通过监控系统看到机器的内存使用在该时间点出现高峰,在初期集群"裸奔"的时期,很多内存参数都没有进行限制,导致 clickhouse-server 内存使用量太高被 OS KILL 掉。


case study


上面推荐的内存参数强烈推荐全部加上,max_memory_usage_for_all_queries 该参数没有正确设置是导致该 case 触发的主要原因。


3)Memory limit (for query) exceeded:would use 9.37 GiB (attempt to allocate chunk of 301989888 bytes), maximum: 9.31 GiB


该异常很直接,就是我们限制了 SQL 的查询内存(max_memory_usage)使用的上线,当内存使用量大于该值的时候,查询被强制 KILL。


对于常规的如下简单的 SQL, 查询的空间复杂度为 O(1) 。


select count(1) from table where condition1 and condition2select c1, c2 from table where condition1 and condition2
复制代码


对于 group by, order by , count distinct,join 这样的复杂的 SQL,查询的空间复杂度就不是 O(1)了,需要使用大量的内存。


  • 如果是 group by 内存不够,推荐配置上 max_bytes_before_external_group_by 参数,当使用内存到达该阈值,进行磁盘 group by

  • 如果是 order by 内存不够,推荐配置上 max_bytes_before_external_sort 参数,当使用内存到达该阈值,进行磁盘 order by

  • 如果是 count distinct 内存不够,推荐使用一些预估函数(如果业务场景允许),这样不仅可以减少内存的使用同时还会提示查询速度

  • 对于 JOIN 场景,我们需要注意的是 clickhouse 在进行 JOIN 的时候都是将"右表"进行多节点的传输的(右表广播),如果你已经遵循了该原则还是无法跑出来,那么好像也没有什么好办法了


4)zookeeper 的 snapshot 文件太大,follower 从 leader 同步文件时超时


上面有说过 clickhouse 对 zookeeper 的依赖非常的重,表的元数据信息,每个数据块的信息,每次插入的时候,数据同步的时候,都需要和 zookeeper 进行交互,上面存储的数据非常的多。


就拿我们自己的集群举例,我们集群有 60 台机器 30 张左右的表,数据一般只存储 2 天,我们 zookeeper 集群的压力 已经非常的大了,zookeeper 的节点数据已经到达 500w 左右,一个 snapshot 文件已经有 2G+左右的大小了,zookeeper 节点之间的数据同步已经经常性的出现超时。


问题解决


  • zookeeper 的 snapshot 文件存储盘不低于 1T,注意清理策略,不然磁盘报警报到你怀疑人生,如果磁盘爆了那集群就处于“残废”状态;

  • zookeeper 集群的 znode 最好能在 400w 以下;

  • 建表的时候添加 use_minimalistic_part_header_in_zookeeper 参数,对元数据进行压缩存储,对于高版本的 clickhouse 可以直接在原表上面修改该 setting 信息,注意修改完了以后无法再回滚的。


5)zookeeper 压力太大,clickhouse 表处于”read only mode”,插入失败


  • zookeeper 机器的 snapshot 文件和 log 文件最好分盘存储(推荐 SSD)提高 ZK 的响应;

  • 做好 zookeeper 集群和 clickhouse 集群的规划,可以多套 zookeeper 集群服务一套 clickhouse 集群。


直播回放



作者介绍


王海胜,趣头条数据中心大数据开发工程师


8 年互联网工作经验,曾在 eBay、唯品会、趣头条等公司从事大数据开发相关工作,有丰富的大数据落地经验。


原文链接


https://mp.weixin.qq.com/s/egzFxUOAGen_yrKclZGVag


2020 年 1 月 16 日 09:303706

评论

发布
暂无评论
发现更多内容

ARTS打卡Week 02

teoking

objective-c LeetCode WebRTC

MAC OS 下 HomeBrew 使用

耳东

macos brew homebrew

你会写测试用例吗

鱼贩

工作 vs 生活

shengjk1

B端产品经理养成记(1):业务场景

涛哥

产品经理 需求 产品开发

1 ARTS 2020-05-31

3.141516

LeetCode

【openlayers】在vue中使用ol

学习委员

Java html Vue 地图 openlayers

时代在变,产品运营能力很重要

夜来妖

程序员人生 程序人生 产品经理 产品推荐 程序媛

ARTS week2

紫枫

ARTS 打卡计划

工厂模式(四)泛型工厂之MyBatis Mapper代理

LSJ

Java 设计模式 泛型 工厂注册中心

如何用CSS选择符(数字开头) 杀死队友

学习委员

Java html css3 前端 Web

愚蠢写作术(1):怎么让你的标题被读者忽视

史方远

个人成长 写作

Apache DolphinScheduler新特性与Roadmap路线

海豚调度

数据中台 大数据任务调度 工作流调度 海豚调度 数据湖调度

转行程序员浅谈进程间的socket通信

WB

Linux socket 转行程序员

RocketMQ - 高可用设计

Java收录阁

RocketMQ

做PO难,难于上青天

刘华Kenneth

敏捷 产品经理 决策 PO

钢铁侠马斯克之仰望星空

池建强

创业 马斯克 Space X

ARTS week 2

刘昱

【ARTS打卡】Week01

Rex

学习

B端产品经理养成记(2):用户故事

涛哥

产品经理 需求 产品开发

【5月】本月读书学到了什么

Neco.W

创业 读书感悟 阅读量

游戏夜读 | 关于构图的困难

game1night

ARTS Week1

姜海天

ARTS打卡第一周5.25-5.31

我笔盒呢

不吹不黑!GitHub 上帮助人们学习编码的 12 个资源,错过血亏...

JackTian

GitHub 学习 程序员 编码 开源项目

ARTS(2020-05-25/2020-05-31)

天行者

正确阅读

写博客的那些事

shengjk1

John 易筋 ARTS打卡Week 02

John(易筋)

ARTS 打卡计划 ARTS活动 arts

Element-UI实战系列:Table+Pagination组件实现已选和全选功能

brave heart

Vue 前端 Element

使用Kotlin语言初始化数组

mengxn

数组 kotlin 初始化

Kafka系列9:面试题是否有必要深入了解其背后的原理?我觉得应该刨根究底(上)

z小赵

大数据 kafka 实时计算

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

趣头条基于ClickHouse玩转每天1000亿数据量-InfoQ