0x00 前言
即使没有数据倾斜,千亿级的数据查询对于系统也是一种巨大负担,对于数据开发来说,如何来优化它,既是挑战,也是机遇!
在上一篇文章 《漫谈千亿级数据优化实践:数据倾斜(纯干货)》中,我们分享了一些在千亿级数据优化实践中和数据倾斜相关的内容。本文将分享千亿级数据优化的另个一点:如何使用使用数据!
注意:
本文会限定一些业务场景和技术架构,因此解决方法会局限于此。很多问题可以通过换架构或者引入新的组件来解决,但是成本可能会很高,因此暂不考虑。
本文不是一篇Hive使用和优化文档,更侧重于梳理笔者的思路,让大家少走些坑。
文章主题
在流行的大数据领域中,Hive绝对占据了很大的一片天地,不管是数据仓库和数据分析,还是数据挖掘和机器学习,凡是需要和大数据量打交道的童鞋们,基本上都要接触Hive。因此,本文将侧重于千亿级数据在Hive中的使用,并通过一个典型的数据使用难题来总结一些在大规模数据场景下的优化方式。
本文主要以一个具体的使用场景为切入点,为了解决该场景下的使用难题,笔者经理了一次次的尝试+失败,最终找到了一种相对比较合适的方式。
文章结构
本文可以看过是一种记录和思考,完全还原笔者在遇到问题时的解决方式。因此全文会以事情的发展为主线,每次尝试一种解决方法,失败后继续查找新的方法,中间会穿插一些技术细节。
文章主线如下:
明确使用场景和困难。
如何解决,这是一个不断推翻重来的过程。
回顾总结
0x01 问题来了!
本章作用主要有二:
明确业务背景和使用场景
明确困难所在
1. 业务背景和使用场景
按照惯例,我又来到了一家电商网站来工作,我们有一张十分重要的表:用户和购买过的商品表。
如下图,该表只有三个字段,分别是用户ID、商品ID。我们可以简单地理解这是一张事实表。对事实表没印象的可以参考这篇《漫谈数据仓库之维度建模》。
我们暂且不管当初为何这样设计的,现在的情况就是
| 列名 | 字段类型 |
| ------------- |:-------------:|
| user_id | string |
| product_id | string |
这张表有哪些使用场景呢:
输入一批用户,找到和他们有相似购买行为的用户
统计用户购买商品的数据区间
......
总之这张表能用到的地方是极多的,相信数据分析和数据挖掘的童鞋们肯定能想到很多场景,这里就不展开讲了。
问题来了: 数据量太大,随便一个查询就是五六个小时,有没有办法优化?
2. 困难
先说明一下问题在哪。
数据量大
这张表里面保存了我站来自全球的50亿用户和他们购买过的商品,粗略估计一下,人均会购买60件商品,也就是说这张表有 3000亿 的数据。
3千亿条记录是什么概念呢,如果存成没有压缩的txt文件的话,大致有30T以上。如果做一个压缩,我们保守一点估计,要有接近10T的数据。
查询速度慢
这么大数据量,查询起来的确比较慢。可能随便跑一个数据,就要3到5个小时。
我们可以大致地分析一下慢的原因:
扫描数据量大
join的时候时间长
因为我们的reduce数量右限制,每个reduce需要处理的数据量太多
shuffle的时候效率太低。
0x02 解决过程
我们在解决这个难题的时候是围绕一些出发点的:
减少扫描的数据量
加快关联查询的速度
1. 分区
第一个思路就是分区,我们可以根据用户的账号分布来进行分区,然后在扫描的时候,只扫描部分分区就行。 好,我们做一个设计。
我们美好的愿望是:假设有一个需求需要查询一定的用户的购买记录,我们不用扫描全量的数据,只扫描其中一部分即可。
下面我们基于几个设定来设计我们的分区规则:
假设我们的用户id都是数字类型的,如下图。
我们按照账号的id来设计分区函数,比如说前四位相同的放在一个分区中。
写入数据,和查询数据使用相同的分区函数
这样我们就有了1万个分区,每个分区中有30万用户的购买记录,也就是说每个分区中会有1800万的记录数,总计约1G的文件大小。
下面就是我们设计出来的分区。
我们的想法是好的,下面举几个场景:
比如现在需要查100个用户的数据,不分区的话,我们需要扫描全量的数据,现在我们可能只要扫描10个分区,最多100个分区,也就是我们的速度回提升100倍以上。
需要查1万个用户的数据,我们假设会命中1000个分区。
需要查10万个用户的数据,我们假设会命中5000个分区。
例子我都举不下去了,实际情况是,如果用户分布比较分散的话,超过20万个用户的话,基本上就命中了所有了分区了。 这个感兴趣的可以测一下。
增加分区数?
这个方案是可以的,比如我们变成10万个分区,这样当然可以,但是让需要查询的用户多的话,效果照样变弱,而且更多的分区意味着每个分区的数据会变少,这样小文件就会多很多。
结论
分区的方式不靠谱!
2. 索引
注意: Hive的索引也是个坑,怪不得没人用,但是我们还是要设计一下。
基于“减少扫描的数据量”这点来讲,索引是一种极妙的方式,有了索引,我们就不必全量扫描所有的数据,速度肯定就快了呀。 但是, Hive的索引是个坑。
下面讲一下Hive索引的机制就明白了。
Hive索引机制
在指定列上建立索引,会产生一张索引表(Hive的一张物理表),里面的字段包括,索引列的值、该值对应的HDFS文件路径、该值在文件中的偏移量。
如下,是Hive的索引表。其中,索引表中key字段,就是原表中key字段的值,_bucketname 字段,代表数据文件对应的HDFS文件路径,_offsets 代表该key值在文件中的偏移量,有可能有多个偏移量,因此,该字段类型为数组。
| 列名 | 字段类型 |
| ------------- |:-------------:|
| user_id | string |
| _bucketname | string |
| _offset | array < bigint > |
在执行索引字段查询时候,首先额外生成一个MR job,根据对索引列的过滤条件,从索引表中过滤出索引列的值对应的hdfs文件路径及偏移量,输出到hdfs上的一个文件中,然后根据这些文件中的hdfs路径和偏移量,筛选原始input文件,生成新的split,作为整个job的split,这样就达到不用全表扫描的目的。
注意: 按照上面的说明,我们的索引其实就是另一张Hive的表,而且数据量还是很大。 下面从两个点说明Hive的索引方案不能用。
经过测试,索引表就有4、5T,我们在查询的时候,要先和这张索引表做关联,然后再和原表做关联,损失太大了。
HDFS文件系统的设计问题。会导致最终我们扫描的还是全表。为什么?下面讲解。
HDFS的设计
我们默认大家对HDFS原理有所认知。这里只说一下这次我们优化的内容。
假设我们10T的数据,按照128M一个文件块,那就是我们有七八万个文件块。和前面的分区的情况类似,当需要查询的用户数量到一定程度,基本上还是要扫描所有的文件块。
结论
索引的方式不靠谱,至少Hive中不可用。
索引的使用方式,就不再描述了,看官网还是挺简单的:Hive官网:Index。
3. 分桶
分桶就不再说了,和前面说的问题类似,也不可用。
到这里我就绝望了,有打算不解决了。准备用最初的一招:按活跃度区分。
4. 区分活跃用户
这也是一种很值得考虑的方式,因为我们大部分对数据的使用都会考虑活跃用户,这里我们把30亿用户中活跃的10亿的用户抽出来放一个分区,这样的话,我们的查询效率能提高3倍左右。
问题
活跃用户不好定义,每个业务方的定义不一样。
运行成本太大,跑这个数据挺耗时间。
结论
这是一种方法,如果没有更好的方法就用这个了。
5. 数据结构
受大神的指点,我们更换了一种在Hive中的存储方式,现在更新表如下
| 列名 | 字段类型 |
| ------------- |:-------------:|
| user_id | string |
| product_list | array< string > |
这是一个很简单的转表,我们使用了Hive中的数据结构Array,把一个用户的所有购买过的商品放入到一个字段中,这样的话,我们的总数据量就只有30亿了,在做关联查询的时候速度必然很快。
实践
经过实践,这样的存储,只需占用之前存储量的1/2左右,也就是只需要不到5T的大小,查询速度从平均一个任务4个小时缩减到半个小时。
问题
这里有两个问题:
数据更新,数据结构的改变导致在更新数据的时候有一些障碍,这点不再展开,方法总是有的,笔者是保留了两份数据,这一份专门供查询用。
使用成本增加,因为数据结构变了,相应的查询sql也要调整。 这里就需要用到
lateral view explode
,详情可看官网。
总结
总的来接,这种方式还是可行的,目前的反馈还都是正向的,额外的sql复杂度不是很大,大部分童鞋都能接收。
0xFF 总结
本文主要是描述了一次千亿级数据量查询优化的过程,回头来看,其实也听简单的,但是身在其中未必能想清楚,这也和经验有关,因此走了很多的弯路。
总的来讲,笔者尝试了5种方式:分区、索引、分桶、活跃度区分、新的数据结构。最后一种方式基本解决了遇到的问题。
本文对一些技术细节没有过多描述,比如建索引,建表,这些在官网很容易找到,因此不再过多涉及。思想到位了,其它的问题都不大。
参考
作者:dantezhao |简书 | CSDN | GITHUB
个人主页:http://dantezhao.com
文章可以转载, 但必须以超链接形式标明文章原始出处和作者信息