CENTOS上的网络安全工具(十一)走向Hadoop(3) MapReduce示例解析

        关于MapReduce的介绍及源码分析网上也不少,但是有些东西看过只是看过,没有亲自手撸一遍的过程,总感觉还是有点那么不够实在。所以为了搞明白MapReduce的Grep示例具体怎么工作的,我们上一篇首先构建了可以支撑我们手撸的环境,这一篇就基于这个环境尝试解剖一下Grep。当然,解剖的程度是以我自己感觉大致明白为界限的,毕竟水平有限也没打算过于深究。只是若读者感兴趣,完全可以在该篇基础上继续深入,如果有兴趣写出来最好,余必拜访学习:)

        一、MapReduce的执行逻辑

        第一段不能免俗,先来张MapReduce的执行逻辑图。只不过,如果只看这张图,会感觉似乎明白MapReduce在干什么,但似乎又确实不知道MapReduce在干什么——我刚接触到这个玩意儿的时候,就这么个感觉。

        毕竟要说他在干什么,打个比方就行:一个叫做DFS的图书馆里,存了好几百书架的书,现在需要统计一下,这些书总共有多少本。如果你是一个人,做法无非就是从第一个书架开始,一本一本数;像快一点的话,那就来一群人,分开数完一加就是。

        但是要像真弄清楚,这个比方还得再戏谑……啊不,再细致点:

        这一群人,组建了一个叫MapReduce的公司。其中,有个大佬比较NB,是董事会请来的CEO,大家都听他(JobTracer进程)的,所以他来安排这个任务原则上怎么干:绝大部分的人(Mapper类),每人负责若干书架,一个书架一个书架的数,数完就会报<第几书架,共多少本>;一个(或少数)会算加法的聪明人(Reducer类)负责统计,把所有的<第几书架,共多少本>报告记下来,把本数加起来,最后报告<总数:多少本>。

        如果把这个数图书馆的事作为一个工程项目的话,Mapper和Reducer这种具体搬砖的人,就是现实生活中苦逼的技术线码农们。如果书架不多还好说,如果书架太多,码农们可能数着数着就忘记自己接下来该数哪个书架了,或者一些聪明的混混们跑去摸鱼去了,Reducer可能好久也凑不齐需要统计的数据。JobTracer作为行政线唯一的大佬,盯着码农们干活这种事当然不能亲自操刀,所以还要找一些具备PUA特长的中层们(TaskTracer进程),这些中层们负责盯着码农们干活,干得快的,就多分点任务给他,让他多数几个书架;摸鱼太久的,叫到办公室批评教育洗洗脑,重新分配任务从头数;累傻累晕的直接拖出去,招个新人进来顶岗……

        老板JobTracer会把任务情况写在公司门口的大白板上,比如任务地点在哪,公司派出多少人,行政线是谁,技术线可调度人员多少,相互怎么配合等等,这个白板叫做Configuration;然后任务(Job类)执行路线会写在一个叫Grep的红头文件里面,安排秘书ToolRunner下发。这个Tool红头文件其实是一个有格式的工单,规定了开展工作必须的要素(Job类——工作内容,Configured接口——执行工作内容需要的附件,也就是白板上的内容,Tool接口——秘书用这个接口签字下发)。JobTracer和TaskTracer会按照这个文件精神督促搬砖的干活。

        当然,光靠学习文件精神是没法把活干起来的,所以Mapper和Reducer们还需要工作文档来记录工作内容,这个文档叫做Context,Mapper和Reducer都会管理和使用。

        哦,对了。为了防止搬砖的对任务理解不一致,Tool文件中规定了任务的输入输出格式,分别叫做InputFormat和OutputFormat。如果遇到任何与规定不符的输入,Mapper和Reducer会拒绝干活,毕竟输入错了责任在输出方,自己傻不愣登接着往下干,责任就说不清,锅就被自己背上了

        :P

        嗯,大公司就有大公司的道理,看起来很简单的事情,真要招一群人来做,其实就没那么简单了。上面这一段看起来就比较复杂,但其实HR部门(Yarn)还没考虑进来。如果公司的业务比数书架要复杂得多,那可能还得有个规划办(ZooKeeper)。

        二、MapReduce主要组件梳理

        上面我们大概了解了一下MapReduce公司的组织架构和各部门的职能分工,接着就从代码的角度挨个梳理:

        1.Configuration

       

 1 package org.apache.hadoop.conf;
 2 
 3 /** Base class for things that may be configured with a {@link Configuration}. */
 4 public class Configured implements Configurable {
 5 
 6   private Configuration conf;
 7 
 8   /** Construct a Configured. */
 9   public Configured() {
10     this(null);
11   }
12   
13   /** Construct a Configured. */
14   public Configured(Configuration conf) {
15     setConf(conf);
16   }
17 
18   // inherit javadoc
19   public void setConf(Configuration conf) {
20     this.conf = conf;
21   }
22 
23   // inherit javadoc
24   public Configuration getConf() {
25     return conf;
26   }
27 
28 }

        Hadoop集群环境配置需要设置4个xml文件,因为之前我们只安装了Single Node版本,所以还没涉及到这一块。不过在官方的Cluster Setup指南里,是如下图这般说的——Site-specific configuration那一行。

        Hadoop使用了Configuration类来维护管理这些xml文件中的配置信息。Configuration源代码中一段描述指出了这个事实:       

