浅谈分布式计算框架MapReduce

一、理解分布式概念

1.1 概念


假设有AB两个任务,则串行、并行、并发的区别如上图所示:

1)串行。A和B两个任务运行在一个CPU线程上,在A任务执行完之前不可以执行B。即,在整个程序的运行过程中,仅存在一个运行上下文,即一个调用栈一个堆。程序会按顺序执行每个指令;

2)并行。并行性指两个或两个以上事件或活动在同一时刻发生。在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。比如,A和B两个任务可以同时运行在不同的CPU线程上,效率较高,但受限于CPU线程数,如果任务数量超过了CPU线程数,那么每个线程上的任务仍然是顺序执行的;

3)并发。并发指多个线程在宏观(相对于较长的时间区间而言)上表现为同时执行,而实际上是轮流穿插着执行,并发的实质是一个物理CPU在若干道程序之间多路复用,其目的是提高有限物理资源的运行效率。 并发与并行串行并不是互斥的概念,如果是在一个CPU线程上启用并发,那么自然就还是串行的,而如果在多个线程上启用并发,那么程序的执行就可以是既并发又并行的;

4)分布式。分布式在并行处理的基础上,强调任务正在执行的物理设备,如处理器、内存等等硬件,在物理上是分开的。而并行计算是指在一台计算机上的计算,在物理上不分开。

1.2 例子

假设有A,B两个任务,任务A需要计算1-100000之间所有质数的和,任务B需要计算100001-200000之间所有质数的和。

  • 采用串行方法设计的程序如下
public class Main {
    //判断是否为质数
    private static boolean isPrime(int n) {
        if(n < 2) return false;
        if(n == 2) return true;
        if(n%2==0) return false;
        for(int i = 3; i < n; i += 2)
            if(n%i == 0) return false;
        return true;
    }
    //串行计算
private static void serial() {
        long time1 = System.currentTimeMillis(), time2,time3;
        long count = 0;
        for(int i=1;i<=100000;++i){
            if(isPrime(i)) count+=i;
        }
        time2=System.currentTimeMillis();
        System.out.println("1-100000之间质数和为"+count+" 耗时:"+(time2- time1) + "毫秒");
        count = 0;
        for(int i=100001;i<=200000;++i){
            if(isPrime(i))
                count+=i;
        }
        time3 = System.currentTimeMillis();
        System.out.println("100001-200000之间质数和为"+count+" 耗时:"+(time3 - time2) + "毫秒");
        System.out.println("总耗时:"+ (time3 - time1) + "毫秒");
    }
    //主函数
    public static void main(String[] args) {
        serial();
    }
}

在串行计算的线程中,只有一个CPU线程,且该线程按顺序执行AB两个任务。程序运行结果如下:

1~100000之间的质数和为454396357耗时:756毫秒
100001~200000之间的质数和为1255204276耗时:1980毫秒
总耗时:2736毫秒
  • 采用并发方法设计的程序如下:
public class Main{
    private static boolean isPrime(int n) {
        if(n < 2) return false;
        if(n == 2) return true;
        if(n%2==0) return false;
        for(int i = 3; i < n; i += 2)
            if(n%i == 0) return false;
        return true;
    }
    public static void main(String[] args) {
        serialConcurrency();
    }
    private static void serialConcurrency() {
        long time = System.currentTimeMillis();
        //任务切换标识,1代表A任务,2代表B任务
        int task = 1;
        //计数器
        long count1 = 0, count2 = 0;
        int i=1,j=100001;
        while (true)
        {
            if(task == 1 && i++<=100000) {
                if(isPrime(i)) count1+=i;
                task = 2;
            }
            else if(task == 2 && j++<=200000) {
                if(isPrime(j)) count2+=j;
                task = 1;
            }
            else{
                break;
            }
        }
        System.out.println("1-100000之间质数和为"+count1);
        System.out.println("100001-200000之间质数和为"+count2);
        System.out.println("总耗时:"+(System.currentTimeMillis() - time) + "毫秒");
    }
}

在并发计算的程序中,同样只有一个CPU线程,但是该线程会在AB两个任务之间进行切换,可以发现,并发计算的总耗时反而大于串行计算,这是因为CPU在任务切换过程中需要消耗一定时间。程序运行结果如下:

1~100000之间的质数和为454396357
100001~200000之间的质数和为1255204276
总耗时:2933毫秒
  • 采用并行方法设计的程序如下:
