这里从四个方面对 Hive 常用的一些性能优化进行了总结。
分区表 是在某一个或者几个维度上对数据进行分类存储,一个分区对应一个目录。如 果筛选条件里有分区字段,那么 Hive 只需要遍历对应分区目录下的文件即可,不需要 遍历全局数据,使得处理的数据量大大减少,从而提高查询效率。
当一个 Hive 表的查询大多数情况下,会根据某一个字段进行筛选时,那么非常适 合创建为分区表。
指定桶的个数后,存储数据时,根据某一个字段进行哈希后,确定存储在哪个桶里, 这样做的目的和分区表类似,也是使得筛选时不用全局遍历所有的数据,只需要遍历 所在桶就可以了。
Apache Hive 支持 Apache Hadoop 中使用的几种熟悉的文件格式。
TextFile 默认格式,如果建表时不指定默认为此格式。
存储方式:行存储。
每一行都是一条记录,每行都以换行符 \n 结尾。数据不做压缩时,磁盘会开销比较 大,数据解析开销也比较大。
可结合 Gzip、Bzip2 等压缩方式一起使用(系统会自动检查,查询时会自动解压), 但对于某些压缩算法 hive 不会对数据进行切分,从而无法对数据进行并行操作。
SequenceFile
一种Hadoop API 提供的二进制文件,使用方便、可分割、个压缩的特点。
支持三种压缩选择:NONE、RECORD、BLOCK。RECORD压缩率低,一般建议使 用BLOCK压缩。
RCFile
存储方式:数据按行分块,每块按照列存储 。 首先,将数据按行分块,保证同一个record在一个块上,避免读一个记录需要读
取多个block。 其次,块数据列式存储,有利于数据压缩和快速的列存取。
ORC
存储方式:数据按行分块,每块按照列存储
Hive 提供的新格式,属于 RCFile 的升级版,性能有大幅度提升,而且数据可以压缩 存储,压缩快,快速列存取。
Parquet
存储方式:列式存储
Parquet 对于大型查询的类型是高效的。对于扫描特定表格中的特定列查询,Parquet
特别有用。Parquet一般使用 Snappy、Gzip 压缩。默认 Snappy。 Parquet 支持 Impala 查询引擎。
表的文件存储格式尽量采用 Parquet 或 ORC,不仅降低存储量,还优化了查询, 压缩,表关联等性能;
Hive 语句最终是转化为 MapReduce 程序来执行的,而 MapReduce 的性能瓶颈在与 网络IO 和 磁盘IO,要解决性能瓶颈,最主要的是 减少数据量,对数据进行压缩是个 好方式。压缩虽然是减少了数据量,但是压缩过程要消耗CPU,但是在Hadoop中, 往往性能瓶颈不在于CPU,CPU压力并不大,所以压缩充分利用了比较空闲的CPU。
常用压缩算法对比
如何选择压缩方式
1. 压缩比率
2. 压缩解压速度 3. 是否支持split
支持分割的文件可以并行的有多个 mapper 程序处理大数据文件,大多数文件不 支持可分割是因为这些文件只能从头开始读。
Hive 在读数据的时候,可以只读取查询中所需要用到的列,而忽略其他的列。这样做 可以节省读取开销,中间表存储开销和数据整合开销。
在查询的过程中只选择需要的分区,可以减少读入的分区数目,减少读入的数据量。
Map 输入合并
在执行 MapReduce 程序的时候,一般情况是一个文件需要一个 mapper 来处理。但 是如果数据源是大量的小文件,这样岂不是会启动大量的 mapper 任务,这样会浪费 大量资源。可以将输入的小文件进行合并,从而减少mapper任务数量。
1 |
set hive.optimize.cp = true; ‐‐ 列裁剪,取数只取查询中需要用到的列,默认 为真 |
1 |
set hive.optimize.pruner=true; // 默认为true |
1
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat ; ‐‐ Map端输入、合并文件之后按照block的大小分割(默认)
大量的小文件会给 HDFS 带来压力,影响处理效率。可以通过合并 Map 和 Reduce
的结果文件来消除影响。
1 2 3 |
set hive.merge.mapfiles=true; ‐‐ 是否合并Map输出文件, 默认值为真 |
合理控制 mapper 数量
减少 mapper 数可以通过合并小文件来实现 增加 mapper 数可以通过控制上一个 reduce
默认的 mapper 个数计算方式
MapReduce 中提供了如下参数来控制 map 任务个数:
从字面上看,貌似是可以直接设置 mapper 个数的样子,但是很遗憾不行,这个参数 设置只有在大于 default_mapper_num 的时候,才会生效。
那如果我们需要减少 mapper 数量,但是文件大小是固定的,那该怎么办呢? 可以通过 mapred.min.split.size 设置每个任务处理的文件的大小,这个大小只有
1 2 3 |
输入文件总大小:total_size |
1 |
set mapred.map.tasks=10; |
2
set hive.input.format=org.apache.hadoop.hive.ql.io.HiveInputFormat; ‐ ‐ Map端输入,不合并
在大于 dfs_block_size 的时候才会生效
1 2 3 |
split_size=max(mapred.min.split.size, dfs_block_size) split_num=total_size/split_size compute_map_num = min(split_num, max(default_mapper_num, mapred.map.tasks) |
这样就可以减少mapper数量了。 总结一下控制 mapper 个数的方法:
如果想增加 mapper 个数,可以设置 mapred.map.tasks 为一个较大的值 如果想减少 mapper 个数,可以设置 maperd.min.split.size 为一个较大的值
如果输入是大量小文件,想减少 mapper 个数,可以通过设 置 hive.input.format 合并小文件
如果想要调整 mapper 个数,在调整之前,需要确定处理的文件大概大小以及文 件的存在形式(是大量小文件,还是单个大文件),然后再设置合适的参数。
如果 reducer 数量过多,一个 reducer 会产生一个结数量果文件,这样就会生成很多 小文件,那么如果这些结果文件会作为下一个 job 的输入,则会出现小文件需要进行 合并的问题,而且启动和初始化 reducer 需要耗费和资源。
如果 reducer 数量过少,这样一个 reducer 就需要处理大量的数据,并且还有可能会 出现数据倾斜的问题,使得整个查询耗时长。 默认情况下,hive 分配的 reducer 个数 由下列参数决定:
参数1: hive.exec.reducers.bytes.per.reducer (默认1G) 参数2: hive.exec.reducers.max (默认为999)
reducer的计算公式为:
可以通过改变上述两个参数的值来控制reducer的数量。 也可以通过
1 |
N = min(参数2, 总输入数据量/参数1) |
直接控制reducer个数,如果设置了该参数,上面两个参数就会忽略。
优先过滤数据 尽量减少每个阶段的数据量,对于分区表能用上分区字段的尽量使用,同时只选择后
面需要使用到的列,最大限度的减少参与 join 的数据量。
小表 join 大表的时应遵守小表 join 大表原则,原因是 join 操作的 reduce 阶段,位于 join 左边的表内容会被加载进内存,将条目少的表放在左边,可以有效减少发生内存 溢出的几率。join 中执行顺序是从左到右生成 Job,应该保证连续查询中的表的大小 从左到右是依次增加的。
在 hive 中,当对 3 个或更多张表进行 join 时,如果 on 条件使用相同字段,那么它们 会合并为一个 MapReduce Job,利用这种特性,可以将相同的 join on 的放入一个 job 来节省执行时间。
mapjoin 是将 join 双方比较小的表直接分发到各个 map 进程的内存中,在 map 进程 中进行 join 操作,这样就不用进行 reduce 步骤,从而提高了速度。只有 join 操作才 能启用 mapjoin。
1 2 3 |
set hive.auto.convert.join = true; ‐‐ 是否根据输入小表的大小,自动将 reduce端的common join 转化为map join,将小表刷入内存中。 set hive.mapjoin.maxsize=1000000; ‐‐ Map Join所处理的最大的行数。超过 此行数,Map Join进程会异常退出 |
1
set mapred.map.tasks=10;
尽量避免一个SQL包含复杂的逻辑,可以使用中间表来完成复杂的逻辑。
当两个分桶表 join 时,如果 join on的是分桶字段,小表的分桶数是大表的倍数时,可
以启用 mapjoin 来提高效率。
默认情况下,Map阶段同一个Key的数据会分发到一个Reduce上,当一个Key的数据
过大时会产生 数据倾斜。进行 group by 操作时可以从以下两个方面进行优化:
事实上并不是所有的聚合操作都需要在 Reduce 部分进行,很多聚合操作都可以先在 Map 端进行部分聚合,然后在 Reduce 端的得出最终结果。
1 |
set hive.optimize.bucketmapjoin = true; ‐‐ 启用桶表 map join |
1 2 3 |
set hive.map.aggr=true; ‐‐ 开启Map端聚合参数设置 set hive.grouby.mapaggr.checkinterval=100000; ‐‐ 在Map端进行聚合操作的 条目数目 |
当选项设定为 true 时,生成的查询计划有两个 MapReduce 任务。在第一个 MapReduce 任务中,map 的输出结果会随机分布到 reduce 中,每个 reduce 做部分 聚合操作,并输出结果,这样处理的结果是相同的 group by key 有可能分发到不同 的 reduce 中,从而达到负载均衡的目的;第二个 MapReduce 任务再根据预处理的数 据结果按照 group by key 分布到各个 reduce 中,最后完成最终的聚合操作。
1 |
set hive.groupby.skewindata = true; ‐‐ 有数据倾斜的时候进行负载均衡(默 认是false) |
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 ,所以执行效率很高。
1 2 3 4 5 |
‐‐ 优化前(只有一个reduce,先去重再count负担比较大): select count(distinct id) from tablename; ‐‐ 优化后(启动两个job,一个job负责子查询(可以有多个reduce),另一个job负责 count(1)): |
有些场景是从一张表读取数据后,要多次利用,这时可以使用 multi insert 语法:
1 2 3 4 5 |
from sale_detail insert overwrite table sale_detail_multi partition (sale_date='2010', region='china' ) select shop_name, customer_id, total_price where ..... insert overwrite table sale_detail_multi partition (sale_date='2011', region='china' ) select shop_name, customer_id, total_price where .....; |
说明: 一般情况下,单个SQL中最多可以写128路输出,超过128路,则报语法错
误。
在一个multi insert中: 对于分区表,同一个目标分区不允许出现多次。
对于未分区表,该表不能出现多次。 对于同一张分区表的不同分区,不能同时有 insert overwrite 和 insert
into 操作,否则报错返回。
1 2 |
set mapreduce.map.output.compress=true; set mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.Sna ppyCodec; |
中间数据压缩就是对 hive 查询的多个 job 之间的数据进行压缩。最好是选择一个节省 CPU耗时的压缩方式。可以采用 snappy 压缩算法,该算法的压缩和解压效率都非常 高。
1 2 3 |
set hive.exec.compress.intermediate=true; set hive.intermediate.compression.codec=org.apache.hadoop.io.compress.Sna ppyCodec; set hive.intermediate.compression.type=BLOCK; |
最终的结果数据(Reducer输出数据)也是可以进行压缩的,可以选择一个压缩效果 比较好的,可以减少数据的大小和数据的磁盘读写时间; 注:常用的gzip,snappy压 缩算法是不支持并行处理的,如果数据源是gzip/snappy压缩文件大文件,这样只会有 有个mapper来处理这个文件,会严重影响查询效率。 所以如果结果数据需要作为其 他查询任务的数据源,可以选择支持splitable的 LZO 算法,这样既能对结果文件进行
压缩,还可以并行的处理,这样就可以大大的提高job执行的速度了。
1 2 3 4 |
set hive.exec.compress.output=true; set mapreduce.output.fileoutputformat.compress=true; set mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io .compress.GzipCodec; set mapreduce.output.fileoutputformat.compress.type=BLOCK; |
Hadoop集群支持以下算法:
org.apache.hadoop.io.compress.DefaultCodec org.apache.hadoop.io.compress.GzipCodec org.apache.hadoop.io.compress.BZip2Codec org.apache.hadoop.io.compress.DeflateCodec org.apache.hadoop.io.compress.SnappyCodec org.apache.hadoop.io.compress.Lz4Codec com.hadoop.compression.lzo.LzoCodec com.hadoop.compression.lzo.LzopCodec
Hive 从 HDFS 中读取数据,有两种方式:启用 MapReduce 读取、直接抓取。 直接抓取数据比 MapReduce 方式读取数据要快的多,但是只有少数操作可以使用直
接抓取方式。
可以通过 hive.fetch.task.conversion 参数来配置在什么情况下采用直接抓取方 式:
minimal:只有 select * 、在分区字段上 where 过滤、有 limit 这三种 场景下才启用直接抓取方式。
more:在 select 、 where 筛选、 limit 时,都启用直接抓取方式。
Hive 在集群上查询时,默认是在集群上多台机器上运行,需要多个机器进行协调运 行,这种方式很好的解决了大数据量的查询问题。但是在Hive查询处理的数据量比较 小的时候,其实没有必要启动分布式模式去执行,因为以分布式方式执行设计到跨网 络传输、多节点协调等,并且消耗资源。对于小数据集,可以通过本地模式,在单台 机器上处理所有任务,执行时间明显被缩短。
1 2 3 |
set hive.exec.mode.local.auto=true; ‐‐ 打开hive自动判断是否启动本地模式 的开关 |
Hive 语句最终会转换为一系列的 MapReduce 任务,每一个MapReduce 任务是由一 系列的Map Task 和 Reduce Task 组成的,默认情况下,MapReduce 中一个 Map Task 或者 Reduce Task 就会启动一个 JVM 进程,一个 Task 执行完毕后,JVM进程 就会退出。这样如果任务花费时间很短,又要多次启动 JVM 的情况下,JVM的启动时 间会变成一个比较大的消耗,这时,可以通过重用 JVM 来解决。
JVM也是有缺点的,开启JVM重用会一直占用使用到的 task 的插槽,以便进行重 用,直到任务完成后才会释放。如果某个 不平衡的job 中有几个 reduce task 执 行的时间要比其他的 reduce task 消耗的时间要多得多的话,那么保留的插槽就会 一直空闲却无法被其他的 job 使用,直到所有的 task 都结束了才会释放。
有的查询语句,hive会将其转化为一个或多个阶段,包括:MapReduce 阶段、抽样阶 段、合并阶段、limit 阶段等。默认情况下,一次只执行一个阶段。但是,如果某些阶
1 |
set mapred.job.reuse.jvm.num.tasks=5; |
1
set hive.fetch.task.conversion=more; ‐‐ 启用fetch more模式
段不是互相依赖,是可以并行执行的。多阶段并行是比较耗系统资源的。
1 2 |
set hive.exec.parallel=true; ‐‐ 可以开启并发执行。 |
在分布式集群环境下,因为程序Bug(包括Hadoop本身的bug),负载不均衡或者资 源分布不均等原因,会造成同一个作业的多个任务之间运行速度不一致,有些任务的 运行速度可能明显慢于其他任务(比如一个作业的某个任务进度只有50%,而其他所 有任务已经运行完毕),则这些任务会拖慢作业的整体执行进度。为了避免这种情况 发生,Hadoop采用了推测执行(Speculative Execution)机制,它根据一定的法则推 测出“拖后腿”的任务,并为这样的任务启动一个备份任务,让该任务与原始任务同时 处理同一份数据,并最终选用最先成功运行完成任务的计算结果作为最终结果。
建议:
如果用户对于运行时的偏差非常敏感的话,那么可以将这些功能关闭掉。如果用 户因为输入数据量很大而需要执行长时间的map或者Reduce task的话,那么启动 推测执行造成的浪费是非常巨大大。
场景:如日志中,常会有信息丢失的问题,比如日志中的 user_id,如果取其中的 user_id 和 用户表中的user_id 关联,会碰到数据倾斜的问题。
解决方法1: user_id为空的不参与关联
1 2 3 |
set mapreduce.map.speculative=true; set mapreduce.reduce.speculative=true; 复制代码 |
结论:方法2比方法1效率更好,不但io少了,而且作业数也少了。解决方法1中 log读取两次,jobs是2。解决方法2 job数是1 。这个优化适合无效 id (比如 99 , ’’, null 等) 产生的倾斜问题。把空值的 key 变成一个字符串加上随机数,就能把倾斜 的数据分到不同的reduce上 ,解决数据倾斜问题。
场景:用户表中user_id字段为int,log表中user_id字段既有string类型也有int类型。当 按照user_id进行两个表的Join操作时,默认的Hash操作会按int型的id来进行分配,这 样会导致所有string类型id的记录都分配到一个Reducer中。
解决方法:把数字类型转换成字符串类型
使用 map join 解决小表(记录数少)关联大表的数据倾斜问题,这个方法使用的频率非 常高,但如果小表很大,大到map join会出现bug或异常,这时就需要特别的处理。 以下例子:
users 表有 600w+ 的记录,把 users 分发到所有的 map 上也是个不小的开销,而且 map join 不支持这么大的小表。如果用普通的 join,又会碰到数据倾斜的问题。
1 |
select * from log a left outer join users b on case when a.user_id is null then concat(‘hive’,rand() ) else a.user_id end = b.user_id; |
1 |
select * from users a left outer join logs b on a.usr_id = cast(b.user_id as string) |
1 |
select * from log a left outer join users b on a.user_id = b.user_id; |
1
select * from log a join users b on a.user_id is not null and a.user_id = b.user_idunion allselect * from log a where a.user_id is null;
解决方法:
1 |
select /*+mapjoin(x)*/* from log a left outer join ( select /*+mapjoin(c)*/d.* from ( select distinct user_id from log ) c join users d on c.user_id = d.user_id ) x on a.user_id = b.user_id; |
假如,log里user_id有上百万个,这就又回到原来map join问题。所幸,每日的会员uv 不会太多,有交易的会员不会太多,有点击的会员不会太多,有佣金的会员不会太多 等等。所以这个方法能解决很多场景下的数据倾斜问题。