一、简介

1.1、概述

  1. MapReduce是Hadoop提供的用于进行分布式计算的框架
  2. MapReduce是仿照Google MapReduce来实现的
  3. MapReduce会将整个计算过程拆分2个阶段:Map(映射)阶段和Reduce(规约)阶段

1.2、组件

1.2.1、Writable - 序列化

  1. 在MapReduce中,要求被传输的数据必须能够序列化
  2. MapReduce提供了一套独立的序列化机制,基于AVRO来实现的,但是在AVRO的基础上进行了封装,使得序列化过程更加的简单 - 只需要让准备序列化的类实现Writable接口即可
  3. MapReduce在序列化的时候不允许属性值为null

1.2.2、Partitioner - 分区

  1. 分区的作用是对数据进行分类
  2. 在MapReduce中,Mapper负责拆分数据封装对象,Partitioner负责对数据进行分类,Reducer负责进行最后的计算
  3. 在MapReduce中,需要对Partitioner来进行编号。编号默认从0开始依次递增
  4. 在MapReduce中,如果不指定,默认只有1个ReduceTask,最后也只会产生1个结果文件。如果将数据分成了多类,那么需要设置对应数量的ReduceTask - 分区的数量决定了ReduceTask的数量

1.2.3、Comparable - 排序

  1. 在MapReduce中,默认会键来进行排序,所以要求放在键的位置上的元素对应的类必须实现Comparable接口 - 考虑到元素还进行序列化,所以实现WritableComparable
  2. 如果compareTo的结果为0,那么MapReduce会认为这两条数据对应的键是同一个键,那么会将这个键对应的值放到一组去形成一个迭代器

1.2.4、Combiner - 合并

  1. Combiner减少数据的条目数但是不改变计算结果
  2. 在Driver中添加job.setCombinerClass(XXX.class);
  3. 像求和、求积、最值、去重等可以传递运算的场景可以使用Combiner;像求平均等不能传递运算的场景就不能使用Combiner

二、基本理论

2.1、数据本地化策略

  1. 在切片的时候,一般会将Split=Block/n
  2. 数据本地化策略
    a 在部署集群的时候,会考虑将TaskTracker和DataNode部署在相同的节点上,保证数据不是跨集群获取而是在本集群内部获取,从而减少了数据在集群之间的传输
    b JobTracker在分配任务的时候,会考虑将MapTask分配到有要处理的数据的节点上,保证任务处理的数据是从本节点来读取而不需要跨节点来读取数据
  3. 切片过程中的问题 - FileInputFormat -> getSplits
    a SplitSize最小是1个字节,最大是Long.MAX_VALUE个字节
    b 如果文件为空,则整个文件作为一个切片处理
    c 文件分为可切和不可切(逻辑切分)。如果不指定,则文件默认是可切的,但是大部分的压缩文件都是不可切的,Hadoop默认只有Bzip2(.bz2)的压缩文件可切
    d 如果文件不可切,则整个文件作为一个切片处理
    e 默认情况下,SplitSize和BlockSize一样大
    f 如果需要调小SplitSize,需要调小maxSize;如果需要调大SplitSize,那么需要调大minSize
    g 在切片过程中,需要注意切片阈值SPLIT_SLOP=1.1;只有剩余字节个数/SplitSize>SPLIT_SLOP,那么才会切分。例如一个650M的文件,会有6个Block:5128+10;但是只有5个Split:4128+138

2.2、Job的执行流程

  1. 准备阶段
    a 检查指定的输入输出路径
    b 为这个任务计算切片
    c 如果有需要,可以设置分布式缓存存根账户信息
    d 将任务的jar包和配置信息提交到HDFS上
    e 将任务提交给JobTracker并且可以选择是否监控这个任务的执行状态
  2. 执行阶段
    a 拆分子任务:当JobTracker收到Job任务之后,会对这个任务进行划分,拆分成子任务(MapTask和ReduceTask) - MapTask的数量由切片数量来决定,ReduceTask的数量由分区数量来决定。拆分完成之后,等待TaskTracker的心跳
    b 分配子任务:当JobTracker收到TaskTracker的心跳之后,会将子任务分配给TaskTracker。注意,在分配的时候,MapTask要尽量考虑满足数据本地化策略;ReduceTask尽量分配到相对空闲的节点上
    c 下载jar包:当TaskTracker通过心跳领取到子任务之后,会去连接对应的节点下载需要的jar包,体现了"逻辑移动数据固定"的思想
    d 执行子任务:下载完jar包之后,TaskTracker在本节点内部去开启一个JVM子进程,然后将子任务分配给这个JVM子进程来处理。默认情况下,JVM子进程只执行一个子任务,执行完之后就会关闭。如果有新的子任务,那么需要重新开启一个新的JVM子进程。如果这个TaskTracker领取到了多个任务,那么需要开启多次JVM子进程也需要关闭多次JVM子进程,从而导致资源的浪费

