逐行解读wordcount mr代码 ---从0开始学MapReduce

主要内容

wordcount实现了对一篇文章的每个单词的计数.driver类是MapReduce的入口,里面的map和reduce分别是实现的两个部分,对于这个例子,我们在map和reduce之间使用了combiner进行了优化(这里的combiner和reduce差不多)

driver类

前置知识

在Java中,包(package)是一个命名空间,用于组织一组相关的类和接口。使用包可以确保不同库或应用程序中的类名不会发生冲突。此外,它还为大型应用程序或库提供了结构和组织,使得代码管理和模块化更为简便。

从你给出的代码中,我们可以看到第一行是:

package hadoop_test.word_count_demo_01;

这意味着WordCountDriver类是hadoop_test.word_count_demo_01包的一部分。这里:

  • hadoop_test 是一个顶级包。
  • word_count_demo_01hadoop_test的子包。

一些关于包的要点:

  1. 目录结构:Java的包结构直接映射到文件系统的目录结构。例如,一个名为hadoop_test.word_count_demo_01的包的类文件将存放在hadoop_test/word_count_demo_01/目录下。

  2. 命名约定:包通常使用小写字母命名。为了确保唯一性,经常使用公司的网站域名的反向命名作为包的开始部分,例如:com.mycompany.myproject.

  3. import语句:如果你想在当前包中的类中使用另一个包中的类,你需要使用import语句来导入那个类。例如,代码中使用了import org.apache.hadoop.conf.Configuration;来导入Hadoop的Configuration类。

  4. 默认包:如果一个Java类没有指定包,那么它属于一个无名的包,通常称为默认包。但使用默认包并不是一个好的实践,因为它不能提供合适的命名空间和组织结构。

总的来说,包为Java应用程序提供了一个结构化的方式来组织和管理代码,也提供了一个命名空间来避免类名冲突。

hadoop库

当然可以。首先,Hadoop是一个开源框架,用于分布式存储和处理大量数据。这个代码示例是基于Hadoop的MapReduce编程模型的Word Count程序,目的是统计文本中每个单词的出现次数。

