案例说明

  大数据分析处理万变不离其宗, 核心思想就是一个WorldCount–单词统计. 单词统计, 顾名思义就是将一个文件中出现的所有单词读一遍, 并对相同单词的个数进行统计. 如何处理这个文件? 如何得到每一个单词? 如何对相同的单词进行统计? 这三个问题是需要解决的核心问题, 接下来就一起来看看是如何对一个文件进行WordCount的.

  首先, 来看一下我们测试的数据, 在这匹数据中, 同一行中每个单词之间使用制表符’\t’ 来分隔, 接下来我们先对这批数据的计算思想进行解析, 然后再分别使用MapReduce和Spark技术的API编码实现.

tom	hello	hadoop	cat
world	hadoop	hello	hive
cat	tom	hive
mr	hive	hello
hive	cat	hadoop	world	hello	mr
hadoop	tom	hive	world
hello	tom	world	hive	mr

  通过对这两种技术编码的比较, 可以帮助大家更好的理解之前所说的Spark在表达能力上相较于Hadoop(MR)的优势 Spark优势链接. 除此之外, 更重要的一点是引入SparkCore中弹性分布式数据集(RDD) 的概念, 对RDD有一定认识之后, 将有利于学习RDD的具体原理以及如何使用等知识.

  在Spark中, 一切计算都是基于RDD实现的, RDD可以看作是一个集合, 类似于Scala中的List, Map, 它有着与这些普通集合相同的方法(map, flatmap, foreach…), 但是RDD是重新写的这些方法, 初次之外还有许多其他的方法, 这些方法在Spark中称为算子, 之后的博客中会对它们进行详细介绍.

计算分析

  1. 无论是MapReduce还是Spark, 在读取数据时都是一行一行读取的而且读取的数据都是字符串类型, 因此在处理时要把一行数据看成一条记录;
  2. 既然一行是一条记录, 那么我们在处理时只需要关注这一条记录即可, 其余记录格式与之相同, 不相同格式的数据一般为脏数据, 需要过滤掉. 相同格式的按照规律进行切分(split).
  3. 数据切分完成后, 就可以得到每一个单词, 然后将每一个单词当作key, 把它的value置为1, 得到一些列KV格式的数据, 这些数据中有的key相同, 有的key不同, 但value都是1.
  4. 对这一系列KV格式的数据进行统计, 先按照Key进行分组, 相同Key, 即同一个单词为一组, 这个Key对应多个Value, 构成一个有一个或多个元素1组成的集合. 然后再将同一个Key中所有的Value进行累加, 累加完成之后将累加值最为新的Value, Key还是原来的Key.
  5. 最新的KV格式的数据中, Key代表的是出现的每一个单词, Value则对应该单词出现的次数.

  图解:
在这里插入图片描述

MapReduce的Java实现

  准备环境:

  1. source中的配置文件, 需要将集群中配置好的配置文件导入到工程的resource目录下, 如图:
    在这里插入图片描述
      其中, 需要在mapred-site.xml中设置几个参数:
<configuration>
	<property>
		<!-- 这种设置是在本机模拟集群环境测试 -->
		<name>mapreduce.framework.name</name>
		<value>local</value>
	</property>
</configuration>
<configuration>
	<property>
		<!-- 这种设置是在提交到yarn集群环境运行 -->
		<name>mapreduce.framework.name</name>
		<value>yarn</value>
	</property>
	<property>
		<name>mapreduce.app-submission.cross-platform</name>
		<value>true</value>
	</property>
</configuration>
  1. 导jar包, MR中需要导入大量jar包, 其存放位置在Hadoop解压目录的share/hadoop/mapreduce下, 需要导其中mapreduce下以及mapreduce/lib下的所有jar包.
    在这里插入图片描述

  如何用MR的JavaAPI实现WC, 实现流程步骤以及说明全放在代码中, 方便理解.

  程序入口(主类):

package com.qb.worldcount;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
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.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