2.3、uber模式

  1. 默认情况下,uber模式不开启,此时JVM子进程只执行一个子任务从而导致资源的浪费
  2. 开启uber模式(配置在mapred-site.xml文件中)

三、Shuffle 过程

3.1、Map端的Shuffle

  1. 当MapTask产生数据的时候,并不是直接将数据发送给ReduceTask而是先将数据临时存储在MapTask自带的缓冲区中
  2. 数据在缓冲区中会进行分区排序,如果指定了Combiner,还会进行combine。因为这次排序是将完全杂乱的数据整理成有序的数据,所以采用的是快速排序
  3. 缓冲区是维系在内存中的,默认大小是100M,本质上是一个环形的字节数组。每一个MapTask都会自带一个缓冲区
  4. 当缓冲区使用达到指定阈值(默认是0.8,即当缓冲区使用达到80%)的时候,会进行溢写(spill),将缓冲区中的数据溢写到磁盘上形成一个溢写文件 - 溢写文件中的数据是已经分好区且排好序的
  5. 溢写完成之后,MapTask后续产生的数据会继续写到缓冲区中,再次达到条件会再次进行溢写。每一次溢写都会产生一个新的溢写文件 - 单个溢写文件中的数据是有序的,所有溢写文件之间是整体无序局部有序的
  6. 当MapTask处理完所有数据之后,会再将产生的所有的溢写文件来进行合并(merge),合并完成之后会产生一个最终的结果文件final out。如果数据处理完成之后,缓冲区中依然有数据,那么缓冲区中的数据会直接写到最终的final out文件中
  7. 在merge过程中,所有数据之间会再次进行分区和排序,所以最后产生的final out文件是分好区且排好序的。如果溢写文件个数>=3个,且指定了Combiner,那么在merge过程中还会再次进行combine操作。因为这次排序是将局部有序的数据整理成整体有序的数据,所以采用的是归并排序
  8. 注意问题
    a 环形缓冲区的优势在于有效的减少寻址时间
    b 阈值的作用在于降低阻塞的几率
    c spill过程不一定产生
    d 原始数据的大小并不能决定是否溢写,关键是要看Mapper的处理逻辑
    e 溢写文件的大小要考虑序列化因素的影响

3.2、Reduce端的Shuffle

  1. ReduceTask的启动阈值默认是0.05,即当有5%的MapTask结束,那么就会启动ReduceTask来抓取数据
  2. ReduceTask启动之后,会启动fetch线程,通过fetch线程来抓取数据。默认条件下,每一个ReduceTask最多可以启动5个fetch线程来获取数据
  3. fetch线程会通过HTTP请求中的GET请求的方式来获取数据。fetch线程在发送GET请求的时候,会携带参数表示分区号,那么此时会只获取当前分区的数据
  4. 每一个fetch线程会将抓起回来的数据存放到本地磁盘上形成一个个小文件
  5. 当fetch线程将所有的数据抓取完之后,ReduceTask会将这些小文件再次进行merge(merge factor默认为10,即每10个文件合并一次),最终合并成一个大的文件。在合并过程中,会再次对数据进行排序 - 归并排序
  6. 合并完成之后,会将相同的键对应的值分到一组去,形成一个伪迭代器(本质上就是基于迭代模式的流),这个过程称之为分组(group)。每一个键调用一次reduce方法

3.3、优化

  1. 增大缓冲区,实际过程中,建议范围在250~400M之间 (重要 )
  2. 适当的增大溢写阈值,但是增加线程阻塞的风险
  3. 考虑增加Combiner过程 (重要 )
  4. 考虑将final out进行压缩传输。这种方案实际上是网络环境和压缩效率的取舍 (重要 )
  5. 适当的调节ReduceTask的启动阈值
  6. 适当的增加fetch线程的数量,实际开发中,这个值一般在1W左右 (重要 )
  7. 增大merge因子,但是导致底层算法的复杂度增加

四、扩展