public class Main {
    public static boolean isPrime(int n) {
        if(n < 2) return false;
        if(n == 2) return true;
        if(n%2==0) return false;
        for(int i = 3; i < n; i += 2)
            if(n%i == 0) return false;
        return true;
    }
    public static void main(String[] args) throws InterruptedException {
        long time1 = System.currentTimeMillis(),time2;
        Task task1 = new Task(1,100000);
        Task task2 = new Task(100001,200000);
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){
            Thread.sleep(1);
        }
        time2 = System.currentTimeMillis();
        System.out.println("总耗时:"+(time2 - time1)+"毫秒");
    }
}
class Task implements Runnable{
    private int start;
    private int end;
    Task(int start, int end) {
        this.start = start;
        this.end = end;
    }
    public void run() {
        long time = System.currentTimeMillis();
        long count = 0;
        for(int i=start;i<=end;++i){
            if(Main.isPrime(i)) count+=i;
        }
        System.out.println(String.format("%d-%d之间质数和为%d,耗时:%d毫秒",start,end,count,(System.currentTimeMillis()- time)));
    }
}

在并行计算的程序中,AB任务各占用一个CPU线程,AB任务同时执行,总共耗费的时间约等于AB任务的最长耗时,程序运行结果如下:

1~100000之间的质数和为454396357,耗时:918毫秒
100001~200000之间的质数和为1255204276,耗时:2275毫秒
总耗时:2277毫秒

1.3 总结

模式CPU线程数总耗时
串行12736毫秒
并发12933毫秒
并行22277毫秒

由上表可知并行的总耗时是最小的,效率最高(如果AB两个任务耗时更接近,则并行计算的效率将更高)。但由于并行计算受限于CPU线程数,当计算量超出单台计算机的计算能力时,人们就开始考虑使用多台计算机同时处理一个任务,分布式计算应用而生。分布式计算将任务分解成许多小的部分,分配给多台计算机进行处理,从而整体上节约了计算时间。Hadoop的MapReduce就是一种分布式计算框架。

二、浅谈MapReduce原理

MapReduce是我们在进行离线大数据处理时候经常要使用的计算模型,它的计算过程被封装得很好,我们只用使用Map函数和Reduce函数,对其整体的计算过程不用太清楚。MapReduce至今已经已经迭代了1.0和2.0两个版本,本文将分别对其讲解。

2.1 MapReduce1.0运行模型

在这里插入图片描述
下面分模块描述其作用。

Input

Input是输入文件的存储位置,默认是HDFS文件系统,但是可以修改,比如可以是本机上的文件位置。
在这里插入图片描述
如上图所示,要和JobTracker打交道是离不开JobClient这个接口的。JobClient中的Run方法会让JobClient把所有Hadoop Job信息,比如mapper reducer jar path, mapper / reducer class name,输入文件的路径等等,告诉JobTrackker,如下面代码所示:

public int run(String[] args) throws Exception {
        
        //create job
        Job job = Job.getInstance(getConf(), this.getClass().getSimpleName());
        
        // set run jar class
        job.setJarByClass(this.getClass());
        
        // set input . output
        FileInputFormat.addInputPath(job, new Path(PropReader.Reader("arg1")));
        FileOutputFormat.setOutputPath(job, new Path(PropReader.Reader("arg2")));
        
        // set map
        job.setMapperClass(HFile2TabMapper.class);
        job.setMapOutputKeyClass(ImmutableBytesWritable.class);
        job.setMapOutputValueClass(Put.class);
        
        // set reduce
        job.setReducerClass(PutSortReducer.class);
        return 0;
    }

除此之外,JobClient.runjob()还会做一件事,使用InputFormat类计算如何把input文件分割成一份一份的,然后交给mapper处理。inputformat.getSplit()函数返回一个InputSplit的List,每一个InputSplit就是一个mapper需要处理的数据。一个Hadoop Job的input既可以是一个很大的file,也可以是多个file;无论怎样,getSplit()都会计算如何分割input。

如果是HDFS文件系统,其可以通过将文件分割为block的形式存放在很多台电脑上,使其可以存放很大的文件。那么Mapper是如何确定一个HDFS文件中block存放在哪几台电脑,有什么数据?

inputFormat实际上是一个interface,需要类来继承,提供分割input的逻辑。Jobclient有一个叫做setInputFormat()的方法,通过它,我们可以告诉JobTracker想要使用的InputFormat类是什么。如果我们不设置,Hadoop默认的是TextInputFormat,它默认为文件在HDFS上的每一个Block生成一个对应的InputSplit。所以大家在使用hadoop时,也可以编写自己的input format,这样可以自由选择分割input的算法,甚至处理存储在HDFS之外的数据。

