Mapreduce计算框架

30 篇文章 0 订阅
1 篇文章 0 订阅

Mapreduce计算框架

介绍

​ MapReduce是一个计算框架,它的原理是Google的MR并行计算思想,它用于离线处理海量数据,将海量数据的计算任务分发到集群的多台机器上,通过并行计算之后再进行合并结果,因此,它就是基于海量数据处理而生的,同时由于它计算的规格化,这些等待处理的数据必须按照一致的格式存储,而基于MR框架的应用在编写过程中就利用这个规格去切割、过滤、计算。目前,MapReduce有两个大版本。

计算原理

​ 上面讲了MapReduce的基础介绍之后,接下来讲一下MapReduce的计算过程,如图:

在这里插入图片描述

​ 计算流程可以分为如下几个步骤:

  • Input(从本地文件系统、HDFS等文件系统上读取数据)
  • Splitting(切割数据)
    • 如果原本的数据是在分布式文件系统上的,就按照数据就近原则,切割成多个Block,其实也就是按行切割。
    • 如果原本数据不是在分布式文件系统上,将采用部分切割,加载到内存的原则,同样采用按行切割。
  • Mapping(Mapping其实就是对数据块进行映射操作,根据键进行映射,最后输出一个key-value)
  • Shuffling(混洗,这是一个介于map与reduce之间的阶段,很重要,后面会详细讲)
  • Reduce(聚合操作,主要是对一个基于<K,List>结构的数据进行聚合操作)
  • Merge(把计算结果进行合并)

Shuffle混洗的详解

混洗的核心机制:就是将 MapTask 输出的处理结果数据,按照 Partitioner 组件制定的规则分发 给 ReduceTask,并在分发的过程中,对数据按 key 进行了分区和排序,以下就是混洗机制的局部实现:

在这里插入图片描述

Spill

Spill过程包括输出、排序、溢写、合并等步骤,如图所示:

在这里插入图片描述

分析:

  • Collect就是一个收集操作:每个Map任务不断地以对的形式把数据输出到在内存中构造的一个环形数据结构中。使用环形数据结构是为了更有效地使用内存空间,在内存中放置尽可能多的数据。

    • 这个kvbuffer的环型数据结构,这个结构中存放了数据、索引等信息
    • 那么,为何要使用环型数据结构呢?其实很简单,环型结构的特点在于它的周长是一定的,即分配的内存是一定的,在使用的过程中,类似于一个光盘,上面的指针相当于是磁针,将数据输出到磁盘之后,新的收集数据又可以继续在原先的基础上填充。(不需要销毁数组后重新申请内存)
  • Sort是一个排序操作:对Collect中的数据进行排序,先把Kvbuffer中的数据按照partition值和key两个关键字升序排序,移动的只是索引数据(不会移动真实的数据,只移动数据对应的索引)排序结果是Kvmeta中数据按照partition为单位聚集在一起,同一partition内的按照key有序

  • Spill简称溢写:由于kvbuffer是一个环型结构,它总有装满的时候,这个时候就需要将对应的数据执行flush disk操作,冲刷到磁盘中进行保存,同时kvbuffer又有了内存空间提供给map任务继续输入。

    • Spill线程为这次Spill过程创建一个磁盘文件:从所有的本地目录中轮训查找能存储这么大空间的目录,找到之后在其中创建一个类似于“spill12.out”的文件。Spill线程根据排过序的Kvmeta挨个partition的把数据吐到这个文件中,一个partition对应的数据吐完之后顺序地吐下个partition,直到把所有的partition遍历完。一个partition在文件中对应的数据也叫段(segment)。

      所有的partition对应的数据都放在这个文件里,虽然是顺序存放的,但是怎么直接知道某个partition在这个文件中存放的起始位置呢?强大的索引又出场了。有一个三元组记录某个partition对应的数据在这个文件中的索引:起始位置、原始数据长度、压缩之后的数据长度,一个partition对应一个三元组。然后把这些索引信息存放在内存中,如果内存中放不下了,后续的索引信息就需要写到磁盘文件中了:从所有的本地目录中轮训查找能存储这么大空间的目录,找到之后在其中创建一个类似于“spill12.out.index”的文件,文件中不光存储了索引数据,还存储了crc32的校验数据。(spill12.out.index不一定在磁盘上创建,如果内存(默认1M空间)中能放得下就放在内存中,即使在磁盘上创建了,和spill12.out文件也不一定在同一个目录下。)

      每一次Spill过程就会最少生成一个out文件,有时还会生成index文件,Spill的次数也烙印在文件名中。索引文件和数据文件的对应关系如下图所示:

