Hadoop學習筆記(六)實戰wordcount

配置工程

  1. 在maven官網上下載最新的maven壓縮包并解壓。

  2. 下載IntelliJ IDEA并安裝。

  3. 在IDEA中新建工程,選擇maven,sdk選擇java jdk的目錄,勾選上Create form archetype,選擇quickstart,下一步。


  4. 填寫GroupId和ArtifactId,version填寫1.0,下一步。


  5. User settings file配置選擇下載解壓后的maven目錄下的conf文件夾的settings.xml,然后下一步,完成。


  6. IDEA創建工程,創建好了之后,修改根目錄下的pom.xm文件,設置以下2個內容:

    <!--配置hadoop的遠程倉庫-->
    <dependency>
        <groupId>org.apache.hadoop</groupId>
        <artifactId>hadoop-client</artifactId>
        <version>2.6.0-cdh5.7.0</version>
    </dependency>
    
    <!--配置hadoop版本,初次配置需要下載,要等一段時間-->
    <repositories>
        <repository>
            <id>cloudera</id>
            <url>http://repository.cloudera.com/artifactory/cloudera-repos/</url>
        </repository>
    </repositories>
    

    配置完成后的文件長這個樣子:


    然后我們通過View—>ToolWindows—>Maven Projects調出Maven窗口,可以看到hadoop需要的包我們已經導入進來了。


  7. 配置完成,接下來在src中新建類,開始寫我們的wordcount處理程序。

Mapper&Redeucer

寫MapReduce不可避免的要用到這兩個類:MapperReducer,通過IDEA我們可以查看這兩個類的代碼。

首先是Mapper

public class Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {

    /**
     * The <code>Context</code> passed on to the {@link org.apache.hadoop.mapreduce.Mapper} implementations.
     */
    public abstract class Context
            implements MapContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
    }

    /**
     * Called once at the beginning of the task.
     */
    protected void setup(org.apache.hadoop.mapreduce.Mapper.Context context
    ) throws IOException, InterruptedException {
        // NOTHING
    }

    /**
     * Called once for each key/value pair in the input split. Most applications
     * should override this, but the default is the identity function.
     */
    @SuppressWarnings("unchecked")
    protected void map(KEYIN key, VALUEIN value,
                       org.apache.hadoop.mapreduce.Mapper.Context context) throws IOException, InterruptedException {
        context.write((KEYOUT) key, (VALUEOUT) value);
    }

    /**
     * Called once at the end of the task.
     */
    protected void cleanup(org.apache.hadoop.mapreduce.Mapper.Context context
    ) throws IOException, InterruptedException {
        // NOTHING
    }

    /**
     * Expert users can override this method for more complete control over the
     * execution of the Mapper.
     *
     * @param context
     * @throws IOException
     */
    public void run(org.apache.hadoop.mapreduce.Mapper.Context context) throws IOException, InterruptedException {
        setup(context);
        try {
            while (context.nextKeyValue()) {
                map(context.getCurrentKey(), context.getCurrentValue(), context);
            }
        } finally {
            cleanup(context);
        }
    }
}

Mapper類中的方法還是不多的,首先是setup方法和cleanup方法,從名字就能看出來這兩個方法一個是初始化時調用的,一個是結束的時候調用的,初始化可以處理一些像打開數據庫連接這樣的操作,結束可以關閉數據庫連接,回收一些不用的資源等等。這兩個方法都只會執行一次

然后是map方法,顯然這才是我們繼承Mapper類需要重點重寫的方法,將map工作的處理邏輯都要放在這個方法中,我們看到map方法有三個參數,這就要看Mapper類的第一行,它有四個泛型變量,KEYIN, VALUEIN, KEYOUT, VALUEOUT分別代表輸入的鍵值類型和輸出的鍵值類型,那么這里map參數的變量也不難理解,一個是輸入的鍵,一個是輸入的值,context是用于最后的write方法的,這樣map方法就說完了。

最后是run方法,看到這個run方法的實現,會有一種似曾相識的感覺,沒錯就是設計模式中的模板方法模式Mapper類中定義了一些方法,用戶可以繼承這個類重寫方法以應對不同的需求,但是這些方法內部的執行順序是確定好的,它封裝了程序的算法,讓用戶能集中精力處理每一部分的具體邏輯。run方法是程序在執行中會默認調用的,從他的執行流程來看也給常符合我們的預期,先進行初始化,如果還有輸入的數據,那么調用map方法處理每一個鍵值對,最終執行結束方法。這就是整個map任務的流程。

有了Mapper分析的基礎,Reducer也就不難理解了,下面是Reducer的源碼:

public class Reducer<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {

    /**
     * The <code>Context</code> passed on to the {@link org.apache.hadoop.mapreduce.Reducer} implementations.
     */
    public abstract class Context
            implements ReduceContext<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {
    }

    /**
     * Called once at the start of the task.
     */
    protected void setup(org.apache.hadoop.mapreduce.Reducer.Context context
    ) throws IOException, InterruptedException {
        // NOTHING
    }

    /**
     * This method is called once for each key. Most applications will define
     * their reduce class by overriding this method. The default implementation
     * is an identity function.
     */
    @SuppressWarnings("unchecked")
    protected void reduce(KEYIN key, Iterable<VALUEIN> values, org.apache.hadoop.mapreduce.Reducer.Context context
    ) throws IOException, InterruptedException {
        for (VALUEIN value : values) {
            context.write((KEYOUT) key, (VALUEOUT) value);
        }
    }

    /**
     * Called once at the end of the task.
     */
    protected void cleanup(org.apache.hadoop.mapreduce.Reducer.Context context
    ) throws IOException, InterruptedException {
        // NOTHING
    }

    /**
     * Advanced application writers can use the
     * {@link #run(org.apache.hadoop.mapreduce.Reducer.Context)} method to
     * control how the reduce task works.
     */
    public void run(org.apache.hadoop.mapreduce.Reducer.Context context) throws IOException, InterruptedException {
        setup(context);
        try {
            while (context.nextKey()) {
                reduce(context.getCurrentKey(), context.getValues(), context);
                // If a back up store is used, reset it
                Iterator<VALUEIN> iter = context.getValues().iterator();
                if (iter instanceof ReduceContext.ValueIterator) {
                    ((ReduceContext.ValueIterator<VALUEIN>) iter).resetBackupStore();
                }
            }
        } finally {
            cleanup(context);
        }
    }
}

Reducer中跟Mapper中相同的就不說了,重點在于reduce方法的參數,它將map處理后的結果作為它的輸入參數,回想一下wordcount的處理流程,經過map任務的處理后,變成了每個單詞對應一個list,每個list是一系列的1,1,1,1,表明這個單詞出現的記錄。輸入的鍵當然是單詞,輸入的值實際上是一個列表,java集合中對于列表提供了一個迭代器用于遍歷,使用迭代器進行遍歷在速度上會快很多,因此這里的參數是一個迭代器也不難理解。而reduce方法的默認實現也是通過迭代器去遍歷每個輸入的結果。

到這里我們已經看完了MapperReducer類,并且也知道了每個方法是干什么的了,寫起來自然也就簡單了。

寫wordcount

首先我們需要分別繼承MapperReducer,并且要分別重寫map和reduce方法用來放map和reduce階段的邏輯。

考慮map任務中的輸入輸出鍵值對,我們之前討論過,map輸入的鍵為文件的偏移量,值為一行的內容(這里有個有意思的問題,為什么是一行呢?你會發現等我們寫完了所有的代碼后都沒有看到哪里有定義是按行讀取的,文章最后我們會討論這個問題)。文件的偏移量是LongWritable類型,實現了WritableComparable接口,我們上篇文章說過每個作為鍵的類都必須實現這個接口。這個類可以理解成MapReduce框架對long數據的一種封裝。至于每行的內容當然是String了,Text同樣可以理解成是對String的封裝。知道輸入后我們不難想出思路,先將讀取一行的內容轉成字符串,然后按照空格拆分成String數組,再對數組進行遍歷,每項就是一個單詞,最后按<單詞,1>鍵值對的形式寫出去。下面是代碼:

public static class MyMapper extends Mapper<LongWritable, Text, Text, LongWritable> {
    LongWritable one = new LongWritable(1);
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        //接收到的每一行數據
        String line = value.toString();
        //按照指定的分隔符進行拆分
        String[] words = line.split(" ");
        for (String word : words) {
            //通過上下文把map的處理結果輸出
            context.write(new Text(word), one);
        }
    }
}

看完了MyMapper類,下面我們來考慮ReducerReducer輸入的鍵是單詞,Text類不用說了;值是一個迭代器,這個迭代器所對應的列表中是單詞出現的記錄,也就是很多個1。reduce要做的任務也很簡單,就是將這些1求和輸出。下面是MyReducer

