Hadoop三大组件原理解析

Yarn调度流程

public class JobSubmitterLinuxToYarn {

    public static void main(String[] args) throws Exception {

        /* 1、配置conf对象,用于指定 */

        Configuration conf = new Configuration();

        conf.set("fs.defaultFS", "hdfs://hdp01:9000");

        conf.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem");

            //设置job提交到哪去运行,如果不指定,则会交由localJobRun在本地运行

        conf.set("mapreduce.framework.name", "yarn");      // 该变量可以在节点的配置文件mapred-site.xml中配置

        conf.set("yarn.resourcemanager.hostname", "hdp01");        

        /* 2、创建job对象 */

        Job job = Job.getInstance(conf);

        job.setJarByClass(JobSubmitterLinuxToYarn.class);

        

        /* 3、设置job的基本参数 */

        job.setMapperClass(WordcountMapper.class);    // 设置map和reduce的实现类

        job.setReducerClass(WordcountReducer.class);

        job.setCombinerClass(WordcountReducer.class);    // 在map端将数据整合,减少发送到reduce端的数据量

        job.setMapOutputKeyClass(Text.class);    // 设置map输出的key和value的类型

        job.setMapOutputValueClass(IntWritable.class);

        job.setOutputKeyClass(Text.class);     // 设置reduce输出的key和value的类型(其实该设置默认同时对map和reduce有效)

        job.setOutputValueClass(IntWritable.class);

        job.setPartitionerClass(ProvincePartitioner.class);    // 设置map任务的分区逻辑,该类中需要实现getPartition(),见flowcount

        job.setNumReduceTasks(1);      // 设置reduce的个数

        // 如果输出目录已经存在,则先将其删除,否则程序会执行失败

        Path output = new Path("/wc/output1");

        FileSystem fs = FileSystem.get(new URI("hdfs://hdp01:9000"),conf,"h1");

        if(fs.exists(output)){

            fs.delete(output, true);

        }

        

        /* 4、封装参数:本次job要处理的输入数据集所在路径和最终结果的输出路径 */

        FileInputFormat.setInputPaths(job, new Path("/wc/input"));    // 由于上面设定了hdfs,所以这是hdfs的文件路径

        FileOutputFormat.setOutputPath(job, output);  // 注意:输出路径必须不存在

        

        /* 5、job的waitForCompletion函数中包含了submit()方法,用于将任务提交到yarn集群上执行 */

        boolean res = job.waitForCompletion(true);

        System.exit(res?0:1);   

    }

}

    

    上面代码中job的waitForCompletion函数中封装了submit(),而submit()方法创建一个内部的JobSubmiter实例,该实例主要实现对任务的提交,具体内容如下:

  1. 向resourcemanager请求一个新应用的ID,作为mapreduce作业ID。
  2. 检查作业的输出路径,如果输出目录已经存在或者没有指定输出目录,则报错。
  3. 依据数据集的输入路径来计算作业的输入分片,如果分片无法计算(例如输入路径不存在),则报错。
  4. 将运行作业所需的资源(作业jar文件、配置文件和输入分片)存到一个以作业ID命名的hdfs目录中,该目录由resourcemanager指定(见下面第四张图上的步骤2)。
  5. 通过调用resourcemanager的submitApplication()方法提交作业。(至此,对应下面第四张图中1-6步骤)

    resourcemanager收到提交的作业请求后,调用自身包含的schedule组件,给作业分配一个容器(容器均是由nodemanager管理的)。接着该容器会去hdfs中对应的作业目录下下载作业资源,然后等待客户端发出shell启动命令后,便在该容器中启动一个application master进程(其主类是MRAppMaster)。application master进程会对之前下载的作业资源进行解析,依据其中的分片信息向resourcemanager请求资源分配,resourcemanager的scheduler就会为每一个分片分配一个容器(用于运行map任务,一个分片由一个map任务来处理),然后application master会通过与nodemanager通信来启动这些容器。在map任务运行之前,容器中的主类yarnchild会去hdfs的对应作业目录中下载作业资源,最后启动map任务去运行。

    resourcemanager的schedule会依据分片的位置来决定在哪些节点上启动map任务,最理想的情况是数据本地化,即需要处理的数据分片和map任务位于同一个节点,减少了分片数据的传输。如果不能数据本地化,那就选择机架本地化。最差的情况的就是map任务跨机架去请求分片数据。但是需要注意,reduce任务能够运行在集群中的任意位置,因为reduce是通过http去下载map的处理结果的。

    resourcemanager主要由两部分构成:schedule(调度器)和application manager。正如前面所提到的,schedule的作用就是资源调度器,为作业的application master进程分配容器,为作业的map和reduce任务分配容器。而application manager则是负责创建application master以及对其进行监控。

    一旦有一个maptask完成任务,application master就会向resourcemanager请求若干个容器去启动若干个yarnchild(reducetask)(其数量可以在程序中自己设定,参考上面的程序代码),这些reducetask启动后就会通过shuffle下载maptask的处理结果中各自对应的分区。

    当map任务或者reduce任务运行时,每一个任务进程都会通过和自己的父进程application master通信,每隔三秒钟汇报一次任务进度,形成一个作业的汇聚视图。客户端job对象的waitForCompletion函数每秒钟轮询一次application master。当application master收到最后一个任务已完成的通知后,便把作业的状态设置为“成功”,然后,在客户端轮询状态时,便知道任务已经成功完成,可以从waitForCompletion()方法返回,job的统计信息和数值也在这个时候输出到控制台。