在这里插入图片描述

环型结构的运行机制

在这里插入图片描述

  • 取环形的一个起始点,同时填充逆时针方向的数据与顺时针方向的数据,直到两个磁针碰撞。
  • 磁针碰撞,即数据填满(这里的满可以是环形结构的一个存储比例),执行溢写操作,释放出来的空间,这个时候就如以上的图4,取该空闲空间的中间点,又开始向两端填充数据,重复以上过程,直到map执行完毕。
MapReduce的瓶颈

Map任务总要把输出的数据写到磁盘上,即使输出数据量很小在内存中全部能装得下,在最后也会把数据刷到磁盘上。 这是MapReduce计算框架的一个瓶颈,导致它不适合处理实时业务,也是Spark框架相对MR框架的优势。

Merge

Map任务如果输出数据量很大,可能会进行好几次Spill,out文件和Index文件会产生很多,分布在不同的磁盘上。最后把这些文件进行合并的merge过程闪亮登场。

Merge过程怎么知道产生的Spill文件都在哪了呢?从所有的本地目录上扫描得到产生的Spill文件,然后把路径存储在一个数组里。Merge过程又怎么知道Spill的索引信息呢?没错,也是从所有的本地目录上扫描得到Index文件,然后把索引信息存储在一个列表里。到这里,又遇到了一个值得纳闷的地方。在之前Spill过程中的时候为什么不直接把这些信息存储在内存中呢,何必又多了这步扫描的操作?特别是Spill的索引数据,之前当内存超限之后就把数据写到磁盘,现在又要从磁盘把这些数据读出来,还是需要装到更多的内存中。之所以多此一举,是因为这时kvbuffer这个内存大户已经不再使用可以回收,有内存空间来装这些数据了。(对于内存空间较大的土豪来说,用内存来省却这两个io步骤还是值得考虑的。(比如Spark))

然后为merge过程创建一个叫file.out的文件和一个叫file.out.Index的文件用来存储最终的输出和索引。

一个partition一个partition的进行合并输出。对于某个partition来说,从索引列表中查询这个partition对应的所有索引信息,每个对应一个段插入到段列表中。也就是这个partition对应一个段列表,记录所有的Spill文件中对应的这个partition那段数据的文件名、起始位置、长度等等。

然后对这个partition对应的所有的segment进行合并,目标是合并成一个segment。当这个partition对应很多个segment时,会分批地进行合并:先从segment列表中把第一批取出来,以key为关键字放置成最小堆,然后从最小堆中每次取出最小的输出到一个临时文件中,这样就把这一批段合并成一个临时的段,把它加回到segment列表中;再从segment列表中把第二批取出来合并输出到一个临时segment,把其加入到列表中;这样往复执行,直到剩下的段是一批,输出到最终的文件中。

最终的索引数据仍然输出到Index文件中。如下图:

在这里插入图片描述

Copy

Reduce任务通过RPC向各个Map任务拖取它所需要的数据。每个节点都会启动一个常驻的RPC server,其中一项服务就是响应Reduce拖取Map数据。当有MapOutput的RPC请求过来的时候,RPC server就读取相应的Map输出文件中对应这个Reduce部分的数据通过网络流输出给Reduce。

Reduce任务拖取某个Map对应的数据,如果在内存中能放得下这次数据的话就直接把数据写到内存中。Reduce要向每个Map去拖取数据,在内存中每个Map对应一块数据,当内存中存储的Map数据占用空间达到一定程度的时候,开始启动内存中merge,把内存中的数据merge输出到磁盘上一个文件中。

