a) MapReduce 执行迭代计算过程中会反复读写 HDFS,因此可以在 HDFS 中观察到每一轮迭代的输出结果。
b) MapReduce 会提交一系列的作业,而 spark 仅有一个应用,在 Yarn 的 UI 显示会不一样。
c) 对于同样规模的数据集,spark 执行时间应当更短。
1)Ubuntu18.04、jdk1.8、云主机、IDEA2020.3.4
2) Hadoop2.10.1、Spark2.4.7、Scala2.11.12
3) 数据集:web-google.txt;因为数据集太大了,换成了 mini-web-google.txt,一共有十个结点,并做了一些改动 page.txt
4) 迭代次数:20
5) 阻尼系数:0.85
1、编写一个 PageRank 应用,观察 HDFS 中的文件。
2、分别基于 MapReduce 和 Spark 编写一个 PageRank 应用,并通过 Yarn 进行提交,观察 Yarn 界面的区别。
3、针对改进的 min-web-google 数据集,分别在 MapReduce 和 Spark 中运行,统计二者的运行时间,并绘制成图表。
1、MapReduce
PageRankMapper.java
package cn.edu.ecnu.mapreduce.example.java.pagerank;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.io.*;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
// 步骤1:确定输入键值对[K1, V1]的数据类型为[LongWritable, Text],输出键值对[K2, V2]的数据类型为[Text, ReducePageRankWritable]
public class PageRankMapper extends Mapper<LongWritable , Text, Text, ReducePageRankWritable> {
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
// 步骤2:编写处理逻辑,将[K1, V1]转换为[K2, V2]并输出
// 以空格为分隔符切割
String[] pageInfo = value.toString().split(" ");
// System.out.println(pageInfo);
// 网页的排名值
double pageRank = Double.parseDouble(pageInfo[1]);
// System.out.println(pageRank);
// 网页的出站链接数
int outLink = (pageInfo.length-2)/2;
// System.out.println(outLink);
ReducePageRankWritable writable;
writable = new ReducePageRankWritable();
// 计算贡献值并保存
writable.setData(String.valueOf(pageRank / outLink));
// 设置对应的标识
writable.setTag(ReducePageRankWritable.PR_L); //贡献值
// 对于每个出战链接,输出贡献值
for (int i = 2; i < pageInfo.length; i += 2) {
// System.out.println(pageInfo[i]);
context.write(new Text(pageInfo[i]), writable);
}
writable = new ReducePageRankWritable();
// 保存网页信息并标识
// System.out.println(value.toString());
writable.setData(value.toString());
writable.setTag(ReducePageRankWritable.PAGE_INFO); //网页信息
// 以输入的网页信息的网页名称为键进行输出
context.write(new Text(pageInfo[0]), writable);
}
}
ReducePageRankWritable.java
package cn.edu.ecnu.mapreduce.example.java.pagerank;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class ReducePageRankWritable implements Writable {
// 保存网页连接关系(网页信息)或网页排名(贡献值)元组
private String data;
// 标识当前对象保存的元组来自网页连接关系还是网页排名
private String tag;
// 用于标识的常量
public static final String PAGE_INFO = "1";
public static final String PR_L = "2";
@Override
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(tag);
dataOutput.writeUTF(data);
}
@Override
public void readFields(DataInput dataInput) throws IOException {
tag = dataInput.readUTF();
data = dataInput.readUTF();
}
// get和set方法
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public String getTag() {
return tag;
}
public void setTag(String tag) {
this.tag = tag;
}
}
PageRankReducer.java
package cn.edu.ecnu.mapreduce.example.java.pagerank;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.io.*;
import javax.print.DocFlavor;
import java.io.IOException;
// 步骤1:确定输入键值对[K2, V2]的数据类型为[Text, ReducePageRankWritable],确定输出键值对[K3, V3]的数据类型为[Text, NullWritable]
public class PageRankReducer extends Reducer<Text, ReducePageRankWritable, Text, NullWritable> {
// 阻尼系数
private static final double D = 0.85;
@Override
protected void reduce(Text key, Iterable<ReducePageRankWritable> values, Context context)
throws IOException, InterruptedException {
// 步骤2:编写处理逻辑将[K2, V2]转换为[K3, V3]并输出
String[] pageInfo = null;
// 从配置项中读取网页的总数
int totalPage = context.getConfiguration().getInt(PageRank.TOTAL_PAGE, 0);
// 从配置项中读取网页当前的迭代步数
int iteration = context.getConfiguration().getInt(PageRank.ITERATION, 0);
double sum = 0;
for (ReducePageRankWritable value : values) {
String tag = value.getTag();
// System.out.println(tag);
// 如果是贡献值则进行求和,否则以空格为分隔符切分后保存到pageInfo
if (tag.equals(ReducePageRankWritable.PR_L)) {
sum += Double.parseDouble(value.getData());
}
else if (tag.equals(ReducePageRankWritable.PAGE_INFO)) {
// System.out.println(value.getData());
pageInfo = value.getData().split(" ");
}
}
// System.out.println(sum);
// 计算排名值
double pageRank = (1-D) / totalPage + D * sum;
// System.out.println(pageRank);
// 跟新网页信息中的排名值
pageInfo[1] = String.valueOf(pageRank);
// 最后一次迭代输出网页名以及排名值,而其余迭代输出网页信息
StringBuilder result = new StringBuilder();
if (iteration == (PageRank.MAX_ITERATION - 1)) {
// 保留5位小数
result.append(pageInfo[0]).append(" ").append(String.format("%.5f", pageRank));
}
else {
for (String data : pageInfo) {
result.append(data).append(" ");
}
}
context.write(new Text(result.toString()), NullWritable.get());
}
}
PageRank.java
package cn.edu.ecnu.mapreduce.example.java.pagerank;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.*;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.*;
public class PageRank extends Configured implements Tool {
// 设置全局配置项
public static final int MAX_ITERATION = 20; // 最大迭代步数
private static int iteration = 0; // 从0开始记录当前迭代步数
public static final String TOTAL_PAGE = "1"; // 配置项中用于记录网页总数的键
public static final String ITERATION = "2"; // 配置项中用于记录当前迭代步数的键
@Override
public int run(String[] args) throws Exception {
// System.out.println(args[0]);
// System.out.println(args[1]);
// System.out.println(args[2]);
// 步骤1:设置作业的信息
// int totalPage = Integer.parseInt(args[2]);
int totalPage = 10;
getConf().setInt(PageRank.ITERATION, iteration);
getConf().setInt(PageRank.TOTAL_PAGE, totalPage);
Job job = Job.getInstance(getConf(), getClass().getSimpleName());
// 设置程序的类名
job.setJarByClass(getClass());
// 设置数据的输入路径
if (iteration == 0) {
FileInputFormat.addInputPath(job, new Path(args[0]));
}
else {
// 将上一次迭代的输出设置为输入
FileInputFormat.addInputPath(job, new Path(args[1] + (iteration - 1)));
}
// 设置数据的输出路径
FileOutputFormat.setOutputPath(job, new Path(args[1] + iteration));
// 设置Map方法及其输出键值对的数据类型
job.setMapperClass(PageRankMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(ReducePageRankWritable.class);
// 设置Reduce方法及其输出键值对的数据类型
job.setReducerClass(PageRankReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
return job.waitForCompletion(true) ? 0 : -1;
}
public static void main(String[] args) throws Exception {
// 步骤2:运行作业,并计算运行时间
int exitCode = 0;
long startTime = System.currentTimeMillis();
while (iteration < MAX_ITERATION) {
exitCode = ToolRunner.run(new PageRank(), args);
if (exitCode == -1) {
break;
}
iteration++;
}
long endTime = System.currentTimeMillis();
System.out.println("程序运行时间:" + (endTime - startTime) + "ms");
}
}
/*
MapReduce计算模型并不支持迭代,我们不可能通过一个MapReduce作业来完成整个迭代计算,而是需要使用一个MapReduce作业来实现单次迭代计算。
计算某一网页p的排名值需要确定链向p的所有网页M(p),并累加其中每一项网页p的排名值与出战链接数的比值PR(p)/L(p),即对王亚茹p的贡献值。这
一需求与Reduce任务的功能相吻合,只要Map任务产生以p的网页名称为键、PR(p)/L(p)为值的键值对,Reduce任务就能够收集到所有链向p的网页对
其的贡献值,从而得到网页p的新排名值。
*/
2、Spark
PageRank.scala
package cn.edu.ecnu.spark.example.scala.pagerank
import org.apache.spark.rdd.RDD
import org.apache.spark.SparkConf
import org.apache.spark.SparkContext
import org.apache.spark.HashPartitioner
import org.apache.hadoop.fs.FileSystem
import org.apache.hadoop.fs.Path
import java.io.File
import java.util.Date
object PageRank {
def run(args: Array[String]): Unit = {
// 步骤1:通过SparkConf设置配置信息,并创建SparkContext
val conf = new SparkConf
conf.setAppName("PageRank")
conf.setMaster("local") // 仅用于本地进行调试,如在集群中运行则删除本行
val sc = new SparkContext(conf)
// 步骤2:按应用逻辑使用操作算子编写DAG,其中包括RDD的创建、转换和行动等
val iterateNum = 20 // 指定迭代次数
val factor = 0.85 // 阻尼系数
// val text = sc.textFile("inputFilePath")
// System.out.println(args(0))
// System.out.println(args(1))
// 读取输入文本数据
val text = sc.textFile(args(0))
// System.out.println("==================================")
// 将文本数据转换成[网页, {链接列表}]键值对
val links = text.map(line => {
val tokens = line.split(" ")
var list = List[String]()
for (i <- 2 until tokens.size by 2) {
list = list :+ tokens(i)
}
(tokens(0), list)
}).cache() // 持久化到内存
// 网页总数
val N = 10
// System.out.println(N)
//初始化每个页面的排名值,得到[网页, 排名值]键值对
var ranks = text.map(line => {
val tokens = line.split(" ")
(tokens(0), tokens(1).toDouble)
})
// 执行iterateNum次迭代计算
for (iter <- 1 to iterateNum) {
val contributions = links
// 将links和ranks做join. 得到[网页, {{链接列表}, 排名值}]
.join(ranks)
// 计算出每个网页对其每个链接网页的贡献值 = 网页排名值 / 链接总数
.flatMap {
case (page, (links, rank)) =>
links.map(dest => (dest, rank / links.size))
}
ranks = contributions
// 聚合对相同网页的贡献值,求和得到对每个网页的总贡献值
.reduceByKey(_+_)
// 根据公式计算得到每个网页的新排名值
.mapValues(v => (1 - factor) * 1.0 / N + factor * v)
}
// 对排名值保留5位小数,并打印最终网页排名结果
ranks.foreach(t => println(t._1 + " ", t._2.formatted("%.5f")))
ranks.saveAsTextFile(args(1))
// 步骤3:关闭SparkContext
sc.stop()
}
def main(args: Array[String]): Unit = {
// 步骤4:运行作业,并计算运行时间
var startTime =new Date().getTime
run(args)
var endTime =new Date().getTime
println("程序运行时间:" + (endTime - startTime) + "ms") //单位毫秒
}
}
/*
与MapReduce网页链接排名的实现相比,MapReduce程序每次迭代结束时会将本次迭代的结果写入HDFS,下一次迭代再从HDFS中读入上一次迭代的结果,
反复读写HDFS的开销大。而Spark程序每一次迭代得到一个RDD,该RDD可以住存在内存中作为下一次迭代的输入,避免了冗余的读写开销。此外,对于在
迭代过程中保持不变的静态数据(例如,网页链接排名中的网页链接数据),Spark可以利用持久化机制将其缓存在内存中,从而避免冗余加载或冗余计算。
因此,Spark在迭代计算方面性能优于MapReduce。
*/
3、Input
pagerank.txt
A 1.0 B 1.0 D 1.0
B 1.0 C 1.0
C 1.0 A 1.0 B 1.0
D 1.0 B 1.0 C 1.0