MapReduce任务流程解析

    上面代码中提到的FileInputFormat函数是所有使用文件作为其数据源的InputFormat实现的基类,其实现了两个功能:一是接受指定的输入文件路径。二是依据输入文件生成分片信息(上面yarn集群的工作机制中有提到)。

    分片通常与hdfs块大小一样,默认是128MB。每个map任务只处理一个输入分片,而每个分片被划分为若干条记录,每条记录是一个key-value对,map会一个接一个的处理记录。分片在代码中表现为InputSplit,其本身不包含数据,而是指向数据的引用。InputSplit一共包含四个属性:存储分片的节点、文件路径、分片的起始位置、分片长度。

    运行作业的客户端会调用getSplits()来计算分片,然后存储到hdfs的作业目录中,供后面的application master进程调用。当map任务在容器中启动后,它会将输入分片传递给InputFormat的createRecordReader()来获取这个分片的RecordReader,RecordReader就像是记录上的迭代器,map任务用其来生成记录的kv,之后再将kv传递给map函数进行操作。

    当map任务收到连续的记录的kv后,其会按照我们编写的map函数进行处理。每个map任务都有一个环形内存缓冲区用于存储任务输出,默认情况下,缓冲区的大小为100MB,一旦缓冲区中的内容达到阈值80%,一个后台spiller线程便会把内容溢出到磁盘,在溢出写的过程中,如果剩余20%的缓冲区也被填满了,则map任务进程便会被阻塞,直到溢出写过程完成。溢出写入磁盘的时候会依据配置文件中的mapreduce.cluster.loacl.dir属性将数据写入本地磁盘的对应目录中。

    在spiller线程将数据写入磁盘前,其会依据reducer的个数将数据划分成相应的分区,之后再对每一个分区进行排序,如果设定了combiner函数(上面代码中有提到,一般情况下和reduce函数一致),便会对每一个分区中的数据进行处理,使得map的输出更紧凑。每次内存缓冲区达到溢出阈值,就会新建一个溢出文件,因此在map任务写完其最后一个输出记录后,会有几个溢出文件,在任务完成之前,所有的溢出文件会被合并成一个已分区且已排序的输出文件。与输出文件一同产生的还有一个分区索引文件,用于指明每个分区的起始位置等信息,reduce会依据分区索引文件来获取其负责的分区文件。

    总结,对map端内存缓冲区中的数据需要经过如下的4个操作才能形成输出文件:分区、排序、combiner(如果有)、各个溢出文件的merge。分区和combiner我们都可以在job中设定,见上面代码,排序需要我们自定义类然后对数据进行封装,该类中实现了compareTo()函数,用于指定排序的顺序。

    由于在yarn集群启动的时候配置了yarn.nodemanager.aux-services参数,其值为mapreduce_shuffle,所以nodemanager会提供一个web服务,将map的输出文件存入其中,类似tomcat文件目录,然后reduce通过http协议去其中获取map的输出结果。

    reducer会启动一个线程定期询问application master以便获取map输出主机的位置,直到获取所有输出的主机。之后,reduce会通过http从每一个map的输出结果中找到自己负责处理的分区文件并下载到本地。随着磁盘上副本增多,后台线程会将他们合并为更大的、排好序的文件供reduce函数使用。reduce会从文件中顺序的一个kv一个kv的去读取,并且调用分组比较器GroupingComparator,将key相同的数据分为一组处理。一组相同key的数据处理完毕之后,其会通过context.write()将结果存入hdfs中,每处理完一组就存一次结果。

    InputFormat和OutputFormat负责mapreduce框架的输入和输出。TextInputFormat是默认的InputFormat接口的实现类,负责处理文本数据,key是LongWritable类型,value是这行的内容。TextInputFormat实现FileInputFormat接口,而FileInputFormat又是继承了最初的InputFormat。FileInputFormat接口有很多的实现类,除了TextInputFormat之外,还有KeyValueTextInputFormat、NLineInputFormat、SequenceFileInputFormat等,他们的区别是处理数据后形成的key-value不同,例如KeyValueTextInputFormat是将一行中分隔符的前半部分作为key,后半部分作为value。除此之外,我们还可以自定义实现输入和输出接口,比如HBase的TableInputFormat的设计初衷是让MapReduce程序操作存放在HBase表中的数据,而TableOutputFormat则是把MapReduce的输出结果写道HBase表中。

HDFS文件读取流程

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.LocatedFileStatus;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.RemoteIterator;