如果在内存中不能放得下这个Map的数据的话,直接把Map数据写到磁盘上,在本地目录创建一个文件,从RPC流中读取数据然后写到磁盘,使用的缓存区大小是64K。拖一个Map数据过来就会创建一个文件,当文件数量达到一定阈值时,开始启动磁盘文件merge,把这些文件合并输出到一个文件。

有些Map的数据较小是可以放在内存中的,有些Map的数据较大需要放在磁盘上,这样最后Reduce任务拖过来的数据有些放在内存中了有些放在磁盘上,最后会对这些来一个全局合并。

以上就是Shuffle的全部过程了,可见,在MR框架中,Shuffle相比map、reduce的工作量要大得多。

工作机制

只有深入学习理解了MR的工作机制,才能够用它来实现复杂的计算以及优化。Hadoop2.0引入了新机制(MR2)

它建立在一个名为YARN的系统上,该系统主要是负责资源管理、任务调度、追踪。

Mapreduce1.x

A.架构组成
  • JobTracker(作业追踪者)

    • 将作业切分成任务:MapTask和ReduceTask
    • 将任务分派给TaskTracker执行
    • 作业的监控,接受心跳信息,如果没有收到心跳信息,就切换到其他TaskTracker执行任务
  • TaskTracker(任务追踪者)

    • 具体的任务派发、管理者
    • 与JobTasker之间维持着交流(心跳)
    • 是TaskRunner的管理者。
  • TaskRunner

    • 由TaskTracker创建出来的实例
    • TaskRunner会创建一个新的JVM进程来执行各个MAP、REDUCE任务,采用子进程的方式,避免这些任务的执行影响到TaskTracker本身。
  • 四种任务(在MAP阶段有,在REDUCE阶段也有)

    • map任务
      • 具体执行内容就是我们实现的map()方法
      • 执行映射操作
    • reduce任务
      • 具体执行内容就是我们实现的reduce()方法
      • 执行聚合操作
    • setup任务
      • 具体执行内容就是我们实现的setup()方法
      • 任务的初始化操作,会在该阶段的xx操作执行之前先进行初始化,一般用于做一些阈值,过滤的设置
    • cleanup任务
      • 具体执行内容就是我们实现的cleanup()方法
      • 所有任务执行完之后就会执行清理任务。
           //通过这个可以时刻了解clean任务的执行进度
           float progress=job.cleanupProgress();
    

B.执行流程

整体的执行流程如下图:

在这里插入图片描述

1、作业提交

  • 执行Job.submit,会创建jobsubmiter,即作业提交者。
    • 向JobTracker申请JobID,同时检查各种输入、输出路径、作业分片计算、需要资源计算,如果出错,向应用程序抛出错误;如果正常,通知JobTracker准备执行任务。

2、任务初始化

  • 通过内部的任务列表,交给作业调度器(Scheduler)调度,并且对任务进行初始化,这个过程包括一些状态、进度信息的初始化。
  • map任务初始化:调度器会去FS种获取计算好的输入分片,为每个分片创建一个map任务(不可控)。
  • reduce任务初始化:调度器通过从jobTracker中拿到job,并且获取job设置的reduceNumbers,从而初始化对应数量的任务。
       //控制reduceTask的任务数
      job.setNumReduceTasks(4);
  • setup任务:调度器创建setup任务,该任务在各阶段的首要执行位置。
  • cleanup任务:创建资源回收任务。

3、任务分配

  • tasktracker通过心跳告知作业追踪者是否空闲、是否存活,以至于让jobtracker给它分配任务。
  • 采用”槽位分配“策略:
    • taskTracker中有一个map槽,一个reduce槽,分配时候会优先分配map任务,再去分配reduce任务

4、任务执行

  • 通过FS将作业的JAR文件复制到TaskTracker所在文件系统,实现JAR本地化,同时,tasktracker会将运行所需要的各个文件通过分布式缓存复制到本地磁盘,创建本地执行目录,同时创建T…Runner,由它来创建执行任务的子进程。

