Hive 作为大数据领域常用的数据仓库组件,在平时设计和查询时要特别注意效率。影响 Hive 效率的几乎从不是数据量过大,而是数据倾斜、数据冗余、Job或I/O过多、MapReduce 分配不合理等等。对Hive 的调优既包含 Hive 的建表设计方面,对HiveHQL 语句本身的优化,也包含 Hive 配置参数 和 底层引擎 MapReduce 方面的调整。
Hive的建表设计层面调优,主要讲的怎么样合理的组织数据,方便后续的高效计算。比如建表的类型,文件存储格式,是否压缩等等。
分区表 是在某一个或者几个维度上对数据进行分类存储,一个分区对应一个目录。如果筛选条件里有分区字段,那么Hive 只需要遍历对应分区目录下的文件即可,不需要遍历全局数据,使得处理的数据量大大减少,从而提高查询效率。
- 当你意识到一个字段经常用来做where,建分区表,使用这个字段当做分区字段
- 在查询的时候,使用分区字段来过滤,就可以避免全表扫描。只需要扫描这张表的一个分区的数据即可
跟分区的概念很相似,都是把数据分成多个不同的类别,区别就是规则不一样
- 1、分区:按照字段值来进行:一个分区,就只是包含这个这一个值的所有记录
不是当前分区的数据一定不在当前分区
当前分区也只会包含当前这个分区值的数据
- 2、分桶:默认规则:Hash散列
一个分桶中会有多个不同的值
如果一个分桶中,包含了某个值,这个值的所有记录,必然都在这个分桶
分桶语法:
CLUSTERED BY(userid) --表示按照 userid 来分桶
SORTED BY(viewTime) --按照viewtime来进行桶内排序
INTO 32 BUCKETS --分成多少个桶
两个表以相同方式(相同字段)划分桶,两个表的桶个数是倍数关系
create table order(cid int,price float) clustered by(cid) into 32 buckets;
create table customer(id int,first string) clustered by(id) into 32 buckets;
select price from order t join customer s on t.cid = s.id;
通常情况下,Sampling 在全体数据上进行采样,这样效率自然就低,它要去访问所有数据。而如果一个表已经对某一列制作了 bucket,就可以采样所有桶中指定序号的某个桶,这就减少了访问量。
如下例所示就是采样了 page_view 中 32 个桶中的第三个桶的全部数据:
SELECT * FROM page_view TABLESAMPLE(BUCKET 3 OUT OF 32);
总结三种采样方式:
分桶抽样:
select * from student tablesample(bucket 3 out of 32);
随机采样:rand() 函数
select * from student order by rand() limit 100; 效率低
select * from student distribute by rand() sort by rand() limit 100; 推荐使用这种
数据块抽样:tablesample()函数
select * from student tablesample(10 percent); # 百分比
select * from student tablesample(5 rows); # 行数
select * from student tablesample(5 M); # 大小
在 HiveSQL 的 create table 语句中,可以使用 stored as ... 指定表的存储格式。Apache Hive支持 Apache Hadoop 中使用的几种熟悉的文件格式,比如 TextFile、SequenceFile、RCFile、Avro、ORC、ParquetFile等。
Hive 语句最终是转化为 MapReduce 程序来执行的,而 MapReduce 的性能瓶颈在与 网络IO 和 磁盘IO,要解决性能瓶颈,最主要的是 减少数据量,对数据进行压缩是个好方式。压缩虽然是减少了数据量,但是压缩过程要消耗 CPU,但是在 Hadoop 中,往往性能瓶颈不在于 CPU,CPU 压力并不大,所以压缩充分利用了比较空闲的 CPU。
压缩格式 | 是否可拆分 | 是否自带 | 压缩率 | 速度 | 是否Hadoop自带 |
gzip | 否 | 是 | 很高 | 比较快 | 是 |
lzo | 是 | 是 | 比较高 | 很快 | 否,要安装 |
snappy | 否 | 是 | 比较高 | 很快 | 否,要安装 |
bzip2 | 是 | 否 | 最高 | 慢 | 是 |
如何选择压缩方式:
- 压缩比率
- 压缩解压速度
- 是否支持split
压缩使用:
## 默认值是false
set mapreduce.output.fileoutputformat.compress=true;
## 默认值是Record
set mapreduce.output.fileoutputformat.compress.type=BLOCK
## 默认值是org.apache.hadoop.io.compress.DefaultCodec
set mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.GzipCodec
Map输出结果也以Gzip进行压缩:
## 启用map端输出压缩
set mapred.map.output.compress=true
## 默认值是org.apache.hadoop.io.compress.DefaultCodec
set mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.GzipCodec
对 Hive 输出结果和中间都进行压缩:
set hive.exec.compress.output=true ## 默认值是false,不压缩
set hive.exec.compress.intermediate=true ## 默认值是false,为true时MR设置的压缩才启用
列裁剪就是在查询时只读取需要的列,分区裁剪就是只读取需要的分区。当列很多或者数据量很大时,如果 select * 或者不指定分区,全列扫描和全表扫描效率都很低。
Hive 在读数据的时候,可以只读取查询中所需要用到的列,而忽略其他的列。这样做可以节省读取开销:中间表存储开销和数据整合开销。
set hive.optimize.cp = true; ## 列裁剪,取数只取查询中需要用到的列,默认是true
select a.*, b.* from a join b on a.id = b.id where b.age > 20;
select a.*, c.* from a join (select * from b where age > 20) c on a.id = c.id;
列裁剪就是在查询时只读取需要的列,分区裁剪就是只读取需要的分区。当列很多或者数据量很大时,如果 select * 或者不指定分区,全列扫描和全表扫描效率都很低。
set hive.optimize.pruner=true; ## 默认是true
如果一个mapreduce job碰到一对小文件作为输入,一个小文件启动一个Task
在执行 MapReduce 程序的时候,一般情况是一个文件的一个数据分块需要一个 mapTask 来处理。但是如果数据源是大量的小文件,这样就会启动大量的 mapTask 任务,这样会浪费大量资源。可以将输入的小文件进行合并,从而减少 mapTask 任务数量。
## Map端输入、合并文件之后按照block的大小分割(默认)
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;
## Map端输入,不合并
set hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat;
大量的小文件会给 HDFS 带来压力,影响处理效率。可以通过合并 Map 和 Reduce 的结果文件来消除影响。
## 是否合并Map输出文件, 默认值为true
set hive.merge.mapfiles=true;
## 是否合并Reduce端输出文件,默认值为false
set hive.merge.mapredfiles=true;
## 合并文件的大小,默认值为256000000
set hive.merge.size.per.task=256000000;
## 每个Map 最大分割大小
set mapred.max.split.size=256000000;
## 一个节点上split的最少值
set mapred.min.split.size.per.node=1; // 服务器节点
## 一个机架上split的最少值
set mapred.min.split.size.per.rack=1; // 服务器机架
第一:MapReduce中的MapTask的并行度机制
Map数过大:当输入文件特别大,MapTask 特别多,每个计算节点分配执行的 MapTask 都很多,这时候可以考虑减少 MapTask 的数量。增大每个 MapTask 处理的数据量。而且 MapTask 过多,最终生成的结果文件数也太多
- Map阶段输出文件太小,产生大量小文件
- 初始化和创建Map的开销很大
Map数太小:当输入文件都很大,任务逻辑复杂,MapTask 执行非常慢的时候,可以考虑增加MapTask 数,来使得每个 MapTask 处理的数据量减少,从而提高任务的执行效率。
- 文件处理或查询并发度小,Job执行时间过长
- 大量作业时,容易堵塞集群
一个MapReduce Job 的 MapTask 数量是由输入分片InputSplit 决定的。而输入分片是由 FileInputFormat.getSplit() 决定的。一个输入分片对应一个MapTask,而输入分片是由三个参数决定的:
参数 | 默认值 | 意义 |
dfs.blocksize | 128M | HDFS默认数据块大小 |
mapreduce.input.fileinputformat.split.minsize | 1 | 最小分片大小(MR) |
mapreduce.input.fileinputformat.split.maxsize | 256M | 最大分片大小(MR) |
输入分片大小的计算是这么计算出来的:
long splitSize = Math.max(minSize, Math.min(maxSize, blockSize))
默认情况下,输入分片大小和 HDFS 集群默认数据块大小一致,也就是默认一个数据块,启用一个MapTask 进行处理,这样做的好处是避免了服务器节点之间的数据传输,提高 job 处理效率。
两种经典的控制MapTask的个数方案:减少MapTask数 或者 增加MapTask数
1、减少 MapTask 数是通过合并小文件来实现,这一点主要是针对数据源
2、增加 MapTask 数可以通过控制上一个 job 的 reduceTask 个数
重点注意:不推荐把这个值进行随意设置!
推荐的方式:使用默认的切块大小即可。如果非要调整,最好是切块的N倍数
第二:合理控制 MapTask 数量
1、减少 MapTask 数可以通过合并小文件来实现
2、增加 MapTask 数可以通过控制上一个 ReduceTask 默认的 MapTask 个数
输入文件总大小:total_size
HDFS 设置的数据块大小:dfs_block_size
default_mapper_num = total_size / dfs_block_size
MapReduce 中提供了如下参数来控制 map 任务个数,从字面上看,貌似是可以直接设置 MapTask 个数的样子,但是很遗憾不行,这个参数设置只有在大于 default_mapper_num 的时候,才会生效。
set mapred.map.tasks=10; ## 默认值是2
那如果我们需要减少 MapTask 数量,但是文件大小是固定的,那该怎么办呢?可以通过 mapred.min.split.size 设置每个任务处理的文件的大小,这个大小只有在大于dfs_block_size 的时候才会生效。
总结一下控制 mapper 个数的方法:
- 如果想增加 MapTask 个数,可以设置 mapred.map.tasks 为一个较大的值
- 如果想减少 MapTask 个数,可以设置 maperd.min.split.size 为一个较大的值
- 如果输入是大量小文件,想减少 mapper 个数,可以通过设置 hive.input.format 合并小文件
如果 ReduceTask 数量过多,一个 ReduceTask 会产生一个结果文件,这样就会生成很多小文件,那么如果这些结果文件会作为下一个 Job 的输入,则会出现小文件需要进行合并的问题,而且启动和初始化ReduceTask 需要耗费资源。
如果 ReduceTask 数量过少,这样一个 ReduceTask 就需要处理大量的数据,并且还有可能会出现数据倾斜的问题,使得整个查询耗时长。
Hadoop MapReduce 程序中,ReducerTask 个数的设定极大影响执行效率,ReducerTask 数量与输出文件的数量相关。如果 ReducerTask 数太多,会产生大量小文件,对HDFS造成压力。如果ReducerTask 数太少,每个ReducerTask 要处理很多数据,容易拖慢运行时间或者造成 OOM。这使得Hive 怎样决定 ReducerTask 个数成为一个关键问题。遗憾的是 Hive 的估计机制很弱,不指定ReducerTask 个数的情况下,Hive 会猜测确定一个ReducerTask 个数,基于以下两个设定:
参数1:hive.exec.reducers.bytes.per.reducer | (默认256M) |
参数2:hive.exec.reducers.max | (默认为1009) |
参数3:mapreduce.job.reduces进行设置) | (默认值为-1,表示没有设置,那么就按照以上两个参数 |
ReduceTask 的计算公式为:
N = Math.min(参数2,总输入数据大小 / 参数1)
也可通过:
set mapred.map.tasks=10;
set mapreduce.job.reduces=10;
通常情况下,有必要手动指定 ReduceTask 个数。考虑到 Mapper 阶段的输出数据量通常会比输入有大幅减少,因此即使不设定 ReduceTask 个数,重设 参数2 还是必要的。
尽量减少每个阶段的数据量,对于分区表能用上分区字段的尽量使用,同时只选择后面需要使用到的列,最大限度的减少参与 Join 的数据量。
小表 join 大表的时应遵守小表 join 大表原则,原因是 join 操作的 reduce 阶段,位于 join 左边的表内容会被加载进内存,将条目少的表放在左边,可以有效减少发生内存溢出的几率。join 中执行顺序是从左到右生成 Job,应该保证连续查询中的表的大小从左到右是依次增加的。
在 hive 中,当对 3 个或更多张表进行 join 时,如果 on 条件使用相同字段,那么它们会合并为一个MapReduce Job,利用这种特性,可以将相同的 join on 放入一个 job 来节省执行时间
尽量避免一个SQL包含复杂的逻辑,可以使用中间表来完成复杂的逻辑。
- 空key过滤:有时join超时是因为某些key对应的数据太多,而相同key对应的数据都会发送到相同的reducer上,从而导致内存不够。此时我们应该仔细分析这些异常的key,很多情况下,这些key对应的数据是异常数据,我们需要在SQL语句中进行过滤。
- 空key转换:有时虽然某个key为空对应的数据很多,但是相应的数据不是异常数据,必须要包含在join的结果中,此时我们可以表a中key为空的字段赋一个随机的值,使得数据随机均匀地分不到不同的reducer上
## 是否根据输入小表的大小,自动将reduce端的common join 转化为map join,将小表刷入内存中。
## 对应逻辑优化器是MapJoinProcessor
set hive.auto.convert.join = true;
## 刷入内存表的大小(字节)
set hive.mapjoin.smalltable.filesize = 25000000;
## hive会基于表的size自动的将普通join转换成mapjoin
set hive.auto.convert.join.noconditionaltask=true;
## 多大的表可以自动触发放到内层LocalTask中,默认大小10M
set hive.auto.convert.join.noconditionaltask.size=10000000;
也可以手动开启mapjoin:
--SQL方式,在SQL语句中添加MapJoin标记(mapjoin hint)
--将小表放到内存中,省去shffle操作
// 在没有开启mapjoin的情况下,执行的是reduceJoin
SELECT /*+ MAPJOIN(smallTable) */ smallTable.key, bigTable.value FROM smallTable JOIN bigTable ON smallTable.key = bigTable.key;
/*+mapjoin(smalltable)*/
它是另一种Hive Join优化技术,使用这个技术的前提是所有的表都必须是分桶表(bucket)和分桶排序的(sort)。分桶表的优化,具体的实现:
- 针对参与join的这两张做相同的hash散列,每个桶里面的数据还要排序
- 这两张表的分桶个数要成倍数。
- 开启 SMB join 的开关
## 当用户执行bucket map join的时候,发现不能执行时,禁止查询
set hive.enforce.sortmergebucketmapjoin=false;
## 如果join的表通过sort merge join的条件,join是否会自动转换为sort merge join
set hive.auto.convert.sortmerge.join=true;
## 当两个分桶表 join 时,如果 join on的是分桶字段,小表的分桶数是大表的倍数时,可以启用mapjoin 来提高效率。
# bucket map join优化,默认值是 false
set hive.optimize.bucketmapjoin=false;
## bucket map join 优化,默认值是 false
set hive.optimize.bucketmapjoin.sortedmerge=false;
# join的键对应的记录条数超过这个值则会进行分拆,值根据具体数据量设置
set hive.skewjoin.key=100000;
# 如果是join过程出现倾斜应该设置为true
set hive.optimize.skewjoin=false;
如果开启了,在 Join 过程中 Hive 会将计数超过阈值 hive.skewjoin.key(默认100000)的倾斜 key 对应的行临时写进文件中,然后再启动另一个 job 做 map join 生成结果。
通过 hive.skewjoin.mapjoin.map.tasks 参数还可以控制第二个 job 的 mapper 数量,默认10000。
当 Hive 设定为严格模式(hive.mapred.mode=strict)时,不允许在 HQL 语句中出现笛卡尔积,这实际说明了 Hive 对笛卡尔积支持较弱。因为找不到 Join key,Hive 只能使用 1 个 reducer 来完成笛卡尔积。
当然也可以使用 limit 的办法来减少某个表参与 join 的数据量,但对于需要笛卡尔积语义的需求来说,经常是一个大表和一个小表的 Join 操作,结果仍然很大(以至于无法用单机处理),这时 MapJoin 才是最好的解决办法。MapJoin,顾名思义,会在 Map 端完成 Join 操作。这需要将 Join 操作的一个或多个表完全读入内存。
PS:MapJoin 在子查询中可能出现未知 BUG。在大表和小表做笛卡尔积时,规避笛卡尔积的方法是,给 Join 添加一个 Join key,原理很简单:将小表扩充一列 join key,并将小表的条目复制数倍,joinkey 各不相同;将大表扩充一列 join key 为随机数。精髓就在于复制几倍,最后就有几个 reduce 来做,而且大表的数据是前面小表扩张 key 值范围里面随机出来的,所以复制了几倍 n,就相当于这个随机范围就有多大 n,那么相应的,大表的数据就被随机的分为了 n 份。并且最后处理所用的 reduce 数量也是 n,而且也不会出现数据倾斜
默认情况下,Map 阶段同一个 Key 的数据会分发到一个 Reduce 上,当一个 Key 的数据过大时会产生数据倾斜。进行 group by 操作时可以从以下两个方面进行优化:
事实上并不是所有的聚合操作都需要在 Reduce 部分进行,很多聚合操作都可以先在 Map 端进行部分聚合,然后在 Reduce 端的得出最终结果。
## 开启Map端聚合参数设置
set hive.map.aggr=true;
# 设置map端预聚合的行数阈值,超过该值就会分拆job,默认值100000
set hive.groupby.mapaggr.checkinterval=100000
当 HQL 语句使用 group by 时数据出现倾斜时,如果该变量设置为 true,那么 Hive 会自动进行负载均衡。策略就是把 MapReduce 任务拆分成两个:第一个先做预汇总,第二个再做最终汇总。
# 自动优化,有数据倾斜的时候进行负载均衡(默认是false)
set hive.groupby.skewindata=true;
当选项设定为 true 时,生成的查询计划有两个 MapReduce 任务:
- 在第一个 MapReduce 任务中,map 的输出结果会随机分布到 reduce 中,每个 reduce 做部分聚合操作,并输出结果,这样处理的结果是相同的`group by key`有可能分发到不同的 reduce 中,从而达到负载均衡的目的;
- 第二个 MapReduce 任务再根据预处理的数据结果按照 group by key 分布到各个 reduce 中,最后完成最终的聚合操作
order by 只能是在一个 reduce 进程中进行,所以如果对一个大数据集进行 order by ,会导致一个reduce 进程中处理的数据相当大,造成查询执行缓慢。
- 在最终结果上进行order by,不要在中间的大数据集上进行排序。如果最终结果较少,可以在一个reduce上进行排序时,那么就在最后的结果集上进行order by。
- 如果是取排序后的前N条数据,可以使用distribute by和sort by在各个reduce上进行排序后前N条,然后再对各个reduce的结果集合合并后在一个reduce中全局排序,再取前N条,因为参与全局排序的order by的数据量最多是reduce个数 * N,所以执行效率会有很大提升。
在Hive中,关于数据排序,提供了四种语法,一定要区分这四种排序的使用方式和适用场景。
- order by:全局排序,缺陷是只能使用一个reduce
- sort by:单机排序,单个reduce结果有序
- cluster by:对同一字段分桶并排序,不能和sort by连用
- distribute by + sort by:分桶,保证同一字段值只存在一个结果文件当中,结合sort by保证每个reduceTask结果有序
Hive HQL 中的 order by 与其他 SQL 方言中的功能一样,就是将结果按某字段全局排序,这会导致所有 map 端数据都进入一个 reducer 中,在数据量大时可能会长时间计算不完。
如果使用 sort by,那么还是会视情况启动多个 reducer 进行排序,并且保证每个 reducer 内局部有序。为了控制map 端数据分配到 reducer 的 key,往往还要配合 distribute by 一同使用。如果不加distribute by 的话,map 端数据就会随机分配到 reducer。
提供一种方式实现全局排序:两种方式:
第一种方式:
##直接使用order by来做。如果结果数据量很大,这个任务的执行效率会非常低
select id,name,age from student order by age desc limit 3;
第二种方式:
## 使用distribute by + sort by 多个reduceTask,每个reduceTask分别有序
set mapreduce.job.reduces=3;
## 范围分桶 0 < 18 < 1 < 20 < 2
create table student_orderby_result as select * from student distribute by (case when age > 20 then 0 when age < 18 then 2 else 1 end) sort by (age desc);
当要统计某一列去重数时,如果数据量很大,count(distinct) 就会非常慢,原因与 order by 类似,count(distinct) 逻辑只会有很少的 reducer 来处理。这时可以用 group by 来改写:
## 先 group by 在 count
select count(1) from (
select age from student
where department >= "MA"
group by age
) t
Hive 在集群上查询时,默认是在集群上多台机器上运行,需要多个机器进行协调运行,这种方式很好的解决了大数据量的查询问题。但是在 Hive 查询处理的数据量比较小的时候,其实没有必要启动分布式模式去执行,因为以分布式方式执行设计到跨网络传输、多节点协调等,并且消耗资源。对于小数据集,可以通过本地模式,在单台机器上处理所有任务,执行时间明显被缩短。
## 打开hive自动判断是否启动本地模式的开关
set hive.exec.mode.local.auto=true;
## map任务数最大值,不启用本地模式的task最大个数
set hive.exec.mode.local.auto.input.files.max=4;
## map输入文件最大大小,不启动本地模式的最大输入文件大小
set hive.exec.mode.local.auto.inputbytes.max=134217728;
Hive 语句最终会转换为一系列的 MapReduce 任务,每一个MapReduce 任务是由一系列的 MapTask和 ReduceTask 组成的,默认情况下,MapReduce 中一个 MapTask 或者 ReduceTask 就会启动一个JVM 进程,一个 Task 执行完毕后,JVM 进程就会退出。这样如果任务花费时间很短,又要多次启动JVM 的情况下,JVM 的启动时间会变成一个比较大的消耗,这时,可以通过重用 JVM 来解决。
set mapred.job.reuse.jvm.num.tasks=5;
JVM也是有缺点的,开启JVM重用会一直占用使用到的 task 的插槽,以便进行重用,直到任务完成后才会释放。如果某个 不平衡的job 中有几个 reduce task 执行的时间要比其他的 reduce task 消耗的时间要多得多的话,那么保留的插槽就会一直空闲却无法被其他的 job 使用,直到所有的 task 都结束了才会释放。
根据经验,一般来说可以使用一个 cpu core 启动一个 JVM,假如服务器有 16 个 cpu core ,但是这个节点,可能会启动 32 个mapTask,完全可以考虑:启动一个JVM,执行两个Task。
有的查询语句,Hive 会将其转化为一个或多个阶段,包括:MapReduce 阶段、抽样阶段、合并阶段、limit 阶段等。默认情况下,一次只执行一个阶段。但是,如果某些阶段不是互相依赖,是可以并行执行的。多阶段并行是比较耗系统资源的。
一个 Hive SQL 语句可能会转为多个 MapReduce Job,每一个 job 就是一个 stage,这些 Job 顺序执行,这个在 cli 的运行日志中也可以看到。但是有时候这些任务之间并不是是相互依赖的,如果集群资源允许的话,可以让多个并不相互依赖 stage 并发执行,这样就节约了时间,提高了执行速度,但是如果集群资源匮乏时,启用并行化反倒是会导致各个 Job 相互抢占资源而导致整体执行性能的下降。启用并行化:
## 可以开启并发执行。
set hive.exec.parallel=true;
## 同一个sql允许最大并行度,默认为8。
set hive.exec.parallel.thread.number=16;
在分布式集群环境下,因为程序Bug(包括Hadoop本身的bug),负载不均衡或者资源分布不均等原因,会造成同一个作业的多个任务之间运行速度不一致,有些任务的运行速度可能明显慢于其他任务(比如一个作业的某个任务进度只有50%,而其他所有任务已经运行完毕),则这些任务会拖慢作业的整体执行进度。为了避免这种情况发生,Hadoop采用了推测执行(Speculative Execution)机制,它根据一定的法则推测出“拖后腿”的任务,并为这样的任务启动一个备份任务,让该任务与原始任务同时处理同一份数据,并最终选用最先成功运行完成任务的计算结果作为最终结果。
# 启动mapper阶段的推测执行机制
set mapreduce.map.speculative=true;
# 启动reducer阶段的推测执行机制
set mapreduce.reduce.speculative=true;
建议:
如果用户对于运行时的偏差非常敏感的话,那么可以将这些功能关闭掉。如果用户因为输入数据量很大而需要执行长时间的MapTask或者ReduceTask的话,那么启动推测执行造成的浪费是非常巨大大。