1、MapReduce序列化(接著昨天的知識繼續學習)
- 序列化 (Serialization)將物件的狀態資訊轉換為可以儲存或傳輸的形式的過程。在序列化期間,物件將其當前狀態寫入到臨時或永續性儲存區。以後,可以透過從儲存區中讀取或反序列化物件的狀態,重新建立該物件。
- 當兩個程序在進行遠端通訊時,彼此可以傳送各種型別的資料。無論是何種型別的資料,都會以二進位制序列的形式在網路上傳送。傳送方需要把這個物件轉換為位元組序列,才能在網路上傳送;接收方則需要把位元組序列再恢復為物件。把物件轉換為位元組序列的過程稱為物件的序列化。把位元組序列恢復為物件的過程稱為物件的反序列化。
- 例子:當將Student類作為Mapper類的輸出型別時,
- 對於Stu學生自定義學生類,作為輸出型別,需要將當前類進行序列化操作 implement Writable 介面
對於各班級中的學生總分進行排序,要求取出各班級中總分前三名學生(序列化)
①Student類進行序列化
package com.mr.top3;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.Serializable;
public class Stu implements Writable {
String id;
String name;
int age;
String gender;
String clazz;
int score;
/*
TODO 使用Hadoop序列化的問題:
java.lang.RuntimeException: java.lang.NoSuchMethodException: com.shujia.mr.top3.Stu.<init>()
*/
public Stu() {
}
public Stu(String id, String name, int age, String gender, String clazz, int score) {
this.id = id;
this.name = name;
this.age = age;
this.gender = gender;
this.clazz = clazz;
this.score = score;
}
@Override
public String toString() {
return id +
", " + name +
", " + age +
", " + gender +
", " + clazz +
", " + score;
}
/*
對於Write方法中是對當前的物件進行序列化操作
*/
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(id);
out.writeUTF(name);
out.writeInt(age);
out.writeUTF(gender);
out.writeUTF(clazz);
out.writeInt(score);
}
/*
readFields方法中是對當前物件進行反序列化操作
*/
@Override
public void readFields(DataInput in) throws IOException {
this.id = in.readUTF(); // 將0101資料反序列化資料並儲存到當前屬性中
this.name = in.readUTF();
this.age = in.readInt();
this.gender = in.readUTF();
this.clazz = in.readUTF();
this.score = in.readInt();
}
}
②Top3主函式
package com.mr.top3;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Top3 {
/*
TODO:將專案打包到Hadoop中進行執行。
*/
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
// TODO MapReduce程式入口中的固定寫法
// TODO 1.獲取Job物件 並設定相關Job任務的名稱及入口類
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "Sort");
// 設定當前main方法所在的入口類
job.setJarByClass(Top3.class);
// TODO 2.設定自定義的Mapper和Reducer類
job.setMapperClass(Top3Mapper.class);
job.setReducerClass(Top3Reducer.class);
// TODO 3.設定Mapper的KeyValue輸出類 和 Reducer的輸出類 (最終輸出)
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(Stu.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// TODO 4.設定資料的輸入和輸出路徑
// 本地路徑
FileSystem fileSystem = FileSystem.get(job.getConfiguration());
Path outPath = new Path("hadoop/out/new_top3");
Path inpath = new Path("hadoop/out/reducejoin");
if (!fileSystem.exists(inpath)) {
throw new FileNotFoundException(inpath+"不存在");
}
TextInputFormat.addInputPath(job,inpath);
if (fileSystem.exists(outPath)) {
System.out.println("路徑存在,開始刪除");
fileSystem.delete(outPath,true);
}
TextOutputFormat.setOutputPath(job,outPath);
// TODO 5.提交任務開始執行
job.waitForCompletion(true);
}
}
③MapTask階段
package com.mr.top3;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/*
TODO
在編寫程式碼之前需要先定義資料的處理邏輯
對於各班級中的學生總分進行排序,要求取出各班級中總分前三名學生
MapTask階段:
① 讀取ReduceJoin的處理結果,並對資料進行提取
② 按照學生的班級資訊,對班級作為Key,整行資料作為Value寫出到 ReduceTask 端
ReduceTask階段:
① 接收到整個班級中的所有學生資訊並將該資料存放在迭代器中
*/
public class Top3Mapper extends Mapper<LongWritable, Text, Text, Stu> {
/**
* 直接將學生物件傳送到Reduce端進行操作
* ① 對於Stu學生自定義學生類,作為輸出型別,需要將當前類進行序列化操作 implement Writable 介面
* ② 同時需要在自定義類中保證 類是具有無參構造的
* 執行時會出現:
* java.lang.RuntimeException: java.lang.NoSuchMethodException: com.shujia.mr.top3.Stu.<init>()
* 從日誌上可以看到呼叫了 Stu.<init>() 指定的就是無參構造
* 從邏輯上:
* 在Mapper端 構建了Stu物件 => 透過呼叫其 write 對其進行了序列化操作
* 在Reducer端 需要對其進行反序列化 => 透過無參構造建立自身的空參物件 => 呼叫readFields方法進行 反序列化
* 將資料賦予給當前的空參物件屬性
*/
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, Stu>.Context context) throws IOException, InterruptedException {
// 1500100009 沈德昌,21,男,理科一班,251 => 表示讀取到的資料
String[] split = value.toString().split("\t");
if (split.length == 2) {
String otherInfo = split[1];
String[] columns = otherInfo.split(",");
if (columns.length == 5) {
String clazz = columns[3];
Stu stu = new Stu(split[0], columns[0], Integer.valueOf(columns[1]), columns[2], columns[3], Integer.valueOf(columns[4]));
context.write(new Text(clazz), stu);
}
}
}
}
④ReduceTask階段
package com.mr.top3;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/*
TODO ReduceTask階段
*/
public class Top3Reducer extends Reducer<Text, Stu, Text, NullWritable> {
/**
* 對一個班級中所有的學生成績進行排序 =>
* 1.將資料儲存在一個容器中
* 2.對容器中資料進行排序操作
* 對排序的結果進行取前三
*
* @param key 表示班級資訊
* @param values 一個班級中所有的學生物件
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void reduce(Text key, Iterable<Stu> values, Reducer<Text, Stu, Text, NullWritable>.Context context) throws IOException, InterruptedException {
/*
TODO 當程式執行到Reducer端時,需要對Values中的資料進行遍歷,獲取每一個學生物件
但是在新增過程中,ArrayList中所有的物件資訊都變成一樣的。
表示當前 ArrayList儲存的物件為1個,每次新增的引用資訊都是指向一個物件地址
如何解決?
每次獲取到物件後,對其進行克隆一份
*/
ArrayList<Stu> stus = new ArrayList<>();
for (Stu stu : values) {
Stu stu1 = new Stu(stu.id, stu.name, stu.age, stu.gender, stu.clazz, stu.score);
stus.add(stu1);
}
// 進行排序操作
Collections.sort(stus,
new Comparator<Stu>() {
@Override
public int compare(Stu o1, Stu o2) {
int compareScore = o1.score - o2.score;
return -compareScore > 0 ? 1 : (compareScore == 0 ? o1.id.compareTo(o2.id) : -1);
}
}
);
// 對排序的結果進行遍歷
for (int i = 0; i < 3; i++) {
context.write(new Text(stus.get(i).toString()+","+(i+1)),NullWritable.get());
}
}
}
MapReduce進階
下面我們進入到進階階段的學習
1、資料切片
1.1MapReduce預設輸入處理類
- InputFormat
- 抽象類,只是定義了兩個方法。
- FileInputFormat
- FileInputFormat是所有以檔案作為資料來源的InputFormat實現的基類,
- FileInputFormat儲存作為job輸入的所有檔案,並實現了對輸入檔案計算splits的方法。至於獲得記錄的方法是有不同的子類——TextInputFormat進行實現的。
- TextInputFormat
- 是預設的處理類,處理普通文字檔案
- 檔案中每一行作為一個記錄,他將每一行在檔案中的起始偏移量作為key,每一行的內容作為value
- 預設以\n或Enter鍵作為一行記錄
1.2資料切片分析
- 在執行mapreduce之前,原始資料被分割成若干split,每個split作為一個map任務的輸入。
- 當Hadoop處理很多小檔案(檔案大小小於hdfs block大小)的時候,由於FileInputFormat不會對小檔案進行劃分,所以每一個小檔案都會被當做一個split並分配一個map任務,會有大量的map task執行,導致效率底下
- 例如:一個1G的檔案,會被劃分成8個128MB的split,並分配8個map任務處理,而10000個100kb的檔案會被10000個map任務處理
- Map任務的數量
- 一個InputSplit對應一個Map task
- InputSplit的大小是由Math.max(minSize, Math.min(maxSize,blockSize))決定
- 單節點建議執行10—100個map task
- map task執行時長不建議低於1分鐘,否則效率低
- 特殊:一個輸入檔案大小為140M,會有幾個map task?
- 對應一個切片,但是其實140M的檔案是對應的兩個block塊的
- FileInputFormat類中的getSplits
具體看資料切片的筆記
2、執行流程
- MR執行流程
2.1MR執行過程-map階段
- map任務處理
- 1.1 框架使用InputFormat類的子類把輸入檔案(夾)劃分為很多InputSplit,預設,每個HDFS的block對應一個InputSplit。透過RecordReader類,把每個InputSplit解析成一個個<k1,v1>。預設,框架對每個InputSplit中的每一行,解析成一個<k1,v1>。
- 1.2 框架呼叫Mapper類中的map(...)函式,map函式的形參是<k1,v1>對,輸出是<k2,v2>對。一個InputSplit對應一個map task。程式設計師可以覆蓋map函式,實現自己的邏輯。
- 1.3
-
(假設reduce存在)框架對map輸出的<k2,v2>進行分割槽。不同的分割槽中的<k2,v2>由不同的reduce task處理。預設只有1個分割槽。
-
(假設reduce不存在)框架對map結果直接輸出到HDFS中。
-
- 1.4 (假設reduce存在)框架對每個分割槽中的資料,按照k2進行排序、分組。分組指的是相同k2的v2分成一個組。注意:分組不會減少<k2,v2>數量。
- 1.5 (假設reduce存在,可選)在map節點,框架可以執行reduce歸約。
- 1.6 (假設reduce存在)框架會對map task輸出的<k2,v2>寫入到linux 的磁碟檔案中。
- 至此,整個map階段結束
2.2MR執行過程-shuffle過程
- 1.每個map有一個環形記憶體緩衝區,用於儲存map的輸出。預設大小100MB(io.sort.mb屬性),一旦達到閥值0.8(io.sort.spill.percent),一個後臺執行緒把內容溢寫到(spilt)磁碟的指定目錄(mapred.local.dir)下的一個新建檔案中。
- 2.寫磁碟前,要partition,sort。如果有combiner,combine排序後資料。
- 3.等最後記錄寫完,合併全部檔案為一個分割槽且排序的檔案。
2.3MR執行過程-reduce過程
- reduce任務處理
- 2.1 框架對reduce端接收的[map任務輸出的]相同分割槽的<k2,v2>資料進行合併、排序、分組。
- 2.2 框架呼叫Reducer類中的reduce方法,reduce方法的形參是<k2,{v2...}>,輸出是<k3,v3>。一個<k2,{v2...}>呼叫一次reduce函式。程式設計師可以覆蓋reduce函式,實現自己的邏輯。
- 2.3 框架把reduce的輸出儲存到HDFS中。
至此,整個reduce階段結束。
2.4注意
-
一個分割槽對應一個reducertask任務
-
溢寫過程中生成溢寫檔案的排序是快速排序,是發生在記憶體中
-
快速排序是發生在記憶體中歸併排序是發生在磁碟上的
-
一個reducertask維護一個程序,只會生成一個檔案
3、shuffle原始碼
-
Shuffle過程
- 廣義的Shuffle過程是指,在Map函式輸出資料之後並且在Reduce函式執行之前的過程。在Shuffle過程中,包含了對資料的分割槽、溢寫、排序、合併等操作
-
Shuffle原始碼主要的內容包含在 MapOutputCollector 的子實現類中,而該類物件表示的就是緩衝區的物件,
4、自定義分割槽排序
- 如果我們想要實現不同的功能,可以自定義分割槽排序規則
- 預設分割槽下,如果Reduce的數量大於1,那麼會使用HashPartitioner對Key進行做Hash計算,之後再對計算得到的結果使用reduce數量進行取餘得到分割槽編號,每個reduce獲取固定編號中的資料進行處理
- 自定義分割槽需要重寫分割槽方法,根據不同的資料計算得到不同的分割槽編號
例項:將不同學生年齡的資料寫入到不同的檔案中
①主入口程式碼
package com.mr.partitioner;
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.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import java.io.FileNotFoundException;
import java.io.IOException;
public class PartitionerMR {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration conf = new Configuration();
// conf.setClass("mapreduce.job.partitioner.class",agePartitioner.class, Partitioner.class);
Job job = Job.getInstance(conf, "partitionerAge");
job.setJarByClass(PartitionerMR.class);
job.setMapperClass(PartitionerMapper.class);
job.setReducerClass(PartitionerReducer.class);
job.setMapOutputKeyClass(IntWritable.class);
job.setMapOutputValueClass(Stu.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
job.setPartitionerClass(agePartitioner.class);
job.setNumReduceTasks(4);
FileSystem fileSystem = FileSystem.get(job.getConfiguration());
Path inPath = new Path("hadoop/data/students.txt");
Path outPath = new Path("hadoop/out/agePartitioner");
if(!fileSystem.exists(inPath)){
throw new FileNotFoundException(inPath+"路徑不存在");
}
TextInputFormat.addInputPath(job,inPath);
if(fileSystem.exists(outPath)){
System.out.println("路徑存在,開始刪除");
fileSystem.delete(outPath,true);
}
TextOutputFormat.setOutputPath(job,outPath);
job.waitForCompletion(true);
}
}
②MapTask階段
package com.mr.partitioner;
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 java.io.IOException;
/*
TODO 程式碼邏輯
Mapper端
① 讀取學生資料,並對資料進行切分處理,包裝成學生物件
② 將年齡作為Key 學生物件作為Value寫出
注意:學生物件需要進行序列化操作
自定義分割槽器
① 接收到key為年齡 Value為學生物件 => 根據資料中的年齡 設定編號
21 -> 0
22 -> 1
23 -> 2
24 -> 3
Reducer端
① 根據分割槽編號以及對應的Key 獲取資料
② 將相同Key的資料彙集,並寫出到檔案中
*/
public class PartitionerMapper extends Mapper<LongWritable, Text, IntWritable, Stu> {
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, IntWritable, Stu>.Context context) throws IOException, InterruptedException {
String oneLine = value.toString();
String[] columns = oneLine.split(",");
if (columns.length == 5) {
// 1500100013,逯君昊,24,男,文科二班
context.write(new IntWritable(Integer.valueOf(columns[2]))
, new Stu(columns[0], columns[1], Integer.valueOf(columns[2]), columns[3], columns[4])
);
}
}
}
③ReduceTask階段
package com.mr.partitioner;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class PartitionerReducer extends Reducer<IntWritable, Stu, Text, NullWritable> {
@Override
protected void reduce(IntWritable key, Iterable<Stu> values, Reducer<IntWritable, Stu, Text, NullWritable>.Context context) throws IOException, InterruptedException {
for (Stu value : values) {
context.write(new Text(value.toString()),NullWritable.get());
}
}
}
④Partitioner
package com.mr.partitioner;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.mapreduce.Partitioner;
public class AgePartitioner extends Partitioner<IntWritable,Stu> {
/*
TODO 自定義分割槽器
① 接收到key為年齡 Value為學生物件 => 根據資料中的年齡 設定編號
21 -> 0
22 -> 1
23 -> 2
24 -> 3
自定義分割槽器寫法:
abstract class Partitioner<KEY, VALUE>
Partitioner是一個抽象類 需要使用extend 並給定泛型
Key 表示 年齡資料 型別為 IntWritable
Value 表示 學生物件 型別為 Stu
*/
@Override
public int getPartition(IntWritable intWritable, Stu stu, int numPartitions) {
int age = intWritable.get();
int valueAge = stu.age;
switch (age){
case 21 :
return 0;
case 22 :
return 1;
case 23:
return 2;
case 24:
return 3;
default:
return 3;
}
}
}
⑤Student類
package com.mr.partitioner;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class Stu implements Writable {
public String id;
public String name;
public int age;
public String gender;
public String clazz;
/*
TODO 使用Hadoop序列化的問題:
java.lang.RuntimeException: java.lang.NoSuchMethodException: com.shujia.mr.top3.Stu.<init>()
*/
public Stu() {
}
public Stu(String id, String name, int age, String gender, String clazz) {
this.id = id;
this.name = name;
this.age = age;
this.gender = gender;
this.clazz = clazz;
}
@Override
public String toString() {
return id +
", " + name +
", " + age +
", " + gender +
", " + clazz ;
}
/*
對於Write方法中是對當前的物件進行序列化操作
*/
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(id);
out.writeUTF(name);
out.writeInt(age);
out.writeUTF(gender);
out.writeUTF(clazz);
}
/*
readFields方法中是對當前物件進行反序列化操作
*/
@Override
public void readFields(DataInput in) throws IOException {
this.id = in.readUTF(); // 將0101資料反序列化資料並儲存到當前屬性中
this.name = in.readUTF();
this.age = in.readInt();
this.gender = in.readUTF();
this.clazz = in.readUTF();
}
}
5、補充學生例項
需求:對於學生資料資訊,按成績降序排序,ID升序排序,同時滿足使用學生類作為mapper的輸出型別
①主入口
package com.mr.sort_by_stu;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import java.io.FileNotFoundException;
import java.io.IOException;
public class SortByStu {
/*
TODO:將專案打包到Hadoop中進行執行。
*/
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
// TODO MapReduce程式入口中的固定寫法
// TODO 1.獲取Job物件 並設定相關Job任務的名稱及入口類
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "Sort");
// 設定當前main方法所在的入口類
job.setJarByClass(SortByStu.class);
// TODO 2.設定自定義的Mapper和Reducer類
job.setMapperClass(SortByStuMapper.class);
job.setReducerClass(SortByStuReducer.class);
// TODO 3.設定Mapper的KeyValue輸出類 和 Reducer的輸出類 (最終輸出)
job.setMapOutputKeyClass(Stu.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 當Reduce數量為1時為全域性排序
job.setNumReduceTasks(1);
// TODO 4.設定資料的輸入和輸出路徑
// 本地路徑
FileSystem fileSystem = FileSystem.get(job.getConfiguration());
Path outPath = new Path("hadoop/out/sortByStu");
Path inpath = new Path("hadoop/out/reducejoin");
if (!fileSystem.exists(inpath)) {
throw new FileNotFoundException(inpath+"不存在");
}
TextInputFormat.addInputPath(job,inpath);
if (fileSystem.exists(outPath)) {
System.out.println("路徑存在,開始刪除");
fileSystem.delete(outPath,true);
}
TextOutputFormat.setOutputPath(job,outPath);
// TODO 5.提交任務開始執行
job.waitForCompletion(true);
}
}
②Mapper階段
package com.mr.sort_by_stu;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
/*
TODO
*/
public class SortByStuMapper extends Mapper<LongWritable, Text, Stu, NullWritable> {
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Stu, NullWritable>.Context context) throws IOException, InterruptedException {
// 直接使用學生類Stu作為排序的依據
// 1500100009 沈德昌,21,男,理科一班,251 => 表示讀取到的資料
String[] split = value.toString().split("\t");
if (split.length == 2) {
String otherInfo = split[1];
String[] columns = otherInfo.split(",");
if (columns.length == 5) {
Stu stu = new Stu(split[0], columns[0], Integer.valueOf(columns[1]), columns[2], columns[3], Integer.valueOf(columns[4]));
context.write(stu,NullWritable.get());
}
}
}
}
③Reducer階段
package com.mr.sort_by_stu;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/*
TODO ReduceTask階段
*/
public class SortByStuReducer extends Reducer<Stu, NullWritable, Text, NullWritable> {
@Override
protected void reduce(Stu key, Iterable<NullWritable> values, Reducer<Stu, NullWritable, Text, NullWritable>.Context context) throws IOException, InterruptedException {
context.write(new Text(key.toString()),NullWritable.get());
}
}
學生類Stu
package com.mr.sort_by_stu;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class Stu implements WritableComparable<Stu> {
String id;
String name;
int age;
String gender;
String clazz;
int score;
/*
TODO 使用Hadoop序列化的問題:
java.lang.RuntimeException: java.lang.NoSuchMethodException: com.shujia.mr.top3.Stu.<init>()
*/
public Stu() {
}
public Stu(String id, String name, int age, String gender, String clazz, int score) {
this.id = id;
this.name = name;
this.age = age;
this.gender = gender;
this.clazz = clazz;
this.score = score;
}
@Override
public String toString() {
return id +
", " + name +
", " + age +
", " + gender +
", " + clazz +
", " + score;
}
/*
對於Write方法中是對當前的物件進行序列化操作
*/
@Override
public void write(DataOutput out) throws IOException {
out.writeUTF(id);
out.writeUTF(name);
out.writeInt(age);
out.writeUTF(gender);
out.writeUTF(clazz);
out.writeInt(score);
}
/*
readFields方法中是對當前物件進行反序列化操作
*/
@Override
public void readFields(DataInput in) throws IOException {
this.id = in.readUTF(); // 將0101資料反序列化資料並儲存到當前屬性中
this.name = in.readUTF();
this.age = in.readInt();
this.gender = in.readUTF();
this.clazz = in.readUTF();
this.score = in.readInt();
}
/*
TODO 當沒有新增 WritableComparable 實現介面時,自定義類作為Key不能進行排序,同時會報
java.lang.ClassCastException: class com.shujia.mr.sort_by_stu.Stu的錯誤
需求變更: 實現學生資料按照成績降序,成績相同時,按照學號升序排序輸出
*/
@Override
public int compareTo(Stu o) {
int compareScore = this.score - o.score;
return -(compareScore > 0 ? 1 : compareScore == 0 ? -this.id.compareTo(o.id) : -1);
}
}
6、Combine及MapJoin
1、Combine
- combiner發生在map端的reduce操作。
- 作用是減少map端的輸出,減少shuffle過程中網路傳輸的資料量,提高作業的執行效率。
- combiner僅僅是單個map task的reduce,沒有對全部map的輸出做reduce。
- 如果不用combiner,那麼,所有的結果都是reduce完成,效率會相對低下。使用combiner,先完成的map會在本地聚合,提升速度。
- 注意:Combiner的輸出是Reducer的輸入,Combiner絕不能改變最終的計算結果。所以,Combine適合於等冪操作,比如累加,最大值等。
- 求平均數不適合:因為使用combine會提前對部分資料進行計算平均值,這樣會對最終的結果平均值產生影響,導致錯誤。
2、MapJoin
MapJoin用於一個大表和一個小表進行做關聯,然後將關聯之後的結果之間做輸出
MapJoin雖然表面上是沒有Reduce階段的,但是實際上是存在Reduce函式的,只是沒有去執行。
- 之所以存在reduce side join,是因為在map階段不能獲取所有需要的join欄位,即:同一個key對應的欄位可能位於不同map中。Reduce side join是非常低效的,因為shuffle階段要進行大量的資料傳輸。
- Map side join是針對以下場景進行的最佳化:兩個待連線表中,有一個表非常大,而另一個表非常小,以至於小表可以直接存放到記憶體中。這樣,我們可以將小表複製多份,讓每個map task記憶體中存在一份(比如存放到hash table中),然後只掃描大表:對於大表中的每一條記錄key/value,在hash table中查詢是否有相同的key的記錄,如果有,則連線後輸出即可。
- 為了支援檔案的複製,Hadoop提供了一個類DistributedCache,使用該類的方法如下:
- (1)使用者使用靜態方法DistributedCache.addCacheFile()指定要複製的檔案,它的引數是檔案的URI(如果是HDFS上的檔案,可以這樣:hdfs://namenode:9000/home/XXX/file,其中9000是自己配置的NameNode埠號)。JobTracker在作業啟動之前會獲取這個URI列表,並將相應的檔案複製到各個TaskTracker的本地磁碟上。
- (2)使用者使用DistributedCache.getLocalCacheFiles()方法獲取檔案目錄,並使用標準的檔案讀寫API讀取相應的檔案。
MapJoin學生例項
①主入口
package com.mr.mapJoin;
import com.shujia.mr.reduceJoin.ReduceJoin;
import com.shujia.mr.reduceJoin.ReduceJoinMapper;
import com.shujia.mr.reduceJoin.ReduceJoinReducer;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;
import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MapJoin {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
/*
TODO:
需求:需要使用Map端對基本資訊資料和成績資料進行關聯
分析:
① 先讀取students.txt檔案中的資料
② 透過其他方式再讀取score.txt中的資料
問題:
由於需要新增兩種檔案的資料,同時map函式計算時,是按行讀取資料的,上一行和下一行之間沒有關係
於是思路:
① 先讀取score.txt中的資料到一個HashMap中
② 之後再將HashMap中的資料和按行讀取的Students.txt中的每一行資料進行匹配
③ 將關聯的結果再進行寫出操作
注意:
需要在讀取students.txt檔案之前就將score.txt資料讀取到HashMap中
*/
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "MapJoin");
job.setJarByClass(MapJoin.class);
job.setMapperClass(MapJoinMapper.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// TODO 4.設定資料的輸入和輸出路徑
// 本地路徑
FileSystem fileSystem = FileSystem.get(job.getConfiguration());
Path outPath = new Path("hadoop/out/mapJoin");
Path studentInpath = new Path("hadoop/data/students.txt");
// TODO 可以在當前位置將需要在setup函式中獲取的路徑進行快取
job.addCacheFile(new Path("hadoop/out/count/part-r-00000").toUri());
if (!fileSystem.exists(studentInpath)) {
throw new FileNotFoundException(studentInpath+"不存在");
}
TextInputFormat.addInputPath(job,studentInpath);
if (fileSystem.exists(outPath)) {
System.out.println("路徑存在,開始刪除");
fileSystem.delete(outPath,true);
}
TextOutputFormat.setOutputPath(job,outPath);
// TODO 5.提交任務開始執行
job.waitForCompletion(true);
}
}
②Mapper階段
package com.mr.mapJoin;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;
public class MapJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
HashMap<String, Integer> scoreHashMap;
//無參構造方法,構建HashMap,執行一次MapTask任務就會新建立一個HashMap
public MapJoinMapper() {
this.scoreHashMap = new HashMap<>();
}
/**
* 在每個MapTask被執行時,都會先執行一次setup函式,可以用於載入一些資料
*
* @param context
* @throws IOException
* @throws InterruptedException
*/
@Override
protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
/*
TODO 需要讀取 score.txt 中的資料
如果在本地執行,那麼可以透過BufferedReader按行讀取資料,如果是在HDFS中獲取資料
需要透過FileSystem建立IO流進行讀取,並且FileSystem也可以讀取本地檔案系統中的資料
*/
/*
TODO 問題:
① 對於每個MapTask都需要執行一次 setup 函式,那麼當MapTask較多時,每個MapTask都儲存一個HashMap的Score資料
該資料是儲存在記憶體當中的 於是對於MapJoin有一個使用的前提條件
一個大表和一個小表進行關聯,其中將小表的資料載入到集合中,大表按行進行讀取資料
同時小表要小到能儲存在記憶體中,沒有記憶體壓力 通常是在 25M-40M以內的資料量
*/
/*
TODO 作業:
① 當前程式碼中完成的是一對一的關係,如果是1對多的關係,如何處理
② 當前實現的是InnerJoin,那麼對於leftJoin fullJoin如何實現呢?
*/
//建立配置類
Configuration configuration = context.getConfiguration();
//透過FileSystem建立IO流進行讀取
FileSystem fileSystem = FileSystem.get(configuration);
// new Path(filePath).getFileSystem(context.getConfiguration());
// 透過context中的getCacheFiles獲取快取檔案路徑
URI[] files = context.getCacheFiles();
//使用for迴圈是方便有多個檔案路徑的讀取
for (URI filePath : files) {
FSDataInputStream open = fileSystem.open(new Path(filePath));
// FSDataInputStream open = fileSystem.open(new Path("hadoop/out/count/part-r-00000"));
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(open));
String oneScore = null;
while ((oneScore = bufferedReader.readLine()) != null) {
String[] column = oneScore.split("\t");
scoreHashMap.put(column[0], Integer.valueOf(column[1]));
}
}
System.out.println("Score資料載入完成,已儲存到HashMap中");
}
@Override
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
// 1500100004,葛德曜,24,男,理科三班
String oneStuInfo = value.toString();
String[] columns = oneStuInfo.split(",");
if (columns.length == 5) {
String id = columns[0];
// TODO 透過HashMap獲取資料,如果沒有獲取到,那麼閣下如何應對?
Integer score = scoreHashMap.get(id);
oneStuInfo += (","+score);
context.write(new Text(oneStuInfo), NullWritable.get());
}
}
}
MapReduce高階
1、小檔案合併
- CombineFileInputFormat
CombineFileInputFormat是一種新的inputformat,用於將多個檔案合併成一個單獨的split作為輸入,而不是通常使用一個檔案作為輸入。另外,它會考慮資料的儲存位置。
相當於合併之後啟動的MapTask會考慮原先檔案的位置去處理它,不會影響原來檔案的資料。
- 當MapReduce的資料來源中小檔案過多,那麼根據FileInputFormat類中的GetSplit函式載入資料,會產生大量的切片從而導致啟動過多的MapTask任務,MapTask啟動過多會導致申請過多資源,並且MapTask啟動較慢,執行過程較長,效率又較低
- 解決方法:
- 可以使用MR中的combineTextInputFormat類,在形成資料切片時,可以對小檔案進行合併,從而減少MapTask任務的數量
- 小檔案合併的用處:
- 如上述在MapReduce做計算時
- 在HDFS上NameNode儲存了整個HDFS上的檔案資訊,,並且是儲存在記憶體中,由於記憶體空間有限,那麼小檔案合併就可以用於HDFS上某個路徑下產生的多個小檔案進行合併,合併成大的檔案,有利於減少HDFS上的檔案數量。
- 小檔案合併的原理
2、輸出類及其自定義
- 對於文字檔案輸出MapReduce中使用FileOutputFormat類作為預設輸出類,但是如果要對輸出的結果檔案進行修改,那麼需要對輸出過程進行自定義。
- 而自定義輸出類需要繼承FileOutputFormat 並在RecordWriter中根據輸出邏輯將對應函式進行重寫
3、Yarn工作流程及其常用命令
3.1Yarn的工作流程
Yarn的主要元件構成如下
- YARN Client
- YARN Client提交Application到RM,它會首先建立一個Application上下文物件,並設定AM必需的資源請求資訊,然後提交到RM。YARN Client也可以與RM通訊,獲取到一個已經提交併執行的Application的狀態資訊等。
- ResourceManager(RM)
- RM是YARN叢集的Master,負責管理整個叢集的資源和資源分配。RM作為叢集資源的管理和排程的角色,如果存在單點故障,則整個叢集的資源都無法使用。在2.4.0版本才新增了RM HA的特性,這樣就增加了RM的可用性。
- NodeManager(NM)
- NM是YARN叢集的Slave,是叢集中實際擁有實際資源的工作節點。我們提交Job以後,會將組成Job的多個Task排程到對應的NM上進行執行。Hadoop叢集中,為了獲得分散式計算中的Locality特性,會將DN和NM在同一個節點上執行,這樣對應的HDFS上的Block可能就在本地,而無需在網路間進行資料的傳輸。
- Container
- Container是YARN叢集中資源的抽象,將NM上的資源進行量化,根據需要組裝成一個個Container,然後服務於已授權資源的計算任務。計算任務在完成計算後,系統會回收資源,以供後續計算任務申請使用。Container包含兩種資源:記憶體和CPU,後續Hadoop版本可能會增加硬碟、網路等資源。
- ApplicationMaster(AM)
- AM主要管理和監控部署在YARN叢集上的Application,以MapReduce為例,MapReduce Application是一個用來處理MapReduce計算的服務框架程式,為使用者編寫的MapReduce程式提供執行時支援。通常我們在編寫的一個MapReduce程式可能包含多個Map Task或Reduce Task,而各個Task的執行管理與監控都是由這個MapReduceApplication來負責,比如執行Task的資源申請,由AM向RM申請;啟動/停止NM上某Task的對應的Container,由AM向NM請求來完成。
那麼Yarn是如何執行一個MapReduce job的
- 首先,Resource Manager會為每一個application(比如一個使用者提交的MapReduce Job) 在NodeManager裡面申請一個container,然後在該container裡面啟動一個Application Master。 container在Yarn中是分配資源的容器(記憶體、cpu、硬碟等),它啟動時便會相應啟動一個JVM(Java的虛擬機器)。然後,Application Master便陸續為application包含的每一個task(一個Map task或Reduce task)向Resource Manager申請一個container。等每得到一個container後,便要求該container所屬的NodeManager將此container啟動,然後就在這個container裡面執行相應的task
等這個task執行完後,這個container便會被NodeManager收回,而container所擁有的JVM也相應地被退出。 - Yarn執行的流程圖簡圖如下
3.2Yarn配置歷史伺服器
- historyserver程序作用
- 把之前本來散落在nodemanager節點上的日誌統計收集到hdfs上的指定目錄中
- 啟動historyserver
修改相關的配置資訊在記事本筆記中
-
執行sbin/mr-jobhistory-daemon.sh start historyserver
-
透過master:19888觀察
-
當提交了一個MapReduce任務到HDFS上,正常執行完成之後,就可以在master:8088即yarn平臺上檢視執行完的日誌資訊
-
在yarn頁面上點選RUNNING,就可以看到有對應執行完的一個檔案,然後點選logs就可以檢視日誌資訊
3.3Yarn常用命令
- application 選項:
- 前面都是預設的yarn application
- -list 列出RM中的應用程式,可以和-appStates搭配使用
- -appStates
檢視對應狀態的應用程式States可以為 SUBMITTED,ACCEPTED, - RUNNING,FINISHED,FAILED,KILLED
- -kill
強制殺死應用程式 - -status
檢視應用狀態
4、Yarn排程器
- 在實際開發過程中,由於伺服器的計算資源,包括CPU和記憶體都是有限的,對於一個經常存在任務執行的叢集,一個應用資源的請求經常需要等待一段時間才能的到相應的資源,而Yarn中分配資源的就是Scheduler。並且根據不同的應用場景,對應排程器的策略也不相同。Yarn中存在有三種排程器可以選擇 ,分別為FIFO Scheduler 、 Capacity Scheduler 、 Fair Scheduler
4.1FIFO Scheduler
- FIFO Scheduler也叫先進先出排程器,根據業務提交的順序,排成一個佇列,先提交的先執行,並且執行時可以申請整個叢集中的資源。邏輯簡單,使用方便,但是容易導致其他應用獲取資源被阻塞,所以生產過程中很少使用該排程器
- 不常用,比較浪費資源
4.2Capacity Scheduler
- Capacity Scheduler 也稱為容量排程器,是Apache預設的排程策略,對於多個部門同時使用一個叢集獲取計算資源時,可以為每個部門分配一個佇列,而每個佇列中可以獲取到一部分資源,並且在佇列內部符合FIFO Scheduler排程規則
- yarn預設的資源排程器
- yarn執行框架裡使用的就是容量排程器
4.3Fair Scheduler
- Fair Scheduler 也稱為公平排程器,現在是CDH預設的排程策略,公平排程在也可以在多個佇列間工作,並且該策略會動態調整每個作業的資源使用情況
對公平排程器的相關解釋
當有新的任務需要資源時,Fair排程器會嘗試透過動態調整資源分配,來滿足新任務的需求。通常情況下,Fair排程器會根據任務的優先順序和資源需求,合理地重新分配資源。這可能包括降低之前任務的資源配額,或者在後續資源分配時優先給新任務。
如果之前的任務持續佔用大量資源,而新任務的資源需求更為緊急或重要,Fair排程器可能會考慮終止或遷移一些之前的任務,以釋放資源給新任務。儘管這可能影響到之前任務的執行,但Fair排程器會在儘可能保證資源公平的前提下,儘量減少對正在執行的任務的影響。
- 如果一個job1任務開始提交,呼叫了全部的資源排程器裡的MapTask,那麼當job2任務也開始提交執行時,資源排程器會將job1的50%的資源分配給job2,同時,如果job1對應50%的資源上執行的任務沒有完成之前的任務,那麼資源排程器會直接將其kill殺死,即之前的工作都被殺死了,等job2執行完成,該部分的工作任務會從頭開始重新執行。
- 注意:對於同等優先順序的job任務會平均分配剩餘的全部資源,相當於同部門之間是同等級分配資源,不同部門之間也是同等級的,同樣的平均分配總資源。