我们在使用 Hive 进行 ETL 开发的过程中,关注更多的是使用 HQL 语言来准确地表达业务逻辑,而很少考虑到 Hive 对 HQL 语句的执行情况。当你辛辛苦苦地码完,将 HQL 语句扔给 Hive 去执行时,就有可能出现各种各样的报错,而其中一种比较常见的错误就是内存溢出(OOM,out of memory),通俗地讲就是内存不够。

0x00 写在前面

本文采用的软件版本如下:

  • hive-2.0.1
  • hadoop-2.7.2

Hive 使用 MapReduce 执行引擎,Hadoop 使用 Yarn 进行资源调度。

接下来将从客户端提交 HQL 语句开始,Hive 生成物理执行计划、Yarn 资源分配、MapReduce 执行,到执行结束,三个重点过程进行阐述,先理论再参数,希望 OOM 参数调优的问题得到收敛。

0x01 Hive 生成物理执行计划

先给出官网上关于 Hive 架构的经典流程图:

Hive架构

从这张图中,我们只需要明白一点即可:客户端提交的HQL语句,在Hive端的最终输出是物理执行计划,或者说是Job的有向无环图(a DAG of stages)。主要包括三种操作:

  • MapReduce作业(a map/reduce job)
  • 元数据操作(a metadata operation)
  • HDFS操作(an operation on HDFS)

需要说明的是:这个过程主要是Hive优化器的执行,没有相关参数去控制内存的使用。当然,对于某些HQL语句适当地设置一些参数,可以得到更优的物理执行计划。比如常见的 Map Join 参数 hive.auto.convert.join 等。

0x02 Yarn 资源分配

YARN 是对 Mapreduce V1 重构得到的,有时候也成为 MapReduce V2。由Hive生成物理执行计划,其中的MapReduce作业提交给Yarn来执行,详细的执行过程如下:

MapReduce在Yarn下执行过程

从上图可以看出,Yarn(以Container方式分配)控制着 NodeManager、ApplicationMaster、Map和Reduce 的内存使用,相关的内存参数有:

重要级别 参数名 默认值
yarn.nodemanager.resource.memory-mb 8192
yarn.scheduler.minimum-allocation-mb 1024
yarn.scheduler.maximum-allocation-mb 8192
yarn.scheduler.increment-allocation-mb 1024
yarn.app.mapreduce.am.resource.mb 1536
yarn.app.mapreduce.am.command-opts -Xmx1024m
yarn.app.mapreduce.am.admin-command-opts
yarn.nodemanager.vmem-pmem-ratio 2.1
yarn.nodemanager.pmem-check-enabled true
yarn.nodemanager.vmem-check-enabled true
mapreduce.reduce.memory.mb 1024
mapreduce.reduce.java.opts
mapreduce.map.memory.mb 1024
mapreduce.map.java.opts

(1) 基础

  • NodeManager可用于分配的最大内存是yarn.nodemanager.resource.memory-mb;
  • Yarn的ResourceManger(简称RM)通过逻辑上的队列分配内存等资源给application,默认情况下RM允许最大AM申请Container资源为8192MB(“yarn.scheduler.maximum-allocation-mb“),默认情况下的最小分配资源为1024M(“yarn.scheduler.minimum-allocation-mb“),如果参数中需要的资源在此范围之外,在任务submit的时候会被直接拒绝掉;
  • AM只能以增量 (“yarn.scheduler.minimum-allocation-mb”) + (“yarn.scheduler.increment-allocation-mb”) 规整每个task需要的内存,并且申请的内存只能在(”yarn.scheduler.minimum-allocation-mb“)和(“yarn.scheduler.maximum-allocation-mb“) 的范围内向RM申请资源;
  • 每个Map任务或Reduce任务分配的内存为mapreduce.reduce.memory.mb或mapreduce.map.memory.mb;

(2) mapreduce.map.java.opts 和 mapreduce.map.memory.mb 区别

JVM进程跑在container中,mapreduce.map.java.opts 能够通过Xmx设置JVM最大的heap的使用,一般设置为0.75倍的 mapreduce.map.memory.mb ,因为需要为java code,非JVM内存使用等预留些空间;mapreduce.reduce.java.opts 和 mapreduce.reduce.memory.mb 同理。

(3) 虚拟内存

默认的(“yarn.nodemanager.vmem-pmem-ratio“)设置为2.1,意味则 map container 或者 reduce container 分配的虚拟内存超过2.1倍的(“mapreduce.reduce.memory.mb“)或(“mapreduce.map.memory.mb“)就会被NM给KILL掉,如果 (“mapreduce.map.memory.mb”) 被设置为1536M那么总的虚拟内存为2.1*1536=3225.6MB

(4) 内存检查