JobTracker尽量把mapper安排在距离它要处理的数据比较近的机器上,以便mapper从本机读取数据,节省网络传输时间。具体是如何实现的呢?

对于每个map任务,我们知道它的split包含的数据所在的主机位置,我们就把mapper安排在那个相应的主机上就好了,至少是比较近的host。这里有个疑问:split里存储的主机位置是HDFS存数据的主机,和MapReduce的主机有什么关系呢?为了达到数据本地性,其实通常把MapReduce和HDFS部署在同一组主机上。由前面可知,一个InputSplit对应一个map任务,那么当map任务收到它所处理数据的位置信息后,就可以从HDFS读取这些数据了。

从map函数看Input

map函数接受的是一个key value对。实际上,Hadoop会对每个mapper的输入数据再次分割,分割成一个个key-value对,然后为每一个key-value对调用Map函数一次。为了这一步分割,Hadoop使用到另一个类:RecordReader。它的主要方法是next(),作用是从InputSplit读出一条key-value对。 RecordReader可以被定义在每个InputFormat类中,当我们通过JobClient.setInputFormat()告诉Hadoop inputFormat类名称时,RecordReader的定义也一并被传递过来。所以整个Input:

  1. JobClient输入输入文件的存储位置
  2. JobClient通过InputFormat接口可以设置分割的逻辑,默认是按照HDFS文件分割
  3. Hadoop把文件再次分割成key-value对
  4. JobTracker负责分配对应的分割块交由对应的maper处理,同时RecordReader负责读取key-value对值
Mapper

JobClient运行后获得所需的配置文件和客户端计算所得的输入划分信息,并将这些信息存放在JobTracker专门为该作业创建的文件夹中,文件夹的名称为该作业的Job ID,JAR文件默认会有10个副本(mapred.submit.replication属性控制)。输入划分信息可以告诉JobTracker应该为这个作业启动多少个map任务等信息。

TaskTracker会向JobTracker汇报其slot情况,每个slot可以接受一个map任务。为了每一台机器map任务的平均分配,JobTracker会接受每一个TaskTracker所监控的slot情况。JobTracker接收到作业后,将其放在一个作业队列里,等待作业调度器对其进行调度。当作业调度器根据自己的调度算法调度到该作业时,会根据输入划分信息为每个划分创建一个map任务,并将map任务分配给TaskTracker执行,分配时根据slot情况作为标准。

TaskTracker每隔一段时间会给JobTracker发送一个心跳,告诉JobTracker它依然在运行,同时心跳中还携带着很多的信息,比如当前map任务完成的进度等信息。当JobTracker收到作业的最后一个任务完成信息时,便把该作业设置成“成功”。当JobClient查询状态时,它将得知任务已完成,便显示一条信息给用户。

Map通过RecordReader读取Input的key/value对。map在根据用户自定义的任务运行完毕后,产生另外一系列key/value,并将其写入到Hadoop的内存缓冲区中,在内存缓冲区中对key/value按照key排序,此时会按照reduce partition进行,分到不同的partition中,一旦内存满就会被写入到本地磁盘的文件里,这个文件叫做spill file。

Shuffle

在这里插入图片描述
每个map函数会输出一组key/value对,shuffle阶段需要从所有map主机上把相同key的key value对组合在一起,组合后传给reduce主机,作为输入进入reduce函数。

HashPartition类会把key放进一个hash函数里,如果两个key的哈希值一样,它们的key/value对就被放到同一个reduce函数里,分配到同一个reduce函数里的key/reduce对叫做一个reduce partition。hash函数最终产生多少不同的结果,这个Hadoop job就会有多少个reduce partition,这些partition最终被JobTracker分配到负责reduce的主机上,进行处理。map阶段可能会产生多个spill file,当Map结束时,这些spill file会被merge起来,然后按照reduce partition分成多个。

当Map tasks成功结束时,他们会通知负责的tasktracker,然后消息通过jobtracker的heartbeat传给jobtracker。这样,对于每一个job,jobtracker知道map output和map tasks的关联。Reduce内部有一个thread负责定期向jobtracker询问map output的位置,直到reduce得到所有它需要处理的map output的位置。

Reduce的另一个thread会把拷贝过来的map output file merge成更大的file,如果map task被配置成需要对map output进行压缩,那么reduce还要对map output进行解压缩。当一个reduce taskk所有的map output都被拷贝到一个它的host上时,reduce就要开始对它们排序了。

