大資料框架之一——Hadoop學習第四天

shmil發表於2024-08-09

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、執行流程

image.png

  • MR執行流程

image.png

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上的檔案數量。
  • 小檔案合併的原理

image.png

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執行的流程圖簡圖如下

image.png

3.2Yarn配置歷史伺服器

  • historyserver程序作用
    • 把之前本來散落在nodemanager節點上的日誌統計收集到hdfs上的指定目錄中
  • 啟動historyserver

修改相關的配置資訊在記事本筆記中

  • 執行sbin/mr-jobhistory-daemon.sh start historyserver

  • 透過master:19888觀察

  • 當提交了一個MapReduce任務到HDFS上,正常執行完成之後,就可以在master:8088即yarn平臺上檢視執行完的日誌資訊

  • 在yarn頁面上點選RUNNING,就可以看到有對應執行完的一個檔案,然後點選logs就可以檢視日誌資訊

image.png

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也叫先進先出排程器,根據業務提交的順序,排成一個佇列,先提交的先執行,並且執行時可以申請整個叢集中的資源。邏輯簡單,使用方便,但是容易導致其他應用獲取資源被阻塞,所以生產過程中很少使用該排程器
  • 不常用,比較浪費資源

image.png

4.2Capacity Scheduler

  • Capacity Scheduler 也稱為容量排程器,是Apache預設的排程策略,對於多個部門同時使用一個叢集獲取計算資源時,可以為每個部門分配一個佇列,而每個佇列中可以獲取到一部分資源,並且在佇列內部符合FIFO Scheduler排程規則
  • yarn預設的資源排程器
  • yarn執行框架裡使用的就是容量排程器

image.png

4.3Fair Scheduler

  • Fair Scheduler 也稱為公平排程器,現在是CDH預設的排程策略,公平排程在也可以在多個佇列間工作,並且該策略會動態調整每個作業的資源使用情況

image.png

對公平排程器的相關解釋

當有新的任務需要資源時,Fair排程器會嘗試透過動態調整資源分配,來滿足新任務的需求。通常情況下,Fair排程器會根據任務的優先順序和資源需求,合理地重新分配資源。這可能包括降低之前任務的資源配額,或者在後續資源分配時優先給新任務。

如果之前的任務持續佔用大量資源,而新任務的資源需求更為緊急或重要,Fair排程器可能會考慮終止或遷移一些之前的任務,以釋放資源給新任務。儘管這可能影響到之前任務的執行,但Fair排程器會在儘可能保證資源公平的前提下,儘量減少對正在執行的任務的影響。

  • 如果一個job1任務開始提交,呼叫了全部的資源排程器裡的MapTask,那麼當job2任務也開始提交執行時,資源排程器會將job1的50%的資源分配給job2,同時,如果job1對應50%的資源上執行的任務沒有完成之前的任務,那麼資源排程器會直接將其kill殺死,即之前的工作都被殺死了,等job2執行完成,該部分的工作任務會從頭開始重新執行。
  • 注意:對於同等優先順序的job任務會平均分配剩餘的全部資源,相當於同部門之間是同等級分配資源,不同部門之間也是同等級的,同樣的平均分配總資源。

相關文章