目录
0 问题现象及原因分析
1 问题解决
解决办法
2 由以上问题引出的问题
3 思考
4 小结
现象:
[Error 20004]: Fatal error occurred when node tried to create
too many dynamic partitions. The maximum number of dynamic
partitions is controlled by hive.exec.max.dynamic.partitions and
hive.exec.max.dynamic.partitions.pernode. Maximum was setto: 100
原因:
Hive对其创建的动态分区数量实施限制。默认值为每个节点100个动态分区,所有节点的总(默认)限制为1000个动态分区。但是,这可以调整。
set hive.exec.dynamic.partition=true;
set hive.exec.max.dynamic.partitions=2048;
set hive.exec.max.dynamic.partitions.pernode=256;
用以上设置后不能保证正常,有时候还需要设置reduce数来配合动态分区使用
|
这几个参数需要满足一下条件:
|
比如上面的例子:2048 / 256 = 8,如果mapred.reduce.tasks小于8就会报错,而hive默认reduce数是跟具数据量来动态调整的,所以有时候需要手动调整。
虽然上述方案能解决报错问题,但是同样引出新的问题,动态分区会产生大量小文件的问题,我们知道hive默认创建文件的上限是100000个(hive.exec.max.created.files),假如我输入的数据量比较大,比如1T数据,此时启动2000个Mapper任务去读取,那么我开启动态分区后,每个任务下有100个分区,将产生2000*100=200000,这样也会超出hive文件创建文件的上限,产生过多的小文件。如下SQL
insert overwrite table test partition(dt)
select *
from table
那么由动态分区差生的小文件应该如何避免和优化呢?
Hive中distribute by就是用来解决数据分发问题的,他按照后面指定的key将数据分发到哪些reduce中去【HASH的方式,类似于spark中的repartition】,我们利用distribute by dt 可以将同一分区的数据直接发到同一reduce中,这样就产生了100个文件,由原来的200000个文件现在降到为100个,解决了文件数过多的问题。SQL如下:
insert overwrite table test partition(dt)
select *
from table
distribute by dt
经过上述改造后运行良好,没有再出现任何问题,但同样又引来了一个新的问题,因为这100个分区的数据是分布不均匀的,有的redcue数据很多有几百个G,有的只有几兆,这样导致reduce会卡在99%,HQL运行很慢。因此我们采用随机数,随机分配给reduce来解决该问题,这样可以使每个Reduce处理的数据大体一致。
(1)通过设定每个Reduce处理的数据量来控制最终生成的文件数。
假定一个redcue我们设定10G数据量,则对于1T的数据总共会起102个左右的Reduces,修改的SQL如下:
set hive.exec.reducers.bytes.per.reducer=10240000000;
insert overwrite table test partition(dt)
select *
from table
distribute by rand()
(2)通过控制rand()函数来控制最终生成多少个文件【推荐】
如果想要具体最后落地生成多少个文件数,使用 distribute by cast( rand() * N as int) 这里的N是指具体最后落地生成多少个文件数,如每个分区目录下生成100个 文件大小基本一致的文件SQL如下:
insert overwrite table test partition(dt)
select *
from table
distribute by cast(rand()*100 as int);
注意这里的技巧:通过 distribute by cast( rand() * N as int)来控制落地文件数,或随机数的具体范围。
为什么Hive底层会限制动态分区的数量呢?
动态分区因为会在短时间内创建大量的分区,可能会占用大量的资源
1 内存方面:在Insert场景下,每个动态目录分区写入器(File Writer)至少会打开一个文件,特别是对于parquert或者orc格式的文件,在写入的时候会首先写到缓冲区中,而这些缓冲区是按照分区来维护的,在运行的时候需要的内存量会随着分区数增加而增加。所以经常导致OOM的mapper或者reduce,可能是由于打开的文件写入器的数量。如常见的错误:Error: GC overhead limit exceeded,针对该问题,对应的解决方案如下:
2 文件句柄:如果分区数过多,那么每个分区都会打开对应的文件句柄写入数据,可能会导致系统文件句柄占用过多,影响系统其他应用运行。因此hive又提出了一个hive.exec.max.created.files参数来控制创建文件数量(默认是100000)
生成动态分区的几个参数说明
hive.exec.dynamic.partition
默认值:false
是否开启动态分区功能,默认false关闭。
使用动态分区时候,该参数必须设置成true;
hive.exec.dynamic.partition.mode
默认值:strict
动态分区的模式,默认strict,表示必须指定至少一个分区为静态分区,nonstrict模式表示允许所有的分区字段都可以使用动态分区。一般需要设置为nonstrict
hive.exec.max.dynamic.partitions.pernode
默认值:100
在每个执行MR的节点上,最大可以创建多少个动态分区。该参数需要根据实际的数据来设定。
比如:源数据中包含了一年的数据,即day字段有365个值,那么该参数就需要设置成大于365,如果使用默认值100,则会报错。
hive.exec.max.dynamic.partitions
默认值:1000
在所有执行MR的节点上,最大一共可以创建多少个动态分区。
同上参数解释。
hive.exec.max.created.files
默认值:100000
整个MR Job中,最大可以创建多少个HDFS文件。
一般默认值足够了,除非你的数据量非常大,需要创建的文件数大于100000,可根据实际情况加以调整。
mapreduce.map.memory.mb
map任务的物理内存分配值,常见设置为1GB,2GB,4GB等。
mapreduce.map.java.opts
map任务的Java堆栈大小设置,一般设置为小于等于上面那个值的75%,这样可以保证map任务有足够的堆栈外内存空间。
mapreduce.input.fileinputformat.split.maxsize
mapreduce.input.fileinputformat.split.minsize
这个两个参数联合起来用,主要是为了方便控制mapreduce的map数量。比如我设置为1073741824,就是为了让每个map处理1GB的文件。
hive.optimize.sort.dynamic.partition
默认值:false
启用hive.optimize.sort.dynamic.partition,将其设置为true。通过这个优化,这个只有map任务的mapreduce会引入reduce过程,这样动态分区的那个字段比如日期在传到reducer时会被排序。由于分区字段是排序的,因此每个reducer只需要保持一个文件写入器(file writer)随时处于打开状态,在收到来自特定分区的所有行后,关闭记录写入器(record writer),从而减小内存压力。这种优化方式在写parquet文件时使用的内存要相对少一些,但代价是要对分区字段进行排序。
第二种方式就是增加每个mapper的内存分配,即增大mapreduce.map.memory.mb和mapreduce.map.java.opts,这样所有文件写入器(filewriter)缓冲区对应的内存会更充沛。
将查询分解为几个较小的查询,以减少每个查询创建的分区数量。这样可以让每个mapper打开较少的文件写入器(file writer)。
本文分析了一种由动态分区产生小文件的或是集群中小文件过多的一种解决方案,采用distribute by cast(rand()*N as int)这一方式能很好的解决集群小文件问题,起到了优化作用。对于使用SparkSQL的用户来说,SparkSQL提供了repartition算子来解决这一问题,在这里其实repartition和distribute by的作用一致,在Spark 2.4.0版中提供了Hive中类似的Hint语法,可以通过如下方式解决
--提示名称不区分大小写
INSERT ... SELECT /*+REPARTITION(n)*/ ...
Repartition Hint可以增加或减少分区数量,它执行数据的完全shuffle,并确保数据平均分配。
repartition增加了一个新的stage,因此它不会影响现有阶段的并行性。
repartition,常用的情况是:上游数据分区数据分布不均匀,才会对RDD/DataFrame等数据集进行重分区,将数据重新分配均匀,
假设原来有N个分区,现在repartition(M)的参数传为M,
当 N < M ,则会根据HashPartitioner (key的hashCode % M)进行数据的重新划分
而 N 远大于 M ,那么还是建议走repartition,这样所有的executor都会运作起来,效率更高,如果还是走coalesce,假定参数是1,那么即使原本申请了10个executor,那么最后执行的也只会有1个executor。
参考链接:Hive/Spark小文件解决方案(企业级实战)