排序并不是一次把所有 file 都排序,而是分几轮。每轮过后产生一个结果,然后再对结果排序。最后一轮就不用产生排序结果了,而是直接向 reduce 提供输入。这时,用户提供的 reduce函数 就可以被调用了。输入就是 map 任务 产生的 key value对。
同时reduce任务并不是在map任务完全结束后才开始的,Map 任务有可能在不同时间结束,所以 reduce 任务没必要等所有 map任务都结束才开始。事实上,每个 reduce任务有一些 threads 专门负责从 map主机复制 map 输出。

Reduce

在这里插入图片描述
reduce()函数以key及对应的 value列表作为输入 ,按照用户自己的程序逻辑,经合并key相同的value值后,产生另外一系列key/value对作为最终输入写入HDFS。

2.2 MapReduce2.0运行模型

Hadoop诞生的目标是为了支持十几台机器的搜索服务,但是随着数据的增加,Hadoop框架的自身问题限制了集群的发展。首先,JobTracker和NameNode的单点问题,严重制约了集群的扩展和可靠性;其次,MapReduce采用基于slot的资源分配模型。slot是一种粗粒度的资源划分单位,通常一个任务不会用完槽位对应的资源,其其它任务也无法使用这些空闲资源。同时map的槽位和reduce的槽位是不可以通用的,会导致部分资源紧张,部分资源空闲。

Year框架

JobTracker在YEAR中大约分成了3块:

  • 一部分是ResourceManager,负责Scheduler和ApplicationsManager
  • 一部分是ApplicationMaster,负责job的生命周期管理
  • 最后一部分是JobHistoryServer,负责日志的展示

为了支持更多的计算模型,把以前的TaskTracker替换成了NodeManager,NodeManager管理各种各样的container,container才是真正干活的。

各大模块分析

ResourceManager

RM是一个全局资源管理器,负责整个系统的资源管理和分配,包括Scheduler和Application Manager,NM Manager。YEAR提供了多种直接可用的调度器。调度器仅根据各个应用程序的资源需求进行资源分配,分配的基本单位是container,container将内存、CPU、网络、磁盘封装到一起。同时用户也可设计自己的调度器

ApplicationMaster

对应用程序管理器来说,包括应用程序提交、与调度器协商资源以启动ApplicationMaster、监控ApplicationMaster运行状态并在失败时重新启动它。用户提交的每个应用程序均包含一个ApplicationMaster,ApplicationMaster可与RM协商获取资源,也可以将得到的任务进行再分配,与NM通信,同时可以监控所有的任务状态。

NodeManager

NodeManager管理container、资源下载、健康检测后汇报等。NM是每个节点上的资源和任务管理器,一方面,它会定时地向RM汇报本节点上的资源使用情况和各个Container的运行状态;另一方面,它接受并处理来自AM的Container启动/停止等各种请求。

Container

Container是YARN中的资源抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等。当AM向RM申请资源时,RM为AM返回的资源是用Container表示的。YARN会为每个任务分配一个Container,且该任务只能是用该Container中描述的资源。

Yarn工作流程

在这里插入图片描述
1)用户向Yarn中提交应用程序,其中包括ApplicationMaster、启动ApplicationMaster的命令、用户程序等;

2)ResourceManager为该应用程序分配第一个Container,并与对应的NodeManager通信,要求它在这个Container中启动应用程序的ApplicationMaster;

3)ApplicationMaster首先向ResourceManager注册,这样用户可以直接通过ResourceManager查看应用程序的运行状态。然后它将为各个任务申请资源,并监控它的运行状态,直到运行结束,即重复步骤4~7;

4)ApplicationMaster采用轮询的方式通过RPC协议向ResourceManager申请和领取资源;

5)一旦ApplicationMaster申请到资源后,就与对应的NodeManager通信,要求它启动任务;

6)NodeManager为任务设置好运行环境(包括环境变量、JAR包、二进制程序等)后,将任务启动命令写到一个脚本中,并通过运行该脚本启动任务;

7)各个任务通过RPC协议向ApplicationMaster汇报自己的状态和进度,以让ApplicationMaster随时掌握各个任务的运行状态,从而可以在任务失败时重新启动任务。在应用程序运行过程中,用户可随时通过RPC向ApplicationMaster查询应用程序的当前运行状态;

8)应用程序运行完成后,ApplicationMaster向ResourceManager注销并关闭自己。

参考资料

https://www.jianshu.com/p/deae44fcc6b3
https://www.jianshu.com/p/461f86936972
https://www.jianshu.com/p/bac54467ac3a

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值