让我们详细地逐行解释代码:

  1. package hadoop_test.word_count_demo_01;

    • 这定义了代码所属的Java包。
  2. 接下来的部分是导入Java库:

    • org.apache.hadoop.* 是Hadoop的核心库。
    • hadoop_test.Utils_hadoop 可能是一个自定义工具类,用于执行与Hadoop文件系统(HDFS)相关的操作。
  3. public class WordCountDriver {

    • 定义一个名为WordCountDriver的公共类。这是整个程序的入口点。
  4. public static void main(String[] args) throws Exception {

    • 主函数,是Java程序的执行入口。
  5. Configuration conf = new Configuration();

    • 创建Hadoop的配置对象。此对象包含了与Hadoop集群通信所需的各种配置信息。
  6. Job job = Job.getInstance(conf);

    • 使用当前的Hadoop配置创建一个新的Job实例。Job代表了一个单独的MapReduce任务。
  7. job.setJarByClass(WordCountDriver.class);

    • 这告诉Hadoop应该使用WordCountDriver类所在的JAR文件来运行任务。这样,Hadoop就可以找到和运行你的代码了。
  8. job.setMapperClass(wordMapper.class);

    • 这指定了要使用的Mapper类。Mapper负责处理输入数据,为每一个输入的行生成一个或多个键值对。
  9. job.setMapOutputKeyClass(Text.class);

    • 指定Mapper输出的键的类型,这里是Text类型,通常用于表示字符串。
  10. job.setMapOutputValueClass(LongWritable.class);

    • 指定Mapper输出的值的类型,这里是LongWritable类型,用于表示长整型数字。
  11. job.setReducerClass(wordReducer.class);

    • 这指定了Reducer类。Reducer负责处理从所有的Mapper传来的数据,并执行合并操作。
  12. job.setOutputKeyClass(Text.class);

    • 指定Reducer输出的键的类型。
  13. job.setOutputValueClass(LongWritable.class);

    • 指定Reducer输出的值的类型。
  14. job.setCombinerClass(WordCountCombine.class);

    • 设置一个Combiner,它是一个本地的Reducer,可以在每个Mapper节点上运行,以减少网络上传输的数据量,这个并不是必须的。
  15. FileInputFormat.setInputPaths(job, new Path("/hadoop_test/word_count/acticle.txt"));

    • 指定输入数据的路径。这里读取/hadoop_test/word_count/acticle.txt文件。
  16. if(Utils_hadoop.testExist(conf,"/hadoop_test/word_count/word_count_result")){ ... }

    • 使用工具类检查输出目录是否存在。如果存在,那么删除这个目录。这是为了确保输出目录在运行前是空的,否则Hadoop会报错。
  17. FileOutputFormat.setOutputPath(job, new Path("/hadoop_test/word_count/word_count_result"));

    • 设置输出数据的路径。
  18. job.waitForCompletion(true);

    • 启动Job并等待其完成。

最后,当你运行这个WordCountDriver类时,它会配置和启动WordCount任务,处理指定的输入文件,并将结果保存到指定的输出目录。

提示

对于一般的MapReduce任务这些操作基本上是固定的,创建 Configuration 实例和job实例,定义map,reduce,combiner(可选),Partitioner(可选)类和他们的输出类型,定义输入输出位置

LongWritable和Text类

LongWritableText 是Apache Hadoop特定的数据类型,它们在Java环境中被设计用于MapReduce的编程模型。它们在C++中没有直接的对应,因为它们是为了解决Hadoop在处理大数据时面临的特定问题而设计的。

  1. LongWritable:

    • LongWritable 是Hadoop的一种序列化形式的长整数数据类型。它对应于Java的原始 long 类型。
    • Writable 是Hadoop中所有可序列化数据类型的接口。这意味着这些数据类型可以被轻松地写入或从存储(如HDFS)和网络传输中读出。
    • LongWritable 提供了比Java内置的 Long 类更加空间和时间高效的序列化方法。在处理大数据集时,这种高效性是很关键的。
  2. Text:

    • Text 是Hadoop的字符串类型,是一种可序列化、可比较的数据类型,类似于Java的 String 类型。
    • 但与String不同,Text 类是可变的。这意味着你可以在不重新分配内存的情况下更改其内容,这在处理大量数据时非常有用,因为它可以减少内存分配和垃圾回收的开销。
    • 另外,Text 类也对UTF-8编码进行了优化。

在C++中,你会有基本的数据类型,如 longstring。但当涉及到分布式计算和大数据处理时,传统的数据类型可能不足以满足性能和可扩展性的要求。这就是为什么Hadoop需要设计自己的数据类型的原因。这些数据类型不仅仅是为了存储数据,还要考虑如何高效地在网络上传输数据、如何高效地序列化和反序列化数据等问题。

序列化

序列化是一个过程,通过它,你可以将一个对象或数据结构转换为一个可以被存储或传输的线性形式,如字节流或字符串。这个线性形式可以随后被重新转换(或反序列化)回原始的对象或数据结构。这样,数据就可以轻松地存储到文件、数据库或通过网络发送。

简单地说,序列化就是将复杂的数据结构(如对象)转换为简单的形式(如字节流),使其易于存储和传输。

这里有一些序列化的常见应用场景:

  1. 文件存储:当你想将一个对象保存到文件中以供稍后使用时,你可以将其序列化到文件中,然后在需要的时候反序列化它。

  2. 网络通信:如果你想在网络上发送一个对象(例如,从客户端到服务器),你可以将其序列化为字节流,通过网络发送,然后在接收端进行反序列化。

  3. 缓存:在某些缓存解决方案中,对象被序列化后存储在缓存中。当对象被检索时,它会被反序列化。

  4. 数据库存储:某些数据库允许你存储序列化的对象。

为了更具体地说明,让我们考虑一个简单的例子:你有一个代表用户的对象,其中包含姓名、地址和电话号码。如果你想将这个对象保存到文件或通过网络发送,你需要将其转换为一个格式(如字节流或JSON字符串),这样它就可以被写入文件或发送。这个转换的过程就是序列化。当你想从文件中读取这个用户或从网络接收时,你将进行反序列化操作,将字节流或JSON字符串转换回用户对象。

需要注意的是,不是所有的对象都可以(或应该)被序列化。对象的序列化可能涉及一些复杂的问题,如循环引用、安全性、版本控制等。

总结

序列化就是将数据(通常是复杂的数据结构或对象)转换成一种格式(通常是字节流或字符串),这种格式方便存储(如保存到文件或数据库中)和发送(如通过网络传输)。反序列化是相反的过程,即从这种格式恢复数据到其原始的数据结构或对象。

这种转换的目的是为了确保数据的持久性、传输性和互操作性。

MapReduce里的数据类型

以下是Java的原始数据类型和Hadoop的Writable类型之间的常见对应关系:

Java 原始数据类型Hadoop Writable 类型
intIntWritable
longLongWritable
floatFloatWritable
doubleDoubleWritable
booleanBooleanWritable
byteByteWritable
StringText

除此之外,Hadoop还提供了一些其他的Writable数据类型,例如:

  • ArrayWritable:一个可变大小的数组。
  • MapWritable:一个可序列化的map。
  • NullWritable:一个不携带任何数据的特殊类型,经常用于某些MapReduce操作中,当不需要特定的键或值时。

需要注意的是,虽然这些Writable类型与Java的原始数据类型在功能上相似,但它们是为Hadoop的特定需求(如高效序列化和反序列化)而优化的。在编写MapReduce程序时,通常会使用这些Writable类型而不是Java的标准原始类型或对象。
因为数据一般较大,所以整数常用LongWritable类型

代码和注释

// 定义了一个名为hadoop_test.word_count_demo_01的包。
package hadoop_test.word_count_demo_01;

// 导入必要的Hadoop库和类。
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import hadoop_test.Utils_hadoop;

// 定义WordCountDriver公共类。
public class WordCountDriver {
    // 主函数,程序的入口点。
    public static void main(String[] args) throws Exception {

        // 创建Hadoop的配置对象。
        Configuration conf = new Configuration();
        // 根据给定的配置创建一个新的Job实例。
        Job job = Job.getInstance(conf);

        // 设置当前类作为job的JAR包的位置,这样Hadoop可以在集群上找到这个JAR包。
        job.setJarByClass(WordCountDriver.class);
        
        // 设置Mapper类。Mapper处理输入数据,并产生键值对作为输出。
        job.setMapperClass(wordMapper.class);
        // 设置Mapper输出的键和值的类型。
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(LongWritable.class);

        // 设置Reducer类。Reducer获取键值对并执行聚合操作。
        job.setReducerClass(wordReducer.class);
        // 设置Reducer输出的键和值的类型。
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(LongWritable.class);

        // 设置Combiner类。Combiner是一个可选的优化,可以在本地执行部分reduce操作,从而减少网络传输。
        job.setCombinerClass(WordCountCombine.class);

        // 指定Job的输入文件路径。
        FileInputFormat.setInputPaths(job, new Path("/hadoop_test/word_count/acticle.txt"));

        // 检查输出目录是否已经存在,如果存在则删除。这是为了确保输出目录是空的。
        if(Utils_hadoop.testExist(conf,"/hadoop_test/word_count/word_count_result")){
            Utils_hadoop.rmDir(conf,"/hadoop_test/word_count/word_count_result");
        }
        // 设置Job的输出文件路径。
        FileOutputFormat.setOutputPath(job, new Path("/hadoop_test/word_count/word_count_result"));
        
        // 启动Job并等待其完成。
        job.waitForCompletion(true);
    }
}

mapper

前置知识

context

在Hadoop的MapReduce框架中,Context是一个非常核心的组件,用于允许Mapper和Reducer与其所运行的外部环境进行交互。通过Context对象,Mapper和Reducer可以与Hadoop系统通信,执行一系列操作,例如:

  1. 写入输出:在Mapper和Reducer中,你会使用context.write(key, value)来写入输出。这些输出将被传递给下一个阶段(从Mapper到Reducer或从Reducer到输出文件)。

  2. 获取配置信息:你可以使用context.getConfiguration()来获取当前作业的配置设置。这使得你可以在Mapper或Reducer中访问由驱动程序设置的参数。

  3. 获取计数器:你可以使用context.getCounter()来获取或设置特定的计数器,这可以帮助你跟踪作业的执行情况或特定事件的发生。

  4. 报告任务的进度:如果你的Mapper或Reducer需要处理大量的数据,那么你可能需要定期地调用context.progress()来确保任务不会因为超时而被杀死。这告诉Hadoop你的任务仍在正常工作。

  5. 获取任务的元数据:例如,你可以使用context.getTaskAttemptID()来获取当前任务尝试的唯一标识符。

简而言之,Context对象为Mapper和Reducer提供了一个与Hadoop框架的接口,使它们可以执行输出、读取配置、更新计数器等任务。

在你给出的wordMapper代码示例中,Context对象被用来写入每个单词及其计数到输出。这是通过以下代码完成的:

context.write(new Text(word), new LongWritable(1));

这会将每个单词(作为键)和数字1(作为值)写入到中间输出,随后这些输出会被传递给Reducer进行进一步的处理。

代码

当然可以,这是一个典型的Hadoop MapReduce程序中的Mapper类。我将为你逐行解释其作用:

package hadoop_test.word_count_demo_01;

这行代码声明了该类所属的包。包用于Java中的命名空间管理和代码组织。

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

这些是你需要导入的库。它们来自Hadoop的API,提供了数据类型(如LongWritableText)和Mapper的基类。

import java.io.IOException;

导入Java的IO异常处理库,因为在map方法中可能会抛出这种异常。

public class wordMapper extends Mapper<LongWritable,Text,Text,LongWritable>{

这是你的Mapper类的声明。它继承了Hadoop的Mapper类。泛型参数 <LongWritable,Text,Text,LongWritable> 指的是:

  • 输入的key是LongWritable类型(通常代表数据的偏移量)。
  • 输入的value是Text类型(一行文本数据)。
  • 输出的key是Text类型(单词)。
  • 输出的value是LongWritable类型(计数,通常为1)。
@Override
protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

这是你需要覆盖的map方法。每处理输入数据的一行,这个方法就会被调用一次。

String line =  value.toString();

将输入的Text类型的值(一行数据)转换为Java的String类型。

System.out.println(line);

将处理的这一行数据打印到控制台(主要用于调试)。

String[] data = line.split(" ");

将这一行数据(已转为字符串)按空格分隔,得到一个字符串数组,每个元素是一个单词。

for (String word: data) {

遍历上述得到的单词数组。

System.out.println("word:"+word+": value:"+1);

将当前处理的单词及其值(此处为1)打印到控制台。

context.write(new Text(word),new LongWritable(1));

使用上下文对象输出单词及其值。这是Mapper的核心功能,即为每个单词输出一个键值对,其中键是单词,值是1。

}

结束单词的遍历循环。

}

结束map方法。

}

结束wordMapper类。

总的来说,这个Mapper类的作用是读取输入数据的每一行,将每一行拆分成单词,然后为每个单词输出一个键值对,其中键是单词,值是1。这是“单词计数”任务的标准Mapper实现。

package hadoop_test.word_count_demo_01;


import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;
//mapper进程,每一个split(block)会启动该类,
public class wordMapper extends Mapper<LongWritable,Text,Text,LongWritable>{
    @Override
//    map方法,对一个block里面的数据 按行 进行读取,处理
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
//  LongWritable key: 指的是偏移量。
// Text value: 每一行的内容
// Context context:上下文
        //    value  =    he  love  bigData

//        1.每行读取文字,变成java的string
        String line =  value.toString();



        System.out.println(line);

//        data =[he,love,bigData]
//        2.业务切分每个单词,切分为字符串数组
        String[] data = line.split(" ");
        System.out.println(line);
//         String ts=  data[3]    ;
//        3.遍历字符串数组,然后一步一步输出(word,1)
        for (String word:
                data
                ) {
//            if(Integer.parseInt(ts)>30 || Integer.parseInt(ts) <39){
//
//            }
//            new Text(word),new LongWritable(1),,  (chess,1)
//            word,1
            System.out.println("word:"+word+": value:"+1);
            context.write(new Text(word),new LongWritable(1));
        }



    }


}

