一、问题分析
现象:
[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数来配合动态分区使用
set mapred.reduce.tasks=10;
这几个参数需要满足一下条件:
dynamic.partitions / dynamic.partitions.pernode <=mapred.reduce.tasks
比如上面的例子:2048 / 256 = 8,如果mapred.reduce.tasks小于8就会报错,而hive默认reduce数是跟具数据量来动态调整的,所以有时候需要手动调整。
三、问题拓展
3.1 动态分区小文件优化
虽然上述方案能解决报错问题,但是同样引出新的问题,动态分区会产生大量小文件的问题。
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
3.2 优化分区倾斜问题
经过上述改造后运行良好,没有再出现任何问题,但同样又引来了一个新的问题,因为这100个分区的数据是分布不均匀的,有的redcue数据很多有几百个G,有的只有几兆,这样导致reduce会卡在99%,HQL运行很慢。
因此我们采用随机数,随机分配给reduce来解决该问题,这样可以使每个Reduce处理的数据大体一致。
方法一:通过设定每个Reduce处理的数据量来控制最终生成的文件数
假定一个redcue我们设定10G数据量,则对于1T的数据总共会起102个左右的Reduces,修改的SQL如下:
--设定reduce处理数据量10G
set hive.exec.reducers.bytes.per.reducer=10240000000;
insert overwrite table test partition(dt)
select *
from table
distribute by rand();
方法二【推荐】:通过控制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底层会限制动态分区的数量呢?
因为动态分区会在短时间内创建大量的分区,可能会占用大量的资源,以下是各个方面的分析:
问题方面 | 原因分析 | 解决方案 |
内存方面 | 在Insert场景下,每个动态目录分区写入器(File Writer)至少会打开一个文件,特别是对于parquert或者orc格式的文件,在写入的时候会首先写到缓冲区中,而这些缓冲区是按照分区来维护的,在运行的时候需要的内存量会随着分区数增加而增加。所以经常导致OOM的mapper或者reduce,可能是由于打开的文件写入器的数量。如常见的错误:Error: GC overhead limit exceeded | 针对该问题,对应的解决方案如下: ●可开启hive.optimize.sort.dynamic.partition参数 ●增加mapper端的内存,设置mapreduce.map.memory.mb和mapreduce.map.java.opts |
文件句柄 | 如果分区数过多,那么每个分区都会打开对应的文件句柄写入数据,可能会导致系统文件句柄占用过多,影响系统其他应用运行。 | 因此hive又提出了一个hive.exec.max.created.files参数来控制创建文件数量(默认是100000) |
五、总结
5.1 用到调优参数
参数 | 默认值 | 说明 |
hive.exec.dynamic.partition | false | 是否开启动态分区功能,默认false关闭。 使用动态分区时候,该参数必须设置成true; |
hive.exec.dynamic.partition.mode | strict | 动态分区的模式,默认strict,表示必须指定至少一个分区为静态分区; 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的map数量。比如我设置为1073741824,就是为了让每个map处理1GB的文件。 | |
mapreduce.input.fileinputformat.split.minsize | ||
hive.optimize.sort.dynamic.partition | false | 启用该参数并将其设置为true,通过这个优化,这个只有map任务的mapreduce会引入reduce过程,这样动态分区的那个字段比如日期在传到reducer时会被排序。由于分区字段是排序的,因此每个reducer只需要保持一个文件写入器(file writer)随时处于打开状态,在收到来自特定分区的所有行后,关闭记录写入器(record writer),从而减小内存压力。 这种优化方式在写parquet文件时使用的内存要相对少一些,但代价是要对分区字段进行排序。 |
5.2 本文小结
本文分析了一种由动态分区产生小文件的或是集群中小文件过多的一种解决方案,采用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。