4.1、InputFormat - 输入格式

  1. InputFormat发生在MapTask之前,用于切分和读取数据。InputFormat会将读取到数据传递给MapTask,所以InputFormat读取出来的数据是什么格式,那么Mapper接收的就是什么格式
  2. 作用
    a getSplits:对数据进行切片
    b createRecordReader:针对指定切片来产生输入流用于读取数据
  3. 如果不指定,默认情况下,使用的输入格式是TextInputFormat。在默认情况下,使用的FileInputFormat来进行切片,使用TextInputFormat来读取数据
  4. TextInputFormat中覆盖了isSplitable方法,用于判断文件是否可切:先判断文件是否是一个压缩文件;如果不是压缩文件,则认为文件时可切的;如果是压缩文件,则判断这是否是一个可切的压缩文件
  5. 在读取过程中,会先判断是否是一个压缩文件。如果不是压缩文件,默认会进行按行读取;如果是压缩文件且压缩文件可切,那么会进行按行读取;如果是压缩文件但是压缩文件不可切,那么解压之后再进行按行读取
  6. TextInputFormat在读取数据的时候,从第二个切片开始,每一个切片对应的MapTask都会从当前切片的第二行开始处理到下一个切片的第一行。第一个MapTask需要多处理一行,最后一个MapTask会少处理一行
  7. 自定义输入格式:定义一个类继承InputFormat。但是考虑到切片过程相对复杂,所以一般会定义一个类继承FileInputFormat而不是直接继承InputFormat - authinput
  8. 多源输入:可以指定多个文件来同时处理。这多个文件可以放在不同的路径下。多个文件的数据格式也可以不一样,Mapper的处理方式也可以不一样,在代码中可以给每一个文件分别执行输入格式和Mapper类 - multipleinput

4.2、OutputFormat - 输出格式

  1. OutputFormat发生在ReduceTask之后,接收ReduceTask产生的数据,然后将结果写到指定位置上
  2. 作用
    a 校验输出路径,例如检查输出路径的不存在
    b 提供输出流来将数据写出到指定的文件系统上
  3. 如果输出路径是HDFS上的路径,这个Hadoop会认为是自己的文件系统,那么不会产生校验文件;如果是写到其他文件系统中,认为这个过程不能保证数据的完整性,所以会提供用CRC方式来加密的校验文件
  4. FileOutputFormat只负责校验输出路径;如果不指定,那么真正用于写出数据用的是TextOutputFormat

4.3、数据倾斜

  1. 数据倾斜指的是任务之间处理的数据量不均等而导致出现慢任务的现象
  2. 在实际开发中,数据倾斜不可避免 - 数据本身就有倾斜特性,即数据本身就是不均等的
  3. Map端也会产生数据倾斜的条件:多源输入、文件不可切且文件大小不均等。这三个条件缺少任何一个都不会构成Map端的倾斜。Map端的数据倾斜一旦产生就无法处理
  4. 在实际生产过程中,绝大部分的数据倾斜都是产生在Reduce端,直接原因是因为对数据进行了分类,本质原因是因为数据本身就不均等。Reduce端一旦产生数据倾斜,可以考虑使用二/两阶段聚合的方式来处理
  5. 二阶段聚合实际上是将一个MapReduce拆分成2个MapReduce来处理 - 数据倾斜度越大,那么二阶段聚合的效率才会越高
    a 第一个阶段先将数据打散,进行小的聚合
    b 第二个阶段会再将数据进行最终的聚合

4.4、Join

  1. 到目前为止,有部分人会将Join看作是数据倾斜的一种解决方案
  2. 如果需要同时处理多个文件,且文件之间还需要相互关联,那么这个过程中需要确定主处理文件,将其他的关联文件放到缓存中来进行使用
  3. 如果需要同时处理多个文件,且文件不关联,那么使用多源输入;如果多个文件之间相互关联,那么使用的是Join

4.5、小文件

  1. 实际生产过程中,并不能完全避免小文件
    。2. 小文件的危害
    a 存储:每一个小文件都会产生一条元数据。如果有大量的小文件,那么就意味着产生大量的元数据。元数据过多,会占用大量内存并且导致查询效率变低
    b 计算:每一个小文件都会对应一个切片,每一个切片都会对应一个MapTask(线程)。如果有大量的小文件,那么就会产生大量的MapTask,导致服务器的卡顿甚至崩溃
  2. 到目前为止,小文件的处理手段无非两种:合并和打包
  3. Hadoop提供了一种原生的打包手段:Hadoop Archive。将指定的一个或者多个小文件打包到一个har包中

4.6、推测执行机制

  1. 推测执行机制实际上是Hadoop针对MapReduce的慢任务提供的一种"优化"方案
  2. 在MapReduce中,当出现慢任务的时候,MapReduce会将这个节点(服务器)上的慢任务拷贝一份到其他节点上,两个节点同时执行相同的任务。谁先执行完,那么这个任务的结果就作为最终结果来使用,没有执行完的任务就会被kill掉。这个过程称之为推测执行机制
  3. 慢任务的场景
    a 任务分配不均匀
    b 节点性能不一致
    c 数据倾斜
  4. 在实际生产过程中,因为数据倾斜导致的慢任务出现的概率更高,此时推测执行机制并没有效果反而会占用更多的服务器资源,所以一般会关闭推测执行机制