Hadoop-MapReduce
MapReduce设计思想
1、分而治之
2、抽象模型
3、统一架构
4、离线框架
5、计算向数据靠拢
将计算放在计算节点上进行
6、顺序处理数据、避免随机访问数据
7、失效被认为是常态
MapRduce计算流程
1.原始数据File
1T数据被切分成块存放在HDFS上,每一个块有128M大小
2.数据块Block
hdfs上数据存储的一个单元,同一个文件中块的大小都是相同的
因为数据存储到HDFS上不可变,所以有可能块的数量和集群的计算能力不匹配
我们需要一个动态调整本次参与计算节点数量的一个单位
3.切片
切片只是一个逻辑概念,
在不改变数据存储的情况下,可以控制参与计算的节点数目
通过切片大小可以达到控制计算节点数量的目的
有多少各切片就会执行多少个Map任务
一般切片大小为Block的证书倍(2, 1/2)
防止对于创建和数据连接
如果Split>Block ,计算节点少了
4.MapTask
5.环形缓冲区
6.分区Partition
7.排序
8.溢写Spill
9.合并Merge
10.组合器
11.拉取
12.合并
13.归并Reduce
14.写出Output
原始数据采用分而治之的方法,切分成多个block。 随后对block进行split操作,切分成多个切片,这些切片是逻辑概念,物理上并不切分,数据还是以block形式存储在hdfs上。根据切片的大小可以控制参与计算的节点数量。 数据读入到环形数据缓冲区中,环形数据缓冲区底层是一个数组,逻辑上首尾相连。环形数据缓冲区默认设置大小是100M,在其中写入切片数据和对应的元数据,当数据写入达到80%时开始溢写,并在剩余20%的中间位置设置新的赤道。通过这种方式,实现无卡顿读写。数据在环形数据缓冲区中就已分好区,排好序,数据的分区信息和排序信息存储在元数据中。 溢写的时候可以使用Combiner组合器进行预聚合,每次溢写生成一个spilln.out和一个spilln.out.index文件,.index文件中记录.out文件 的分区信息和数据长度。一个切片会生成多个.index和.out文件,最后都会对他们进行合并,并生成 file.out和file.out.index文件,合并时,一次最多同时合并十个文件。当有超过三个文件需要合并时, 可以加一个Conbiner进行预聚合。 reduce阶段根据index索引文件将相同分区的数据拉取到一起,存储在ReduceTask的内存缓冲区中。该缓冲区大小默认是当前节点可用内存的70%,默认溢写阈值是66%,溢写时也可以有一个 Combiner,随后合并输出到磁盘上,接着进行分组操作。分组操作的规则如下:是否定义了分组规 则,有,则使用;若无,则判断是否定义了比较器,若有,则使用;若无,则根据MapTask写出Key的比较器进行分组。reduce最后的输出可以保存在hdfs、DB(关系型数据库)、NoSQL(非关系型数据 库)上。至此,MR的流程结束。
注;每一个切片只会生成一个maptask计算任务,每一次溢写会生成一个文件。
零散琐碎
MR
1.在从切片到环形缓冲区读取时,入口类不一致,如果切片比Block小,就用Linerecordread(行读取器)。如果切片比Block大,就用combinerread(组合读取器)。
2.设置成环形缓冲区可以实现数据的无卡顿读写。
3.环形缓冲区的大小可以设置,当设置大以后,一次性读入的数据增多,溢写的文件变少,有效地减少了磁盘IO(从内存到磁盘),属于MR优化。
4.预聚合(使用combiner)也属于优化的一种,预聚合以后可以减少从MapTask到reduceTask的数据拉取,降低网络IO。
5.在环形缓冲区实现分区(默认哈希分区)、快排(根据key排序,不改变数据存储的位置,只改变元数据索引的位置)。
6.ReduceTask可以拉取数据是因为溢写出的文件记录了分区的起始位置和数据长度。
YARN
1.YARN是资源是指内存和CPU虚拟核(8核16线程,这里CPU虚拟核指的是16线程)
YARN
案例(好友推荐)
思路
这儿得理清,站在当前登录的用户视角,只要他们是间接好友就记为1,直接好友就记为0。
Map代码
Reduce代码
压缩
压缩技术能够有效减少存储系统的读写字节数,提高网络带宽和磁盘空间的效率。
基本原则
计算密集型作业,少用压缩
特点:要进行大量的计算,消耗CPU资源。
计算密集型作业虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应道等于CPU核心数。
计算密集型任务主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的运行脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务最好用C语言。
IO密集型作业,多用压缩
特点:CPU消耗很少,任务的大部分时间都在等待IO操作的完成(因为IO的速度远远低于CPU和内存的速度)。
涉及到网络、磁盘IO的任务都是IO密集型任务。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间花费在IO上,花在CPU上的时间很少,因此用运行速度极快的C语言替换Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言首选,C语言最差。
压缩比较
压缩选择
压缩零散
假如HDFS中的文件没有切片,有可能导致数据倾斜,节点内存溢出,发生OOM。
MR优化
概述
1.可构建在廉价机器上,设备成本相对较低。
2.高容错性,HDFS将数据自动保存为多个副本,副本丢失后,自动恢复,防止数据丢失或损坏。
3.适合批处理,HDFS适合一次写入、多次查询(读取)的情况,适合在已有数据的情况下进行多次分析,稳定性好。
4.适合存储大文件,其中的大表示存储单个大文件,因为是分块存储。也可以表示存储大量的数据,但不适合小文件。
小文件优化
小文件产生的问题
1.从存储方面来说:Hadoop 存储的每个文件都会在 NameNode 上记录元数据,如果同样大小的文件,文件很小的话,就会产生很多元数据文件,造成 NameNode 的压力;
2. 从读取方面来说:同样大小的文件分为很多小文件的话,会增加磁盘寻址次数,降低性能;
3.从计算方面来说:我们知道一个MapTask默认处理一个分片或者一个文件,如果MapTask的启动时间比数据处理的时间还长,那么就会造成低性能。而且在Map端溢写磁盘的时候每一个MapTask最终会产生Reduce数量个数的中间结果,如果MapTask数量特别多,就会造成临时文件很多,造成Reduce拉取数据的时候增加磁盘的IO。
解决小文件问题
1.从源头解决问题,也就是在HDFS上不要存储小文件,在数据上传至HDFS时提前合并小文件。
2.如果小文件合并后的文件过大,可以更换文件存储格式或压缩存储,当然压缩存储需要考虑是否能切片的问题。
Ⅰ.归档文件格式(如TAR): TAR是一种将多个文件打包成单一文件的格式,但不进行压缩。它可以方便地将文件组织在一起,减少存储和传输的开销。
Ⅱ.数据库文件格式: 如果这些小文件包含结构化数据,将其存储在数据库中可能是一个好主意。关系型数据库如SQLite、MySQL或非关系型数据库如MongoDB都可以作为存储结构化数据的选择。
Ⅲ.文档数据库格式: 如果小文件包含文本、JSON、XML等文档类型数据,可以考虑使用文档数据库,如MongoDB、CouchDB等。
3.如果小文件已经存储在HDFS上,那么在FileInputFormat读取的时候使用实现类Combine-FileInputFormat,然后再使用LineRecordReader读取数据,在读取数据的时候进行合并。
数据倾斜
MapReduce 是一个并行处理框架(分布式),那么处理的时间肯定是作业中所有任务最慢的那个了,可谓木桶效应。为什么会这样呢?
如何解决数据倾斜
1.不使用默认的哈希分区,采用自定义分区,结合业务特点,使每个分区数据基本平衡。
2.或者既然有默认的分区算法,那么我们可以修改分区的键,让其符合Hash分区,并且使得最后的分区平衡,比如在key前加随机数或者盐n-key。
3.既然Reduce处理慢,那么可以增加Reduce的memory(内存)和vcore(虚拟核心[几核几线程])
4.如果是因为只有一个Reduce导致作业很慢,可以增加Reduce的数量来分摊压力,然后再来一个作业实现最终聚合。
推测执行
如果不是数据倾斜带来的问题,而是节点服务有问题造成某些 Map 和 Reduce 执行缓慢呢?可以使用推测执行,你跑的慢,我们可以找 个其他节点重启一样的任务进行竞争,谁快以谁为准。推测执行是空间换时间的一种优化思想,会带来集群资源的浪费,给集群增加压力,所以一般情况下集群的推测执行都是关闭的,可以根据实际情况选择是否开启。
推测执行参数如下
- # 是否启用 MapTask 推测执行,默认为 true
- mapreduce.map.speculative=true
- # 是否启用 ReduceTask 推测执行,默认为 true
- mapreduce.reduce.speculative=true
- # 推测任务占当前正在运行的任务数的比例,默认为 0.1
- mapreduce.job.speculative.speculative-cap-running-tasks=0.1;
- # 推测任务占全部要处理任务数的比例,默认为 0.01
- mapreduce.job.speculative.speculative-cap-total-tasks=0.01
- # 最少允许同时运行的推测任务数量,默认为 10
- mapreduce.job.speculative.minimum-allowed-tasks=10;
- # 本次推测没有任务下发,执行下一次推测任务的等待时间,默认为 1000(ms)
- mapreduce.job.speculative.retry-after-no-speculate=1000;
- # 本次推测有任务下发,执行下一次推测任务的等待时间,默认为 15000(ms)
- mapreduce.job.speculative.retry-after-speculate=15000;
- # 标准差,任务的平均进展率必须低于所有正在运行任务的平均值才会被认为是太慢的任务,默认为 1.0
- mapreduce.job.speculative.slowtaskthreshold=1.0;
MapReduce执行流程优化
Map阶段
临时文件
上面我们从 Hadoop 的某些特定场景下聊了 MapReduce 的优化,接下来我们从 MapReduce 的执行流程进行优化。
前面我们已经聊过小文件在数据读取这里也可以做优化,所以选择一个合适的数据文件的读取类(FIleInputFormat 的实现类)也很重 要。我们在作业提交的过程中,会把作业 Jar 文件,配置文件,计算所得输入分片,资源信息等提交到 HDFS 的临时目录(Job ID 命名的目录下),默认 10 个副本,可以通过 mapreduce.client.submit.file.replication 参数修改副本数量。后期作业执行时会下载这些文件到本地,中间会产生磁盘 IO。如果集群很大的时候,可以增加该参数的值,这样集群很多副本都可以供 NM 访问,从而提高下载的效率。
注:我们提交的job作业会返回给客户端一个临时目录(存放jar包,配置文件,切片信息等)并上传到HDFS中,HDFS中默认时10份副本,这个副本就几M,如果集群的节点数很多,后期作业执行时会下载这些文件到本地,中间会产生磁盘 IO,可以通过修改默认副本数,让每个节点有一个副本,这样每个节点都可以有副本供NameManger使用,不需要到其他节点去拉取产生IO。
分片
回顾一下源码中分片的计算公式
- // getFormatMinSplitSize():一个切片最少应该拥有 1 个字节
- // getMinSplitSize(job):读取程序员设置的切片的最小值,如果没有设置默认读取 1
- long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
- // 读取程序员设置的切片的最大值,如果没有设置默认读取 Long.MAX_VALUE
- long maxSize = getMaxSplitSize(job);
- // 获取 Block 的大小(默认为 128M)
- long blockSize = file.getBlockSize();
- // 获取 Split 的大小,切片的默认大小为 Block 的大小
- // return Math.max(minSize, Math.min(maxSize, blockSize));
- // minSize 为 64M --> 最终返回 128M,minSize 为 256M --> 最终返回 256M
- // maxSize 为 64M --> 最终返回 64M,maxSize 为 256M --> 最终返回 128M
- // 如果需要调大切片,则调节 minSize;如果需要调小切片,则调节 maxSize
- long splitSize = computeSplitSize(blockSize, minSize, maxSize);
因为 Map 数没有具体的参数指定(默认情况下一个切片一个 MapTask),所以可以通过如上的公式调整切片的大小,这样就可以实现动态设置 Map 数了,那么问题来了,Map 数该如何设置呢?
资源
- # MapTask 的执行内存,默认为 1024MB
- mapreduce.map.memory.mb=2048
- # MapTask 的虚拟核数,默认为 1C
- mapreduce.map.cpu.vcores=1
- # ResourceManager 中每个容器可以申请内存资源的最小值,默认值为 1024MB
- yarn.scheduler.minimum-allocation-mb=1024
- # ResourceManager 中每个容器可以申请内存资源的最大值,默认值为 8192MB
- yarn.scheduler.maximum-allocation-mb=8192
- yarn.scheduler.minimum-allocation-vcores=1
- yarn.scheduler.maximum-allocation-vcores=32
- # NodeManager 节点最大可用内存,结合实际物理内存调整,默认值为 -1。表示该节点上 YARN 可使用的物理内存总量。
- # 如果设置为 -1 且 yarn.nodemanager.resource.detect-hardware-capabilities 为 true(默认为 false),则会自动计算(在Windows和Linux环境下)。在其他情况下,默认为 8192MB
- yarn.nodemanager.resource.memory-mb=-1
注:当map的数量固定时,我们可以通过修改map的资源(内存和CPU)来提升map的处理能力,map默认资源为1G1核,我们可以把它调大,但map又跑在YARN集群上的Container中,Container默认最大为8G4核,所以map的资源设置不能超过Container的最大范围,而且得考虑资源是否充足。如果map的资源设置超过了Container的资源,那么得把Container资源设置到更大。
环形缓冲区&溢写
从源头上确定好 Map 之后,接下来看看 Map 的具体执行过程。首先写环形数据缓冲区,为啥要写环形数据缓冲区呢,为什么不直接写磁盘?这样的目的主要是为了减少磁盘 IO。
每个 Map 任务不断地将键值对输出到在内存中构造的一个环形数据结构中。使用环形数据结构是为了更有效地使用内存空间,在内存中 放置尽可能多的数据。该缓冲默认为 100M( mapreduce.task.io.sort.mb 参数控制),当达到 80%( mapreduce.map.sort.spill.percent 参数控制)时就会溢写至磁盘,每达到 80% 都会重写溢写到一个新的文件。
可以根据机器的配置和数据量来设置这两个参数,当内存足够时,增大mapreduce.task.io.sort.mb=500 会提高溢写的过程,而且会减少中间结果的文件数量。
- mapreduce.task.io.sort.mb=500
- mapreduce.map.sort.spill.percent=0.8
注:可以提高环形缓冲区的大小,减少溢出的文件数目,从而减少磁盘IO,提高计算性能。但是得考虑计算机资源是否充足。
合并
当文件溢写完后,Map 会对这些文件进行 Merge 合并,默认每次最多合并 10 个溢写的文件,由参数mapreduce.task.io.sort.factor 进行设置。调大可以减少合并的次数,提高合并的并行度,降低对磁盘操作的次数。
mapreduce.task.io.sort .factor = 50
注:默认10个合并一次,可以增大合并的数量,从而减少合并。
输出
组合器
在 Reduce 拉取数据之前,我们可以使用 Combiner 实现 Map-Side 的预聚合(不影响最终结果的情况下),如果自定义了 Combiner,此时会根据 Combiner 定义的函数对 map 方法的结果进行合并,这样可以减少数据的传输,降低磁盘和网络 IO,提升性能。
压缩
终于走到了 Map 到 Reduce 的数据传输过程了,这中间主要的影响无非就是磁盘 IO,网络 IO,数据量的大小了(是否压缩),其实减少 数据量的大小,就可以做到优化了,所以我们可以选择性压缩数据,压缩后数据量会进一步减少,降低磁盘和网络 IO,提升性能。
开启压缩后,数据会被压缩写入磁盘,Reduce 读的是压缩数据所以需要解压,在实际经验中 Hive 在 Hadoop 的运行的瓶颈一般都是 IO而不是 CPU,压缩一般可以 10 倍的减少 IO 操作。具体可以通过以下参数进行配置。
- # Map 的输出在通过网络发送之前是否被压缩,默认为 false 不压缩
- mapreduce.map.output.compress=false
- # 如果 Map 的输出被压缩,那么应该如何压缩它们,默认为 org.apache.hadoop.io.compress.DefaultCodec
- mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.SnappyCodec
响应线程
(拿到map阶段的主机IP地址后,map内部暴露一个HTTP Server的服务)Map 流程完成之后,会通过运行一个 HTTP Server 暴露自身,供 Reduce 端获取数据。这里用来响应 Reduce 数据请求的线程数量是可以配置的,通过 mapreduce.shuffle.max.threads 属性进行配置,默认为 0,表示当前机器内核数量的两倍。注意该配置是针对NodeManager 配置的,而不是每个作业配置。具体如下。
- mapreduce.shuffle.max.threads=0
注:通过在reduce的拉取中配置NodeManager中的响应线程数来用于拉取数据
容错(重试机制)
Reduce 的每一个下载线程在下载某个 Map 数据的时候,有可能因为那个 Map 中间结果所在的机器发生错误,或者中间结果的文件丢失,或者网络中断等等情况,这样 Reduce 的下载就有可能失败,所以 Reduce 的下载线程并不会无休止的等待下去,当一定时间后下载仍然 失败,那么下载线程就会放弃这次下载,并在随后尝试从其他的地方下载(因为这段时间 Map 可能会重跑)。
为什么会从其他地方下载呢?因为 Map/Reduce Task 有容错机制,当任务执行失败后会尝试重启任务,相关参数如下。
- # MapTask 最大重试次数,一旦重试次数超过该值,则认为 MapTask 运行失败,其对应的输入数据将不会产生任何结果,默认为 4
- mapreduce.map.maxattempts=4
- # ReduceTask最大重试次数,一旦重试次数超过该值,则认为ReduceTask运行失败,其对应的输入数据将不会产生任何结果,默认为4
- mapreduce.reduce.maxattempts=4
- # 当一个 NodeManager 上有超过 3 个任务失败时,ApplicationMaster 会将该节点上的任务调度到其他节点上执行
- # 该值必须小于 Map/Reduce Task 最大重试次数,否则失败的任务将永远不会在不同的节点上尝试
- mapreduce.job.maxtaskfailures.per.tracker=3
- # 当 NodeManager 发生故障,停止向 ResourceManager 节点发送心跳信息时,ResourceManager 节点并不会立即移除 NodeManager,而是要等待一段时间,该参数如下,默认为 600000ms
- yarn.nm.liveness-monitor.expiry-interval-ms=600000
- # 如果一个 Task 在一定时间内没有任务进度的更新(ApplicationMaster 一段时间没有收到任务进度的更新),即不会读取新的数据,也没有输出数据,则认为该 Task 处于 Block 状态,可能是临时卡住,也可能会永远卡住。为了防止 Task 永远 Block 不退出,则设置了一个超时时间(单位毫秒),默认为 600000ms,为 0 表示禁用超时 注:需要根据集群资源和集群稳定性调整。稳定性好,说明集群一般不会有问题,可以多等一等,调大。集群资源充足,说明再启动一个任务也无关紧要,可以少等一等甚至不等,调小。
- mapreduce.task.timeout=600000
- # YARN 中的应用程序失败之后,最多尝试的次数,默认为 2,即当 ApplicationMaster 失败 2 次以后,运行的任务将会失败
- mapreduce.am.max-attempts=2
- # YARN 对 ApplicationMaster 的最大尝试次数做了限制,每个在 YARN 中运行的应用程序不能超过这个数量限制
- yarn.resourcemanager.am.max-attempts=2
- # Hadoop 对 ResourceManager 节点提供了检查点机制,当所有的 ResourceManager 节点失败后,重启 ResouceManager 节点,可以从上一个失败的 ResourceManager 节点保存的检查点进行状态恢复
- # 检查点的存储由 yarn-site.xml 配置文件中的 yarn-resourcemanager.store.class 属性进行设置,默认是保存到文件中,把他设置到保存在zookeeper中,保存到文件中容易丢失,而zookeeper会默认保存三份。
- yarn.resourcemanager.store.class=org.apache.hadoop.yarn.server.resourcemanager.recovery.FileSystemRMStateStore
<!-- 设置 ResourceManager 的状态信息存储在 ZooKeeper 集群 --><property><name> yarn.resourcemanager.store.class </name><value> org.apache.hadoop.yarn.server.resourcemanager.recovery.ZKRMStateStore </value></property>
Reduce阶段
资源
接下来就是 Reduce 了,首先可以通过参数设置合理的 Reduce数量(mapreduce.job.reduces 参数控制),以及通过参数设置每个Reduce 的资源。具体如下。
- # 默认为 1
- mapreduce.job.reduces=1
- # 默认为 1024MB
- mapreduce.reduce.memory.mb=4096
- # 默认为 1
- mapreduce.reduce.cpu.vcores=1
- # Map 和 Reduce 共享,当 MapTask 完成的比例达到该值后会为 ReduceTask 申请资源,默认是 0.05
- mapreduce.job.reduce.slowstart.completedmaps=0.05
可以设置Reduce中的资源(内存和CPU),但不能超过YARN集群中Container中的资源。
拉取
Reduce 在 Copy 的过程中默认使用 5 个( mapreduce.reduce.shuffle.parallelcopies 参数控制)并行度进行数据复制,可以将其调大例如 100。
注:调大Reduce的拉取线程,但得考虑资源是否充足。这个考虑的时能同时有几个线程运行。
在Hadoop MapReduce中,这两个参数分别与Map阶段和Reduce阶段的Shuffle操作相关。
Map阶段
1.mapreduce.shuffle.max.threads(响应线程):
这个参数主要影响Map阶段的Shuffle操作。在Map阶段,任务将数据划分为若干个分区,每个分区对应一个Reduce任务。在Shuffle阶段,各个Map任务的输出结果需要传输给对应的Reduce任务。mapreduce.shuffle.max.threads 控制的是Reducer节点从各个Map节点拉取数据的最大并发线程数。更多的线程意味着能够更快地从不同的Map任务获取数据,从而加速Shuffle阶段的进行。更高的线程数允许更多的并行拉取操作,有助于提高性能,但需要平衡资源使用。
个人理解:这个考虑的是能同时从几个map中获取数据。
Reduce阶段
2.mapreduce.reduce.shuffle.parallelcopies(拉取线程):
这个参数同样与Map阶段的Shuffle操作相关,但更具体地说,它影响Reduce阶段的Shuffle Copy操作。在Shuffle Copy阶段,Reducer节点从多个Map任务节点同时获取数据的副本。mapreduce.reduce.shuffle.parallelcopies 控制的是每个Reduce任务并行从多个Map任务节点获取数据的副本的数量。通过增加并行复制数,可以提高数据传输的速度,从而加速Shuffle Copy操作。然而,设置过高的并行复制数可能会导致网络和系统资源的过度使用。
个人理解:这个考虑的是每个reduce能同时有几个线程运行拉取数据。
综合起来,这两个参数都与MapReduce中Shuffle阶段的不同方面有关,一个控制Reducer节点拉取数据的线程数,另一个控制每个Reduce任务获取数据的并行复制数。通过调整它们,可以优化整个MapReduce任务的性能。
Reduce 的每一个下载线程在下载某个 Map 数据的时候,有可能因为那个 Map 中间结果所在的机器发生错误,或者中间结果的文件丢失,或者网络中断等等情况,这样 Reduce 的下载就有可能失败,所以 Reduce 的下载线程并不会无休止的等待下去,当一定时间后下载仍然失败,那么下载线程就会放弃这次下载,并在随后尝试从其他的地方下载(因为这段时间 Map 可能会重跑)。Reduce 下载线程的最大下载时间段可以通过 mapreduce.reduce.shuffle.read.timeout (默认为 180000 秒)进行调整。
缓冲区&溢写
Copy 过来的数据会先放入内存缓冲区中,然后当使用内存达到一定量的时候才 Spill 磁盘。这里的缓冲区大小要比 Map 端的更为灵活,它基于 JVM 的 Heap Size 进行设置。该内存大小不像 Map 一样可以通过 mapreduce.task.io.sort.mb 来设置,而是通过另外一个参数 mapreduce.reduce.shuffle.input.buffer.percent (默认为 0.7)进行设置。意思是说,Shuffile 在 Reduce 内存中的数据最多使用内存量为:0.7 * maxHeap of reduce task,内存到磁盘 Merge 的启动门限可以通过 mapreduce.reduce.shuffle.merge.percent (默认为 0.66)进行设置。
假设 mapreduce.reduce.shuffle.input.buffer.percent 为 0.7,ReduceTask 的 max heapsize 为 1G,那么用来做拉取数据缓存的内存大概为700MB 左右。这 700MB 的内存跟 Map 端一样,也不是要等到全部写满才会往磁盘溢写,而是达到指定的阈值就会开始往磁盘溢写(溢写前会先做 sortMerge)。这个限度阈值可以通过参数 mapreduce.reduce.shuffle.merge.percent 来设定(默认为 0.66)。整个过程同 Map 类似,如果用户设置了 Combiner,也会被启用,然后磁盘中会生成众多的溢写文件。这种 Merge 方式一直在运行,直到没有 Map 端的数据时才会结束,然后启动磁盘到磁盘的 Merge 方式生成最终的文件。
注:可以设置缓冲区的大小,设置的大了,溢写出的文件数量就会减少,磁盘IO就会降低,但得考虑资源。
合并
同 Map 一样,当文件溢写完后,Reduce 会对这些文件进行 Merge 合并。最大合并因子默认为 10,由参数mapreduce.task.io.sort.factor 进行设置。如果 Map 输出很多,则需要合并很多趟,所以可以减少合并的次数,提高合并的并行度,降低对磁盘操作的次数。
注:调大后可以减少合并的次数,提高合并的并行度,降低对磁盘操作的次数。
读缓存
默认情况下,数据达到一个阈值的时候,缓冲区中的数据就会写入磁盘,然后 Reduce 会从磁盘中获得所有的数据。也就是说,缓冲区和 Reduce 是没有直接关联的,中间会有多次写磁盘 -> 读磁盘的过程,既然有这个弊端,那么可以通过修改参数,使得缓冲区中的一部分数据可以直接输送到 Reduce(缓冲区 -> 读缓存 -> Reduce),从而减少 IO 开销。
修改参数 mapreduce.reduce.input.buffer.percent ,默认为 0.0,表示不开启缓存,直接从磁盘读。当该值大于 0 的时候,会保留指定比例的内存用于缓存(缓冲区 -> 读缓存 -> Reduce),从而提升计算的速度。这样一来,设置缓冲区需要内存,读取数据需要内存,Reduce 计算也需要内存,所以要根据作业的用运行情况进行调整。
当 Reduce 计算逻辑消耗内存很小时,可以分一部分内存用来缓存数据,可以提升计算的速度。默认情况下都是从磁盘读取数据,如果内存足够大的话,务必设置该参数让 Reduce 直接从缓存读数据。
注:读缓存默认不开启,直接从磁盘读(内存溢写到磁盘)。 当开启后,缓冲区直接溢写到读缓存,不需要经过落盘操作,从而提升计算速度。设置缓存区需要内存,读缓存也需要内存,reduce计算也需要内存,所以根据内存资源是否充足来设置。