5、进度状态更新

  • 层层向上汇报进度、状态
    • runner->tasktracker->(心跳)jobtracker->(请求)[jobClient->(进程内部调用)app]
  • 整个MR作业可以分为三个阶段:MAP、SHUFFLE、REDUCE
    • 假如reduce执行了1/2,则整体执行了5/6.

6、完成作业

  • 当jobTracker收到最后一个任务的完成通知,就把JOB设置为成功,这样在客户端查询的过程中,就会知道任务已经完成。
  • jobTracker可以通过指定job.end.notification.url来执行任务通知请求。

Mapreduce2.x

​ Mapreduce2与上一代的差别并不是在mapreduce这个计算框架本身,而是在作业资源管理、调度方面有了巨大的变化,它使用了性能更好、职责更加分明、耦合程度更低的资源管理、调度系统,即Yarn(Yet Another resource Nefotiator)另一种资源协调者。一般在使用的时候通过配置参数mapreduce.framework.name=yarn配置。

A.架构组成
  • ResourceManager:RM
    • 负责集群资源的统一管理和调度
    • 处理客户端对作业的请求
    • 监控NM,随时准备其他NM替换它
  • NodeManager:NM
    • 负责本身节点资源(容器)的管理和使用
    • 定期向RM汇报心跳信息
    • 接受处理RM的命令
    • 接受处理AM的命令
    • 单个节点的资源管理
  • ApplicationMaster:AM
    • 每个应用程序对应一个MRApp或者SparkApp,负责应用程序的管理
    • 向RM申请资源(core、meory),分配给内部task
    • 与NM通信,AM在某个NM中
  • Container
    • 封装了CPU、Memory等资源的一个容器
    • 任务环境的抽象
  • Client
    • 提交作业
    • 查看作业进度
    • 杀死作业
B.执行流程

在这里插入图片描述

1、向YARN提交应用程序

2、RM分配第一个容器,并通知NM要在该容器中启动AM

3、AM向RM注册,使得RM可以监控状态

4、AM以轮询的方式,通过RPC向RM申请资源,并与NM交流,启动对应的任务

5、NM为各个任务设置环境之后,即创建容器,之后启动任务

6、各个任务通过RPC与AM交流,而用户可以通过RM实时查看状态

7、执行完毕之后,AM向RM申请注销并关闭自己

C.采用Yarn的优势
Mapreduce1(存在问题)
JobTracker 是Map-reduce的集中处理点,存在单点故障
JobTracker 完成了太多的任务,造成了过多的资源消耗,当map-reduce job非常多的时候,会造成很大的内存开销,潜在来说,也增加了JobTracker fail的风险,这也是业界普遍总结出老hadoop 的Map-Reduce只能支持4000节点主机的上限
TaskTracker 以map/reduce task的数目作为资源的表示过于简单,没有考虑到cpu/内存的占用情况,如果两个大内存消耗的task被调度到了一块,很容易出现OOM(内存溢出)
TaskTracker端,把资源强制划分为map task slot和reduce task slot,如果当系统中只有map task或者只有reduce task的时候,会造成资源的浪费,也就是前面提到过的集群资源利用的问题。

基于上述的问题:在MR2中采用了YARN系统,基本思想就是将JobTracker两个主要的功能分离成单独的组件,这两个功能是:资源管理、任务调度/监控。

  • ResourceManager有两个主要组件:Scheduler和ApplicationsManager。
    • Scheduler即是资源的调度器,负责分配资源,管理资源,在YARN中,资源以容器的方式进行管理,这些资源包括CPU、内存、磁盘、网络等。
    • ApplicationsManager即任务的调度、监控,负责与第一个NM协商启动AppMaster.
  • NM负责每个结点的资源监控与向Scheduler汇报、同时负责执行应用程序。
  • AppMaster负责从Scheduler协商适当的资源容器,并与NodeManager跟踪其状态并监视进度。

总体来讲,MR2使用YARN的优势如下:

  • 避免单点故障,分离JobTracker任务
  • 采用Container的形式,对资源进行更好的分配、管理
  • 取消了map task slot与reduce task slot槽位概念,采用完全内存概念,避免空槽浪费

优化思路

TODO

demo