public class HdfsWordcount {
    public static void main(String[] args) throws Exception{  
        /**
         * 初始化工作。调用配置文件中的参数,设置hdfs的输入路径与输出路径,以及用于处理的map函数。
         */
        Properties props = new Properties();
        props.load(HdfsWordcount.class.getClassLoader().getResourceAsStream("job.properties"));    
        Path input = new Path(props.getProperty("INPUT_PATH"));    // 输入路径
        Path output = new Path(props.getProperty("OUTPUT_PATH"));    // 输出路径
        Class<?> mapper_class = Class.forName(props.getProperty("MAPPER_CLASS"));
        Mapper mapper = (Mapper) mapper_class.newInstance();
        Context context  =  new Context();
        


        /**
         * 读取数据。先通过hadoop的FileSystem类连接hdfs文件系统,然后调用FSDataInputStream读取数据并进行处理。
         */
        FileSystem fs = FileSystem.get(new URI("hdfs://hdp-01:9000"), new Configuration(), "root");
        RemoteIterator<LocatedFileStatus> iter = fs.listFiles(input, false);
        while(iter.hasNext()){
            LocatedFileStatus file = iter.next();
            FSDataInputStream in = fs.open(file.getPath());      // 打开希望读取的文件,hdfs读文件操作从这里开始
            BufferedReader br = new BufferedReader(new InputStreamReader(in));
            String line = null;
            // 逐行读取
            while ((line = br.readLine()) != null) {
                // 调用一个方法对每一行进行业务处理
                mapper.map(line, context);   
            }     
            br.close();
            in.close();
        }
        
 
        /**
         * 输出结果到hdfs中。调用FSDataOutputStream类,将文件写入hdfs中。
         */
        HashMap<Object, Object> contextMap = context.getContextMap();
        if(fs.exists(output)){
            throw new RuntimeException("指定的输出目录已存在,请更换......!");
        }
        FSDataOutputStream out = fs.create(new Path(output,new Path("res.dat")));
        Set<Entry<Object, Object>> entrySet = contextMap.entrySet();
        for (Entry<Object, Object> entry : entrySet) {
            out.write((entry.getKey().toString()+"\t"+entry.getValue()+"\n").getBytes());
        }
        out.close();
        fs.close();
        System.out.println("恭喜!数据统计完成.....");   
    }   

}

    客户端调用FileSystem对象(其是DistributedFileSystem抽象类的一个实例)的open()方法来打开希望读取的文件,open()函数中封装的操作是通过RPC(远程过程调用)来调用namenode,给出输入文件的路径,返回文件所对应的块所在的datanode(从下面的图2中可以看到是一次返回文件对应的所有的块的位置信息)。open()函数会返回一个FSDataInputStream类的结果对象,以便通过其读取数据。FSDataInputStream类中封装了DFSInputStream对象,用于管理datanode和namenode的I/O。DFSInputStream会依据datanode的地址去连接datanode,然后调用read()读取数据块,一旦数据读取完毕,就关闭与datanode的连接,然后寻找下一个块的datanode地址。一旦客户端读取完成后,就对FSDataInputStream调用close()方法。

    客户端通过对FileSystem对象调用create()来新建一个文件,给出文件名和路径后,namenode会执行各种不同的检查以确保这个文件不存在以及客户端有权限新建该文件。如果检查均通过,namenode就会创建一条新的记录。FileSystem类对象会返回一个FSDataOutputStream对象,该对象负责处理与datanode和namenode的通信。FSDataOutputStream会将数据切分成不同的块,然后在准备写入第一个块的数据的时候会请求namenode为这个块分配3个datanode主机节点用于存储数据块,然后其会挑选一个datanode,建立数据传输链接。第一个datanode会存储来自客户端数据块并与第二个datanode建立数据传输链路,然后将数据块传到第二个datanode,第二个datanode再将数据块传输给第三个datanode。三个节点全部存储完毕后,由第一个节点向客户端发送完成通知,然后客户端会依据相同的流程将第二个块、第三个块......都通过同样的方式存储hdfs中。具体流程参见下面第三张图。

HDFS、MapReduce和Yarn原理图解

mapreduce框架内部核心工作机制图解

客户端从hdfs中读数据的过程图解

客户端将数据写入hdfs过程图解

mapreduce程序在yarn上启动-运行-注销全流程图解

疑惑

1. 提交的作业资源是存储在hdfs的哪个目录中的?

    解:如果是将作业提交到yarn集群上的话,那么该目录是由yarn指定的,yarn会依据创建的作业id,为每一个作业在/tmp/root/yarn..../jobid/下创建一个任务目录,用于存储作业资源。

2. 如何计算输入分片?

3. map端和reduce端的context有什么用?

    解:map端的context会调用write()将map处理好的数据写入内存缓冲区中。而reduce中的context则会调用context()将reduce处理好的结果存入hdfs中。

4. InputFormat有多个实现类,这些类的区别以及适用场合?

5. map端数据分区原理?

6. 当map端有多个溢出文件,如果其数据量很大,内存放不下,如何将多个溢出文件合并成一个输出文件?

7. 代码中job.setOutputKeyClass和job.setOutputValueClass有什么用?

8. InputFormat中的二进制输入文件的格式详解,key是什么?value是什么?

9. 一个hdfs块中如何存储小文件?

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值