public class WordCount {
	/**   
	 * 运行模式:
	 * 	1.local(在本地eclipse中启动多个线程模拟map task和reduce task执行)
	 * 		修改mapred-site.xml中的mapreduce.framework.name,值为local
	 * 	2.提交到集群运行,需先将代码(只需打包类即可)打成jar包然后提交客户端服务器
	 * 		使用hadoop jar main函数所在的类的全路径 /input	/output 命令
	 * 	3.eclipse直接提交任务到集群中运行
	 * 		修改mapred-site.xml中的mapreduce.framework.name,值为yarn
	 * 		修改mapred-site.xml中的mapreduce.app-submission.cross-platform,值为true
	 * 		第二个配置设置是否支持跨平台
	 * 		将应用打成jar包,使用job.setJar("jar包位置")将jar包提交到集群
	 */
	public static void main(String[] args) throws Exception {

		/**
		 * 写MR程序,先创建配置对象.参数置为true说明会自动加载配置文件的信息
		 * org.apache.hadoop.conf.Configuration;
		 */
		Configuration conf = new Configuration(true);
		
		/**
		 * 获取一个Job,Job一个作业即一个MR程序
		 * org.apache.hadoop.mapreduce.Job;
		 */
		Job job = Job.getInstance(conf);
		
		/**
		 * 将当前类设置到job中,设置当前main函数的类
		 * 运行代码的入口函数,需要设置
		 */
		job.setJarByClass(WordCount.class);
		
		job.setJar("f:/wordcount.jar");
		/**
		 * 设置Job对象的输入路径(HDFS路径)
		 * org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
		 */
		FileInputFormat.setInputPaths(job, "/wc.txt"); 		
		
		//选择输出路径
		Path outputPath = new Path("/output/wc");
		
		/**
		 * 输出路径如果存在会报错,先判断是否存在,存在再删除
		 * 操作HDFS需要拿到FileSystem对象(org.apache.hadoop.fs.FileSystem)
		 * Path对象可以直接拿到FileSystem对象
		 */
		FileSystem fs = outputPath.getFileSystem(conf);
		
		if(fs.exists(outputPath)){
			fs.delete(outputPath, true);
		}
		
		/**
		 * 设置Job对象的输出路径,输出路径为Path类型(HDFS路径)
		 * org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
		 */
		FileOutputFormat.setOutputPath(job, outputPath);
		
		/**
		 * 自定义Map class,其中编写map端的逻辑
		 */
		job.setMapperClass(WCMapper.class);
			
		/**
		 * 设置Map输出<key,value>的类型
		 * MapReduce中数据类型使用实现序列化接口的常用类型
		 * String->Text,基本数据类型->Writable
		 */
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(IntWritable.class);
		
		
		/**
		 * 自定义Reduce class,编写reduce端的逻辑
		 */
		job.setReducerClass(WCReduce.class);
		
		//设置reduce的个数
		job.setNumReduceTasks(2);
		
		/**
		 * 最后调用job的waitForCompletion()
		 * 意为是否等待MR完成.
		 * true会一直阻塞直到运行结束,打印执行信息
		 */
		job.waitForCompletion(true);		
	}
}

  设置Map class, 编写map task逻辑.

package com.qb.worldcount;

import java.io.IOException;

import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.util.StringUtils;

/** 
 * 类名:  WCMapper 
 * 类描述:Map class继承Mapper类,需指定泛型
 * 	<KEYIN, VALUEIN, KEYOUT, VALUEOUT>
 * 	<输入k类型,输入v类型,输出key类型,输出value类型>   
 */
public class WCMapper extends Mapper<LongWritable, Text, Text, IntWritable> {

	//分别创建一个输出key和value属性
	Text outKey = new Text();
	IntWritable outValue = new IntWritable(1);
	/**
	 * 重写map方法
	 * LongWritable key:输入的key
	 * Text value:输入的value
	 * Context context:上下文,用于写出每一条数据k-v的内容
	 */
	@Override
	protected void map(LongWritable key, Text value, Context context)
			throws IOException, InterruptedException {
		
		System.out.println(key + "-----------");
		
		//将value切割
		String[] words = StringUtils.split(value.toString(), ' ');
		
		//遍历每一个词,设值为1
		for (String word : words) {
			outKey.set(word);
			//将数据写入buffer
			context.write(outKey, outValue);
		}
	}
}

  设置Reduce class, 编写reduce task逻辑.