一个计算用户在过去某个时间段内的充值金额总数的mr应用:

import com.qgailab.ha.hmhb.pretasks.MapreduceLoader;
import com.qgailab.ha.utils.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import java.io.IOException;
import java.text.ParseException;

//<p>处理的数据格式是:username,2019-05-01 00:00:00,money
/**
 * @author linxu
 * a class which can run mr to compute which user is all top up and top up than the threshold.
 * <p>
 * 1、用于离线计算
 * 2、可以调用mr框架来计算用户的充值情况
 * </p>
 */
@Slf4j
public class TopUpRecordCollector {
    private final static String JOB_NAME = "TopUpRecordCollection_Job";

    /**
     * @author linxu
     * mapper.
     */
    public static class RecordMapper extends Mapper<Object, Text, Text, IntWritable> {

        /**
         * 界定符
         */
        private String delim;
        /**
         * 时间过滤器
         * tips:
         * 1、设置时间过滤器,可以在map阶段过滤某些不需要计算的数据,提高计算速度。
         */
        private String timeFilter;
        /**
         * 使用用户名充当key
         */
        private Text keyByUserName = new Text();
        /**
         * value为用户的充值金额
         */
        private IntWritable topUpAll = new IntWritable(0);
		
        @Override
        protected void setup(Context context) throws IOException, InterruptedException {
            timeFilter = context.getConfiguration().get("filter.time", "2019-05-01 00:00:00");
        }

        /**
         * 分布式计算
         *
         * @param key     Object : 原文件位置偏移量。
         * @param value   Text : 原文件的一行字符数据。
         * @param context Context : 出参。
         * @throws IOException , InterruptedException
         */
        @Override
        protected void map(Object key, Text value, Context context) throws IOException, InterruptedException {
            String originalLine = value.toString();
            String date = originalLine.substring(3, 22);
            try {
                //获取某个时间之后的数据
                if (DateUtil.compare(date, timeFilter) >= 0) {
                    String username = originalLine.substring(0, 2);
                    keyByUserName.set(username);
                    String money = originalLine.substring(23);
                    Double m = Double.parseDouble(money);
                    double f = m;
                    //处理可能存在边界问题
                    topUpAll.set((int) f);
                    //构造k-v
                    context.write(keyByUserName, topUpAll);
                }
            } catch (ParseException e) {
                log.error("time parse error! it is :{}", date);
            }
        }
    }

    /**
     * @author linxu
     * reducer
     * reducing 阶段的计算任务:
     * 统计每个user的充值金额总数
     */
    public static class ComputeReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
        // Statistical results;统计结果。
        private IntWritable result = new IntWritable();
        private int topUpThreshold;

        @Override
        protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
            int sum = 0;
            for (IntWritable val : values) {
                sum += val.get();
            }
            if (sum < topUpThreshold) {
                log.info("User:{},top Up is not enough.", key.toString());
                return;
            }
            result.set(sum);
            context.write(key, result);
        }

        @Override
        protected void setup(Context context) throws IOException, InterruptedException {
            topUpThreshold = context.getConfiguration().getInt("money.threshold", 480000);
        }

    }

    public static void main(String[] args) {
        try {
          //configuration的加载使用一个加载器实现。
            MapreduceLoader.init();
            Job job = Job.getInstance(MapreduceLoader.getConf(), TopUpRecordCollector.JOB_NAME);
            job.setJarByClass(TopUpRecordCollector.class);
            job.setMapperClass(RecordMapper.class);
            job.setReducerClass(ComputeReducer.class);
            job.setOutputKeyClass(Text.class);
            job.setOutputValueClass(IntWritable.class);
            //控制reduceTask的任务数
            //job.setNumReduceTasks(4);
            //通过这个可以时刻了解clean任务的执行进度
            //float progress=job.cleanupProgress();
            FileInputFormat.setInputPaths(job, new Path("hdfs://hacluster/tmp/qgr2/topup.txt"));
            //can verify the output path is exist or not.keep safe.
            FileOutputFormat.setOutputPath(job, new Path("hdfs://hacluster/tmp/waitdel"));
            job.waitForCompletion(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值