public static class MyReducer extends Reducer<Text, LongWritable, Text, LongWritable> {
    @Override
    protected void reduce(Text key, Iterable<LongWritable> values, Context context) throws IOException, InterruptedException {
        long sum = 0;
        for (LongWritable value : values) {
            sum += value.get();
        }
        //最終統計結果的輸出
        context.write(key, new LongWritable(sum));
    }

最后就是main方法了:

public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
    //創建Configuration
    Configuration configuration = new Configuration();
    //準備清理已存在的輸出目錄
    Path outputPath = new Path(args[1]);
    FileSystem fileSystem = FileSystem.get(configuration);
    if (fileSystem.exists(outputPath)) {
        fileSystem.delete(outputPath);
    }
    //創建Job
    Job job = Job.getInstance(configuration, "wordcount");
    //設置Job的處理類
    job.setJarByClass(WordCountApp.class);
    //設置作業處理的輸入路徑
    FileInputFormat.setInputPaths(job, new Path(args[0]));
    //設置map相關參數
    job.setMapperClass(MyMapper.class);
    job.setMapOutputKeyClass(Text.class);
    job.setMapOutputValueClass(LongWritable.class);
    //設置reduce相關參數
    job.setReducerClass(MyReducer.class);
    job.setOutputKeyClass(Text.class);
    job.setOutputValueClass(LongWritable.class);
    //設置作業輸出目錄
    FileOutputFormat.setOutputPath(job, new Path(args[1]));
    //提交作業
    System.exit(job.waitForCompletion(true) ? 0 : 1);
}

有幾點要提一下:

  • 清理已存在的輸出目錄的作用是為了防止程序多次執行時報錯,因為如果輸出目錄已存在的話,程序執行時會報錯;
  • 創建job的第二個參數“wordcount”是作業名;
  • 設置的map參數分別是map處理類,map輸出的鍵值對類型,reduce參數同理;
  • 以上所有的代碼都是在WordCountApp一個類中的,最后會貼出總的代碼。

至此,整個wordcount的例子就寫完了,其實還是很簡單的,主要是理解MapReduce的框架的思想和寫法,以后再寫別的例子的時候就可以舉一反三了。下面就是運行過程了。

提交運行

  1. 在shell的.bash_profile文件中配置maven根目錄,并將maven根目錄下的bin目錄加入到PATH變量中。這樣shell中就可以使用mvn命令打包了。

  2. 進入helloworld根目錄,或者直接在IDEA最下面打開Terminal,輸入mvn clean package -DskipTests。將代碼打成jar包。

    打包完成后會顯示BUILD SUCCESS,并且會在項目目錄下產生一個target文件夾,jar包就放在這個地方。

  3. 將jar包放到服務器根目錄上去,使用scp命令scp helloworld-1.0.jar wangsheng@localhost:~/

  4. 運行,這一步是接在上一步進入localhost之后的,運行的命令格式為:

    hadoop jar jar包地址 主程序完整類名 輸入文件目錄 輸出目錄

    所以可以寫出來命令為:

    hadoop jar /Users/wangsheng/helloworld-1.0.jar com.wangsheng.hadoop.WordCountApp hdfs://localhost:8020/test/hello.txt hdfs://localhost:8020/test/output
    

    如果運行成功,那么在output中會有以下文件,然后我們檢驗一下,hello.txt中的內容與wordcount計算的結果是否一致。

到這里wordcount的例子就完全完成了。

有意思的問題

前面我們提到,我全篇都沒有看到一個按行處理的代碼,為什么在map方法中value的值就是一行文本呢?而且怎么知道LongWritable對應的就是文件的偏移量呢?

我們回頭去看上一篇文章說MapReduce執行過程的那張圖,文件經過InputFormat后被拆分成了split,然后有個關鍵的步驟是通過RecordReader進行讀取,并且我們之前說InputFormat里面有一個getRecordReader方法,這個方法就是得到InputFormat對應的RecordReader

于是我們從InputFormat入手,InputFormat是個抽象類,我們看看它有哪些實現類:

它的實現類對應各種不同的文件的處理,InputFormat提供了不同實現類去處理,在這些子類中我們發現了一個TextInputFormat類,顧名思義應該是處理文本文件的,進入這個類看一下:

我們發現這個類在獲取RecordReader的時候,返回了一個LineRecordReader對象,這是不是跟我們的目的有些接近了,其實這個類就是從split中按行讀取內容,而這個LineRecorderReaderRecordReader的實現類,所以針對不同的文件,有不同的InputFormat實現類來處理拆分文件,同樣從拆分后的split中讀取內容也對應著不同的RecordReader,在明白了這個原理后,那么如果我們想在文本文件中一次讀取多行的需求是不是也就不難滿足了,也就是說我們可以根據自己的策略定義RecordReader的讀取規則。

至于為什么LongWritable可以作為文件的偏移量,這個在LineRecordReader中的nextKeyValue方法中已經很明顯體現出來了:

這個小問題也是我在寫博客的過程中偶然發現的,通過看源碼的方式加深了對MapReduce框架的理解。

源碼

github源碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容