combiner

前置知识

shuffle 和 sorting

在MapReduce模型中,Mapper和Reducer的工作方式及其之间的数据传递是一个核心概念。当Mapper处理数据并使用context.write()写出数据时,这并不直接将数据传递给Reducer。相反,Hadoop的框架会处理这个数据传输的过程,并进行必要的排序和分组。让我们逐步解释这一过程:

  1. Mapper的输出
    假设我们有以下数据,并且我们的任务是计算每个单词的出现次数:
hello world
hello hadoop

对于上述数据,Mapper可能会产生以下输出:

<hello, 1>
<world, 1>
<hello, 1>
<hadoop, 1>

//这个是上个map阶段输出的数据
这是使用context.write(new Text(word), new LongWritable(1));生成的。

  1. Shuffling and Sorting
    在Mapper和Reducer之间,有一个被称为"Shuffling and Sorting"的阶段。在这个阶段,Hadoop会收集所有Mapper的输出,并按键进行排序。同时,所有具有相同键的值都会被组合在一起。这是如何得到Iterable<LongWritable> values的原因。

对于我们的例子,"Shuffling and Sorting"阶段的输出可能是:

<hello, [1, 1]>
<hadoop, [1]>
<world, [1]>
  1. Reducer的输入
    现在,当Reducer开始执行时,它每次会接收一个键和该键的所有相关值的迭代器。这就是Text keyIterable<LongWritable> values