package com.qb.worldcount;

import java.io.IOException;

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

/** 
 * 类名:  WCReduce 
 * 类描述:Recude class继承Reducer类,需指定泛型
 * 	<KEYIN, VALUEIN, KEYOUT, VALUEOUT>
 * 	<输入k类型,输入v类型,输出key类型,输出value类型>
 * 	输入的key-value与Map输出的类型一致   
 */
public class WCReduce extends Reducer<Text, IntWritable, Text, IntWritable> {

	/**
	 * 重写reduce方法
	 * 相同的key为一组,一组数据调用一次reduce方法
	 * 此处对组内数据进行累加
	 */
	@Override
	protected void reduce(Text key, Iterable<IntWritable> values,
			Context context) throws IOException, InterruptedException {
		int sum=0;
		for (IntWritable value : values) {
			sum += value.get();
		}
		//写reduce输出文件内容
		context.write(key, new IntWritable(sum));
	}
}

SparkCore的Scala实现

  在SparkCore中一切得计算都是基于RDD(弹性分布式数据集), R(Resilient) D(Distributed ) D(Dataset). RDD 调用的方法称为算子,一般情况下RDD的算子返回的还是RDD. 先对RDD有个大概的了解, 之后再对其进行详细地介绍.

  准备环境:

  1. Scala运行环境, Scala环境搭建
  2. 导入jar包, 开发Spark应用程序时, 只需要导入一个整合包即可.
    在这里插入图片描述

  用Spark写WC:

package com.hpe.spark.core

import org.apache.spark.SparkConf
import org.apache.spark.SparkContext

object WCSpark {
  def main(args: Array[String]): Unit = {

    //创建配置对象
    val conf = new SparkConf()
    //设置App的名称-->方便在监控页面找到 
    conf.setAppName("WCSpark")
    //设置Spark的运行模式-->local本地运行-->用于测试环境
    conf.setMaster("local")
    
    //创建Spark上下文 他是通往集群的唯一通道
    val sc = new SparkContext(conf)    

    // textFile()读取上述数据,读取时是一行行读取,可以是本地也可是HDFS的数据,返回RDD类型的数据
    val lineRDD = sc.textFile("d:/wc.txt")
    // 基于lineRDD中的数据按照\t进行分词
    val wordRDD = lineRDD.flatMap { _.split("\t") }
    // 将wordRDD中的每一条数据封装成一个二元组,每一个单词计数为1  pairRDD[(K:word V:1)]
    val pairRDD = wordRDD.map { (_,1) }    
    // 将pairRDD中相同的单词分为一组,对组内的数据进行累加     
    val restRDD = pairRDD.reduceByKey((v1,v2)=>v1+v2)
    //可简写为:val restRDD = pairRDD.reduceByKey(_+_)
    
    // 根据单词出现的次数来排序,sortBy():根据指定字段来排序,false:指定为降序;
    // foreach对RDD中排好序的数据进行遍历
    restRDD
      .sortBy(x=>x._2, false)
      .foreach(println)
    
    //一直启动,为查看而写  
    while(true){}
    //释放资源
    sc.stop()
  }
}

  但从代码的编写上来看, 不难发现, Spark的表达能力着实比MR强, 上述代码中间处理部分其实还可以更加简洁:

val lineRDD = sc.textFile("d:/wc.txt")
	.flatMap { _.split("\t") }
	.map { (_,1) }   
	.reduceByKey(_+_)
	.sortBy(_._2, false)
	.foreach(println)

  MR中复杂的程序, 在Spark中了了几行就可以轻松解决, 既可以看出Scala语言的灵活性, 又表现了Spark超强的表达能力, 因此Spark在计算上逐渐取代MR.

  这里最后一句while(true){} ,让程序一直执行, 可以在WebUI的监控页面http://localhost:4040进行查看,

posted on 2018-11-21 11:39  七宝嘤嘤怪  阅读(682)  评论(0编辑  收藏  举报