/*
 * <p>Unless explicitly turned off, Hadoop by default specifies two 
 * resources, loaded in-order from the classpath: <ol>
 * <li><tt>
 * <a href="{@docRoot}/../hadoop-project-dist/hadoop-common/core-default.xml">
 * core-default.xml</a></tt>: Read-only defaults for hadoop.</li>
 * <li><tt>core-site.xml</tt>: Site-specific configuration for a given hadoop
 * installation.</li>
 * </ol>
*/

        当然,也可以自己构建xml文件放在同目录下,使用Configuration进行操作——然而,这些内容我们就不打算关注了。理解mapreduce只需要知道,Configuration记录了hadoop的配置信息,而mapreduce任务如何分解、如何分发等等是需要使用这些信息的,所以,每个job里面都需要携带并传递Configuration的句柄,以确保想知道配置的时候能够读得到。

        Configuration类实现了Iterable和Writable接口。Hadoop.io.Writable主要是2个方法:

                方法1:write(Java.io.DataOutput arg)

                方法2:readFields(Java.io.DataInput arg)

        Java.lang.Iterable主要是2个方法:

                方法1:forEach(Consumer<? Super T> action)

                方法2:spliterator()

       从此也可见Configuration的主要功能就是读写枚举配置信息。

       2.Configurable接口和Configured类

        Configured类用于具体实现Configurable接口

        上图来自org.apache.hadoop.conf-Configured_weixin_34148508的博客-CSDN博客 文章。

        Configurable接口主要维护一个私有属性:

        属性1:private Configuration conf

        并定义2个方法:

        方法1:setConf(Configuration conf)

        方法2:getConf(Configuration conf)

        可见,该接口主要确定继承自Configured类的类可维护Configuration配置环境,并提供对该配置环境的管理手段。

        3.Tool接口

public interface Tool extends Configurable {
    int run(String [] args) throws Exception;
    }
}
Public class Configured implements  Configurable{
    Private Configuration conf;
    Public Configured(Configuration conf){setConf(conf);}//构造方法
    Public void setConf(Configuration conf)
    {
        This.conf=conf;
    }
    Public getConf()
    {
        Return conf;
    }
}

           Tool接口继承自Configurable接口,新增了1个方法:

        方法1:run(String[] args)

        由于后面可以看到,ToolRunner会调用实现MapReduce算法的类的run方法,以启动一个MapReduce工作。所以,实现MapReduce算法的类需要实现Tool接口。

        在Tool类的注释中,给出了Tool.run()的典型实现方法:

public class MyApp extends Configured implements Tool {
    public int run(String[] args) throws Exception {
        Configuration conf = getConf();//获取Hadoop环境
        JobConf Job = new JobConf(conf,MyApp.class);//构建MR环境
        Path in = new Path(args[1])
        Path out = new Path(args[2])
        //设置MapReduce任务
        Job.setJobName(“my-app”);
        Job.setInputPath(in);      //该方法并不存在,可能是伪代码
        Job.setOutputPath(out);     //该方法并不存在,可能是伪代码
        Job.setMapperClass(MyMapper.class)
        Job.setReducerClass(MyReducer.class)
        //提交MapReduce任务并等待结束
        RunningJob runningJob = JobClient.runJob(Job);
        if(runningJob.isSuccessful())return 0;
        else return 1;
    }
    public static void main(String[] args) throws Exception{
        //新建Hadoop环境,新建MR任务,并使用ToolRunner启动
        int res = ToolRunner.run(new Configuration(),new MyApp(),args);
        system.exit(res);
    }
}

        4.Grep类

        Grep类用于实现“Grep”MapReduce算法,所以需要实现Tool接口;且如前所述,Tool.run()中需要使用Hadoop的环境配置,故Grep也需要实现Configurable接口。所以,Grep的定义方式是:

        public class Grep extends Configurable implements Tool{}

        和Tool类注释中给出的示例大体上是一样的,区别在于Tool类中示例使用了JobConf类直接控制MR任务的配置,并使用JobClient类,以JobConf为配置参数直接提交任务;而Grep类示例中,使用Job类,似乎是封装更严实,提供的选项更多些:

//设置并构置Hadoop环境
Configuration conf = getConf();
Job grepJob = Job.getInstance(conf);
grepJob.setJobName("grep-search");
grepJob.setJarByClass(Grep.class);
//配置MapReduce环境和任务
FileInputFormat.setInputPaths(grepJob, args[0]);
grepJob.setMapperClass(RegexMapper.class);
grepJob.setCombinerClass(LongSumReducer.class);
grepJob.setReducerClass(LongSumReducer.class);
FileOutputFormat.setOutputPath(grepJob, tempDir);
grepJob.setOutputFormatClass(SequenceFileOutputFormat.class);
grepJob.setOutputKeyClass(Text.class);
grepJob.setOutputValueClass(LongWritable.class);
//启动并等待任务
grepJob.waitForCompletion(true);

        实际上,Grep是在Job.waitForCompletion(true)中,调用了Job.submit();在Job.submit()中,调用了JobSubmitter.submitJobInternal(Job.this,cluster)。

        而在Tool的示例中,是使用静态方法JobClient.runJob(JobConf job),以传入的Hadoop配置环境conf为参数新建一个JobClient对象,并以该对象调用了JobClient类的submitJob方法,其中又调用了JobClient.submitJobInternal(conf);在submitJobInternal中则新建Job对象,并调用了重写的Job.run()方法;该重写方法主要作用是跳过MR配置过程——因为已经配置过了,直接使用Job.submit()提交MR任务,最终也还是归于JobSubmitter.submitJobInternal(Job.this,cluster)。

        输入输出方面,需要注意的是JobConf并没有实现setInputPath方法,但是在JobConf的注释里给出了对应的示例,使用的是FileInputFormat的setInputPaths方法,这个就和Grep示例一样了,内部都是调用的Configuration.set方法。

        总而言之——

        简单的理解,开始一个MR任务的主要动作,其实都是在配置Configuration的内部参数,手段是通过Configuration的set方法、setClass方法、setJar方法等等。配置完成后,以配置好的Configuration为参数创建一个Job对象,submit就行了。

        5.ToolRunner类        