继续使用我们的例子,Reducer会三次被调用:

  • 第一次:键是hello,值的迭代器包含两个1。
  • 第二次:键是hadoop,值的迭代器包含一个1。
  • 第三次:键是world,值的迭代器包含一个1。

然后Reducer(或Combiner,如你所示的例子)可以迭代这些值并将它们累加起来,得到每个单词的总计数。

总之,context.write(new Text(word), new LongWritable(1));在Mapper中产生一个键值对。但在Reducer开始工作之前,Hadoop框架会进行"Shuffling and Sorting",将具有相同键的所有值组合在一起,为Reducer提供一个键和该键的所有相关值的迭代器。这就是你在Reducer的reduce方法中看到的Iterable<LongWritable> values

combiner

combiner是对map的一个局部聚合,它与reduce的输入输出相同(输入输出都是(key,value))的键值对,所以它继承reduce类即使它和reduce有的时候不同,事实上很多时候(比如这个任务它可以复用reduce的代码)

代码和解释

这段代码是一个Hadoop MapReduce的Combiner类。Combiner在MapReduce中起到了一个"本地汇总"的作用,它的行为很像Reducer,但它在Map任务之后、Reduce任务之前运行。其主要目的是减少从Map任务到Reduce任务的数据传输量。

让我们逐行解析这段代码:

  1. package hadoop_test.word_count_demo_01;
    这行声明了这个类所属的Java包。