如果虚拟内存检查被打开(yarn.nodemanager.vmem-check-enabled 默认情况下为true),然后YARN将把抽取出来的容器及其子进程的VSIZE加起来和容器最大允许使用的虚拟内存进行比较。最大允许使用的虚拟内存是容器最大可使用的物理内存乘以 yarn.nodemanager.vmem-pmem-ratio(默认值是2.1)。所以,如果你的YARN容器配置的最大可使用物理内存为2GB,然后我们乘以 2.1 得到的就是容器最大可用的虚拟内存 4.2G 。

如果物理内存检查被打开(yarn.nodemanager.pmem-check-enabled 默认情况为true),然后YARN将把抽取出来的容器及其子进程的RSS加起来和容器最大允许使用的物理内存进行比较。

如果物理内存或者虚拟内存其中一个的使用大于最大允许使用的,YARN将会被这个容器杀掉。

(5) 参数全局图

参数多不要慌,下面来张图梳理下:

Yarn内存参数

0x03 MapReduce执行

MapReduce作业的重点是Shuffle过程,还是老套路,先给出官网上关于这个过程的经典流程图:

Shuffle过程

当Map任务或Reduce任务以Container方式申请到相应的内存资源后,就进入了实际的执行过程中,其中涉及的参数有:

重要级别 参数名 默认值
mapreduce.job.maps 2
mapreduce.input.fileinputformat.split.minsize 1
dfs.blocksize 134217728
mapreduce.job.reduces 1
mapreduce.task.io.sort.mb 100
mapreduce.map.sort.spill.percent 0.80
mapreduce.task.io.sort.factor 10
mapreduce.map.output.compress false
mapreduce.map.output.compress.codec org.apache.hadoop.io.compress.DefaultCodec
mapreduce.job.reduce.slowstart.completedmaps 0.05
mapreduce.reduce.shuffle.parallelcopies 5
mapreduce.reduce.shuffle.input.buffer.percent 0.70

为了更好地理解每个参数作用的阶段,建议先阅读 MapReduce之Shuffle过程详解

(1) Map任务

(1)split分片:split是在逻辑上对输入数据进行的分片,并不会在磁盘上将其切分成分片进行存储。每个split都作为一个独立单位分配给一个map task去处理。决定split分片大小的参数有:

  • mapreduce.job.maps
  • mapreduce.input.fileinputformat.split.minsize
  • dfs.blocksize (会话级别不可设置)

(2)内存缓冲区:经过map处理后的键值对,不会立马写入磁盘,而是暂时保存在内存中的MapOutputBuffe内部的环形数据缓冲区,设置缓冲区大小的参数有:

  • mapreduce.task.io.sort.mb
  • mapreduce.map.sort.spill.percent

(3)压缩:map端在写磁盘的时候采用压缩的方式将map的输出结果进行压缩是一个减少网络开销很有效的方法。其实,在Hadoop中早已为我们提供了一些压缩算法的实现,直接配置参数即可。

  • mapreduce.map.output.compress
  • mapreduce.map.output.compress.codec

(2) Reduce任务

(1)文件拷贝:默认情况下,当整个MapReduce作业的所有已执行完成的Map Task任务数超过Map Task总数的 mapreduce.job.reduce.slowstart.completedmaps (默认为0.05) 后,ApplicationMaster便会开始调度执行Reduce Task任务。然后Reduce Task任务默认启动 mapred.reduce.parallel.copies (默认为5) 个MapOutputCopier线程到已完成的Map Task任务节点上分别copy一份属于自己的数据。 这些copy的数据会首先保存的内存缓冲区中,当内冲缓冲区的使用率达到一定阀值后,则写到磁盘上。

(2)内存缓冲区:这个内存缓冲区大小的控制就不像map那样可以通过 mapreduce.task.io.sort.mb 来设定了,而是通过另外一个参数来设置:mapred.job.shuffle.input.buffer.percent(default 0.7), 这个参数其实是一个百分比,意思是说,shuffile在reduce内存中的数据最多使用内存量为:0.7 × maxHeap of reduce task。

0x04 HQL语句的日志输出

经过漫长的理论铺垫,终于要到解决问题的时候了,HQL语句的内存溢出主要从日志分析开始。

  • HQL语句的执行过程中,有哪些日志输出呢?分别存放在什么地方?如何分析出有用信息?
  • 内存溢出包括哪几类?典型日志有哪些?调优什么参数可以解决?
  • 小文件太多是如何产生的?调优什么参数可以合并小文件?

等等一系列有关问题,我们下一次接着说。

参考文献

Hive Architecture Overview
Yarn下Mapreduce的内存参数理解
HIVE参数调优(汇总)
MapReduce过程详解及其性能优化
hadoop fair scheduler 的坑
Yarn 内存分配管理机制及相关参数配置