public class ToolRunner {
public static int run(Configuration conf, Tool tool, String[] args) 
    throws Exception{
    if(conf == null) {
      conf = new Configuration();
    }
    GenericOptionsParser parser = new GenericOptionsParser(conf, args);
    //set the configuration back, so that Tool can configure itself
    tool.setConf(conf)
    String[] toolArgs = parser.getRemainingArgs();
    return tool.run(toolArgs);
  }
   public static int run(Tool tool, String[] args) 
    throws Exception{
    return run(tool.getConf(), tool, args);
  }
  
  public static void printGenericCommandUsage(PrintStream out) {
    GenericOptionsParser.printGenericCommandUsage(out);
  }
  
}

        ToolRunner是一个封装类,主要目的是提供了一个静态run函数,用于启动实现tool.run接口的mapreduce任务,通常该run函数在main方法中调用。函数包含3个参数:

        参数1:Configuration conf是整个mapreduce的运行环境封装

        参数2:Tool tool是Mapreduce的算法流程封装

        参数3:args argstring输入输出。

        概要代码如下:

    If(conf==nullptr)conf = new Configuration();
    tool.setConf(conf);
    tool.run(args);

        6.RegexMapper

        Grep使用MapReduce提供的正则搜索Mapper算法RegexMapper,对输入文件进行分片处理并按行匹配模板。RegexMapper类继承自Mapper类。

        Mapper作为模板类共4个参数,用来定义输入KV对和输出KV对的数据类型:

        参数1,2:KEYIN,VALUEIN

        参数3,4:KEYOUT,VALUEOUT

        Mapper内部还嵌入了一个Context抽象类,该类用于管理Mapper算法内部的数据处理,比如:①输出Mapper结果时,是调用Context的write方法;②枚举需要Mapper的输入时,调用的是Context的getCurrentKey、getCurrentValue和nextKeyvalue方法。

        实际上,实现任何一个Map算法都应该从继承Mapper类开始,并重写(override)Mapper的setup、cleanup、map、run方法。

        方法1:setup(Context context),在初始化时调用一次,可用于设置全局参数,比如在RegexMapper中,此处用于初始化pattern、group这2个私有变量的值,其具体取值是通过Grep在run方法中调用conf.set(RegexMapper.PATTERN,args[2])和conf.set(RegexMapper.GROUP,args[3])设置在常量中的。

        方法2:cleanup(Context context),用于在对象销毁时提供清理资源的机会

        方法3:map(KEYIN key,VALUEIN value,Context context),用于定制map方法。每次map被调用时,实际都是一个键值对被传输进来,定制用户需要根据输入key,value值来确定输出键值对,并通过Context.write方法输出。比如mapper中内置了这个默认实现,直接将输入的key,value通过Context.write给写了出去。

        方法4:run(Context context)。run方法代码如下,可见当mapper类被加载时,其主要工作就是从context中枚举被拆分的键值对,然后调用map方法。

{
    setup(context);
    try {
        while (context.nextKeyValue()) {
            map(context.getCurrentKey(), context.getCurrentValue(), context);
        }
    }
    finally {
      cleanup(context);
    }
}

        7.TextInputFormat/FileInputFormat/InputFormat

        在示例代码中,第一次MR操作仅仅对输出的键值对数据类型做了配置,没有对输入进行配置。事实上,在未显式设置的情况下,MapReduce默认使用TextInputFormat作为输入类型,即输入文件为文本文件,输入键值对为<LongWritable,Text>类型,其中Text为文本文件中的每一行,为TextInputFormat中的createRecordReader方法通过实例化LineRecordReader构造。

        TextInputFormat类定义为public abstract class TextInputFormat extends FileInputFormat<longWritable,Text>,可见默认情况下其键值对的类型已明确定义,且TextInputForamt继承自FileInputFormat。

        继承自FileInputFormat类定义为Public abstract class FileInputFormat<K,V> extends InputFormat<K,V>,即该类继承自InputFormat。

        TextInputFormat主要重写了3个方法:

        方法1:RecordReader<LongWirtable,Text> createRecordReader(InputWritable split, TaskAttemptContext context);通过实例化LineRecordReader类,将根据JobSubmitDir中的信息读取的文件分块进一步切分为键值对。

        方法2:protected Boolean isSplitable(JobContext context,Path file),用来指示通过JobSubmit提交的splite是否需要进一步切分。