2-5. import 语句
这几行引入了需要的类和库。

  1. public class WordCountCombine extends Reducer<Text,LongWritable,Text,LongWritable>{
    这定义了一个名为WordCountCombine的类,它继承自Reducer类。类的泛型参数定义了输入键值对和输出键值对的类型。

9-12. 注释
这些行为你提供了对于这个类如何工作的简短解释。

  1. @Override
    这标记意味着下面的方法是从父类中继承过来的,并且在这里被重写。

14-22. protected void reduce(...)
这是Combiner的核心方法,它负责对每个键的值进行汇总。例如,在单词计数的场景中,这个方法会对每个单词的出现次数进行汇总。

  • Text key: 这是一个单词。

  • Iterable<LongWritable> values: 这是与那个单词相关的值的列表(在此例中是出现次数)。

  • Context context: 这是与Hadoop框架的接口,用于写入输出。

  1. long result=0;
    这行初始化一个变量来计算这个单词的总计数。

17-20. 循环
这个循环遍历与当前键(单词)关联的所有值,并将它们累加到result中。

  1. context.write(key, new LongWritable(result));
    这行将累加的结果写入到输出中,以便传递给Reduce任务(或输出到文件,如果没有Reduce任务)。

总的来说,这个Combiner类的目的是在Map任务和Reduce任务之间对数据进行一个"本地汇总",从而减少传输给Reduce任务的数据量。在单词计数的例子中,这意味着如果一个Mapper发现了"apple"这个词10次,而不是传输10个(“apple”, 1)的键值对,它会只传输一个(“apple”, 10)的键值对。

全部代码

package hadoop_test.word_count_demo_01;

import java.io.IOException;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

public class WordCountCombine extends Reducer<Text,LongWritable,Text,LongWritable>{
//Reducer<key_in,value_in,key_out,value_out>
//  key_in,value_in 为map端的输出
//    key_out,value_out 为reduce端的输入
//    这个就是做了局部(本机器下的map汇总)的汇总
	@Override
	protected void reduce(Text key, Iterable<LongWritable> values,
			Context context) throws IOException, InterruptedException {
		long result=0;
		for(LongWritable value:values){
			result=result+value.get();
		}
		context.write(key, new LongWritable(result));
	}
}

reduce

reduce和combiner代码一样(针对wordcount)不再重复(甚至可以在driver里使用相同的类)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值