FileInputFormat主要重写了1个方法:

        方法3:public List<InputSplit> getSplits(JobContext job);主要用于将大文件切分为MapReduce适宜调度的块。该方法将在工作调度程序中使用。

InputFormat是一个抽象类,主要定义了需要被重写的initialize、getSplits、createRecordReader方法。

        8.LineRecordReader/RecordReader

        RecordReader是用于维护管理键值对的抽象类,主要定义了5个方法。

        方法1:initialize(InputSplit,TaskAttemptContext);在初始化时调用一次,可在这里实现对split的进一步拆分,构建键值对列表

        方法2:nextKeyValue();移动到下一个键值对;

        方法3:KEYIN getCurrentKey();得到当前键值对的键;

        方法4:VALUEIN getCurrentValue();得到当前键值对的值;

        方法5:float getProgress();得到当前RecordReader的处理进度(0.0-1.0)

        LineRecordReader继承了recordReader,重写了initialize、nextKeyValue、getCurrentKey、getCurrentValue和getProgress等方法。值得提一下的是,LineRecordReader似乎并未在initialize中直接将split完全拆开后保存,而是仅构建了维护信息,读取了首个键值对。其余键值对仅在nextkeyValue被调用时通过in.readLine(value,……)临时读出。这应该是针对海量文件批处理的特点考虑,毕竟都放在内存里面可能就爆了。

        RecordReader的5个方法,和Context所实现的TaskAttemptContext接口有明显的意义对应关系,容易猜到,在mapper的run方法中,context对应的接口,应该是调用了RecordReader的方法。这个实际上是在MapContextImpl类的nextKeyValue中实现的——MapContextImpl实现了Context规定的接口。

        9.MapReduce的mapper的总体执行流程

        前面提到,整个MapReduce的工作流程都是在JobTracer和TaskTracer的管理下运转的。事实上,这也是人云亦云的结果。我们在3.3.4版本的Single Node中没有观察到这2个进程。当然,这可能是版本(原描述来自于2.0版本)和模式(Single Node不可能出现多进程)的结果。然而我们还是选择相信,因为即使在SingleNode模式下的多线程方式,其工作流程和人云的方式也是非常相似的。至于Multi模式就不尝试了,毕竟我们的目的只是大致了解一下MR的工作流程而已,Single Node模式的流程如下图所示。也由于不求甚解的原因,部分代码并没有深究,所以不能保证完全正确:

         图中,实线箭头代表进程执行流程和函数调用关系;虚线代表参数关联关系;空心箭头代表线程迁移关系;更细的空心箭头则代表数据在被处理过程中的形态变化。

        Grep调用时,InputFormat的具体类型是TextInputFormat,其父类是FileInputFormat,此时getSplits的作用是按照Hadoop默认的块大小切分文件。

        JobSplitWriter.createSplitFiles将切分的文件的文件名、偏移等信息写回文件系统,代码中称其为JobSubmitDir,临时文件位置在/tmp/Hadoop/mapred/staging/下。

        然后,在JobSubmitter中开始正式提交工作,使用的是submit name

        调用submitClient.submitJob()后就看不到代码了,下一次命中是在Local Job Runner Map Task Executor线程中。

        MapTask.run(),这里读取JobSubmitDir,并根据任务状态(setup/cleanup/runnewmapper或者runoldmapper)执行动作。如调用runNewMapper函数,构建mapper;进一步调用NewTrackingRecordReader方法,其中会调用具体输入类TextInputFormat的createRecordReader方法,基于LineRecordReader类,在split的基础上,再次拆分split为RecordReader。

        然后,MapTask会真正的启动Map任务:

        Input.initialize(split,mapperContext);

        Mapper.run(mapperContext);

        在Mapper.run中对RecordReader拆分的键值对进行处理。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值