Hadoop學習筆記



 

1       氣象數據導入... 4

2       MapperReducer... 5

3       找最高氣溫... 7

4       JOB JAR運行... 9

5       數據流... 9

6       combiner. 11

6.1        Hadoop2 NameNode元數據相關文件目錄解析... 12

7       MapReduce輸入輸出類型... 13

8       新舊API14

9       hadoop目錄結構... 14

9.1        hadoop1. 14

9.2        Hadoop文件系統元數據fsimage和編輯日志edits. 15

9.3        Hadoop 1.xfsimageedits合並實現... 15

9.4        Hadoop 2.xfsimageedits合並實現... 18

9.5        Hadoop2.2.0HDFS的高可用性實現原理... 19

10         命令... 21

11         HDFSHadoop Distributed Filesystem.. 21

11.1          ... 21

11.2      namenodedatanode. 21

11.3          聯邦HDFS. 22

11.4      HDFS高可用性... 22

11.5      Hadoop文件系JAVA接口... 22

11.5.1       FileSystem繼承圖... 22

11.5.2       讀取數據... 23

11.5.3       寫入數據... 25

11.5.4       上傳本地文件... 26

11.5.5       重命名或移動文件... 26

11.5.6       刪除文件目錄... 26

11.5.7       創建目錄... 26

11.5.8       查看目錄及文件信息... 26

11.5.9       列出文件(狀態)... 27

11.5.10      獲取Datanode信息... 28

11.5.11      文件通配... 28

11.5.12      過濾文件... 29

11.6          數據流... 31

11.6.1       文件讀取過程... 31

11.6.2       文件寫入過程... 32

11.6.3       緩存同步... 32

12              壓縮... 33

12.1          使用CompressionCodec對數據流進行壓縮與解壓... 33

12.2          通過CompressionCodecFactory自動獲取CompressionCodec. 34

12.3          本地native壓縮庫... 35

12.4      CodecPool壓縮池... 36

12.5          壓縮數據分片問題... 37

12.6          Mapreduce中使用壓縮... 37

12.6.1       Map任務輸出進行壓縮... 38

13              序列化... 38

13.1      Writable接口... 38

13.2      WritableComparable接口、WritableComparator ... 39

13.2.1       比較方式優先級(WritableComparableWritableComparator... 41

13.3      Writable實現類... 42

13.3.1       Java基本類型對應的Writable實現類... 42

13.3.2       可變長類型VIntWritable VLongWritable. 43

13.3.3       Text. 43

13.3.4       BytesWritable. 45

13.3.5       NullWritable. 45

13.3.6       ObjectWritableGenericWritable. 45

13.3.7       Writable集合... 46

13.4          自定義Writable. 47

14              順序文件結構... 49

14.1      SequenceFile. 49

14.1.1       ... 49

14.1.2       ... 51

14.1.3       使用命令查看文件... 52

14.1.4       將多個順序文件排序合並... 52

14.1.5       SequenceFile文件格式... 53

14.2      MapFile. 54

14.2.1       ... 54

14.2.2       ... 56

14.2.3       特殊的MapFile. 57

14.2.4       SequenceFile轉換為MapFile. 58

15              MapReduce應用開發... 61

15.1      Configuration... 61

15.2          作業調用... 62

16              MapReduce工作原理... 62

16.1          經典的mapreduceMapReduce 1... 63

16.2      YARNMapReduce 2... 64

16.3      Shuffle and Sort. 65

16.3.1       Shuffle詳解... 66

16.3.2       map... 68

16.3.3       reduce... 70

16.3.4       shuffle配置調優... 72

16.4      hadoop 配置項的調優... 73

16.5      MapReduce作業的默認配置... 75

16.6          輸入格式... 76

16.6.1       輸入分片與記錄... 76

16.6.2       文本輸入... 87

16.6.3       二進制輸入... 90

16.6.4       多個輸入... 90

16.6.5       數據庫輸入... 90

16.7          輸出格式... 97

16.7.1       文本輸出... 97

16.7.2       二進制輸出... 98

16.7.3       多個輸出... 98

16.7.4       禁止空文件輸出... 102

16.7.5       數據庫輸出... 102

16.8      Counters計數器... 102

16.8.1       任務計數器... 103

16.8.2       作業計數器... 106

16.8.3       自定義計數器... 107

16.8.4       獲取計數器... 108

16.9          排序... 109

16.9.1       氣象數據轉換為順序文件... 109

16.9.2       部分排序... 111

16.9.3       全排序... 115

16.9.4       第二排序(復合Key... 120

16.10        連接... 125

16.10.1     Map端連接... 125

16.10.2     Reducer端連接... 126

16.10.3      自連接... 130

16.11        mapreduce函數參數傳遞... 133

16.11.1      通過 Configuration 傳遞... 135

16.11.2      通過DefaultStringifier... 136

16.12        Distributed Cache分布式緩存... 138

16.12.1     MR1. 139

16.12.2     MR2. 142

16.12.3      相關配置... 142

 

1         氣象數據導入

ftp://ftp.ncdc.noaa.gov下載,下載下來的目錄結構:

每下個文件夾存放了每一年所有氣象台的氣象數據:

每一個文件就是一個氣象站一年的數據

將上面目錄上傳到Linux中:

編寫以下Shell腳本,將每一年的所有不現氣象站所產生的文件合並成一個文件,即每年只有一個文件,並上傳到Hadoop系統中:

#!/bin/bash

#Hadoop權威指南氣像數據按每一年合並成一個文件,並上傳到Hadoop系統中

rm -rf /root/ncdc/all/*

/root/hadoop-1.2.1/bin/hadoop fs -rm -r /ncdc/all/*

#這里的/*/*中第一個*表示年份文件夾,其下面存放的就是每年不同氣象站的氣象文件

for file in /root/ncdc/raw/*/*

do

echo "追加$file.."

path=`dirname $file`

target=${path##*/}

gunzip -c $file >> /root/ncdc/all/$target.all

done

 

for file in /root/ncdc/all/*

do

echo "上傳$file.."

/root/hadoop-1.2.1/bin/hadoop fs -put $file /ncdc/all

done

 

腳本運行完后,HDFS上的文件如下:

2         MapperReducer

每個Mapper都需要繼承org.apache.hadoop.mapreduce.Mapper類,需重寫其map方法:

                   protectedvoid map(KEYIN key, VALUEIN value, Context context)

每個Reducer都需要繼承org.apache.hadoop.mapreduce.Reducer類,需重寫其

                   protectedvoid reduce(KEYIN key, Iterable<VALUEIN> values, Context context )

 

 

publicclassMapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT//Map父類中的方法定義如下

  /**

   * Called once at the beginning of the task.在任務開始執行前會執行一次

   */

  protectedvoid setup(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.會被run()方法循環調用,每對鍵值都會被調用一次

   */

  @SuppressWarnings("unchecked")

  protectedvoid map(KEYIN key, VALUEIN value,

                     Context context) throws IOException, InterruptedException {

    context.write((KEYOUT) key, (VALUEOUT) value);//map()方法提供了默認實現,即直接輸出,不做處理

  }

 

  /**

   * Called once at the end of the task.任務結束后會調用一次

   */

  protectedvoid cleanup(Context context

                         ) throws IOException, InterruptedException {

    // NOTHING

  }

 

  /**

   * Expert users can override this method for more complete control over the

   * execution of the Mapper.map()方法實質上就是被run()循環調用的,我們可以重寫這個方法,加一些處理邏輯

   */

  publicvoid run(Context context) throws IOException, InterruptedException {

    setup(context);

    try {

      while (context.nextKeyValue()) {//每對鍵值對都會調用一次map()方法

        map(context.getCurrentKey(), context.getCurrentValue(), context);

      }

    } finally {

      cleanup(context);

    }

  }

}

 

 

publicclassReducer<KEYIN,VALUEIN,KEYOUT,VALUEOUT> {

  /**

   * Called once at the start of the task.在任務開始執行前會執行一次

   */

  protectedvoid setup(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.reduce()方法會被run()循環調用

   */

  @SuppressWarnings("unchecked")

  protectedvoid reduce(KEYIN key, Iterable<VALUEIN> values, Context context

                        ) throws IOException, InterruptedException {

    for(VALUEIN value: values) {

      context.write((KEYOUT) key, (VALUEOUT) value);//提供了默認實現,不做處理直接輸出

    }

  }

 

  /**

   * Called once at the end of the task.任務結束后會調用一次

   */

  protectedvoid cleanup(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.

   */

  publicvoid run(Context context) throws IOException, InterruptedException {

    setup(context);

    try {

      while (context.nextKey()) {//每鍵值對都會調用一次reduce()

        reduce(context.getCurrentKey(), context.getValues(), context);

        // If a back up store is used, reset it

        Iterator<VALUEIN> iter = context.getValues().iterator();

        if(iterinstanceof ReduceContext.ValueIterator) {

          ((ReduceContext.ValueIterator<VALUEIN>)iter).resetBackupStore();       

        }

      }

    } finally {

      cleanup(context);

    }

  }

}

 

 

Reducerreduce方法每執行完一次,就會產生一個結果文件

reduce方法的輸入類型必須匹配map方法的輸出類型

 

map的輸出文件名為 part-m-nnnnn ,reduce的輸出文件名為 part-r-nnnnn  (nnnnn為分區號,即該文件存放的是哪個分區的數據,從0開始),其中part文件名可以修改

 

publicclass Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {Mapper類有4個范型參數:

KEYINMap Key輸入類型,如果輸入是文本文件,固定為LongWritable,表示每一行文本所在文件的起始位置,從0開始(即第一行起始為位置為0

        publicvoid map(Object key, Text value, Context context)

                throws IOException, InterruptedException {

            System.out.println("key=" + key + "; value=" + value);

       

        [root@hadoop-master /root]# hadoop fs -get /wordcount/input/wordcount /root/wordcount

        換行顯示 $'\n'),Tab字符顯示^I^M '\r', 回車符

        [root@hadoop-master /root]# cat -A /root/wordcount

hello world^M$

hello hadoop

VALUEINMap value輸入類型,如果輸入是文本文件,則一般為Text,表示文本文件中讀取到的一行內容(注:Map是以行為單位進行處理的,即每跑一次Map,即處理一行文本,即輸入也是以行為單位進行輸入的)

KEYOUT, VALUEOUT:為Reduce輸出Key與輸出Value的類型

3         找最高氣溫

//文本文件是按照一行一行傳輸到Mapper中的

publicclass MaxTemperatureMapper

  extends Mapper<LongWritable/*輸入鍵類型:行的起始位置,從0開始*/, Text/*輸入值類型:為文本的一行內容*/, Text/*輸出鍵類型:年份*/, IntWritable/*輸出值類型:氣溫*/> {

 

  privatestaticfinalintMISSING = 9999;

 

  @Override

  publicvoid map(LongWritable key, Text value, Context context)

      throws IOException, InterruptedException {

   

    String line = value.toString();

    String year = line.substring(15, 19);//取年份

    int airTemperature;

    if (line.charAt(87) == '+') { //如果溫度值前有加號時,去掉,因為parseInt不支持加號

    airTemperature = Integer.parseInt(line.substring(88, 92));

    } else {

    airTemperature = Integer.parseInt(line.substring(87, 92));

    }

    String quality = line.substring(92, 93);//空氣質量

    //如果是有效天氣,則輸出

if (airTemperature != MISSING && quality.matches("[01459]")) {

 //每執行一次map方法,可能會輸出多個鍵值對,但這里只輸出一次,這些輸出合並后傳遞給reduce作用輸入

    context.write(new Text(year), new IntWritable(airTemperature));

    }

  }

}

 

publicclass MaxTemperatureReducer extends

        Reducer<Text, IntWritable, Text, IntWritable> {

    @Override

    //reduce的輸入即為Map的輸出,這里的輸入值為一個集合,Map輸出后會將相同Key的值合並成一個數組后

    //再傳遞給reduce,所以值類型為Iterable

    publicvoid reduce(Text key, Iterable<IntWritable> values, Context context)

            throws IOException, InterruptedException {

 

        int maxValue = Integer.MIN_VALUE;

        for (IntWritable value : values) {

            maxValue = Math.max(maxValue, value.get());

        }

        //write出去的結果會寫入到輸出結果文件

        context.write(key, new IntWritable(maxValue));

    }

}

 

publicclass MaxTemperature {

    publicstaticvoid main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        conf.set("mapred.job.tracker", "hadoop-master:9001");

 

        Job job = Job.getInstance(conf, "weather");

        // 根據設置的calss找到它所在的JAR任務包,而不需要明確指定JAR文件名

        job.setJarByClass(MaxTemperature.class);

        job.setJobName("Max temperature");

 

        job.setMapperClass(MaxTemperatureMapper.class);

        job.setReducerClass(MaxTemperatureReducer.class);

 

//設置mapreduce的輸出類型,一般它們的輸出類型都相同,如果不同,則map可以使用setMapOutputKeyClasssetMapOutputValueClass來設置

        job.setOutputKeyClass(Text.class);

        job.setOutputValueClass(IntWritable.class);

// addInputPath除了支持文件、目錄,還可以使用文件通匹符?

        FileInputFormat.addInputPath(job, new Path(

                "hdfs://hadoop-master:9000/ncdc/all/1901.all"));

        FileInputFormat.addInputPath(job, new Path(

                "hdfs://hadoop-master:9000/ncdc/all/1902.all"));

        FileInputFormat.addInputPath(job, new Path(

                "hdfs://hadoop-master:9000/ncdc/all/1903.all"));

        FileInputFormat.addInputPath(job, new Path(

                "hdfs://hadoop-master:9000/ncdc/all/1904.all"));

        FileInputFormat.addInputPath(job, new Path(

                "hdfs://hadoop-master:9000/ncdc/all/1905.all"));

        FileOutputFormat.setOutputPath(job, new Path(

                "hdfs://hadoop-master:9000/ncdc/output2"));

        System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

 

MapReduce邏輯數據流:

4         JOB JAR運行

./hadoop jar /root/ncdc/weather.jar ch02.MaxTemperature

如果weather.jar包里的MANIFEST.MF 文件里指定了Main Class

則運行時可以不用指定主類:
./hadoop jar /root/ncdc/weather.jar

 

hadoop2里可以這樣執行:

./yarn jar /root/ncdc/weather.jar

 

如果在運行前指定了export HADOOP_CLASSPATH=/root/ncdc/weather.jar,如果設置了HADOOP_CLASSPATH應用程序類路徑環境變量,則可以直接運行:

./hadoop MaxTemperature

./yarn MaxTemperature

 

以上都沒有寫輸入輸出文件夾,因為應用程序啟動類里寫了

5         數據流

map是移動算法而不是數據。在集群上,map任務(算法代碼)會移動到數據節點Datanode(計算數據在哪就移動到哪台數據節點上),但reduce過程一般不能避免數據的移動(即不具備本地化數據的優勢),單個reduce任務的輸入通常來自於所有mapper的輸出,因此map的輸出會傳輸到運行reduce任務的節點上,數據在reduce端合並,然后執行用戶自定義的reduce方法

reduce任務的完整數據流:

虛線表示節點,虛線箭頭表示節點內部數據傳輸,實線箭頭表示不同節點間的數據傳輸

 

有時,map任務(程序)所需要的三台機(假設配置的副本數據為3)正在處理其他的任務時,則Jobtracker就會在這三份副本所在機器的同一機架上找一台空親的機器,這樣數據只會在同一機架上的不同機器上進行傳輸,這樣比起在不同機架之間的傳輸效率要高

 

數據與map程序可能在同一機器上,可能在同一機架上的不同機器上,還有可能是在不同機架上的不同機器上,即數據與map程序分布情況有以下三種:

a(本地數據):同一機器,b(本地機架):同一機架上不同機器,c(跨機架):不同機架上不同機器。顯然a這種情況下,執行效率是最高的

 

從上圖來看,應該盡量讓數據與map任務程序在一機器上,這就是為什么分片最大的大小與HDFS塊大小相同,因為如果分片跨越多個數據塊時,而這些塊又不在同一機器上時,就需要將其他的塊傳輸到map任務所在節點上,這本地數據相比,這種效率低

 

為了避免計算時不移動數據,TaskTracker是跑在DataName上的

 

reduce的數量並不是由輸入數據大小決定的,而是可以單獨指定的

 

如果一個任務有很多個reduce任務,則每個map任務就需要對輸出數據進行分區partition處理,即輸入數據交給哪個reduce進行處理。每個reduce需要建立一個分區,每個分區就對應一個reduce,默認的分區算法是根據輸出的鍵哈希法:Key的哈希值 MOD Reduce數量),等到分區號,分區號 小於等於 Reduce數量的整數,從0開始。比如有3reduce任務,則會分成三個分區。

 

分區算法也是可以自定義的

 

mapreduce之間,還有一個shuffle過程:包括分區、排序、合並

 

reduce任務數據流:

一個Map輸出數據可能輸出到不同的reduce,一個reduce的輸入也可能來自不同的map輸出

 

一個作業可以沒有reduce任務,即無shuffle過程

 

Hadoop將作業分成若干個小任務進行執行,其中包括兩類任務:map任務與reduce任務。

有兩類節點控制着任務的執行:一個JobTracker,與若干TaskTrackerJobTracker相當於NameNode的,是用來管理、調度TaskTrackerTaskTracker相當於DataName,需要將任務執行狀態報告給JobTracker

 

HadoopMapReduce的輸入數據划分成等長的小數據塊,稱為輸入分片——input split

 

每個分片構建一個map任務,一個map任務就是我們繼承Mapper並重寫的map方法

 

數據分片,可以多個map任務進行並發處理,這樣就會縮短整個計算時間,並且分片可以很好的解決負載均衡問題,分片越細(小),則負載均衡越高,但分片太小需要建造很多的小的任務,這樣可能會影響整個執行時間,所以,一個合理的分片大小為HDFS塊的大小,默認為64M

 

map任務將其輸出結果直接寫到本地硬盤上,而不是HDFS,這是因為map任務輸出的是中間結果,該輸出傳遞給reduce任務處理后,就可以刪除了,所以沒有必要存儲在HDFS

 

6         combiner

可以為map輸出指定一個combiner(就像map通過分區輸出到reduce一樣),combiner函數的輸出作為reduce的輸入。

 

combiner屬於優化,無法確定map輸出要調用combiner多少次,有可能是01、多次,但不管調用多少次,reduce的輸出結果都是一樣的

 

假設1950年的氣象數據很大,map前被分成了兩片,這樣1950的數據就會由兩個map任務去執行,假設第一個map輸出為:

(1950, 0)

(1950, 20)

(1950, 10)

第二個map任務輸出為:

(1950, 25)

(1950, 15)

如果在沒有使用combiner時,reducer的輸入會是這樣的:(1950, [0, 20, 10, 25, 15]),最后輸入結果為:(1950, 25);為了減少map的數據輸出,這里可以使用combiner函數對每個map的輸出結果進行查找最高氣溫(第一個map任務最高為20,第二個map任務最高為25),這樣一來,最后傳遞給reducer的輸入數據為:(1950, [20, 25]),最后的計算結果也是(1950, 25),這一過程即為:

max(0, 20, 10, 25, 15) = max(max(0, 20, 10), max(25, 15)) = max(20, 25) = 25

上面是找最高氣溫,並不是所有業務需求都具有此特性,如求平均氣溫時,就不適用combiner,如:

mean(0, 20, 10, 25, 15) = 14

但:

mean(mean(0, 20, 10), mean(25, 15)) = mean(10, 20) = 15

 

combinerreducer的計算邏輯是一樣的,所以不需要重定義combiner類(如果輸入類型與reducer不同,則需要重定義一個,但輸入類型一定相同),而是在Job啟動內中通過job.setCombinerClass(MaxTemperatureReducer.class);即可,即combinerreducer是同一實現類

 

publicclass MaxTemperatureWithCombiner {

    publicstaticvoid main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        conf.set("mapred.job.tracker", "hadoop-master:9001");

 

        Job job = Job.getInstance(conf, "weather");

        job.setJarByClass(MaxTemperature.class);

        job.setJobName("Max temperature");

 

        job.setMapperClass(MaxTemperatureMapper.class);

        job.setReducerClass(MaxTemperatureReducer.class);

       job.setCombinerClass(MaxTemperatureReducer.class);

 

        job.setOutputKeyClass(Text.class);

        job.setOutputValueClass(IntWritable.class);

 

        FileInputFormat.addInputPath(job, new Path(

                "hdfs://hadoop-master:9000/ncdc/all/1950.all"));

        FileOutputFormat.setOutputPath(job, new Path(

                "hdfs://hadoop-master:9000/ncdc/output2"));

        System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

 

Map端,用戶自定義實現的Combine優化機制類Combiner在執行Map端任務的節點本身運行,相當於對map函數的輸出做了一次reduce。使用Combine機制的意義就在於使Map端輸出更緊湊,使得寫到本地磁盤和傳給Reduce端的數據更少

Combiner通常被看作是一個Map端的本地reduce函數的實現類Reducer

 

選用Combine機制下的Combiner雖然減少了IO,但是等於多做了一次reduce,所以應該查看作業日志來判斷combine函數的輸出記錄數是否明顯少於輸入記錄的數量,以確定這種減少和花費額外的時間來運行Combiner相比是否值得

 

Combine優化機制執行時機

  ⑴ Mapspill的時候

  在Map端內存緩沖區進行溢寫的時候,數據會被划分成相應分區,后台線程在每個partition內按鍵進行內排序。這時如果指定了Combiner,並且溢寫次數最少為 3min.num.spills.for.combine屬性的取值)時,Combiner就會在排序后輸出文件寫到磁盤之前運行。   ⑵ Mapmerge的時候

  在Map端寫磁盤完畢前,這些中間的輸出文件會合並成一個已分區且已排序的輸出文件,按partition循環處理所有文件,合並會分多次,這個過程也會伴隨着Combiner的運行。

  ⑶ Reducemerge的時候

   從Map端復制過來數據后,Reduce端在進行merge合並數據時也會調用Combiner來壓縮數據。

 

Combine優化機制運行條件

  ⑴ 滿足交換和結合律[10]

  結合律:

  (1+2+3+4+5+6==1+2+3+4+5+6== ...

  交換律:

  1+2+3+4+5+6==2+4+6+1+2+3== ...

  應用程序在滿足如上的交換律和結合律的情況下,combine函數的執行才是正確的,因為求平均值問題是不滿足結合律和交換律的,所以這類問題不能運用Combine優化機制來求解。

  例如:mean1020304050=30

  但meanmean1020),mean304050))=22.5

  這時在求平均氣溫等類似問題的應用程序中使用Combine優化機制就會出錯。

 

6.1     Hadoop2 NameNode元數據相關文件目錄解析

下面所有的內容是針對Hadoop 2.x版本進行說明的,Hadoop 1.x和這里有點不一樣。

在第一次部署好Hadoop集群的時候,我們需要在NameNodeNN)節點上格式化磁盤:

[wyp@wyp hadoop-2.2.0]$  $HADOOP_HOME/bin/hdfs namenode -format

格式化完成之后,將會在$dfs.namenode.name.dir/current目錄下如下的文件結構

current/

|-- VERSION

|-- edits_*

|-- fsimage_0000000000008547077

|-- fsimage_0000000000008547077.md5

|-- seen_txid

其中的dfs.namenode.name.dir是在hdfs-site.xml文件中配置的,默認值如下:

<property>

  <name>dfs.namenode.name.dir</name>

  <value>file://${hadoop.tmp.dir}/dfs/name</value>

</property>

 

hadoop.tmp.dir是在core-site.xml中配置的,默認值如下

<property>

  <name>hadoop.tmp.dir</name>

  <value>/tmp/hadoop-${user.name}</value>

  <description>A base for other temporary directories.</description>

</property>

dfs.namenode.name.dir屬性可以配置多個目錄,如/data1/dfs/name,/data2/dfs/name,/data3/dfs/name,....。各個目錄存儲的文件結構和內容都完全一樣,相當於備份,這樣做的好處是當其中一個目錄損壞了,也不會影響到Hadoop的元數據,特別是當其中一個目錄是NFS(網絡文件系統Network File SystemNFS)之上,即使你這台機器損壞了,元數據也得到保存。

下面對$dfs.namenode.name.dir/current/目錄下的文件進行解釋。

1、  VERSION文件是Java屬性文件,內容大致如下:

#Fri Nov 15 19:47:46 CST 2013

namespaceID=934548976

clusterID=CID-cdff7d73-93cd-4783-9399-0a22e6dce196

cTime=0

storageType=NAME_NODE

blockpoolID=BP-893790215-192.168.24.72-1383809616115

layoutVersion=-47

其中
  (1)、namespaceID是文件系統的唯一標識符,在文件系統首次格式化之后生成的;
  (2)、storageType說明這個文件存儲的是什么進程的數據結構信息(如果是DataNodestorageType=DATA_NODE);
  (3)、cTime表示NameNode存儲時間的創建時間,由於我的NameNode沒有更新過,所以這里的記錄值為0,以后對NameNode升級之后,cTime將會記錄更新時間戳;
  (4)、layoutVersion表示HDFS永久性數據結構的版本信息, 只要數據結構變更,版本號也要遞減,此時的HDFS也需要升級,否則磁盤仍舊是使用舊版本的數據結構,這會導致新版本的NameNode無法使用;
  (5)、clusterID是系統生成或手動指定的集群ID,在-clusterid選項中可以使用它;如下說明

a、使用如下命令格式化一個Namenode

$ $HADOOP_HOME/bin/hdfs namenode -format [-clusterId <cluster_id>]

選擇一個唯一的cluster_id,並且這個cluster_id不能與環境中其他集群有沖突。如果沒有提供cluster_id,則會自動生成一個唯一的ClusterID

b、使用如下命令格式化其他Namenode

$ $HADOOP_HOME/bin/hdfs namenode -format -clusterId <cluster_id>

c、升級集群至最新版本。在升級過程中需要提供一個ClusterID,例如:

$ $HADOOP_PREFIX_HOME/bin/hdfs start namenode --config $HADOOP_CONF_DIR  -upgrade -clusterId <cluster_ID>

如果沒有提供ClusterID,則會自動生成一個ClusterID

6)、blockpoolID:是針對每一個Namespace所對應的blockpoolID,上面的這個BP-893790215-192.168.24.72-1383809616115就是在我的ns1NameNode節點)的namespace下的存儲塊池的ID,這個ID包括了 其對應的NameNode節點的ip地址。

 

2、  $dfs.namenode.name.dir/current/seen_txid非常重要,是存放transactionId的文件,format之后是0,它代表的是namenode里面的edits_*文件的尾數,namenode重啟的時候,會按照seen_txid的數字,循序從頭跑edits_0000001~seen_txid的數字。所以當你的hdfs發生異常重啟的時候,一定要比對seen_txid內的數字是不是你edits最后的尾數,不然會發生建置namenodemetaData的資料有缺少,導致誤刪Datanode上多余Block的資訊。

 

3、  $dfs.namenode.name.dir/current目錄下在format的同時也會生成fsimageedits文件,及其對應的md5校驗文件。fsimageeditsHadoop元數據相關的重要文件,請參考Hadoop文件系統元數據fsimage和編輯日志edits

7         MapReduce輸入輸出類型

 

一般來說,map函數輸入的健/值類型(K1V1)不同於輸出類型(K2V2),雖然reduce函數的輸入類型必須與map函數的輸出類型相同,但reduce函數的輸出類型(K3V3)可以不同於輸入類型

如果使用combine函數,它與reduce函數的形式相同(它也是Reducer的一個實現),不同之處是它的輸出類型是中間的鍵/值對類型(K2V2),這些中間值可以輸入到reduce函數:

map: (K1, V1) → list(K2, V2)
combine: (K2, list(V2)) → list(K2, V2)

partition(K2, V2) → integer //將中間鍵值對分區,返回分區索引號。分區內的鍵會排序,相同的鍵的所有值會合並
reduce: (K2, list(V2)) → list(K3, V3)

上面是mapcombinereduce的輸入輸出格式,如map輸入的是單獨的一對key/value(值也是值);而combinereduce的輸入也是鍵值對,只不過它們的值不是單值,而是一個列表即多值;它們的輸出都是一樣,鍵值對列表;另外,reduce函數的輸入類型必須與map函數的輸出類型相同,所以都是K2V2類型

 

job.setOutputKeyClassjob.setOutputValueClas在默認情況下是同時設置map階段和reduce階段的輸出(包括KeyValue輸出),也就是說只有mapreduce輸出是一樣的時候才會這樣設置;當mapreduce輸出類型不一樣的時候就需要通過job.setMapOutputKeyClassjob.setMapOutputValueClas來單獨對map階段的輸出進行設置,當使用job.setMapOutputKeyClassjob.setMapOutputValueClas后,setOutputKeyClass()setOutputValueClas()此時則只對reduce輸出設置有效了。

 

8         新舊API

1、API傾向於使用抽像類,而不是接口,這樣更容易擴展。在舊API中使用MapperReducer接口,而在新API中使用抽像類

2、API放在org.apache.hadoop.mapreduce包或其子包中,而舊API則是放在org.apache.hadoop.mapred

3、API充分使用上下文對象,使用戶很好的與MapReduce交互。如,新的Context基本統一了舊API中的JobConf OutputCollector Reporter的功能,使用一個Context就可以搞定,易使用

4、API允許mapperreducer通過重寫run()方法控制執行流程。如,即可以批處理鍵值對記錄,也可以在處理完所有的記錄之前停止。這在舊API中可以通過寫MapRunnable類在mapper中實現上述功能,但在reducer中無法實現

5、新的API中作業是Job類實現,而非舊API中的JobClient類,新的API中刪除了JobClient

6、API實現了配置的統一。舊API中的作業配置是通過JobConf完成的,它是Configuration的子類。在新API中,作業的配置由Configuration,或通過Job類中的一些輔助方法來完成配置

輸出的文件命名方法稍有不同。在舊的APImapreduce的輸出被統一命名為 part-nnmm,但在APImap的輸出文件名為 part-m-nnnnn,而reduce的輸出文件名為 part-r-nnnnn(nnnnn為分區號,即該文件存放的是哪個分區的數據,從0開始)其中part文件名可以修改

7、 

8、API中的可重寫的用戶方法拋出ava.lang.InterruptedException異常,這意味着可以使用代碼來實現中斷響應,從而可以中斷那些長時間運行的作業

9、API中,reduce()傳遞的值是java.lang.Iterable類型的,而非舊API中使用java.lang.Iterator類型,這就可以很容易的使用for-each循環結構來迭代這些值:for (VALUEIN value : values) { ... }

9         hadoop目錄結構

9.1     hadoop1

存放的本地目錄是可以通過hdfs-site.xml配置的:

hadoop1:

<property>

  <name>dfs.name.dir</name>

  <value>${hadoop.tmp.dir}/dfs/name</value>

  <description>Determines where on the local filesystem the DFS name node

      should store the name table(fsimage).  If this is a comma-delimited list

      of directories then the name table is replicated in all of the

      directories, for redundancy. </description>

</property>

 

9.2     Hadoop文件系統元數據fsimage和編輯日志edits

在《Hadoop NameNode元數據相關文件目錄解析》文章中提到NameNode$dfs.namenode.name.dir/current/文件夾的幾個文件:

current/

|-- VERSION

|-- edits_*

|-- fsimage_0000000000008547077

|-- fsimage_0000000000008547077.md5

`-- seen_txid

其中存在大量的以edits開頭的文件和少量的以fsimage開頭的文件。那么這兩種文件到底是什么,有什么用?下面對這兩中類型的文件進行詳解。在進入下面的主題之前先來搞清楚editsfsimage文件的概念:

  (1)、fsimage文件其實是Hadoop文件系統元數據的一個永久性的檢查點,其中包含Hadoop文件系統中的所有目錄和文件idnode的序列化信息;

  (2)、edits文件存放的是Hadoop文件系統的所有更新操作的路徑,文件系統客戶端執行的所有寫操作首先會被記錄到edits文件中。

  fsimageedits文件都是經過序列化的,在NameNode啟動的時候,它會將fsimage文件中的內容加載到內存中,之后再執行edits文件中的各項操作,使得內存中的元數據和實際的同步,存在內存中的元數據支持客戶端的讀操作。

 

  NameNode起來之后,HDFS中的更新操作會重新寫到edits文件中,因為fsimage文件一般都很大(GB級別的很常見),如果所有的更新操作都往fsimage文件中添加,這樣會導致系統運行的十分緩慢,但是如果往edits文件里面寫就不會這樣,每次執行寫操作之后,且在向客戶端發送成功代碼之前,edits文件都需要同步更新。如果一個文件比較大,使得寫操作需要向多台機器進行操作,只有當所有的寫操作都執行完成之后,寫操作才會返回成功,這樣的好處是任何的操作都不會因為機器的故障而導致元數據的不同步。

 

  fsimage包含Hadoop文件系統中的所有目錄和文件idnode的序列化信息;對於文件來說,包含的信息有修改時間、訪問時間、塊大小和組成一個文件塊信息等;而對於目錄來說,包含的信息主要有修改時間、訪問控制權限等信息。fsimage並不包含DataNode的信息,而是包含DataNode上塊的映射信息,並存放到內存中,當一個新的DataNode加入到集群中,DataNode都會向NameNode提供塊的信息,而NameNode會定期的“索取”塊的信息,以使得NameNode擁有最新的塊映射。因為fsimage包含Hadoop文件系統中的所有目錄和文件idnode的序列化信息,所以如果fsimage丟失或者損壞了,那么即使DataNode上有塊的數據,但是我們沒有文件到塊的映射關系,我們也無法用DataNode上的數據!所以定期及時的備份fsimageedits文件非常重要!

 

  在前面我們也提到,文件系統客戶端執行的所以寫操作首先會被記錄到edits文件中,那么久而久之,edits會非常的大,而NameNode在重啟的時候需要執行edits文件中的各項操作,那么這樣會導致NameNode啟動的時候非常長!在下篇文章中我會談到在Hadoop 1.x版本Hadoop 2.x版本是怎么處理edits文件和fsimage文件的。

9.3     Hadoop 1.xfsimageedits合並實現

NameNode運行期間,HDFS的所有更新操作都是直接寫到edits中,久而久之edits文件將會變得很大;雖然這對NameNode運行時候是沒有什么影響的,但是我們知道NameNode重啟的時候,NameNode先將fsimage里面的所有內容映像到內存中,然后再一條一條地執行edits中的記錄,當edits文件非常大的時候,會導致NameNode啟動操作非常地慢,而在這段時間內HDFS系統處於安全模式,這顯然不是用戶要求的。能不能在NameNode運行的時候使得edits文件變小一些呢?其實是可以的,本文主要是針對Hadoop 1.x版本,說明其是怎么將editsfsimage文件合並的,Hadoop 2.x版本editsfsimage文件合並是不同的。

  用過Hadoop的用戶應該都知道在Hadoop里面有個SecondaryNamenode進程,從名字看來大家很容易將它當作NameNode的熱備進程。其實真實的情況不是這樣的。SecondaryNamenodeHDFS架構中的一個組成部分,它是用來保存namenode中對HDFS metadata的信息的備份,並減少namenode重啟的時間而設定的!一般都是將SecondaryNamenode單獨運行在一台機器上,那么SecondaryNamenode是如何減少namenode重啟的時間的呢?來看看SecondaryNamenode的工作情況:

  (1)、SecondaryNamenode會定期的和NameNode通信,請求其停止使用edits文件,暫時將新的寫操作寫到一個新的文件edit.new上來,這個操作是瞬間完成,上層寫日志的函數完全感覺不到差別;

  (2)、SecondaryNamenode通過HTTP GET方式從NameNode上獲取到fsimageedits文件,並下載到本地的相應目錄下;

  (3)、SecondaryNamenode將下載下來的fsimage載入到內存,然后一條一條地執行edits文件中的各項更新操作,使得內存中的fsimage保存最新;這個過程就是editsfsimage文件合並;

  (4)、SecondaryNamenode執行完(3)操作之后,會通過post方式將新的fsimage文件發送到NameNode節點上

5)、NameNode將從SecondaryNamenode接收到的新的fsimage替換舊的fsimage文件,同時將edit.new替換edits文件,通過這個過程edits就變小了!整個過程的執行可以通過下面的圖說明:

說明: \

說明: fsimage_edits

在(1)步驟中,我們談到SecondaryNamenode會定期的和NameNode通信,這個是需要配置的,可以通過core-site.xml進行配置,下面是默認的配置:

<property>

  <name>fs.checkpoint.period</name>

  <value>3600</value>

  <description>The number of seconds between two periodic checkpoints.

  </description>

</property>

其實如果當fs.checkpoint.period配置的時間還沒有到期,我們也可以通過判斷當前的edits大小來觸發一次合並的操作,可以通過下面配置:

<property>

  <name>fs.checkpoint.size</name>

  <value>67108864</value>

  <description>The size of the current edit log (in bytes) that triggers

       a periodic checkpoint even if the fs.checkpoint.period hasn't expired.

  </description>

</property>

edits文件大小超過以上配置,即使fs.checkpoint.period還沒到,也會進行一次合並。順便說說SecondaryNamenode下載下來的fsimageedits暫時存放的路徑可以通過下面的屬性進行配置:

<property>

  <name>fs.checkpoint.dir</name>

  <value>${hadoop.tmp.dir}/dfs/namesecondary</value>

  <description>Determines where on the local filesystem the DFS secondary

      name node should store the temporary images to merge.

      If this is a comma-delimited list of directories then the image is

      replicated in all of the directories for redundancy.

  </description>

</property>

 

<property>

  <name>fs.checkpoint.edits.dir</name>

  <value>${fs.checkpoint.dir}</value>

  <description>Determines where on the local filesystem the DFS secondary

      name node should store the temporary edits to merge.

      If this is a comma-delimited list of directoires then teh edits is

      replicated in all of the directoires for redundancy.

      Default value is same as fs.checkpoint.dir

  </description>

</property>

從上面的描述我們可以看出,SecondaryNamenode根本就不是Namenode的一個熱備,其只是將fsimageedits合並。其擁有的fsimage不是最新的,因為在他從NameNode下載fsimageedits文件時候,新的更新操作已經寫到edit.new文件中去了。而這些更新在SecondaryNamenode是沒有同步到的!當然,如果NameNode中的fsimage真的出問題了,還是可以用SecondaryNamenode中的fsimage替換一下NameNode上的fsimage,雖然已經不是最新的fsimage,但是我們可以將損失減小到最少

  在Hadoop 2.x通過配置JournalNode來實現Hadoop的高可用性,可以參見《Hadoop2.2.0HDFS的高可用性實現原理》,這樣主被NameNode上的fsimageedits都是最新的,任何時候只要有一台NameNode掛了,也可以使得集群中的fsimage是最新狀態!關於Hadoop 2.x是如何合並fsimageedits的,可以參考《Hadoop 2.xfsimageedits合並實現

9.4     Hadoop 2.xfsimageedits合並實現

在《Hadoop 1.xfsimageedits合並實現》文章中,我們談到了Hadoop 1.x上的fsimageedits合並實現,里面也提到了Hadoop 2.x版本的fsimageedits合並實現和Hadoop 1.x完全不一樣,今天就來談談Hadoop 2.xfsimageedits合並的實現。

  我們知道,在Hadoop 2.x中解決了NameNode的單點故障問題;同時SecondaryName已經不用了,而之前的Hadoop 1.x中是通過SecondaryName來合並fsimageedits以此來減小edits文件的大小,從而減少NameNode重啟的時間。而在Hadoop 2.x中已經不用SecondaryName,那它是怎么來實現fsimageedits合並的呢?首先我們得知道,在Hadoop 2.x中提供了HA機制(解決NameNode單點故障),可以通過配置奇數個JournalNode來實現HA,如何配置今天就不談了!HA機制通過在同一個集群中運行兩個NNactive NN & standby NN)來解決NameNode的單點故障,在任何時間,只有一台機器處於Active狀態;另一台機器是處於Standby狀態。Active NN負責集群中所有客戶端的操作;而Standby NN主要用於備用,它主要維持足夠的狀態,如果必要,可以提供快速的故障恢復。

  為了讓Standby NN的狀態和Active NN保持同步,即元數據保持一致,它們都將會和JournalNodes守護進程通信。Active NN執行任何有關命名空間的修改(如增刪文件),它需要持久化到一半(由於JournalNode最少為三台奇數台,所以最少要存儲到其中兩台上)以上的JournalNodes(通過edits log持久化存儲)Standby NN負責觀察edits log的變化,它能夠讀取從JNs中讀取edits信息,並更新其內部的命名空間。一旦Active NN出現故障,Standby NN將會保證從JNs中讀出了全部的Edits,然后切換成Active狀態。Standby NN讀取全部的edits可確保發生故障轉移之前,是和Active NN擁有完全同步的命名空間狀態(更多的關於Hadoop 2.xHA相關知識,可以參考本博客的《Hadoop2.2.0HDFS的高可用性實現原理》)。

那么這種機制是如何實現fsimageedits的合並?在standby NameNode節點上會一直運行一個叫做CheckpointerThread的線程,這個線程調用StandbyCheckpointer類的doWork()函數,而doWork函數會每隔Math.min(checkpointCheckPeriod, checkpointPeriod)秒來做一次合並操作,相關代碼如下:

    try {

         Thread.sleep(1000 * checkpointConf.getCheckPeriod());

    } catch (InterruptedException ie) {}

    publiclong getCheckPeriod() {

      return Math.min(checkpointCheckPeriod, checkpointPeriod);

    }

    checkpointCheckPeriod = conf.getLong(DFS_NAMENODE_CHECKPOINT_CHECK_PERIOD_KEY,

                                    DFS_NAMENODE_CHECKPOINT_CHECK_PERIOD_DEFAULT);

    checkpointPeriod = conf.getLong(DFS_NAMENODE_CHECKPOINT_PERIOD_KEY,

                                DFS_NAMENODE_CHECKPOINT_PERIOD_DEFAULT);

上面的checkpointCheckPeriodcheckpointPeriod變量是通過獲取hdfs-site.xml以下兩個屬性的值得到:

<property>

  <name>dfs.namenode.checkpoint.period</name>

  <value>3600</value>

  <description>The number of seconds between two periodic checkpoints.

  </description>

</property>

 

<property>

  <name>dfs.namenode.checkpoint.check.period</name>

  <value>60</value>

  <description>The SecondaryNameNode and CheckpointNode will poll the NameNode

  every 'dfs.namenode.checkpoint.check.period' seconds to query the number

  of uncheckpointed transactions.

  </description>

</property>

當達到下面兩個條件的情況下,將會執行一次checkpoint

boolean needCheckpoint = false;

if (uncheckpointed >= checkpointConf.getTxnCount()) {

     LOG.info("Triggering checkpoint because there have been " +

                uncheckpointed + " txns since the last checkpoint, which " +

                "exceeds the configured threshold " +

                checkpointConf.getTxnCount());

     needCheckpoint = true;

} else if (secsSinceLast >= checkpointConf.getPeriod()) {

     LOG.info("Triggering checkpoint because it has been " +

            secsSinceLast + " seconds since the last checkpoint, which " +

             "exceeds the configured interval " + checkpointConf.getPeriod());

     needCheckpoint = true;

}

當上述needCheckpoint被設置成true的時候,StandbyCheckpointer類的doWork()函數將會調用doCheckpoint()函數正式處理checkpoint。當fsimageedits的合並完成之后,它將會把合並后的fsimage上傳到Active NameNode節點上,Active NameNode節點下載完合並后的fsimage,再將舊的fsimage刪掉(Active NameNode上的)同時清除舊的edits文件。步驟可以歸類如下:

1)、配置好HA后,客戶端所有的更新操作將會寫到JournalNodes節點的共享目錄中,可以通過下面配置

<property>

  <name>dfs.namenode.shared.edits.dir</name>

  <value>qjournal://XXXX/mycluster</value>

</property>

 

<property>

  <name>dfs.journalnode.edits.dir</name>

  <value>/export1/hadoop2x/dfs/journal</value>

</property>

2)、Active NamenodeStandby NameNodeJournalNodesedits共享目錄中同步edits到自己edits目錄中;
  (3)、Standby NameNode中的StandbyCheckpointer類會定期的檢查合並的條件是否成立,如果成立會合並fsimageedits文件;
  (4)、Standby NameNode中的StandbyCheckpointer類合並完之后,將合並之后的fsimage上傳到Active NameNode相應目錄中;
  (5)、Active NameNode接到最新的fsimage文件之后,將舊的fsimageedits文件清理掉;
  (6)、通過上面的幾步,fsimageedits文件就完成了合並,由於HA機制,會使得Standby NameNodeActive NameNode都擁有最新的fsimageedits文件(之前Hadoop 1.xSecondaryNameNode中的fsimageedits不是最新的)

9.5     Hadoop2.2.0HDFS的高可用性實現原理

Hadoop2.0.0之前,NameNode(NN)HDFS集群中存在單點故障single point of failure),每一個集群中存在一個NameNode,如果NN所在的機器出現了故障,那么將導致整個集群無法利用,直到NN重啟或者在另一台主機上啟動NN守護線程。

  主要在兩方面影響了HDFS的可用性:

  (1)、在不可預測的情況下,如果NN所在的機器崩潰了,整個集群將無法利用,直到NN被重新啟動;

  (2)、在可預知的情況下,比如NN所在的機器硬件或者軟件需要升級,將導致集群宕機。

  HDFS的高可用性將通過在同一個集群中運行兩個NNactive NN & standby NN)來解決上面兩個問題,這種方案允許在機器破潰或者機器維護快速地啟用一個新的NN來恢復故障。

  在典型的HA集群中,通常有兩台不同的機器充當NN。在任何時間,只有一台機器處於Active狀態;另一台機器是處於Standby狀態。Active NN負責集群中所有客戶端的操作;而Standby NN主要用於備用,它主要維持足夠的狀態,如果必要,可以提供快速的故障恢復。

  為了讓Standby NN的狀態和Active NN保持同步,即元數據保持一致,它們都將會和JournalNodes守護進程通信Active NN執行任何有關命名空間的修改,它需要持久化到一半(奇數個,一般為3,所以需要持久到2台)以上的JournalNodes(通過edits log持久化存儲)Standby NN負責觀察edits log的變化,它能夠讀取從JNs中讀取edits信息,並更新其內部的命名空間。一旦Active NN出現故障,Standby NN將會保證從JNs中讀出了全部的Edits,然后切換成Active狀態。Standby NN讀取全部的edits可確保發生故障轉移之前,是和Active NN擁有完全同步的命名空間狀態。

  為了提供快速的故障恢復,Standby NN也需要保存集群中各個文件塊的存儲位置。為了實現這個,集群中所有的DataNode將配置好Active NNStandby NN的位置,並向它們發送塊文件所在的位置及心跳,如下圖所示:

說明: Hadoop-HA

說明: http://static.oschina.net/uploads/space/2016/0304/150223_403j_103310.png

在任何時候,集群中只有一個NN處於Active 狀態是極其重要的。否則,在兩個Active NN的狀態下NameSpace狀態將會出現分歧,這將會導致數據的丟失及其它不正確的結果。為了保證這種情況不會發生,在任何時間,JNs只允許一個NN充當writer。在故障恢復期間,將要變成Active 狀態的NN將取得writer的角色,並阻止另外一個NN繼續處於Active狀態。

  為了部署HA集群,你需要准備以下事項:

  (1)、NameNode machines:運行Active NNStandby NN的機器需要相同的硬件配置;

  (2)、JournalNode machines:也就是運行JN的機器。JN守護進程相對來說比較輕量,所以這些守護進程可以與其他守護線程(比如NNYARN ResourceManager)運行在同一台機器上。在一個集群中,最少要運行3JN守護進程,這將使得系統有一定的容錯能力。當然,你也可以運行3個以上的JN,但是為了增加系統的容錯能力,你應該運行奇數個JN357等),當運行NJN,系統將最多容忍(N-1)/2JN崩潰。

  在HA集群中,Standby NN也執行namespace狀態的checkpoints,所以不必要運行Secondary NNCheckpointNodeBackupNode;事實上,運行這些守護進程是錯誤的。

 

 

 

10    命令

hadoop fs help

% hadoop fs -copyFromLocal /input/docs/quangle.txt quangle.txt  將本地文件復制到HDFS中,目的地為相對地址,相對的是HDFS上的/user/root目錄,root為用戶,不同的用戶執行,則不同

hadoop fs -copyToLocal quangle.txt quangle.copy.txt        HDFS中下載文件到本地

% hadoop fs -mkdir books

% hadoop fs -ls .

Found 2 items

drwxr-xr-x - tom supergroup 0 2009-04-02 22:41 /user/tom/books

-rw-r--r--  1 tom supergroup 118 2009-04-02 22:29 /user/tom/quangle.txt

第一列為文件模式,第二列文件的副本數,如果目錄則沒有;第三、四列表示文件的所屬用戶和用戶組;第五列為文件大小,目錄沒有。

一個文件的執行權限X沒有什么意義,因為你不可能在HDFS系統時執行一個文件,但它對目錄是有用的,因為在訪問一個目錄的子項時是不需要此種權限的

 

11    HDFSHadoop Distributed Filesystem

處理超大的文件:一個文件可以是幾百M,甚至是TB級文件

流式數據訪問:一次寫入,多次讀取

廉價的機器

不適用於低延遲數據訪問,如果需要可以使用Hbase

不適用於大量的小文件,因為每個文件存儲在DFS中時,都會在NameNode上保存文件的元數據信息,這會會急速加太NameNode節點的內存占用,目前每個文件的元數據信息大概占用150字節,如果有一百萬個文件,則要占用300MB的內存

不支持並發寫,並且也不支持文件任意位置的寫入,只能一個用戶寫在文件最末

11.1       

默認塊大小為64M,如果某個文件不足64,則不會占64,而是文件本身文件大小,這與操作系統文件最小存儲單元塊不同

 

分塊存儲的好處:

一個文件的大小可以大於網絡中的任意一個硬盤的容量,如果不分塊,則不能存儲在硬盤中,當分塊后,就可以將這個大文件分塊存儲到集群中的不同硬盤中

分塊后適合多副本數據備份,保證了數據的安全

 

可以通過以下命令查看塊信息:

hadoop fsck / -files -blocks

11.2        namenodedatanode

一個namenode(管理者),多個datanode(工作者,存放數據)

 

namenode管理文件系統的命名空間,它維護着文件系統樹及整個樹里的文件和目錄,這些系統以兩個文件持久化硬盤上永久保存着:命名空間鏡像文件fsimage和編輯日志文件edits

 

namenode記錄了每個文件中各個塊所在的datanode信息,但它並不將這些位置信息持久化硬盤上,因為這些信息會在系統啟動時由datanode上報給namenode節點后重建

 

如果在沒有任何備份,namenode如果壞了,則整個文件系統將無法使用,所以就有了secondnamenode輔助節點:

secondnamenode除了備份namenode上的元數據持久信息外,最主要的作用是定期的將namenode上的fsimageedits兩個文件拷貝過來進行合並后,將傳回給namenodesecondnamenode的備份並非namenode上所有信息的完全備份,它所保存的信息就是滯后於namenode的,所以namenode壞掉后,盡管可以手動從secondnamenode恢復,但也難免丟失部分數據(最近一次合並到當前時間內所做的數據操作)

11.3        聯邦HDFS

由於1.X只能有一個namenode,隨着文件越來越多,namenode的內存就會受到限制,到某個時候肯定是存放不了更多的文件了(雖然datanode可以加入新的datanode可以解決存儲容量問題),不可以無限在一台機器上加內存。在2.X版本中,引入了聯邦HDFS允許系統通過添加namenode進行擴展,這樣每個namenode管理着文件系統命名空間的一部分元數據信息

11.4        HDFS高可用性

聯邦HDFS只解決了內存數據擴展的問題,但並沒有解決namenode單節點問題,即當某個namenode壞掉所,由於namenode沒有備用,所以一旦毀壞后還是會導致文件系統無法使用。

 

HDFS高可用性包括:水平擴展namenode以實現內存擴展、高安全(壞掉還有其他備用的節點)及熱切換(壞掉后無需手動切換到備用節點)到備用機

 

2.x中增加了高可用性支持,通過活動、備用兩台namenode來實現,當活動namenode失效后,備用namenode就會接管安的任務並開始服務於來自客戶端的請求,不會有任何明顯中斷服務,這需要架構如下:

n  Namenode之間(活動的與備用的兩個節點)之間需要通過共享存儲編輯日志文件edits,即這edits文件放在一個兩台機器都能訪問得到的地方存儲,當活動的namenode毀壞后,備用namenode自動切換為活動時,備用機將edits文件恢復備用機內存

n  Datanode需要現時向兩個namenode發送數據塊處理報告

n  客戶端不能直接訪問某個namenode了,因為一旦某個出問題后,就需要通過另一備用節點來訪問,這需要用戶對namenode訪問是透明的,不能直接訪問namenode,而是通過管理這些namenode集群入口地址透明訪問

在活動namenode失效后,備用namenode能夠快速(幾十秒的時間)實現任務接管,因為最新的狀態存儲在內存中:包括最新的編輯日志和最新的數據塊映射信息

 

備用切換是通過failover_controller故障轉移控制器來完成的,故障轉移控制器是基於ZooKeeper實現的;每個namenode節點上都運行着一個輕量級的故障轉移控制器,它的工作就是監視宿主namenode是否失效(通過一個簡單的心跳機制實現)並在namenode失效時進行故障切換;用戶也可以在namenode沒有失效的情況下手動發起切換,例如在進行日常維護時;另外,有時無法確切知道失效的namenode是否已經停止運行,例如在網絡異常情況下,同樣也可能激發故障轉換,但先前的活動着的namenode依然運行着並且依舊是活動的namenode,這會出現其他問題,但高可用實現做了“規避”措施,如殺死行前的namenode進程,收回訪問共享存儲目錄的權限等

 

偽分布式: fs.default.name=hdfs://localhost:8020dfs.replication=1,如果設置為3,將會持續收到塊副本不中的警告,設置這個屬性后就不會再有問題了

 

11.5        Hadoop文件系統JAVA接口

Hadoop本身是由Java編寫的

11.5.1   FileSystem繼承圖

org.apache.hadoop.fs.FileSystem是文件系統的抽象類,常見有以下實現類:

文件系統

URI scheme

Java實現類

描述

Local

file

org.apache.hadoop.fs.LocalFileSystem

使用了客戶端校驗和的本地文件系統(未使用校驗和的本地文件系統請使用RawLocalFileSystem

HDFS

hdfs

org.apache.hadoop.hdfs.DistributedFileSystem

Hadoop分布式文件系統

HFTP

hftp

org.apache.hadoop.hdfs.HftpFileSystem

通過HttpHdfs進行只讀訪問的文件系統,用於實現不同版本HDFS集群間的數據復制

HSFTP

hsftp

org.apache.hadoop.hdfs.HsftpFileSystem

同上,只是https協議

 

 

org.apache.hadoop.

 

11.5.2   讀取數據

獲取FileSystem實例有以下幾個靜態方法:

publicstatic FileSystem get(Configuration conf) throws IOException//獲取core-sit.xmlfs.default.name配置屬性所配置的URI來返回相應的文件系統,由於core-sit.xml已配置,所以一般調用這個方法即可

publicstatic FileSystem get(URI uri, Configuration conf) throws IOException//根據uri參數里提供scheme信息返回相應的文件系統,即hdfs://hadoop-master:9000,則返回的是hdfs文件系統

publicstatic FileSystem get(URI uri, Configuration conf, String user) throws IOException

 

有了FileSystem后,就可以調用open()方法獲取文件輸入流:

public FSDataInputStream open(Path f) throws IOException //默認緩沖4K

publicabstract FSDataInputStream open(Path f, int bufferSize) throws IOException

 

示例:將hdfs文件系統中的文件內容在標准輸出顯示

import java.io.InputStream;

import java.net.URI;

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.fs.FileSystem;

import org.apache.hadoop.fs.Path;

import org.apache.hadoop.io.IOUtils;

 

publicclass FileSystemCat {

    publicstaticvoid main(String[] args) throws Exception {

        // 如果為默認端口8020,則可以省略端口

        String uri = "hdfs://hadoop-master:9000/wordcount/input/wordcount.txt";

        Configuration conf = new Configuration();

        // FileSystem fs = FileSystem.get(URI.create(uri), conf);

        // 因為get方法的URI參數只需要URI scheme,所以只需指定服務地址即可,無需同具體到某個文件

        FileSystem fs = FileSystem.get(URI.create("hdfs://hadoop-master:9000"), conf);

        //或者這樣使用

        // conf.set("fs.default.name", "hdfs://hadoop-master:9000");

        // FileSystem fs = FileSystem.get(conf);

        InputStream in = null;

        try {

            in = fs.open(new Path(uri));

            IOUtils.copyBytes(in, System.out, 4096, false); //無需使用循環對流進行拷貝,借助於工具類IOUtils即可

        } finally {

            IOUtils.closeStream(in);//不直接調用輸入輸出流的close方法,而是使用IOUtils工具類

        }

    }

}

 

實際上,FileSystemopen方法返回的是FSDataInputStream類型的對象,而非Java標准輸入流,這個類繼承了標准輸入流DataInputStream

publicclassFSDataInputStreamextends DataInputStream

    implements Seekable, PositionedReadable, Closeable, HasFileDescriptor {

並且FSDataInputStream類實現了Seekable接口,支持隨機訪問,因此可以從流的任意位置讀取數據。

Seekable接口支持在文件中找到指定的位置,並提供了一個查詢當前位置相當於文件起始位置偏移量的方法getPos()

publicinterfaceSeekable {

    // 定位到文件指定的位置,與標准輸入流的InputStream.skip不同的是,seek可以定位到文件中的任意絕對位置,而

    // skip只能相對於當前位置才能定位到新的位置。這里會傳遞的是相對於文件開頭的絕對位置,不能超過文件長度。注:seek開銷很高,謹慎調用

    void seek(long pos) throws IOException;

    // 返回當前相對於文件開頭的偏移量

    long getPos() throws IOException;

    boolean seekToNewSource(long targetPos) throws IOException;

}

 

示例:改寫上面實例,讓它輸出兩次

import java.net.URI;

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.IOUtils;

publicclass FileSystemCat {

    publicstaticvoid main(String[] args) throws Exception {

        String uri = "hdfs://hadoop-master:9000/wordcount/input/wordcount.txt";

        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(URI.create(uri), conf);

        FSDataInputStream in = null;

        try {

            in = fs.open(new Path(uri));

            IOUtils.copyBytes(in, System.out, 4096, false);

            System.out.println("\n");

            in.seek(0);//跳到文件的開頭

            IOUtils.copyBytes(in, System.out, 4096, false);

        } finally {

            IOUtils.closeStream(in);

        }

    }

}

 

FSDataInputStream類還實現了PositionedReadable接口,這可以從一個指定的偏移量處讀取文件的一部分:

publicinterfacePositionedReadable {

    // 從文件指定的position處讀取最多length字節的數據並存入緩沖區buffer的指定偏移量offset處,返回的值是

    // 實際讀取到的字節數:調用者需要檢查這個值,有可能小於參數length

    publicint read(long position, byte[] buffer, int offset, int length) throws IOException;

    // 與上面方法相當,只是如果讀取到文件最末時,被讀取的字節數可能不滿length,此時則會拋異常

    publicvoid readFully(long position, byte[] buffer, int offset, int length) throws IOException;

    // 與上面方法相當,只是每次讀取的字節數為buffer.length

    publicvoid readFully(long position, byte[] buffer) throws IOException;

}

注:上面這些方法都不會修改當前所在文件偏移量

11.5.3   寫入數據

FileSystem類有一系列參數不同的create創建文件方法,最簡單的方法:

  publicFSDataOutputStreamcreate(Path f) throws IOException {

還有一系列不同參數的重載方法,他們最終都是調用下面這個抽象方法實現的:

  publicabstract FSDataOutputStream create(Path f,

      FsPermission permission, //權限

      boolean overwrite, //如果文件存在,傳false時會拋異常,否則覆蓋已存在的文件

      int bufferSize, //緩沖區的大小

      short replication, //副本數量

      long blockSize, //塊大小

      Progressable progress) throws IOException; //處理進度的回調接口

一般調用簡單方法時,如果文件存在,則是會覆蓋,如果不想覆蓋,可以指定overwrite參數為false,或者使用FileSystem類的exists(Path f)方法進行判斷:

  publicbooleanexists(Path f) throws IOException {//可以用來測試文件文件夾是否存在

 

 

進度回調接口,當數據每次寫完緩沖數據后,就會回調該接口顯示進度信息:

package org.apache.hadoop.util;

publicinterface Progressable {

  publicvoid progress();//返回處理進度給Hadoop應用框架

}

 

另一種新建文件的方法是使用append方法在一個已有文件末尾追加數據(該方法也有一些重載版本):

  public FSDataOutputStream append(Path f) throws IOException {

 

示例:帶進度的文件上傳

publicclass FileCopyWithProgress {

    publicstaticvoid main(String[] args) throws Exception {

        InputStream in = new BufferedInputStream(new FileInputStream("d://1901.all"));

        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(URI.create("hdfs://hadoop-master:9000"), conf);

        OutputStream out = fs.create(new Path("hdfs://hadoop-master:9000/ncdc/all/1901.all"), new Progressable() {

            publicvoid progress() {

                System.out.print(".");

            }

        });

        IOUtils.copyBytes(in, out, 4096, true);

    }

}

 

FSDataInputStream 一樣,FSDataOutputStream類也有一個getPos方法,用來查詢當前位置,但與FSDataInputStream不同的是,不允許在文件中定位,這是因為HDFS只允許對一個已打開的文件順序寫入,或在現有文件的末尾追加數據,安不支持在除文件末尾之外的其他位置進行寫入,所以就沒有seek定位方法了

11.5.4   上傳本地文件

publicvoid copyFromLocalFile(Path src, Path dst)

publicvoid copyFromLocalFile(boolean delSrc, Path src, Path dst)

publicvoid copyFromLocalFile(boolean delSrc, boolean overwrite,Path[] srcs, Path dst)

publicvoid copyFromLocalFile(boolean delSrc, boolean overwrite,Path src, Path dst)

 

delSrc - whether to delete the src是否刪除源文件

overwrite - whether to overwrite an existing file是否覆蓋已存在的文件

srcs - array of paths which are source 可以上傳多個文件數組方式

dst path 目標路徑,如果存在,且都是目錄的話,會將文件存入它下面,並且上傳文件名不變;如果不存在,則會創建並認為它是文件,即上傳的文件名最終會成為dst指定的文件名

 

Configuration conf = new Configuration();

conf.set("fs.default.name", "hdfs://hadoop-master:9000");

FileSystem fs = FileSystem.get(conf);

fs.copyFromLocalFile(new Path("c:/t_stud.txt"), new Path("hdfs://hadoop-master:9000/db1/output1"));

11.5.5   重命名或移動文件

fileSystem.rename(src, dst);

 

形為重命名,實際上該方法還可以移動文件,與上傳目的地dst參數一樣:如果dst為存在的目錄,則會放在它下面;如果不存在,則會創建並認為它是文件,即上傳的文件名最終會成為dst指定的文件名

 

Configuration conf = new Configuration();

conf.set("fs.default.name", "hdfs://hadoop-master:9000");

FileSystem fs = FileSystem.get(conf);

fs.rename(new Path("hdfs://hadoop-master:9000/db1/output2"), new Path("hdfs://hadoop-master:9000/db3/output2"));

 

11.5.6   刪除文件目錄

FileSystemdelete()方法可以用來刪除文件目錄

  publicabstractboolean delete(Path f, boolean recursive) throws IOException;

如果f是一個文件或空目錄,那么recursive的值就會被忽略。只有在recursive值為true時,非空目錄及其內容才會被刪除(如果刪除非空目錄時recursivefalse,則會拋IOException異常?

11.5.7   創建目錄

FileSystem提供了創建目錄的方法:

  publicbooleanmkdirs(Path f) throws IOException {

如果父目錄不存在,則也會自動創建,並返回是否成功

通常情況下,我們不需要調用這個方法創建目錄,因為調用create方法創建文件時,如果父目錄不存在,則會自動創建

11.5.8   查看目錄及文件信息

FileStatus類封裝了文件系統中的文件和目錄的元數據,包括文件長度、大小、副本數、修改時間、所有者、權限等

FileSystemgetFileStatus方法可以獲取FileStatus對象:

  publicabstract FileStatus getFileStatus(Path f) throws IOException;

 

示例:獲取文件(夾)狀態信息

publicclass ShowFileStatus {

    publicstaticvoid main(String[] args) throws IOException {

        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(URI.create("hdfs://hadoop-master:9000"), conf);

        OutputStream out = fs.create(new Path("/dir/file"));

        out.write("content".getBytes("UTF-8"));

        //out.close();

IOUtils.closeStream(out);

 

        // 文件的狀態信息

        Path file = new Path("/dir/file");

        FileStatus stat = fs.getFileStatus(file);

        System.out.println(stat.getPath().toUri().getPath());

        System.out.println(stat.isDir());//是否文件夾

        System.out.println(stat.getLen());//文件大小

        System.out.println(stat.getModificationTime());//文件修改時間

        System.out.println(stat.getReplication());//副本數

        System.out.println(stat.getBlockSize());//文件系統所使用的塊大小

        System.out.println(stat.getOwner());//文件所有者

        System.out.println(stat.getGroup());//文件所有者所在組

        System.out.println(stat.getPermission().toString());//文件權限

        System.out.println();

        // 目錄的狀態信息

        Path dir = new Path("/dir");

        stat = fs.getFileStatus(dir);

        System.out.println(stat.getPath().toUri().getPath());

        System.out.println(stat.isDir());

        System.out.println(stat.getLen());//文件夾為0

        System.out.println(stat.getModificationTime());

        System.out.println(stat.getReplication());//文件夾為0

        System.out.println(stat.getBlockSize());//文件夾為0

        System.out.println(stat.getOwner());

        System.out.println(stat.getGroup());

        System.out.println(stat.getPermission().toString());

    }

}

11.5.9   列出文件(狀態)

除了上面FileSystemgetFileStatus一次只能獲取一個文件或目錄的狀態信息外,FileSystem還可以一次獲取多個文件的FileStatus或目錄下的所有文件的FileStatus,這可以調用FileSystemlistStatus方法,該方法有以下重載版本:

  publicabstract FileStatus[] listStatus(Path f) throws IOException;

  public FileStatus[] listStatus(Path f, PathFilter filter) throws IOException {

  public FileStatus[] listStatus(Path[] files) throws IOException {

  public FileStatus[] listStatus(Path[] files, PathFilter filter) throws IOException {

當傳入的參數是一個文件時,它會簡單轉成以數組方式返回長度為1FileStatus對象。當傳入的是一個目錄時,則返回0或多個FileStatus對象,包括此目錄中包括的所有文件和目錄

 

listStatus方法可以列出目錄下所有文件的文件狀態,所以就可以借助於這個特點列出某個目錄下的所有文件(包括子目錄):

publicclass ListStatus {

    publicstaticvoid main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(URI.create("hdfs://hadoop-master:9000"), conf);

 

        Path[] paths = new Path[2];

        // 目錄

        paths[0] = new Path("hdfs://hadoop-master:9000/ncdc");

        // 文件

        paths[1] = new Path("hdfs://hadoop-master:9000/wordcount/input/wordcount.txt");

 

        // 只傳一個目錄進去。注:listStatus方法只會將直接子目錄或子文件列出來,

        // 而不會遞歸將所有層級子目錄文件列出

        FileStatus[] status = fs.listStatus(paths[0]);

        Path[] listedPaths = FileUtil.stat2Paths(status);

        for (Path p : listedPaths) {

            // 輸出輸入目錄下的所有文件及目錄的路徑

            System.out.println(p);

        }

        System.out.println();

 

        // 只傳一個文件進去

        status = fs.listStatus(paths[1]);

        listedPaths = FileUtil.stat2Paths(status);

        for (Path p : listedPaths) {

            // 輸出輸入文件的路徑

            System.out.println(p);

        }

        System.out.println();

 

        //傳入的為一個數組:包括文件與目錄

        status = fs.listStatus(paths);

        // FileStatus數組轉換為Path數組

        listedPaths = FileUtil.stat2Paths(status);

        for (Path p : listedPaths) {

            // 輸出所有輸入的文件的路徑,以及輸入目錄下所有文件或子目錄的路徑

            System.out.println(p);

        }

    }

}

11.5.10            獲取Datanode信息

Configuration conf = new Configuration();

conf.set("fs.default.name", "hdfs://hadoop-master:9000");

FileSystem fs = FileSystem.get(conf);

DistributedFileSystem hdfs = (DistributedFileSystem) fs;

DatanodeInfo[] dns = hdfs.getDataNodeStats();

for (int i = 0, h = dns.length; i < h; i++) {

    System.out.println("datanode_" + i + "_name:  " + dns[i].getHostName());

}

通過DatanodeInfo可以獲得datanode更多的消息

11.5.11            文件通配

FileSystem提供了兩個通配的方法:

  public FileStatus[] globStatus(Path pathPattern) throws IOException {

  public FileStatus[] globStatus(Path pathPattern, PathFilter filter) throws IOException {

pathPattern參數是通配,filter是進一步驟過濾

注:根據通配表達式,匹配到的可能是目錄,也可能是文件,這要看通配表達式是只到目錄,還是到文件。具體示例請參考下面的PathFilter

11.5.12            過濾文件

有時通配模式並不總能多精確匹配到我們想要的文件,此時此要使用PathFilter參數進行過濾。FileSystemlistStatus() globStatus()方法就提供了此過濾參數

publicinterfacePathFilter {

  boolean accept(Path path);

}

 

示例:排除匹配指定正則表達式的路徑

publicclass RegexExcludePathFilter implements PathFilter {

    privatefinal String regex;

    public RegexExcludePathFilter(String regex) {

        this.regex = regex;

    }

    publicboolean accept(Path path) {

        return !path.toString().matches(regex);

    }

}

 

    publicstaticvoid main(String[] args) throws IOException {

        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(URI.create("hdfs://hadoop-master:9000"), conf);

        FileStatus[] status = fs.globStatus(new Path("/2007/*/*"));//匹配到文件夾

        Path[] listedPaths = FileUtil.stat2Paths(status);

        for (Path p : listedPaths) {

            System.out.println(p);

        }

    }

    publicstaticvoid main(String[] args) throws IOException {

        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(URI.create("hdfs://hadoop-master:9000"), conf);

        FileStatus[] status = fs.globStatus(new Path("/2007/*/*/*30.txt"));//匹配到文件

        Path[] listedPaths = FileUtil.stat2Paths(status);

        for (Path p : listedPaths) {

            System.out.println(p);

        }

    }

    publicstaticvoid main(String[] args) throws IOException {

        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(URI.create("hdfs://hadoop-master:9000"), conf);

        FileStatus[] status = fs.globStatus(new Path("/2007/*/*"), new RegexExcludePathFilter(

                "^.*/2007/12/31$"));//過濾掉31號的目錄

        Path[] listedPaths = FileUtil.stat2Paths(status);

        for (Path p : listedPaths) {

            System.out.println(p);

        }

    }

11.6        數據流

11.6.1   文件讀取過程

1、  客戶端調用DistributedFileSystem在編程時我們一般直接調用的是其抽像父類FileSystemopen方法,對於HDFS文件系統來說,實質上調用的還是DistributedFileSystemopen方法)的open()方法來打開要讀取的文件,並返回FSDataInputStream類對象(該類實質上是對DFSInputStream的封裝,由它來處理與datanodenamenode的通信,管理I/O

2、  DistributedFileSystem通過使用RPC來調用namenode,查看文件在哪些datanode上,並返回這些datanode的地址(注:由於同一文件塊副本的存放在很多不同的datanode節點上,返回的都是網絡拓撲中距離客戶端最近的datanode節點地址,距離算法請參考后面)

3、  客戶端調用FSDataInputStream對象的read()方法

4、  FSDataInputStream去相應datanode上讀取第一個數據塊(這一過程並不需要namenode的參與,該過程是客戶端直接訪問datanote

5、  FSDataInputStream去相應datanode上讀取第二個數據塊如此讀完所有數據塊(注:數據塊讀取應該是同時並發讀取,即在讀取第一塊時,也同時在讀取第二塊,只是在拼接文件時需要按塊順序組織成文件)

6、  客戶端調用FSDataInputStreamclose()方法關閉文件輸入流

 

假設有數據中心d1機架r1中的n1節點表示為 /d1/r1/n1

distance(/d1/r1/n1, /d1/r1/n1) = 0 (processes on the same node)同一節點

distance(/d1/r1/n1, /d1/r1/n2) = 2 (different nodes on the same rack)同一機架上不同節點

distance(/d1/r1/n1, /d1/r2/n3) = 4 (nodes on different racks in the same data center)同一數據中心不同機架上不同節點

distance(/d1/r1/n1, /d2/r3/n4) = 6 (nodes in different data centers)不同數據中心

 

哪些節點是哪些機架上是通過配置實現的,具體請參考后面的章節

 

11.6.2   文件寫入過程

1、  客戶端調用DistributedFileSystemcreate()創建文件,並向客戶端返回FSDataOutputStream類對象(該類實質上是對DFSOutputStream的封裝,由它來處理與datanodenamenode的通信,管理I/O

2、  DistributedFileSystemnamenode發出創建文件的RPC調用請求,namenode會告訴客戶端該文件會寫到哪些datanode

3、  客戶端調用FSDataOutputStreamwrite方法寫入數據

4、  FSDataOutputStreamdatanode寫數據

5、  當數據塊寫完(要達到dfs.replication.min副本數)后,會返回確認寫完的信息給FSDataOutputStream。在返回寫完信息的后,后台系統還要拷貝數據副本要求達到dfs.replication設置的副本數,這一過程是在后台自動異步復制完成的,並不需要等所有副本都拷貝完成后才返回確認信息到FSDataOutputStream

6、  客戶端調用FSDataOutputStreamclose方法關閉流

7、  DistributedFileSystem發送文件寫入完成的信息給namenode

 

數據存儲在哪些datanode上,這是有默認布局策略的:

在客戶端運行的datanode節點上放第一個副本(如果客戶端是在集群外的機器上運行的話,會隨機選擇一個空閑的機器),第二個副本則放在與第一個副本不在同一機架的節點上,第三個副本則放在與第二個節點同一機架上的不同節點上,超過3個副本的,后繼會隨機選擇一台空閑機器放后繼其他副本。這樣做的目的兼顧了安全與效率問題

11.6.3   緩存同步

當新建一個文件后,在文件系統命名空間立即可見,但數據不一定能立即可見,即使數據流已刷新:

Path p = new Path("p");

OutputStream out = fs.create(p);

out.write("content".getBytes("UTF-8"));

out.flush();

assertThat(fs.getFileStatus(p).getLen(), is(0L));

當寫入數據超過一個塊后,第一個數據塊對新的reader就是可見的,之后的塊也是一樣,當后面的塊寫入后,前面的塊才能可見。總之,當前正在寫入的塊對其他reader是不可見的

FSDataOutputStream提供了一個方法sync()來使所有緩存與數據節點強行同步,當sync()方法調用成功后,對所有新的reader而言都可見:

Path p = new Path("p");

FSDataOutputStream out = fs.create(p);

out.write("content".getBytes("UTF-8"));

out.flush();

out.sync();

assertThat(fs.getFileStatus(p).getLen(), is(((long) "content".length())));

注:如果調用了FSDataOutputStreamclose()方法,該方法也會調用sync()

 

12    壓縮

文件壓縮有兩大好處:減少存儲文件所需要的磁盤空間,並加速數據在網絡和磁盤上的傳輸

 

所有的壓縮算法要權衡時間與空間,壓縮時間越短,壓縮率超低,壓縮時間越長,壓縮率超高。上表時每個工具都有9個不同的壓縮級別:-1為優化壓縮速度,-9為優化壓縮空間。如下面命令通過最快的壓縮方法創建一個名為file.gz的壓縮文件:

gzip -1 file

不同壓縮工具有不同的壓縮特性。gzip是一個通用的壓縮工具,在空間與時間比較均衡。bzip2壓縮能力強於gzip,但速度會慢一些。另外,LZOLZ4Snappy都優化了壓縮速度,比gzip快一個數量級,但壓縮率會差一些(LZ4Snappy的解壓速度比LZO高很多)

上表中的“是否可切分”表示數據流是否可以搜索定位(seek)。

上面這些算法類都實現了CompressionCodec接口。

CompressionCodec接口包含兩個方法,可以用於壓縮和解壓。如果要對數據流進行壓縮,可以調用createOutputStream(OutputStream out)方法得到CompressionOutputStream輸出流;如果要對數據流進行解壓,可以調用createInputStream(InputStream in)方法得到CompressionInputStream輸入流

CompressionOutputStreamCompressionInputStream類似java.util.zip.DeflaterOutputStreamjava.util.zip.DeflaterInputStream,只不過前兩者能夠重置其底層的壓縮與解壓算法

12.1        使用CompressionCodec對數據流進行壓縮與解壓

示例:壓縮從標准輸入讀取的數據,然后將其寫到標准輸出

publicstaticvoid main(String[] args) throws Exception {

    ByteArrayInputStream bais = new ByteArrayInputStream("測試".getBytes("GBK"));

 

    Class<?> codecClass = Class.forName("org.apache.hadoop.io.compress.GzipCodec");

    Configuration conf = new Configuration();

    CompressionCodec codec = (CompressionCodec) ReflectionUtils.newInstance(codecClass, conf);

 

    CompressionOutputStream out = codec.createOutputStream(System.out);// 壓縮流,構造時會輸出三字節的頭信息:31-117 8

//1F=16+15=31;負數是以補碼形勢存儲的,8B的二進制為10001011,先減一得到10001010,再除符號位各們取反得到原碼11110101,即得到 -117

    System.out.println();

    IOUtils.copyBytes(bais, out, 4096, false);// 將壓縮流輸出到標准輸出

    out.finish();

    System.out.println();

 

    bais = new ByteArrayInputStream("測試".getBytes("GBK"));

    ByteArrayOutputStream baos = new ByteArrayOutputStream(4);

    out = codec.createOutputStream(baos);

    IOUtils.copyBytes(bais, out, 4096, false);// 將壓縮流輸出到緩沖

    out.finish();

 

    bais = new ByteArrayInputStream(baos.toByteArray());

    CompressionInputStream in = codec.createInputStream(bais);// 解壓縮流

    IOUtils.copyBytes(in, System.out, 4096, false);// 將壓縮流輸出到標准輸出

 

    // ---------將壓縮文件上傳到Hadoop

    // 注:hadoop默認使用的是UTF-8編碼,如果使用GBK上傳,使用 hadoop fs -text /gzip_test 命令

    // Hadoop系統中查看時顯示不出來,但Down下來后可以

    bais = new ByteArrayInputStream("測試".getBytes("UTF-8"));

    FileSystem fs = FileSystem.get(URI.create("hdfs://hadoop-master:9000"), conf);

    out = codec.createOutputStream(fs.create(new Path("/gzip_test.gz")));

    IOUtils.copyBytes(bais, out, 4096);

 

    IOUtils.closeStream(out);

    IOUtils.closeStream(in);

    IOUtils.closeStream(fsout);

}

12.2通過CompressionCodecFactory自動獲取CompressionCodec

在讀取一個壓縮文件時,可以通過文件擴展名推斷需要使用哪個codec,如以.gz結尾,則使用GzipCodec來讀取。可以通過調用CompressionCodecFactorygetCodec()方法根據擴展名來得到一個CompressionCodec

 

示例:根據文件擴展名自動選取codec解壓文件

publicclass FileDecompressor {

    publicstaticvoid main(String[] args) throws Exception {

        String uri = "hdfs://hadoop-master:9000/gzip_test.gz";

        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(URI.create(uri), conf);

 

        Path inputPath = new Path(uri);

        CompressionCodecFactory factory = new CompressionCodecFactory(conf);

        // 根據文件的擴展名自動找到對應的codec

        CompressionCodec codec = factory.getCodec(inputPath);

        if (codec == null) {

            System.err.println("No codec found for " + uri);

            System.exit(1);

        }

 

        String outputUri = CompressionCodecFactory.removeSuffix(uri, codec.getDefaultExtension());

        InputStream in = null;

        OutputStream out = null;

        try {

            in = codec.createInputStream(fs.open(inputPath));

            // 將解壓出的文件放在hdoop上的同一目錄下

            out = fs.create(new Path(outputUri));

            IOUtils.copyBytes(in, out, conf);

        } finally {

            IOUtils.closeStream(in);

            IOUtils.closeStream(out);

        }

    }

}

 

CompressionCodecFactoryio.compression.codecscore-site.xml配置文件里)配置屬性里定義的列表中找到codec

<property>

  <name>io.compression.codecs</name>

  <value>org.apache.hadoop.io.compress.DefaultCodec,org.apache.hadoop.io.compress.GzipCodec,org.apache.hadoop.io.compress.BZip2Codec,org.apache.hadoop.io.compress.SnappyCodec</value>

  <description>A list of the compression codec classes that can be used for compression/decompression.</description>

</property>

12.3        本地native壓縮庫

運行上面示例時,會報以下警告:

WARN  [main] org.apache.hadoop.io.compress.snappy.LoadSnappy  - Snappy native library not loaded

hdfs://hadoop-master:9000/gzip_test

WARN  [main] org.apache.hadoop.io.compress.zlib.ZlibFactory  - Failed to load/initialize native-zlib library

這是因為程序是在Windows上運行的,在本地沒有搜索到native類庫,而使用Java實現來進行壓縮與解壓。如果將程序打包上傳到Linux上運行時,第二個警告會消失:

[root@hadoop-master /root/tmp]# hadoop jar /root/tmp/FileDecompressor.jar

16/04/26 11:13:31 INFO util.NativeCodeLoader: Loaded the native-hadoop library

16/04/26 11:13:31 WARN snappy.LoadSnappy: Snappy native library not loaded

16/04/26 11:13:31 INFO zlib.ZlibFactory: Successfully loaded & initialized native-zlib library

但第一個警告還是有,原因是Linux系統上沒有安裝snappy,下面安裝:

一、安裝snappy

         yum install snappy snappy-devel

二、使得Snappy類庫對Hadoop可用

         ln -sf /usr/lib64/libsnappy.so /root/hadoop-1.2.1/lib/native/Linux-amd64-64

再次運行:

[root@hadoop-master /root/hadoop-1.2.1/lib/native/Linux-amd64-64]# hadoop jar /root/tmp/FileDecompressor.jar

16/04/26 11:42:19 WARN snappy.LoadSnappy: Snappy native library is available

16/04/26 11:42:19 INFO util.NativeCodeLoader: Loaded the native-hadoop library

16/04/26 11:42:19 INFO snappy.LoadSnappy: Snappy native library loaded

16/04/26 11:42:19 INFO zlib.ZlibFactory: Successfully loaded & initialized native-zlib library

 

 

與內置的Java實現相比,原生的gzip類庫可以減少約束一半的解壓時間與約10%的壓縮時間,下表列出了哪些算法有Java實現,哪些有本地實現:

默認情況下,Hadoop會根據自身運行的平台搜索原生代碼庫,如果找到則自加載,所以無需為了使用原生代碼庫而修改任何設置,但是,如果不想使用原生類型,則可以修改hadoop.native.lib配置屬性(core-site.xml)為false

<property>

  <name>hadoop.native.lib</name>

  <value>false</value>

  <description>Should native hadoop libraries, if present, be used.</description>

</property>

12.4        CodecPool壓縮池

如何使用的是代碼庫,並且需要在應用中執行大量壓縮與解壓操作,可以考慮使用CodecPool,它支持反復使用壓縮秘解壓,減少創建對應的開銷

 

publicstaticvoid main(String[] args) throws Exception {

    //注:這里使用GBK,如果使用UTF-8,則輸出到標准時會亂碼,原因操作系統標准輸出為GBK解碼

    ByteArrayInputStream bais = new ByteArrayInputStream("測試".getBytes("GBK"));

    ByteArrayOutputStream bois = new ByteArrayOutputStream();

    Class<?> codecClass = Class.forName("org.apache.hadoop.io.compress.GzipCodec");

    Configuration conf = new Configuration();

    CompressionCodec codec = (CompressionCodec) ReflectionUtils.newInstance(codecClass, conf);

 

    CompressionOutputStream out = null;

    CompressionInputStream in = null;

    Compressor cmpressor = null;// 壓縮實例

    Decompressor decompressor = null;// 解壓實例

    try {

        // 從池中獲取或新建一個Compressor壓縮實例

        cmpressor = CodecPool.getCompressor(codec);

        // 從池中獲取或新建一個Compressor解壓縮實例

        decompressor = CodecPool.getDecompressor(codec);

        out = codec.createOutputStream(bois, cmpressor);

 

        System.out.println();

        IOUtils.copyBytes(bais, out, 4096, false);// 將壓縮流輸出到緩沖

        out.finish();

 

        bais = new ByteArrayInputStream(bois.toByteArray());

        in = codec.createInputStream(bais, decompressor);// 解壓壓縮流

        IOUtils.copyBytes(in, System.out, 4096, false);// 解壓后標准輸出

 

    } finally {

        IOUtils.closeStream(out);

        CodecPool.returnCompressor(cmpressor);// 用完之后返回池中

        CodecPool.returnDecompressor(decompressor);

    }

}

12.5        壓縮數據分片問題

如果壓縮數據超過塊大小后,會被分成多塊,如果每個片斷數據單獨作傳遞給不同的Map任務,由於gzip數據是不能單獨片斷進行解壓的,所以會出問題。但實際上Mapreduce任務還是可以處理gzip文件的,只是如果發現(根據擴展名)是gz,就不會進行文件任務切分(其他算法也一樣,只要不支持單獨片斷解壓的,都會交給同一Map進行處理),而將這個文件塊都交個同一個Map任務進行處理,這樣會影響性能問題。

只有bzip2壓縮格式的文件支持數據任務的切分,哪些壓縮能切分請參考這里

12.6        Mapreduce中使用壓縮

要想壓縮mapreduce作業的輸出(即這里講的是對reduce輸出壓縮),應該在mapred-site.xml配置文件的配置項mapred.output.compress設置為truemapred.output.compression.code設置為要使用的壓縮算法:

<property>

  <name>mapred.output.compress</name>

  <value>false</value>

  <description>Should the job outputs be compressed?

  </description>

</property>

 

<property>

  <name>mapred.output.compression.codec</name>

  <value>org.apache.hadoop.io.compress.DefaultCodec</value>

  <description>If the job outputs are compressed, how should they be compressed?

  </description>

</property>

也可以直接在作業啟動程序里通過FileOutputFormat進行設置:

publicstaticvoid main(String[] args) throws Exception {

    Configuration conf = new Configuration();

    conf.set("mapred.job.tracker", "hadoop-master:9001");

    Job job = Job.getInstance(conf, "MaxTemperatureWithCompression");

 

    job.setJarByClass(MaxTemperatureWithCompression.class);

    //map的輸入可以是壓縮格式的,也可直接是未壓縮的文本文件,輸入map前會自動根據文件后綴來判斷是否需要解壓,不需要特殊處理或配置

    FileInputFormat.addInputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/1901_1902.txt.gz"));

    FileOutputFormat.setOutputPath(job, new Path(

            "hdfs://hadoop-master:9000/ncdc/MaxTemperatureWithCompression"));

 

    job.setOutputKeyClass(Text.class);

    job.setOutputValueClass(IntWritable.class);

    //mapred-site.xml配置文件里的mapred.output.compress配置屬性等效:job輸出是否壓縮,即對reduce輸出是否采用壓縮

    FileOutputFormat.setCompressOutput(job, true);

    //mapred-site.xml配置文件里的mapred.output.compression.codec配置屬性等效

    FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);

    job.setMapperClass(MaxTemperatureMapper.class);

    job.setCombinerClass(MaxTemperatureReducer.class);

    job.setReducerClass(MaxTemperatureReducer.class);

 

    System.exit(job.waitForCompletion(true) ? 0 : 1);

}

如果Job輸出生成的是順序文件(sequence file),則可以設置mapred.output.compression.typemapred-site.xml)來控制限制使用壓縮格式,默認值為RECORD,表示針對每一條記錄進行壓縮。如果將其必為BLOCK,將針對一組記錄進行壓縮,這也是推薦的壓縮策略,因為它的壓縮效率更高

<property>

  <name>mapred.output.compression.type</name>

  <value>RECORD</value>

  <description>If the job outputs are to compressed as SequenceFiles, how should

               they be compressed? Should be one of NONE, RECORD or BLOCK.

  </description>

</property>

該屬性還可以直接在JOB啟動任務程序里通過SequenceFileOutputFormatsetOutputCompressionType()來設定

 

mapred-site.xml配置文件里可以對Job作業輸出壓縮進行配置的三個配置項:

12.6.1   Map任務輸出進行壓縮

如果對map階段的中間輸出進行壓縮,可以獲得不少好處。由於map任務的輸出需要寫到磁盤並通過網絡傳輸到reducer節點,所以如果使用LZOLZ4或者Snappy這樣的快速壓縮方式,是可以獲得性能提升的,因為要傳輸的數據減少了。

啟用map任務輸出壓縮和設置壓縮格式的三個配置屬性如下(mapred-site.xml):

 

也可在程序里設定(新的API設置方式):

Configuration conf = new Configuration();

conf.setBoolean("mapred.compress.map.output", true);

conf.setClass("mapred.map.output.compression.codec", GzipCodec.class,

CompressionCodec.class);

Job job = new Job(conf);

API設置方式,通過conf對象的方法設置:

conf.setCompressMapOutput(true);

conf.setMapOutputCompressorClass(GzipCodec.class);

13    序列化

13.1        Writable接口

package org.apache.hadoop.io;

import java.io.DataOutput;

import java.io.DataInput;

import java.io.IOException;

publicinterface Writable {

    void write(DataOutput out) throws IOException;//序列化:即將實例寫入到out輸出流中

    void readFields(DataInput in) throws IOException;//反序列化:即從in輸出流中讀取實例

}

Hadoop中可序列化的類都實現了Writable這個接口,比如數據類型類BooleanWritableByteWritableDoubleWritableFloatWritableIntWritableLongWritableText

publicstaticvoid main(String[] args) throws IOException {

    IntWritable iw = new IntWritable(163);

    // 序列化

    byte[] bytes = serialize(iw);

    // Java里整型占兩個字節

    System.out.println(StringUtils.byteToHexString(bytes).equals("000000a3"));//true

 

    // 反序列化

    IntWritable niw = new IntWritable();

    deserialize(niw, bytes);

    System.out.println(niw.get() == 163);//true

}

// 序列化

publicstaticbyte[] serialize(Writable writable) throws IOException {

    ByteArrayOutputStream out = new ByteArrayOutputStream();

    DataOutputStream dataOut = new DataOutputStream(out);//最終還是借助於Java API中的ByteArrayOutputStream DataOutputStream 來完成序列化:即將基本類型的值(這里為整數)轉換為二進制的過程

    writable.write(dataOut);

    dataOut.close();

    return out.toByteArray();

}

// 反序列化

publicstaticvoid deserialize(Writable writable, byte[] bytes) throws IOException {

    ByteArrayInputStream in = new ByteArrayInputStream(bytes);

    DataInputStream dataIn = new DataInputStream(in); //最終還是借助於Java API中的ByteArrayInputStreamDataInputStream來完成反序列化:即將二進制轉換為基本類型的值(這里為整數)的過程

    writable.readFields(dataIn);

    dataIn.close();

}

 

IntWritable類的序列化與反序列化實現:

publicclass IntWritable implements WritableComparable<IntWritable> {

  privateintvalue;

  @Override

  publicvoidreadFields(DataInput in) throws IOException {

    value = in.readInt();

  }

 

  @Override

  publicvoidwrite(DataOutput out) throws IOException {

    out.writeInt(value); //實質上最后就是將整型值以二進制存儲起來了

  }

...

}

 

13.2        WritableComparable接口、WritableComparator

IntWritable實現了WritableComparable接口,而WritableComparable接口繼承了Writable接口與java.lang.Comparable接口

publicclassIntWritableimplements WritableComparable {

publicinterfaceWritableComparable<T> extendsWritable, Comparable<T> {

 

publicinterface Comparable<T> {

    publicintcompareTo(T o);

}

IntWritable實現了ComparablecompareTo方法,具體實現:

  /** Compares two IntWritables. */

  publicint compareTo(Object o) {

    int thisValue = this.value;

    int thatValue = ((IntWritable)o).value;

    return (thisValue<thatValue ? -1 : (thisValue==thatValue ? 0 : 1));

  }

 

除了實現了Comparable比較能力接口,Hadoop提供了一個優化接口是繼承自java.util.Comparator比較接口的RawComparator接口:

publicinterfaceRawComparator<T> extendsComparator<T> {

  publicint compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2);

}

RawComparator原生比較,即基於字節的比較

 

publicinterface Comparator<T> {

    intcompare(T o1, T o2);

    boolean equals(Object obj);

}

為什么說是優化接口呢?因為該接口中的比較方法可以直接對字節進行比較,而不需要先反序列化后再比(因為是靜態內部類實現

/** A WritableComparable for ints. */

publicclass IntWritable implements WritableComparable {

    ...

  /** A Comparator optimized for IntWritable. */

  publicstaticclass Comparator extends WritableComparator {

    publicint compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {

    ...

    }

  }

...

}

),這樣就避免了新建對象(即不需要通過反序列化重構Writable對象后,才能調用該對象的compareTo()比較方法)的額外開銷,而Comparable接口比較時是基於對象本身的(屬於非靜態實現):

/** A WritableComparable for ints. */

publicclass IntWritable implements WritableComparable {

...

  /** Compares two IntWritables. */

  publicint compareTo(Object o) {

    ...

  }

...

}

),所以比較前需要對輸入流進行反序列重構成Writable對象后再比較,所以性能不高。如IntWritable的內部類IntWritable.Comparator就實現了RawComparator原生比較接口,性能比IntWritable.compareTo()比較方法高

  publicstaticclassComparatorextends WritableComparator {

    public Comparator() {

      super(IntWritable.class);

    }

    //這里實現的實際上是重寫WritableComparator里的方法。注:雖然WritableComparator已經提供了該方法的默認實現,但不要直接使用,因為父類WritableComparator提供的默認實現也是先反序列化后,再通過回調IntWritable里的compareTo()來完成比較的,所以我們在為自定義Key時,一定要自己重寫WritableComparator里提供的默認實現

@Override

publicint compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {

      int thisValue = readInt(b1, s1);// readInt為父類WritableComparator中的方法,將字節數組轉換為整型(具體請參考后面),這樣不需要將字節數組反序列化成IntWritable后再進行大小比對,而是直接對IntWritable里封裝的int value進行比對

      int thatValue = readInt(b2, s2);

      return (thisValue<thatValue ? -1 : (thisValue==thatValue ? 0 : 1));

    }

  }字符串類型Text基於字節數組原生比較請參考這里

WritableComparator又是實現了RawComparator接口中的compare()方法,同時還實現了Comparator類中的:

publicclass WritableComparator implements RawComparator{

  publicint compare(byte[] b1, ints1, intl1, byte[] b2, ints2, intl2) {//該方法實現的是RawComparator接口里的方法

    try {

      buffer.reset(b1, s1, l1);                   // parse key1

      key1.readFields(buffer);

     

      buffer.reset(b2, s2, l2);                   // parse key2

      key2.readFields(buffer);

     

      buffer.reset(null, 0, 0);                   // clean up reference

    } catch (IOException e) {

      thrownew RuntimeException(e);

    }

    return compare(key1, key2);                   // compare them

  }

  @SuppressWarnings("unchecked")//該方法被上下兩個方法調用,是WritableComparator里自己定義的方法,不是重寫或實現

  publicint compare(WritableComparable a, WritableComparable b) {

    returna.compareTo(b);

  }

 

  @Override//該方法實現的是Comparatorcompare(T o1, T o2)方法

  publicint compare(Object a, Object b) {

    return compare((WritableComparable)a, (WritableComparable)b);

  }

}

WritableComparable是一個接口;而WritableComparator 是一個類WritableComparator提供一個默認的基於對象(非字節)的比較方法compare(如上面所貼),這與實現Comparable接口的比較方法是一樣的:都是基於對象的,所以性能也不高

 

獲取IntWritable的內部類Comparator的實例:

RawComparator<IntWritable> comparator = WritableComparator.get(IntWritable.class);

這樣可以取到RawComparator實例,原因是在IntWritable實現里注冊過

  static {                                        // register this comparator

    WritableComparator.define(IntWritable.class, newComparator());

  }

 

這個comparator實例可以用於比較兩個IntWritable對象:

IntWritable w1 = new IntWritable(163);

IntWritable w2 = new IntWritable(67);

assertThat(comparator.compare(w1, w2), greaterThan(0));// comparator.compare(w1, w2)會回調IntWritable.compareTo方法

或是IntWritable對象序列化的字節數組:

byte[] b1 = serialize(w1);

byte[] b2 = serialize(w2);

assertThat(comparator.compare(b1, 0, b1.length, b2, 0, b2.length),greaterThan(0));//這里才真正調用IntWritable.Comparator.compare()方法進行原生比較

 

上面分析的是IntWritable類型,其他類型基本上也是這樣

13.2.1   比較方式優先級(WritableComparableWritableComparator

KeyMapshuffle過程中是需要進行排序的,這就要求Key是實現WritableComparable的類,或者如果不實現WritableComparable接口時,需要通過Job指定比較類,他們的優先選擇順序如下:

1、  如果配置了mapred.output.key.comparator.class比較類,或明確地通過jobsetSortComparatorClass(Class<? extends RawComparator> cls)方法(舊APIsetOutputKeyComparatorClass() on JobConf)指定過,則使用指定類(一般從WritableComparator繼承)的實例進行排序(這種情況要不需要WritableComparable,而只需實現Writable即可)

2、  否則,Key必須是實現了WritableComparable的類(因為在實現內部靜態比較器繼承時需要繼承WritableComparator,其構造函數需要傳進一個實現了WritableComparableKey,並在WritableComparator類里提供的默認比較會回調Key類所實現的compareTo()方法,所以需要實現WritableComparable類),並且如果該Key類內部通過靜態塊(WritableComparator.define(Class c, WritableComparator comparator))注冊過基於字節比較的類WritableComparator(實現RawComparator的抽象類,RawComparator又繼承了Comparator接口),則使用字節比較方式進行排序(一般使用這種)

3、  否則,如果沒有使用靜態注冊過內部實現WritableComparator,則使用WritableComparablecompareTo()進行對象比較(這需要先反序列化成對象之后)(注:此情況下Key也必須是實現WritableComparable類)

13.3        Writable實現類

13.3.1   Java基本類型對應的Writable實現類

Writable很多的實現類實質上是對Java基本類型(但除char沒有對應的Writable實現類外,char可以存放在IntWritable中)的再一次封裝,get()set()方法就是對這些封裝的基本值的讀取與設定:

13.3.2   可變長類型VIntWritable VLongWritable

從上表可以看出,VIntWritable1~5)與 VLongWritable1~9)為變長。如果數字在-112~127之間時,變長格式就只用一個字節進行編碼;否則,使用第一個字節來存放正負號,其他字節就存放值(究竟需要多少字節來存放,則是看數字的大小,如int類型的值需要1~4個字節不等)。如 值為163需要兩個字節,而不是4個字節:第一個字節存符號為(不同長度的數這個字節存儲的不太一樣),第二個字節存放的是值;而257則需要三個字節來存放了;

 

可變長度類型有點像UTF-8一樣,編碼是變長的,如果傳輸內容中的數字都是比較小的數時(如果內容都是英文的字符,UTF-8就會大大縮短編碼長度),用可變長度則可以減少數據量,這些數的范圍:-65536 =< VIntWritable =< 65535此范圍最多只占3字節,包括符號位;-281474976710656L =< VLongWritable =< 28147497671065L此范圍最多只占7字節,包括符號位,如果超過了這些數,建議使用定長的,因為此時定長的所占字節還少,因為在接近最大IntLong時,變長的VintWritable達到5個字節(如2147483647就占5字節),VlongWritable達到9個字節(如9223372036854775807L就占9字節),而定長的最多只有4字節與8字節

 

另外,同一個數用VintWritableVlongWritable最后所占有字節數是一樣的,比如2147483647這個數,都是8c7fffffff,占5字節,既然同一數字的編碼長度都一樣,所以優先推薦使用 VlongWritable,因為他存儲的數比VintWritable更大,有更好的擴展

 

雖然VintWritableVlongWritable所占最大字節可能分別達到59位,但它們允許的最大數的范圍也 基本類型 intlong是一樣的,即VintWritable允許的數字范圍:-2147483648 =< VintWritable =< 2147483647VlongWritable允許的數字范圍:-9223372036854775808L =< VlongWritable =< 9223372036854775807L,因為它們的構造函數參數的類型就是基本類型intlong

  public VIntWritable(int value) { set(value); }

  public VLongWritable(long value) { set(value); }

13.3.3   Text

提供了序列化、反序列化和在字節級別上比較文本的方法。它的長度類型是整型,采用0壓縮序列化格式。另外,它還支持在不將字符數組轉換為字符串的情況下進行字符串遍歷

 

相當於Java中的String類型,采用UTF-8編解碼,它是對 byte[] 字節數組的封裝,而不直接是String

length存儲了字符串所占的字節數,為int類型,所以最大可達2GB

 

getLength():返回的是字節數組bytes的所存儲內容的有效長度,而非字符串個數,取長度時不要直接通過getBytes().length來獲取,因為在通過set()方法重置Text后,有時數組整個長度會大於所存內容的長度

getBytes():返回字符串原生字節數組,但數據的有效長度到getLength()

 

String不同的是,Text是可變的,可以通過set()方法重用

 

Text索引位置都是以字節為單位進行索引的,並不像String那樣是以字符為單位進行索引的

 

TextIntWritable一樣,也是可序列化與可比較的

 

由於Text在內存中使用的是UTF-8編碼的字節碼,而Java中的String則是Unicode編碼,所以是有區別的

 

    Text t = new Text("江正軍");

    //字符所占字節數,而非字符個數

    System.out.println(t.getLength());// 9 UTF-8編碼下每個中文占三字節

    //取單個字符,charAt()返回的是Unicode編碼

    System.out.println((char) t.charAt(0));

    System.out.println((char) t.charAt(3));// 第二個字符,注意:傳入的是byte數組中的索引,不是字符位置索引

    System.out.println((char) t.charAt(6));

    //轉換成String

    System.out.println(t.toString());// 江正軍

    ByteBuffer buffer = ByteBuffer.wrap(t.getBytes(), 0, t.getLength());

    int cp;

    // 遍歷每個字符

    while (buffer.hasRemaining() && (cp = Text.bytesToCodePoint(buffer)) != -1) {

        System.out.println((char) cp);

    }

    // 在末尾附加字符

    t.append("".getBytes("UTF-8"), 0, "".getBytes("UTF-8").length);

    System.out.println(t.toString());// 江正軍江

    // 查找字符返回第一次出現的字符位置(也是在字節數組中的偏移量,而非字符位置),類似StringindexOf,注:這個位置指字符在UTF-8字節數組的索引位置,而不是指定字符所在位置

    System.out.println(t.find(""));// 0

    System.out.println(t.find("", 1));// 9   從第2個字符開始向后查找

    Text t2 = new Text("江正軍江");

    //比較Text:如果相等,返回0

    System.out.println(t.compareTo(t2));// 0

    System.out.println(t.compareTo(t2.getBytes(), 0, t2.getLength()));//0

 

下表列出Text字符(實為UTF-8字符)與String(實為Unicode字符)所占字節:如果是拉丁字符如大寫字母A,則存放在Text中只占一個字節,而String占用兩字節;大於127的都占有兩字節;漢字時Text占有三字節,String占兩字節;后面的U+10400不知道是什么擴展字符?反正表示一個字符,但都占用了4個字節:

  @Test

  publicvoid string() throws UnsupportedEncodingException {   

    String s = "\u0041\u00DF\u6771\uD801\uDC00";

    assertThat(s.length(), is(5));

    assertThat(s.getBytes("UTF-8").length, is(10));

   

    assertThat(s.indexOf("\u0041"), is(0));

    assertThat(s.indexOf("\u00DF"), is(1));

    assertThat(s.indexOf("\u6771"), is(2));

    assertThat(s.indexOf("\uD801\uDC00"), is(3));

   

    assertThat(s.charAt(0), is('\u0041'));

    assertThat(s.charAt(1), is('\u00DF'));

    assertThat(s.charAt(2), is('\u6771'));

    assertThat(s.charAt(3), is('\uD801'));

    assertThat(s.charAt(4), is('\uDC00'));

   

    assertThat(s.codePointAt(0), is(0x0041));

    assertThat(s.codePointAt(1), is(0x00DF));

    assertThat(s.codePointAt(2), is(0x6771));

    assertThat(s.codePointAt(3), is(0x10400));

  } 

  @Test

  publicvoid text() {

    Text t = new Text("\u0041\u00DF\u6771\uD801\uDC00");

    assertThat(t.getLength(), is(10));

   

    assertThat(t.find("\u0041"), is(0));

    assertThat(t.find("\u00DF"), is(1));

    assertThat(t.find("\u6771"), is(3));

    assertThat(t.find("\uD801\uDC00"), is(6));

 

    assertThat(t.charAt(0), is(0x0041));

    assertThat(t.charAt(1), is(0x00DF));

    assertThat(t.charAt(3), is(0x6771));

    assertThat(t.charAt(6), is(0x10400));

  } 

13.3.4   BytesWritable

Text一樣,BytesWritable是對二進制數據的封裝

序列化時,前4個字節存儲了字節數組的長度:

publicstaticvoid main(String[] args) throws IOException {

    BytesWritable b = new BytesWritable(newbyte[] { 3, 5 });

    byte[] bytes = serialize(b);

    System.out.println((StringUtils.byteToHexString(bytes)));//000000020305

}

// 序列化

publicstaticbyte[] serialize(Writable writable) throws IOException {

    ByteArrayOutputStream out = new ByteArrayOutputStream();

    DataOutputStream dataOut = new DataOutputStream(out);

    writable.write(dataOut);

    dataOut.close();

    return out.toByteArray();

}

 

BytesWritable也是可變的,可以通過set()方法進行修改。與Text一樣,BytesWritablegetBytes()返回的是字節數組長——容量——也可以無法體現所存儲的實際大小,可以通過getLength()來確定實際大小,可以通過 setCapacity(int new_cap) 方法重置緩沖大小

13.3.5   NullWritable

它是一個Writable特殊類,它序列化長度為0,即不從數據流中讀取數據,也不寫入數據,充當占位符。如在MapReduce中,如果你不需要使用鍵或值,你就可以將鍵或值聲明為NullWritable

 

它是一個單例,可以通過NullWritable.get()方法獲取實例

13.3.6   ObjectWritableGenericWritable

ObjectWritable是對Java基本類型、StringenumWritablenull或這些類型組成的一個通用封裝:

當一個字段中包含多個類型時(比如在map輸出多種類型時),ObjectWritable非常有用,例如:如果SequenceFile中的值包含多個類型,就可以將值類型聲明為ObjectWritable

 

可以通過getDeclaredClass()獲取ObjectWritable封裝的類型

 

ObjectWritable在序列會時會將封裝的類型名一並輸出,這會浪費空間,我們可以使用GenericWritable來解決這個問題:如果封裝的類型數量比較少並且能夠提交知道需要封裝哪些類型,那么就可以繼承GenericWritable抽象類,並實現這個類將要對哪些類型進行封裝的抽象方法:

  abstractprotected Class<? extends Writable>[] getTypes();

這們在序列化時,就不必直接輸出封裝類型名稱,而是這些類型的名稱的索引(在GenericWritable內部會它他們分配編號),這樣就減少空間來提高性能

 

class MyWritable extendsGenericWritable{

    MyWritable(Writable writable) {

        set(writable);

    }

    publicstatic Class<? extends Writable>[] CLASSES = new Class[] { Text.class };

    @Override

    protected Class<? extends Writable>[] getTypes() {

        returnCLASSES;

    }

    publicstaticvoid main(String[] args) throws IOException {

        Text text = new Text("\u0041\u0071");

        MyWritable myWritable = new MyWritable(text);

        System.out.println(StringUtils.byteToHexString(serialize(text)));// 024171

        System.out.println(StringUtils.byteToHexString(serialize(myWritable)));// 00024171

        ObjectWritable ow = new ObjectWritable(text); //00196f72672e6170616368652e6861646f6f702e696f2e5465787400196f72672e6170616368652e6861646f6f702e696f2e54657874024171  紅色前面都是類型名序列化出來的結果,占用了很大的空間

        System.out.println(StringUtils.byteToHexString(serialize(ow)));

    }

 

    publicstaticbyte[] serialize(Writable writable) throws IOException {

        ByteArrayOutputStream out = new ByteArrayOutputStream();

        DataOutputStream dataOut = new DataOutputStream(out);

        writable.write(dataOut);

        dataOut.close();

        return out.toByteArray();

    }

}

GenericWritable的序列化只是把類型在type數組里的索引放在了前面,這樣就比ObjectWritable節省了很多空間,所以推薦大家使用GenericWritable

13.3.7   Writable集合

6種集合類:ArrayWritable, ArrayPrimitiveWritable, TwoDArrayWritable, MapWritable,SortedMapWritable, EnumSetWritable.

 

ArrayWritableTwoDArrayWritable是對Writable的數組和二維數據(數組的數組)的實現:

 

ArrayWritableTwoDArrayWritable中所有元素必須是同一類型的實例(在構造函數中指定):

ArrayWritable writable = new ArrayWritable(Text.class);

TwoDArrayWritable writable = new TwoDArrayWritable(Text.class);

 

ArrayWritableTwoDArrayWritable都有getsettoArray方法,注:toArray方法是獲取的數組(或二維數組)的副本(淺復制,雖然數組殼是復制了一份,只里面存放的元素未深度復制)

  publicvoid set(Writable[] values) { this.values = values; }

  publicWritable[] get() { returnvalues; }

 

  publicvoid set(Writable[][] values) { this.values = values; }

  publicWritable[][] get() { returnvalues; }

 

ArrayPrimitiveWritable是對Java基本數組類型的一個封裝,調用set()方法時可以識別相應組件類型,因此無需通過繼承來設置類型

 

MapWritable SortedMapWritable分別實現了java.util.Map<Writable,Writable> java.util.SortedMap<WritableComparable, Writable>接口。它們在序列化時,類型名稱也是使用索引來替代一起輸出,如果存入的是自定義Writable內,則不要超過127個,因它這兩個類里面是使用一個byte來存放自定義Writable類型名稱索引的,而那些標准的Writable則使用-127~0之間的數字來編號索引

 

對集合的枚舉類型可以采用EnumSetWritable。對於單類型的Writable列表,使用ArrayWritable就足夠了。但如果需要把不同的Writable類型存放在單個列表中,可以使用GenericWritable將元素封裝在一個ArrayWritable

13.4        自定義Writable

Hadoop中提供的現有的一套標准Writable是可以滿足我們決大多數需求的。但在某些業務下需我們定義具有自己數據結構的Writable

定制的Writable可以完全控制二進制表示和排序順序。由於WritableMapReduce數據路徑的核心,所以調整二進制表示能對性能產生顯著效果。雖然Hadoop自帶的Writable實現已經過很好的性能調優,但如果希望將結構調整得更好,更好的做法就是新建一個Writable類型

 

示例:存儲一對Text對象的自定義Writable如果是Int整型,可以參考后面示例IntPair,如果復合鍵如果由整型與字符型組成,則可能同時參考這兩個類來定義:

publicclassTextPairimplements WritableComparable<TextPair> {

    private Text first;

    private Text second;

 

    public TextPair() {

        set(new Text(), new Text());

    }

 

    public TextPair(String first, String second) {

        set(new Text(first), new Text(second));

    }

 

    public TextPair(Text first, Text second) {

        set(first, second);

    }

 

    publicvoid set(Text first, Text second) {

        this.first = first;

        this.second = second;

    }

 

    public Text getFirst() {

        returnfirst;

    }

 

    public Text getSecond() {

        returnsecond;

    }

 

    @Override

    publicvoid write(DataOutput out) throws IOException {

        first.write(out);

        second.write(out);

    }

 

    @Override

    publicvoid readFields(DataInput in) throws IOException {

        first.readFields(in);

        second.readFields(in);

    }

    /*

     * HashPartitioner(MapReuce中的默認分區類)通常用hashcode()方法來選擇reduce分區,所

     * 以應該確保有一個比較好的哈希函數來保證每個reduce數據分區大小相似

     */

    @Override

    publicint hashCode() {

        returnfirst.hashCode() * 163 + second.hashCode();

    }

 

    @Override

    publicboolean equals(Object o) {

        if (o instanceof TextPair) {

            TextPair tp = (TextPair) o;

            returnfirst.equals(tp.first) && second.equals(tp.second);

        }

        returnfalse;

    }

    /*

     * TextOutputFormat將鍵或值輸出時,會調用此方法,所以也需重寫

     */

    @Override

    public String toString() {

        returnfirst + "\t" + second;

    }

 

    /*

     * VIntWritableVLongWritable這兩個Writable外,大多數的Writable類本身都實現了

     * Comparable比較能力的接口compareTo()方法,並且又還在Writable類靜態的實了Comparator

     * 比較接口的compare()方法,這兩個方法在Writable中的實現的性能是不一樣的:Comparable.

     * compareTo()方法在比較前,需要將字節碼反序列化成相應的Writable實例后,才能調用;而

     * Comparator.compare()比較前是不需要反序列化,它可以直接對字節碼(數組)進行比較,所

     * 這個方法的性能比較高,屬於原生比較

     *

     * VIntWritableVLongWritable這兩個類里沒有靜態的實現Comparator接口,可能是因為

     * 變長的原因,

     *

     */

    @Override//WritableComparator里自定義比較方法 compare(WritableComparable a, WritableComparable b) 會回調此方法

    publicintcompareTo(TextPair tp) {

        int cmp = first.compareTo(tp.first);

        if (cmp != 0) {//先按第一個字段比,如果相等,再比較第二個字段

            return cmp;

        }

        returnsecond.compareTo(tp.second);

    }

//整型類型IntWritable基於字節數組原生比較請參考這里

    publicstaticclassComparatorextends WritableComparator {

        privatestaticfinal Text.Comparator TEXT_COMPARATOR = new Text.Comparator();

//或者這樣來獲取Text.Comparator實例?

// RawComparator<IntWritable> comparator = WritableComparator.get(Text.class);

        public Comparator() {

            super(TextPair.class);

        }

        /*

         * 這個方法(下面注釋掉的方法)從Text.Comparator.compare()方法拷過來的 l1l2表示字節數有效的長度

         *

         * 由於Text在序列化時(這一序列化過程可參照Text的序列化方法write()源碼來了解):首先是將Text的有效字節數 length

         * VIntWritable方式序列化(即length在序列化時所在字節為 1~5), 然后再將整個字節數組序列化

         * (字節數組序列化時也是先將字節有效長度輸出,不過此時為Int,而非VInt,請參考后面貼出的源碼)

         * 下面是Text的序列化方法源碼:

         * public void write(DataOutput out) throws IOException {

         *          WritableUtils.writeVInt(out, length);

         *          out.write(bytes,0, length);

         * }

         *

         * 下面是BytesWritable的序列化方法源碼:

         * public void write(DataOutput out) throws IOException {

         *          out.writeInt(size);

         *          out.write(bytes, 0, size);

         * }

         *

         * WritableUtils.decodeVIntSize(b1[s1]):讀出Text序列化出的串前面多少個字節是用來表示Text的長度的,

         * 這樣在取Text字節內容時要跳過長度信息串。傳入時只需傳入字節數組的第一個字節即可

         *

         * compareBytes(b1, s1 + n1, l1 - n1, b2, s2 + n2, l2 - n2):此方法才是真正按一個個字節進行大小比較

         * b1s1 + n1開始l1 - n1個字節才是Text真正字節內容

         *

         */

        // public int compare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {//此方法是從Text.Comparator中拷出來的

        //      int n1 = WritableUtils.decodeVIntSize(b1[s1]);//序列化串中前面多少個字節是長度信息

        //      int n2 = WritableUtils.decodeVIntSize(b2[s2]);

        //      return compareBytes(b1, s1 + n1, l1 - n1, b2, s2 + n2, l2 - n2);

        // }

        @Override

        publicintcompare(byte[] b1, int s1, int l1, byte[] b2, int s2, int l2) {

            try {

                //WritableUtils.decodeVIntSize(b1[s1])表示Text有效長度序列化輸出占幾個字節

                //readVInt(b1, s1):將Text有效字節長度是多少讀取出來。

                //最后firstL1 表示的就是第一個Text屬性成員序列化輸出的有效字節所占長度

                int firstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1);

                int firstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2);

                //比較第一個Text:即first屬性。本身Text里就有Comparator的實現,這里只需要將first

                //second所對應的字節截取出來,再調用Text.Comparator.compare()即根據字節進行比較

                int cmp = TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2);

                if (cmp != 0) {

                    return cmp;

                }//如果第一個Text first 不等,則比較第二個Test:即second屬性

                //s1 + firstL1為第二個Text second的起始位置,l1 - firstL1為第二個Text second的字節數

                returnTEXT_COMPARATOR

                        .compare(b1, s1 + firstL1, l1 - firstL1, b2, s2 + firstL2, l2 - firstL2);

            } catch (IOException e) {

                thrownew IllegalArgumentException(e);

            }

        }

    }

 

    static {

        WritableComparator.define(TextPair.class, new Comparator());

    }

}

14    順序文件結構

14.1        SequenceFile

SequenceFile:順序文件、或叫序列文件。它是一種具有一定存儲結構的文件,數據以在內存中的二進制寫入。Hadoop在讀取與寫入這類文件時效率會高

 

順序文件——相對於MapFile只能順序讀取,所以稱順序文件

序列文件——寫入文件時,直接將數據在內存中存儲的二進寫入到文件,所以寫入后使用記事本無法直接閱讀,但使用程序反序列化后或通過Hadoop命令可以正常閱讀顯示:hadoop fs -text /sequence/seq1

SequenceFile類提供了Writer,Reader SequenceFile.Sorter 三個類用於完成寫,讀,和排序

14.1.1  

publicclass SequenceFileWriteDemo {

    privatestaticfinal String[] DATA = { "One, ", "Three, ", "Five, ", "Seven, ", "Nine, " };

 

    publicstaticvoid main(String[] args) throws IOException {

        String uri = "hdfs://hadoop-master:9000/sequence/seq1";

        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(URI.create(uri), conf);

        Path path = new Path(uri);

 

        IntWritable key = new IntWritable();

        Text value = new Text();

        SequenceFile.Writer writer = null;

        try {

            /*

             * 該方法有很多的重載版本,但都需要指定FileSystem+Configuration(FSDataOutputStream+Configuration)

             * 、鍵的class、值的class;另外,其他可選參數包括壓縮類型CompressionType以及相應的CompressionCodec

             * 、 用於回調通知寫入進度的Progressable、以及在Sequence文件頭存儲的Metadata實例

             *

             * 存儲在SequenceFile中的鍵和值並不一定需要Writable類型,只要能被Serialization序列化和反序列化

             * ,任何類型都可以

             */

            // 通過靜態方法獲取順序文件SequenceFile寫實例SequenceFile.Writer

            writer = SequenceFile.createWriter(fs, conf, path, key.getClass(), value.getClass());

 

            for (int i = 0; i < 10; i++) {

                key.set(10 - i);

                value.set(DATA[i % DATA.length]);

                // getLength()返回文件當前位置,后繼將從此位置接着寫入(注:當SequenceFile剛創建時,就已

                // 寫入元數據信息,所以剛創建后getLength()也是非零的

                System.out.printf("[%s]\t%s\t%s\n", writer.getLength(), key, value);

                /*

                 * 同步點:用來快速定位記錄(鍵值對)的邊界的一個特殊標識,在讀取SequenceFile文件時,可以通過

                 * SequenceFile.Reader.sync()方法來搜索這個同步點,即可快速找到記錄的起始偏移量

                 *

                 * 加入同步點的順序文件可以作為MapReduce的輸入,由於訪類順序文件允許切分,所以該文件的不同部分可以

                 * 由不同的map任務單獨處理

                 *

                 * 在每條記錄(鍵值對)寫入前,插入一個同步點,這樣是方便讀取時,快速定位每記錄的起始邊界(如果讀取的

                 * 起始位置不是記錄邊界,則會拋異常SequenceFile.Reader.next()方法會拋異常)

                 *

                 * 在真正項目中,可能不是在每條記錄寫入前都加上這個邊界同步標識,而是以業務數據為單位(多條記錄)加入

                 * ,這里只是為了測試,所以每條記錄前都加上了

                 */

                writer.sync();

                // 只能在文件末尾附加健值對

                writer.append(key, value);

            }

        } finally {

            // SequenceFile.Writer實現了java.io.Closeable,可以關閉流

            IOUtils.closeStream(writer);

        }

    }

}

寫入后在操作系統中打開顯示亂的:

從上面可以看出這種文件的前面會寫入一些元數據信息:鍵的Class、值的Class,以及壓縮等信息

 

如果使用Hadoop來看,則還是可以正常顯示的,因為該命令會給我們反序列化后再展示出來:

14.1.2  

publicclass SequenceFileReadDemo {

    publicstaticvoid main(String[] args) throws IOException {

        String uri = "hdfs://hadoop-master:9000/sequence/seq1";

        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(URI.create(uri), conf);

        Path path = new Path(uri);

 

        SequenceFile.Reader reader = null;

        try {

            // 通過SequenceFile.Reader實例進行讀

            reader = new SequenceFile.Reader(fs, path, conf);

            /*

             * 通過reader.getKeyClass()方法從SequenceFile文件頭的元信息中讀取鍵的class類型

             * 通過reader.getValueClass()方法從SequenceFile文件頭的元信息中讀取值的class類型

             * 然后通過ReflectionUtils工具反射得到KeyValue類型實例

             */

            Writable key = (Writable) ReflectionUtils.newInstance(reader.getKeyClass(), conf);

            Writable value = (Writable) ReflectionUtils.newInstance(reader.getValueClass(), conf);

            // 返回當前位置,從此位置讀取下一健值對

            long position = reader.getPosition();

            // 讀取下一健值對,並分別存入keyvalue變量中,如果到文件尾,則返回false

            while (reader.next(key, value)) {

                // 如果讀取的記錄(鍵值對)前有邊界同步特殊標識時,則打上*

                String syncSeen = reader.syncSeen() ? "*" : "";

                // position為當前輸入鍵值對的起始偏移量

                System.out.printf("[%s%s]\t%s\t%s\n", position, syncSeen, key, value);

                position = reader.getPosition(); // beginning of next

                                                    // record下一對健值對起始偏移量

            }

            System.out.println();

            //設置讀取的位置,注:一定要是鍵值對起始偏移量,即記錄的邊界位置,否則拋異常

            reader.seek(228);

            System.out.print("[" + reader.getPosition() + "]");

            reader.next(key, value);

            System.out.println(key + "   " + value + "    [" + reader.getPosition() + "]");

 

            //這個方法與上面seek不同,傳入的位置參數不需要是記錄的邊界起始偏移的准確位置,根據邊界同步特殊標記可以自動定位到記錄邊界,這里從223位置開始向后搜索第一個同步點

            reader.sync(223);

            System.out.print("[" + reader.getPosition() + "]");

            reader.next(key, value);

            System.out.println(key + "   " + value + "    [" + reader.getPosition() + "]");

        } finally {

            IOUtils.closeStream(reader);

        }

    }

}

14.1.3   使用命令查看文件

hadoop fs –text命令除可以顯示純文本文件,還可以以文本形式顯示SequenceFile文件、MapFile文件、gzip壓縮文件,該命令可以自動力檢測出文件的類型,根據檢測出的類型將其轉換為相應的文本。

對於SequenceFile文件、MapFile文件,會調用KeyValuetoString方法來顯示成文本,所以要重寫好自定義的Writable類的toString()方法

14.1.4   將多個順序文件排序合並

MapReduce是對一個或多個順序文件進行排序(或合並)最好的方法。MapReduce本身是並行的,並就可以指定reducer的數量(即分區數),如指定1reducer,則只會輸出一個文件,這樣就可以將多個文件合並成一個排序文件了。

 

除了自己寫這樣一個簡單的排序合並MapReduce外,我們可以直接使用Haddop提供的官方實例來完成排序合並,如將前面寫章節中產生的順序文件重新升級排序(原輸出為降序):

[root@hadoop-master /root/hadoop-2.7.2/share/hadoop/mapreduce]# hadoop jar ./hadoop-mapreduce-examples-2.7.2.jar sort-r 1 -inFormat org.apache.hadoop.mapreduce.lib.input.SequenceFileInputFormat -outFormat org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat -outKey org.apache.hadoop.io.IntWritable -outValue org.apache.hadoop.io.Text /sequence/seq1 /sequence/seq1_sorted

 

[root@hadoop-master /root/hadoop-2.7.2/share/hadoop/mapreduce]# hadoop fs -text /sequence/seq1_sorted/part-r-00000

1       Nine,

2       Seven,

3       Five,

4       Three,

5       One,

6       Nine,

7       Seven,

8       Five,

9       Three,

10      One,

 

    System.out.println("sort [-r <reduces>] " +                                          //reduces的數量

                       "[-inFormat <input format class>] " +     

                       "[-outFormat <output format class>] " +

                       "[-outKey <output key class>] " +

                       "[-outValue <output value class>] " +

                       "[-totalOrder <pcnt> <num samples> <max splits>] " +

                       "<input> <output>");

注:官方提供的Sort示例除了排序合並順序文件外,還可以合並普通的文本文件,下面是它的部分源碼:

    job.setMapperClass(Mapper.class);       

    job.setReducerClass(Reducer.class);

    job.setNumReduceTasks(num_reduces);

    job.setInputFormatClass(inputFormatClass);

    job.setOutputFormatClass(outputFormatClass);

    job.setOutputKeyClass(outputKeyClass);

    job.setOutputValueClass(outputValueClass);

14.1.5   SequenceFile文件格式

順序文件由文件頭Header、隨后的一條或多條記錄Record、以及記錄間邊界同步點特殊標識符Sync(可選):

此圖為壓縮前和記錄壓縮Record compression后的順序文件的內部結構

 

順序文件的前三個字節為SEQ(順序文件代碼),緊隨其后的一個字節表示順序文件的版本號,文件頭還包括其他字段,例如鍵和值的名稱、數據壓縮細節、用戶定義的元數據,此外,還包含了一些同步標識,用於快速定位到記錄的邊界

每個文件都有一個隨機生成的同步標識,存儲在文件頭中。同步標識位於順序文件中的記錄與記錄之間,同步標識的額外存儲開銷要求小於1%,所以沒有必要在每條記錄末尾添加該標識,特別是比較短的記錄

 

記錄的內部結構取決於是否啟用壓縮,SeqeunceFile支持兩種格式的數據壓縮,分別是:記錄壓縮record compression和塊壓縮block compression

record compression如上圖所示,是對每條記錄的value進行壓縮

默認情況是不啟用壓縮,每條記錄則由記錄長度(字節數)Record length、健長度Key length、鍵Key和值Value組成,長度字段占4字節

 

記錄壓縮(Record compression)格式與無壓縮情況基本相同,只不過記錄的值是用文件頭中定義的codec壓縮的,注,鍵沒有被壓縮(指記錄壓縮方式的Key是不會被壓縮的,而如果是塊壓縮方式的話,整個記錄的各個部分信息都會被壓縮,請看下面塊壓縮)

 

塊壓縮(Block compression)是指一次性壓縮多條記錄,因為它可以利用記錄間的相似性進行壓縮,所以比單條記錄壓縮方式要好,塊壓縮效率更高。block compression是將一連串的record組織到一起,統一壓縮成一個block

上圖:采用塊壓縮方式之后,順序文件的內部結構,記錄的各個部分都會被壓縮,不只是Value部分

可以不斷向數據塊中壓縮記錄,直到塊的字節數不小於io.seqfile.compress.blocksize(core-site.xml)屬性中設置的字節數,默認為1MB

<property>

  <name>io.seqfile.compress.blocksize</name>

  <value>1000000</value>

  <description>The minimum block size for compression in block compressed  SequenceFiles.

  </description>

</property>

每一個新塊的開始處都需要插入同步標識block數據塊的存儲格式:塊所包含的記錄數(vint1~5個字節,不壓縮)、每條記錄Key長度的集合(Key長度集合表示將所有Key長度信息是放在一起進行壓縮)、每條記錄Key值的集合(所有Key放在一起再起壓縮)、每條記錄Value長度的集合(所有Value長度信息放在一起再進行壓縮)和每條記錄Value值的集合(所有值放在一起再進行壓縮)

14.2        MapFile

MapFile是已經排過序的SequenceFile,它有索引,索引存儲在另一單獨的index文件中,所以可以按鍵進行查找,注:MapFile並未實現java.util.Map接口

MapFile是對SequenceFile的再次封裝,分為索引數據兩部分:

publicclass MapFile {

  /** The name of the index file. */

  publicstaticfinal String INDEX_FILE_NAME = "index";

  /** The name of the data file. */

  publicstaticfinal String DATA_FILE_NAME = "data";

 

  publicstaticclass Writer implements java.io.Closeable {

    private SequenceFile.Writer data;

private SequenceFile.Writer index;

    /** Append a key/value pair to the map.  The key must be greater or equal 

     * to the previous key added to the map. Append時,Key的值一定要大於或等於前面的已加入的值,即升序,否則拋異常*/

    publicsynchronizedvoid append(WritableComparable key, Writable val)

      throws IOException {

...

  publicstaticclass Reader implements java.io.Closeable {

    // the data, on disk

    private SequenceFile.Reader data;

    private SequenceFile.Reader index;

...

14.2.1  

SequenceFile一樣,也是使用append方法在文件末寫入,而且鍵要是WritableComparable類型的具有比較能力的Writable,值與SequenceFile一樣也是Writable類型即可

privatestaticfinal String[] DATA = { "One, ", "Three, ", "Five, ", "Seven, ", "Nine, " };

publicstaticvoid main(String[] args) throws IOException {

    String uri = "hdfs://hadoop-master:9000/map";

    Configuration conf = new Configuration();

    FileSystem fs = FileSystem.get(URI.create(uri), conf);

    IntWritable key = new IntWritable();

    Text value = new Text();

    MapFile.Writer writer = null;

    try {

        /*

         * 注:在創建writer時與SequenceFile不太一樣,這里傳進去的URI,而不是具體文件的Path

         * 這是因為MapFile會生成兩個文件,一個是data文件,一個是index文件,可以查看MapFile源碼:

         * //The name of the index file.

         *  public static final String INDEX_FILE_NAME = "index";

         * //The name of the data file.

         * public static final String DATA_FILE_NAME = "data";

         *

         * 所以不需要具體的文件路徑,只傳入URI即可,且傳入的URI只到目錄級別,即使包含文件名也會看作目錄

         */

        writer = new MapFile.Writer(conf, fs, uri, key.getClass(), value.getClass());

        for (int i = 0; i < 1024; i++) {

            key.set(i);

            value.set(DATA[i % DATA.length]);

            // 注:append時,key的值要大於等前面已加入的鍵值對

            writer.append(key, value);

        }

    } finally {

        IOUtils.closeStream(writer);

    }

}

[root@localhost /root]# hadoop fs -ls /map

Found 2 items

-rw-r--r--   3 Administrator supergroup        430 2016-05-01 10:24 /map/data

-rw-r--r--   3 Administrator supergroup        203 2016-05-01 10:24 /map/index

會在map目錄下創建兩個文件dataindex文件這兩個文件都是SequenceFile

 

[root@localhost /root]# hadoop fs -text /map/data | head

0       One,

1       Three,

2       Five,

3       Seven,

4       Nine,

5       One,

6       Three,

7       Five,

8       Seven,

9       Nine,

 

[root@localhost /root]# hadoop fs -text /map/index

0       128

128     4013

256     7918

384     11825

512     15730

640     19636

768     23541

896     27446

Index文件存儲了部分鍵(上面顯示的第一列)及在data文件中的起使偏移量(上面顯示的第二列)。從index輸出可以看到,默認情況下只有每隔128個鍵才有一個包含在index文件中,當然這個間隔是可以調整的,可調用MapFile.Writer實例的setIndexInterval()方法來設置(或者通過io.map.index.interval屬性配置也可)。增加索引間隔大小可以有效減少MapFile存儲索引所需要的內存,相反,如果減小間隔則可以提高查詢效率。因為索引index文件只保留一部分鍵,所以MapFile不能夠提供枚舉或計算所有的鍵的方法,唯一的辦法是讀取整個data文件

 

下面可以根據index的索引seek定位到相應位置后讀取相應記錄:

publicstaticvoid main(String[] args) throws IOException {

    String uri = "hdfs://hadoop-master:9000/map/data";

    Configuration conf = new Configuration();

    FileSystem fs = FileSystem.get(URI.create(uri), conf);

    Path path = new Path(uri);

    SequenceFile.Reader reader = null;

    try {

        reader = new SequenceFile.Reader(fs, path, conf);

        Writable key = (Writable) ReflectionUtils.newInstance(reader.getKeyClass(), conf);

        Writable value = (Writable) ReflectionUtils.newInstance(reader.getValueClass(), conf);

        reader.seek(4013);

        System.out.print("[" + reader.getPosition() + "]");

        reader.next(key, value);

        System.out.println(key + "  " + value + "  [" + reader.getPosition() + "]");

    } finally {

        IOUtils.closeStream(reader);

    }

}

[4013]128   Seven,     [4044]

14.2.2  

MapFile遍歷文件中所有記錄與SequenceFile一樣:先建一個MapFile.Reader實例,然后調用next()方法,直到返回為false到文件尾:

    /** Read the next key/value pair in the map into <code>key</code> and

     * <code>val</code>.  Returns true if such a pair exists and false when at

     * the end of the map */

    publicsynchronizedboolean next(WritableComparable key, Writable val)

      throws IOException {

通過調用get()方法可以隨機訪問文件中的數據:

    /** Return the value for the named key, or null if none exists. */

    publicsynchronized Writable get(WritableComparable key, Writable val)

      throws IOException {

根據指定的key查找記錄,如果返回null,說明沒有相應的條目,如果找到相應的key,則將該鍵對應的值存入val參變量中

 

 

publicstaticvoid main(String[] args) throws IOException {

    String uri = "hdfs://hadoop-master:9000/map/data";

    Configuration conf = new Configuration();

    FileSystem fs = FileSystem.get(URI.create(uri), conf);

    MapFile.Reader reader = null;

    try {

        //構造時,路徑只需要傳入目錄即可,不能到data文件

        reader = new MapFile.Reader(fs, "hdfs://hadoop-master:9000/map", conf);

        IntWritable key = (IntWritable) ReflectionUtils.newInstance(reader.getKeyClass(), conf);

        Writable value = (Writable) ReflectionUtils.newInstance(reader.getValueClass(), conf);

        key.set(255);

        //根據給定的key查找相應的記錄

        reader.get(key, value);

        System.out.println(key + "   " + value);// 255   One,

    } finally {

        IOUtils.closeStream(reader);

    }

}

get()時,MapFile.Reader首先將index文件讀入內存,接着對內存中的索引進行二分查找,最后在index中找到小於或等於搜索索引的鍵255,這里即為128,對應的data文件中的偏移量為4013,然后從這個位置順序讀取每條記錄,拿出Key一個個與255進行對比,這里很不幸運,需要比較128(由io.map.index.interval決定)次直到找到鍵255為止。

 

getClosest()方法與get()方法類似,只不過它返回的是與指定鍵匹配的最接近的鍵,而不是在不匹配的返回null,更准確地說,如果MapFile包含指定的鍵,則返回對應的條目;否則,返回MapFile中的第一個大於(或小於,由相應的boolean參數指定)指定鍵的鍵

 

大型MapFile的索引全加載到內存會占據大量內存,如果不想將整個index加載到內存,不需要修改索引間隔之后再重建索引,而是在讀取索引時設置io.map.index.skip屬性(編程時可通過Configuration來設定)來加載一定比例的索引鍵,該屬性通常設置為0,意味着加載index時不跳過索引鍵全部加載;如果設置為1,則表示加載index時每次跳過索引鍵中的一個,這樣索引會減半;如果設置為2,則表示加載index時每次讀取索引時跳過2個鍵,這樣只加載索引的三分一的鍵,以此類推,設置的值越大,節省大量內存,但增加搜索時間

14.2.3   特殊的MapFile

l  SetFile是一個特殊的MapFile,用於只存儲Writable的集合,鍵必須升序添加

publicclass SetFile extends MapFile {

  publicstaticclass Writer extends MapFile.Writer {

    /** Append a key to a set.  The key must be strictly greater than the

     * previous key added to the set. */

    publicvoid append(WritableComparable key) throws IOException{

      append(key, NullWritable.get());//只存鍵。由於調用MapFile.Writer.append()方法實現,所以鍵也只能升序添加

    }

. . .

  /** Provide access to an existing set file. */

  publicstaticclass Reader extends MapFile.Reader {

    /** Read the next key in a set into <code>key</code>.  Returns

     * true if such a key exists and false when at the end of the set. */

    publicboolean next(WritableComparable key)

      throws IOException {

      return next(key, NullWritable.get());//也只讀取鍵

}

 

l  ArrayFile也是一個特殊的MapFile,鍵是一個整型,表示數組中的元素索引,而值是一個Writable

publicclass ArrayFile extends MapFile {

  /** Write a new array file. */

  publicstaticclass Writer extends MapFile.Writer {

    private LongWritable count = new LongWritable(0);

    /** Append a value to the file. */

    publicsynchronizedvoid append(Writable value) throws IOException {

      super.append(count, value);                 // add to map 鍵是元素索引

      count.set(count.get()+1);                   // increment count 每添加一個元素后,索引加1

    }

. . .

  /** Provide access to an existing array file. */

  publicstaticclass Reader extends MapFile.Reader {

    private LongWritable key = new LongWritable();

    /** Read and return the next value in the file. */

    publicsynchronized Writable next(Writable value) throws IOException {

      return next(key, value) ? value : null;//只返回值

    }

 

    /** Returns the key associated with the most recent call to {@link

     * #seek(long)}, {@link #next(Writable)}, or {@link

     * #get(long,Writable)}. */

    publicsynchronizedlong key() throws IOException {//如果知道是第幾個元素,則是可以調用此方法

      returnkey.get();

    }

 

    /** Return the <code>n</code>th value in the file. */

    publicsynchronized Writable get(long n, Writable value)//根據數組元素索引取值

      throws IOException {

      key.set(n);

      return get(key, value);

}

 

l  BloomMapFile文件構建在MapFile的基礎之上:

publicclass BloomMapFile {

  publicstaticfinal String BLOOM_FILE_NAME = "bloom";

   publicstaticclass Writer extends MapFile.Writer {

唯一不同之處就是,除了dataindex兩個文件外,還增加了一個bloom文件,該bloom文件主要包含一張二進制的過濾表,該過濾表可以提高key-value的查詢效率。在每一次寫操作完成時,會更新這個過濾表,其實現源代碼如下:

publicclass BloomMapFile {

  publicstaticclass Writer extends MapFile.Writer {

    publicsynchronizedvoid append(WritableComparable key, Writable val)

        throws IOException {

      super.append(key, val);

      buf.reset();

      key.write(buf);

      bloomKey.set(byteArrayForBloomKey(buf), 1.0);

      bloomFilter.add(bloomKey);

}

它有兩個調優參數,一個是io.mapfile.bloom.size,指出map文件中大概有多少個條目;另一個是io.mapfile.bloom.error.rate , BloomMapFile中使用布隆過濾器失敗比率. 如果減少這個值,使用的內存會成指數增長。

 

 

 

說明: http://my.csdn.net/uploads/201207/24/1343098889_3000.jpg

VERSION: 過濾器的版本號;

nbHash: 哈希函數的數量;

hashType: 哈希函數的類型;

vectorSize: 過濾表的大小;

nr: BloomFilter可記錄key的最大數量;

currentNbRecord: 最后一個BloomFilter記錄key的數量;

numer: BloomFilter的數量;

vectorSet: 過濾表;

14.2.4   SequenceFile轉換為MapFile

前提是SequenceFile里是按鍵升序存放的,這樣才可以為它創建index文件

 

publicclass MapFileFixer {

  publicstaticvoid main(String[] args) throws Exception {

    String mapUri =  "hdfs://hadoop-master:9000/sequence2map";

    Configuration conf = new Configuration();

    FileSystem fs = FileSystem.get(URI.create(mapUri), conf);

    Path map = new Path(mapUri);

    //如果data文件名不是data也是可以的,但這里為默認的data,所以指定MapFile.DATA_FILE_NAME即可

    Path mapData = new Path(map, MapFile.DATA_FILE_NAME);   

    // Get key and value types from data sequence file

    SequenceFile.Reader reader = new SequenceFile.Reader(fs, mapData, conf);

    Class keyClass = reader.getKeyClass();

    Class valueClass = reader.getValueClass();

    reader.close();

   

    // Create the map file index file

    long entries = MapFile.fix(fs, map, keyClass, valueClass, false, conf);

    System.out.printf("Created MapFile %s with %d entries\n", map, entries);

  }

}

fix()方法通常用於重建已損壞的索引,如果要將某個SequenceFile轉換為MapFile,則一般經過以下幾步:

1、  保證SequenceFile里的數據是按鍵升序存放的,否則使用MapReduce任務對文件進行一次輸入輸出,就會自動排序合並,如:

//創建兩個SequenceFile

publicclass SequenceFileCreate {

    privatestaticfinal String[] DATA = { "One, ", "Three, ", "Five, ", "Seven, ", "Nine, " };

    publicstaticvoid main(String[] args) throws IOException {

        String uri = "hdfs://hadoop-master:9000/sequence/seq1";

        Configuration conf = new Configuration();

        FileSystem fs = FileSystem.get(URI.create(uri), conf);

        Path path = new Path(uri);

        Path path2 = new Path("hdfs://hadoop-master:9000/sequence/seq2");

 

        IntWritable key = new IntWritable();

        Text value = new Text();

        SequenceFile.Writer writer = null, writer2 = null;

        try {

            //創建第一個SequenceFile

            writer = SequenceFile.createWriter(fs, conf, path, key.getClass(), value.getClass());

            for (int i = 0; i < 10; i++) {

                key.set(10 - i);

                value.set(DATA[i % DATA.length]);

                System.out.printf("[%s]\t%s\t%s\n", writer.getLength(), key, value);

                writer.append(key, value);

            }

            //創建第二個SequenceFile

            writer2 = SequenceFile.createWriter(fs, conf, path2, key.getClass(), value.getClass());

            for (int i = 10; i < 20; i++) {

                key.set(30 - i);

                value.set(DATA[i % DATA.length]);

                System.out.printf("[%s]\t%s\t%s\n", writer2.getLength(), key, value);

                writer2.append(key, value);

            }

        } finally {

            IOUtils.closeStream(writer);

            IOUtils.closeStream(writer2);

        }

    }

}

//將前面生成的兩個SequenceFile排序合並成一個SequenceFile文件

publicclass SequenceFileCovertMapFile {

    publicstaticclass Mapper extends

            org.apache.hadoop.mapreduce.Mapper<IntWritable, Text, IntWritable, Text> {

 

        @Override

        publicvoid map(IntWritable key, Text value, Context context) throws IOException,

                InterruptedException {

            context.write(key, value);

            System.out.println("key=" + key + "  value=" + value);

        }

    }

 

    publicstaticclass Reducer extends

            org.apache.hadoop.mapreduce.Reducer<IntWritable, Text, IntWritable, Text> {

        @Override

        publicvoid reduce(IntWritable key, Iterable<Text> values, Context context) throws IOException,

                InterruptedException {

            for (Text value : values) {

                context.write(key, value);

            }

        }

    }

 

    publicstaticvoid main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        conf.set("mapred.job.tracker", "hadoop-master:9001");

 

        Job job = Job.getInstance(conf, "SequenceFileCovert");

        job.setJarByClass(SequenceFileCovertMapFile.class);

        job.setJobName("SequenceFileCovert");

 

        job.setMapperClass(Mapper.class);

        job.setReducerClass(Reducer.class);

        // 注意這里要設置輸入輸出文件格式為SequenceFile

        job.setInputFormatClass(SequenceFileInputFormat.class);

        job.setOutputFormatClass(SequenceFileOutputFormat.class);

        job.setOutputKeyClass(IntWritable.class);

        job.setOutputValueClass(Text.class);

        job.setNumReduceTasks(1);//默認就是1,一個Reduce就只輸出一個文件,這樣就將多個輸入文件合並成一個文件了

        SequenceFileInputFormat.addInputPath(job, new Path("hdfs://hadoop-master:9000/sequence"));

 

        SequenceFileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop-master:9000/sequence2map"));

        System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

2、  SequenceFile文件名修改為datahadoop fs -mv /sequence2map/part-r-00000 /sequence2map/data

3、  使用最前面的MapFileFixer程序創建index

15    MapReduce應用開發

15.1        Configuration

org.apache.hadoop.conf.Configuration類是用來讀取特定格式XML配置文件的

 

<?xml version="1.0"?>

<configuration>

  <property>

    <name>color</name>

    <value>yellow</value>

    <description>Color</description>

  </property>

 

  <property>

    <name>size</name>

    <value>10</value>

    <description>Size</description>

  </property>

 

  <property>

    <name>weight</name>

    <value>heavy</value>

    <final>true</final> <!--該屬性不能被后面加進來的同名屬性覆蓋-->

    <description>Weight</description>

  </property>

 

  <property>

    <name>size-weight</name>

    <value>${size},${weight}</value><!配置屬性可以引用其他屬性或系統屬性-->

    <description>Size and weight</description>

  </property>

</configuration>

 

Configuration conf = new Configuration();

conf.addResource("configuration-1.xml");//如有多個XML,可以多添調用此方法添加,相同屬性后面會覆蓋前面的,除非前面是final屬性

System.out.println(conf.get("color"));//yellow

System.out.println(conf.get("size"));//10

System.out.println(conf.get("breadth", "wide"));//wide    如果不存在breadth配置項,則返回后面給定的wide默認值

System.out.println(conf.get("size-weight"));//10,heavy

 

系統屬性的優先級高於XML配置文件中定義的屬性,但還是不能覆蓋finaltrue的屬性

System.setProperty("size", "14");//系統屬性

System.out.println(conf.get("size-weight"));//14,heavy

系統屬性還可以通過JVM參數 -Dproperty=value 來設置

 

雖然可以通過系統屬性來覆蓋XML配置文件中非final屬性,但如果XML中不存在該屬性,則僅配置系統屬性后,通過Configuration是獲取不到的:

System.setProperty("length", "2");

System.out.println(conf.get("length"));//null  由於XML中未配置length這個屬性,所以為null

15.2        作業調用

 

16    MapReduce工作原理

1.X及以前版本中,mapred.job.tracker決定了執行MapReuce程序的方式。如果這個配置屬性被設置為local(默認值),則使用本地的作業運行器。

如果mapred.job.tracker被設置為用冒號分開的主機和端口,那么該配置屬性就被解釋為一個jobtracker地址,運行器則將作業提交該地址的jobtracker

 

Hadoop2.0引入了一種新的執行機制,即Yarn資源管理運行框架,是否使用此執行框架,則由mapreduce.framework.name屬性來決定,它有三種取值:

1、  local,表示本地的作業運行器

2、  classic表示不使用Yarn執行框架,而是經典的MapReduce框架,也稱MapReduce1,它使用一個jobtracker和多個tasktracker

3、  yarn表示使用新的框架

16.1        經典的mapreduceMapReduce 1

提交作業后,waitForCompletion()每秒輪詢作業的進度,如果發現自上次報告后有改變,則把進度報告到控制台

將作業資源JAR包拷貝到 HDFS中(步驟3),並且有多個副本,這個副本數由mapred.submit.replication決定,默認為10。因此,運行作業時,集群中有很多個副本可供tasktracker訪問

 

JobTracker接收到對其submitJob()方法調用后,把此調用放入一個內部隊列中,交由作業調度器(job scheduler)進行調度(步驟4

為了創建任務運行列表,Jobtracker上的作業調度器首先從共享文件系統中獲取客戶端已經計算好的輸入分片(步驟6),然后為每個分片創建一個map任務;創建的reduce任務的數量是由Jobmapred.reduce.tasks屬性決定,也可用setNumReduceTasks()方法來設置,然后調度器創建相應數量的reduce任務

 

tasktracker運行一個簡的循環來定期發送心跳給jobtracket

 

對於map任務,jobtracker會考慮tasktracker的網絡位置,並選取一個距離其輸入分片文件最近的tasktracker。在最理想的情況下,任務是數據本地化的(data-local),也就是任務運行在輸入分片所在的節點上;同樣,任務也可能是機架本地化的(rack-local):任務和輸入分片在同一個機架,但不在同一節點上;另外,一些任務即不是數據本地化,也不是機架本地化,數據是來自於不同機器上的節點,此情況是最差的一種

 

被分配任務的tasktracker會從共享文件系統把任務的JAR文件復制到tasktracker所在的節點中,同時,tasktracker將應用程序所需要的全部文件從分布式緩存復制到本地磁盤。然后,tasktracker為任務新建一個本地工作目錄,並把JAR文件中的內容解壓到這個文件夾下,最后tasktracker新建一個TaskRunner實例來運行該任務

 

作業狀態更新:

 

16.2        YARNMapReduce 2

 

如果要使用YARNMapReduce 2)來運行MapReduce任務的話,需要將mapreduce.framework.name設置為yarn

 

與經典MapReduce 1一樣,也會將作業資源打包成JAR並上傳到HDFS系統中,也是多個副本存放(步驟3),最后並通過調用資源管理器resourcemanagersubmitApplication()方法提交作用(步驟4);resourcemanager收到調用它的submitApplication()消息后,便將請求傳遞給調度器(scheduler)。調度器分配一個容器,然后資源管理器在節點管理器的管理下在容器中啟動應用程序的master進程(步驟5a5b

 

MapReduce作業的application master是一個Java應用程序,它的主類是MRAppMaster。它對每個分片創建一個map任務,以及創建mapreduce.job.reducesreduce任務

 

如果作用很小,application master就選擇MapReduce作業在與它同一個JVM上運行任務,這樣的作業稱為uberized,或者作為uber任務運行。但MapReduce 1從不在單個tasktracker上運行小作業

小作業就是map任務小於10,只有一個reduce且輸入小於一個HDFS塊大小(可以通過修改mapreduce.job.ubertask.maxmaps,mapreduce.job.ubertask.maxreduces, and mapreduce.job.ubertask.maxbytes的值來改變uber的限制范圍),然后也可以單獨設置mapreduce.job.ubertask.enablefalse來關閉作為uber任務來運行

 

如果作業不適合作用為uber任務運行,那么application master就會為該作業中的所有map任務和reduce任務向資源管理器resourcemanager請求容器(步驟8),然后根據心跳信息里的數據分片所在位置信息,以最優的方式將任務分配到離數據最近的節點上執行

 

默認情況下,map任務與reduce任務都分配1024MB內存,但可以通過mapreduce.map.memory.mb mapreduce.reduce.memory.mb來修改。在MapReduce 1中,tasktrackers在集群配置時設置了固定的“slots,每個任務在一個slots里運行,每個slots的內存也是固定分配的,導致任務占用較小內存時無法充分利用內存以及大任務內存不足的問題;在YARN中,資源分配更細,所以可以避免上述問題。應用程序可以請求最小到最大限制范圍內的任意最小值位數的內存容量,默認值最小值是1024MB(由yarn.scheduler.capacity.minimum-allocation-mb設定),默認的最大值是10240MB(由yarn.scheduler.capacity.maximum-allocation-mb設置),因此,任務可以通過適當設置mapreduce.map.memory.mb mapreduce.reduce.memory.mb來請求1GB10GB間的任意1GB倍數的內存容量

 

一量資源管理器的調度器為任務分配了容器,application master就通過與節點管理器通信來啟動容器(步驟9a9b),該任務由主類為YarnChildJava應用程序執行。在它運行任務前,首先將任務需要的資源本地化,包括作業的配置、JAR文件和所有來自分布式緩存的文件(步驟10)。最后,運行map任務或reduce任務(步驟11

 

16.3        Shuffle and Sort

MapReduce要求每個reducer的輸入都是按鍵排序的。

 

Shuffle描述着數據從map task輸出到reduce task輸入的這段過程

16.3.1   Shuffle詳解

WordCount為例,並假設它有8map task3reduce task。從上圖看出,Shuffle過程橫跨mapreduce兩端,所以下面我也會分兩部分來展開。

先看看map端的情況,如下圖:

  上圖可能是某個map task的運行情況。拿它與官方圖的左半邊比較,會發現很多不一致。官方圖沒有清楚地說明partition sortcombiner到底作用在哪個階段。我畫了這張圖,希望讓大家清晰地了解從map數據輸入到map端所有數據准備好的全過程。

 

整個流程我分了四步。簡單些可以這樣說,每個map task都有一個內存緩沖區,存儲着map的輸出結果,當緩沖區快滿的時候需要將緩沖區的數據以一個臨時文件的方式存放到磁盤,當整個map task結束后再對磁盤中這個map task產生的所有臨時文件做合並,生成最終的正式輸出文件,然后等待reduce task來拉數據。

 

當然這里的每一步都可能包含着多個步驟與細節,下面我對細節來一一說明:

   map task執行時,它的輸入數據來源於HDFSblock,當然在MapReduce概念中,map task只讀取splitSplitblock的對應關系可能是多對一,默認是一對一。在WordCount例子里,假設map的輸入數據都是像“aaa”這樣的字符串。

 

   在經過mapper的運行后,我們得知mapper的輸出是這樣一個key/value對: key是“aaa”, value是數值1。因為當前map端只做加1的操作,在reduce task里才去合並結果集。前面我們知道這個job3reduce task,到底當前的“aaa”應該交由哪個reduce去做呢,這就是分區是需要現在Map輸出結果寫入到緩沖時決定的疑問:分區到底是在map輸出寫入緩沖前完成的還是溢寫磁盤前完成的?這與官方圖矛盾!!!

        MapReduce提供Partitioner接口,它的作用就是根據keyvaluereduce的數量來決定當前的這對輸出數據最終應該交由哪個reduce task處理。默認對key hash后再以reduce task數量取模。默認的取模方式只是為了平均reduce的處理能力,如果用戶自己對Partitioner有需求,可以訂制並設置到job上。

        在我們的例子中,“aaa”經過Partitioner后返回0,也就是這對值應當交由第一個reducer來處理。接下來,需要將數據寫入內存緩沖區中,緩沖區的作用是批量收集map結果,減少磁盤IO的影響。我們的key/value對以及Partition的結果都會被寫入緩沖區。當然寫入之前,keyvalue值都會被序列化成字節數組

        整個內存緩沖區就是一個字節數組,它的字節索引及key/value存儲結構我沒有研究過。如果有朋友對它有研究,那么請大致描述下它的細節吧。

   這個內存緩沖區是有大小限制的,默認是100MB。當map task的輸出結果很多時,就可能會撐爆內存,所以需要在一定條件下將緩沖區中的數據臨時寫入磁盤,然后重新利用這塊緩沖區。這個從內存往磁盤寫數據的過程被稱為Spill,中文可譯為溢寫,字面意思很直觀。這個溢寫是由單獨線程來完成,不影響往緩沖區寫map結果的線程。溢寫線程啟動時不應該阻止map的結果輸出,所以整個緩沖區有個溢寫的比例spill.percent。這個比例默認是0.8,也就是當緩沖區的數據已經達到閾值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢寫線程啟動,鎖定這80MB的內存,執行溢寫過程。Map task的輸出結果還可以往剩下的20MB內存中寫,互不影響。

        當溢寫線程啟動后,需要在溢寫前(生成溢寫文件時)對這80MB空間內的key排序(Sort)。排序是MapReduce模型默認的行為,這里的排序也是對序列化的字節做的排序

 

        在這里我們可以想想,因為map task的輸出是需要發送到不同的reduce端去,而內存緩沖區沒有對將發送到相同reduce端的數據做合並(但會做分區),那么這種合並應該是體現是磁盤文件中的。從官方圖上也可以看到寫到磁盤中的溢寫文件是對不同的reduce端的數值做過合並。所以溢寫過程一個很重要的細節在於,如果有很多個key/value對需要發送到某個reduce端去,那么需要將這些key/value值拼接到一塊,減少與partition相關的索引記錄。

 

        在針對每個reduce端而合並數據時,有些數據可能像這樣:“aaa/1 aaa/1。對於WordCount例子,就是簡單地統計單詞出現的次數,如果在同一個map task的結果中有很多個像“aaa”一樣出現多次的key,我們就應該把它們的值合並到一塊,這個過程叫reduce也叫combiner。但MapReduce的術語中,reduce只指reduce端執行從多個map task取數據做計算的過程,除reduce外,非正式地合並數據只能算做combine了。其實大家知道的,MapReduce中將Combiner等同於Reducer,所以其輸出結果與Reducer輸出是一樣的,只是部分reduce過程提前到map端來完成

 

        如果client設置過Combiner,那么現在(溢寫時)就是使用Combiner的時候了。將有相同keykey/value對的value加起來,減少溢寫到磁盤的數據量Combiner會優化MapReduce的中間結果,所以它在整個模型中會多次使用。那哪些場景才能使用Combiner呢?從這里分析,Combiner的輸出是Reducer的輸入,Combiner絕不能改變最終的計算結果。所以從我的想法來看,Combiner只應該用於那種Reduce的輸入key/value與輸出key/value類型完全一致,且不影響最終結果的場景。比如累加最大值等,但不適用於求平均值Combiner的使用一定得慎重,如果用好,它對job執行效率有幫助,反之會影響reduce的最終結果。

 

   每次溢寫會在磁盤上生成一個溢寫文件,如果map的輸出結果真的很大,有多次這樣的溢寫發生,磁盤上相應的就會有多個溢寫文件存在。當map task真正完成時,內存緩沖區中的數據也全部溢寫到磁盤中形成一個溢寫文件。最終磁盤中會至少有一個這樣的溢寫文件存在(如果map的輸出結果很少,當map執行完成時,只會產生一個溢寫文件),因為最終的文件只有一個,所以需要將這些溢寫文件歸並到一起,這個過程就叫做MergeMerge是怎樣的?如前面的例子,“aaa”從某個溢寫文件中讀取過來時值是5,從另外一個溢寫文件讀取來的值是8,因為它們有相同的key,所以得mergegroup。什么是group。對於“aaa”就是像這樣的:{aaa, [5, 8, 2, ]},數組中的值就是從不同溢寫文件中讀取出來的,然后再把這些值加起來。請注意,因為merge是將多個溢寫文件合並到一個文件,所以可能也有相同的key存在,在這個過程中(合並時)如果client設置過Combiner也會使用Combiner來合並相同的key

 

        至此,map端的所有工作都已結束,最終生成的這個文件也存放在TaskTracker夠得着的某個本地目錄內。每個reduce task不斷地通過RPCJobTracker那里獲取map task是否完成的信息,如果reduce task得到通知,獲知某台TaskTracker上的map task執行完成,Shuffle的后半段過程開始啟動。

 

 

簡單地說,reduce task在執行之前的工作就是不斷地拉取當前job里每個map task的最終結果,然后對從不同地方拉取過來的數據不斷地做merge,也最終形成一個文件作為reduce task的輸入文件。見下圖:

說明: http://dl.iteye.com/upload/attachment/456527/608c7e08-896d-3697-a57e-a8ca60cf79ea.jpg

map 端的細節圖,Shufflereduce端的過程也能用圖上標明的三點來概括。當前reduce copy數據的前提是它要從JobTracker獲得有哪些map task已執行結束,這段過程不表,有興趣的朋友可以關注下。Reducer真正運行之前,所有的時間都是在拉取數據,做merge,且不斷重復地在做。如前面的方式一樣,下面我也分段地描述reduce 端的Shuffle細節:

   Copy過程,簡單地拉取數據。Reduce進程啟動一些數據copy線程(Fetcher),通過HTTP方式請求map task所在的TaskTracker獲取map task的輸出文件。因為map task早已結束,這些文件就歸TaskTracker管理在本地磁盤中。

   Merge階段。這里的mergemap端的merge動作,只是數組中存放的是不同mapcopy來的數值。Copy過來的數據會先放入內存緩沖區中,這里的緩沖區大小要比map端的更為靈活,它基於JVMheap size設置,因為Shuffle階段Reducer不運行,所以應該把絕大部分的內存都給Shuffle用。這里需要強調的是,merge有三種形式1)內存到內存  2)內存到磁盤  3)磁盤到磁盤默認情況下第一種形式不啟用,讓人比較困惑,是吧。當內存中的數據量到達一定閾值,就啟動內存到磁盤的merge。與map 端類似,這也是溢寫的過程,這個過程中如果你設置有Combiner,也是會啟用的,然后在磁盤中生成了眾多的溢寫文件。第二種merge方式(內存到磁盤)一直在運行,直到沒有map端的數據時才結束,然后啟動第三種磁盤到磁盤的merge方式生成最終的那個文件。

   Reducer的輸入文件。不斷地merge后,最后會生成一個“最終文件”。為什么加引號?因為這個文件可能存在於磁盤上,也可能存在於內存中。對我們來說,當然希望它存放於內存中,直接作為Reducer的輸入,但默認情況下,這個文件是存放於磁盤中的。至於怎樣才能讓這個文件出現在內存中,之后的性能優化篇我再說。當Reducer的輸入文件已定,整個Shuffle才最終結束。然后就是Reducer執行,把結果放到HDFS上。

16.3.2   map

map task開始運算,並產生中間數據時,其產生的中間結果並非直接就簡單的寫入磁盤。這中間的過程比較復雜,並且利用到了內存buffer來進行已經產生的部分結果的緩存,並在內存buffer中進行一些預排序來優化整個map的性能。每一個map都會對應存在一個內存bufferMapOutputBuffer,即上圖的buffer in memory),map會將已經產生的部分結果先寫入到該buffer中,這個buffer默認是100MB大小,此值可以通過修改io.sort.mbmapred-default.xml)配置項一調整。當map的產生數據非常大時,並且把io.sort.mb調大,那么map在整個計算過程中spill的次數就勢必會降低,map task對磁盤的操作就會變少,如果map tasks的瓶頸在磁盤上,這樣調整就會大大提高map的計算性能。mapsortspill內存結構如下如所示:

說明: https://images0.cnblogs.com/blog/360373/201310/05162532-23cd6c989c1e4f0fa5c2107534f18035.jpg

 

map在運行過程中,不停的向該buffer中寫入已有的計算結果,但是該buffer並不一定能將全部的map輸出緩存下來,當map輸出超出一定閾值(比如100M),那么map就必須將該buffer中的數據寫入到磁盤中去,這個過程在mapreduce中叫做spill,在溢寫時,map的輸出繼續寫到緩沖區(那剩下的20%空閑空間),但如果在此期間緩沖區被填滿(指那剩下的20%空閑也被使用完了),則map會被阻塞直到寫磁盤過程完成。map並不是要等到將該buffer全部寫滿時才進行spill,因為如果全部寫滿了再去寫spill,勢必會造成map的計算部分等待buffer釋放空間的情況。所以,map其實是當buffer被寫滿到一定程度(比如80%)時,就開始進行spill。這個閾值也是由一個job的配置參數來控制,即io.sort.spill.percent,默認為0.8080%。這個參數同樣也是影響spill頻繁程度,進而影響map task運行周期對磁盤的讀寫頻率的。但非特殊情況下,通常不需要人為的調整。調整io.sort.mb對用戶來說更加方便。

 

map task的計算部分全部完成后,就會生成一個或者多個spill文件,這些文件就是map的輸出結果。map在正常退出之前,需要將這些spill合並(merge)成一個,所以map在結束之前還有一個merge的過程。merge的過程中,有一個參數可以調整這個過程的行為,該參數為:io.sort.factor。該參數默認為10。它表示當merge spill文件時,最多能有多少並行的stream(即可以同時打開多少個文件進行讀取)向merge文件中寫入。比如如果map產生的數據非常的大,產生的spill文件大於10,而io.sort.factor使用的是默認的10,那么當map計算完成做merge時,就沒有辦法一次將所有的spill文件merge成一個,而是會分多次,每次最多10stream。這也就是說,當map的中間結果非常大,調大io.sort.factor,有利於減少merge次數,進而減少map對磁盤的讀寫頻率,有可能達到優化作業的目的。

 

job指定了combiner的時候,我們都知道map介紹后會在map端根據combiner定義的函數將map結果進行合並。運行combiner函數的時機有可能會是merge完成之前,或者之后,這個時機可以由一個參數控制,即min.num.spills.for.combinedefault 3),當job中設定了combiner,並且spill數最少有3個的時候,那么combiner函數就會在merge產生結果文件之前運行。通過這樣的方式,就可以在spill非常多需要merge,並且很多數據需要做conbine的時候,減少寫入到磁盤文件的數據數量,同樣是為了減少對磁盤的讀寫頻率,有可能達到優化作業的目的。

 

減少中間結果讀寫進出磁盤的方法不止這些,還有就是壓縮。也就是說map的中間,無論是spill的時候,還是最后merge產生的結果文件,都是可以壓縮的。壓縮的好處在於,通過壓縮減少寫入讀出磁盤的數據量。對中間結果非常大,磁盤速度成為map執行瓶頸的job,尤其有用。控制map中間結果是否使用壓縮的參數為:mapred.compress.map.output(true/false)。將這個參數設置為true時,那么map在寫中間結果時,就會將數據壓縮后再寫入磁盤,讀結果時也會采用先解壓后讀取數據。這樣做的后果就是:寫入磁盤的中間結果數據量會變少,但是cpu會消耗一些用來壓縮和解壓。所以這種方式通常適合job中間結果非常大,瓶頸不在cpu,而是在磁盤的讀寫的情況。說的直白一些就是用cpuIO。根據觀察,通常大部分的作業cpu都不是瓶頸,除非運算邏輯異常復雜。所以對中間結果采用壓縮通常來說是有收益的。以下是一個wordcount中間結果采用壓縮和不采用壓縮產生的map中間結果本地磁盤讀寫的數據量對比:

map中間結果不壓縮:

說明: https://images0.cnblogs.com/blog/360373/201310/05162830-b7f3d6f55b6d4abcb1dbbff65b697b03.jpg

map中間結果壓縮:

說明: https://images0.cnblogs.com/blog/360373/201310/05162836-b93ef951cb3646a89190361e24b54ba3.jpg

可以看出,同樣的job,同樣的數據,在采用壓縮的情況下,map中間結果能縮小將近10倍,如果map的瓶頸在磁盤,那么job的性能提升將會非常可觀。

當采用map中間結果壓縮的情況下,用戶還可以選擇壓縮時采用哪種壓縮格式進行壓縮,現在hadoop支持的壓縮格式有:GzipCodecLzoCodecBZip2CodecLzmaCodec等壓縮格式。通常來說,想要達到比較平衡的cpu和磁盤壓縮比,LzoCodec比較適合。但也要取決於job的具體情況。用戶若想要自行選擇中間結果的壓縮算法,可以設置配置參數:mapred.map.output.compression.codec=org.apache.hadoop.io.compress.DefaultCodec或者其他用戶自行選擇的壓縮方式。

 

-------------------------------------------------------------------------------------------------------------------------------

 

溢寫文件存放目錄由mapred.local.dir配置項指定

 

在寫磁盤之前,線程首先根據reducer的數量把數據划分成相應數據的分區(partition)。每個分區中的數據都會按鍵在內存中進行排序,如果指定了combiner,則對排序輸出再次運行combiner方法。運行combiner可以減少map的輸出,這樣就減少寫到磁盤的數據和傳遞給reducer的數據

 

每次溢寫都會產生一個文件,所以同一個map任務的輸出是有可能會產生多個文件,在Map任務完成后,會將這些溢寫文件再次分區排序寫入到一個總的文件中,每次可以合並多少個文件由io.sort.factor(默認值為10,即可打開10 I/O同時進行文件操作)控制;如果至少存在3(由min.num.spills.for.combine設置)個及以上的溢寫文件合並時,在輸出到文件時前會再次調用combiner對數據進行合並計算(如果設置了combiner,在寫spill文件的時候也會調用)。combiner可以反復調用而不會影響輸出結果。如果只有一兩個溢寫文件需要合並,是不會(不值得)調用combiner

 

如果將寫到磁盤的map輸出數據進行壓縮,將會節約磁盤空間,寫減少寫磁盤時間,且還可以減少傳遞給reducer的時間。默認情況下是不開啟Map輸出壓縮的,但可以通過配置mapred.compress.map.output屬性為true,則可以自動開啟,並且壓縮方法由mapred.map.output.compression.codec屬性指定

 

map輸出的分區文件是通過HTTP方式傳遞給reducer端的,map端上(每個tasktracker)默認開40個(可由tasktracker.http.threads配置)HTTP服務線程來為文件傳輸工作。在MapReduce 2中,該配置屬性不適用,因為使用最大線程數是由機器處理器數量自動設定的,默認情況下允許值為處理數量的兩倍

16.3.3   reduce

reduce的運行是分成三個階段的。分別為copy->sort->reduce。由於job的每一個map都會根據reducen將數據分成npartition,所以map的中間結果中是有可能包含多個reduce需要處理的部分數據的。所以,為了優化reduce的執行時間,hadoop中是等job的第一個map結束后,所有的reduce就開始嘗試從完成的map中下載該reduce對應的partition部分數據(而不是等到所有map任務都完成后才開始copy)。這個過程就是通常所說的shuffle,也就是copy過程。

 

Reduce task在做shuffle時,實際上就是從不同的已經完成的map上去下載屬於自己這個reduce的部分數據,由於map通常有許多個,所以對一個reduce來說,下載也可以是並行的從多個map下載,這個並行度是可以調整的,調整參數為:mapred.reduce.parallel.copiesdefault 5)。默認情況下,每個只會有5個並行的下載線程在從map下數據,如果一個時間段內job完成的map100個或者更多,那么reduce也最多只能同時下載5map的數據,所以這個參數比較適合map很多並且完成的比較快的job的情況下調大,有利於reduce更快的獲取屬於自己部分的數據。

 

reduce的每一個下載線程在下載某個map數據的時候,有可能因為那個map中間結果所在機器發生錯誤,或者中間結果的文件丟失,或者網絡瞬斷等等情 況,這樣reduce的下載就有可能失敗,所以reduce的下載線程並不會無休止的等待下去,當一定時間后下載仍然失敗,那么下載線程就會放棄這次下載,並在隨后嘗試從另外的地方下載(因為這段時間map可能重跑)。所以reduce下載線程的這個最大的下載時間段是可以調整的,調整參數為:mapred.reduce.copy.backoffdefault 300秒)。如果集群環境的網絡本身是瓶頸,那么用戶可以通過調大這個參數來避免reduce下載線程被誤判為失敗的情況。不過在網絡環境比較好的情況下,沒有必要調整。通常來說專業的集群網絡不應該有太大問題,所以這個參數需要調整的情況不多。

 

Reducemap結果下載到本地時,同樣也是需要進行merge的,所以io.sort.factor的配置選項同樣會影響reduce進行merge時的行為,該參數的詳細介紹上文已經提到,當發現reduceshuffle階段io wait非常的高的時候,就有可能通過調大這個參數來加大一次merge時的並發吞吐,優化reduce效率。

 

Reduceshuffle階段對下載來的map數據,並不是立刻就寫入磁盤的,而是會先緩存在內存中,然后當使用內存達到一定量的時候才刷入磁盤。這個內存大小的控制就不像map一樣可以通過io.sort.mb來設定了,而是通過另外一個參數來設置:mapred.job.shuffle.input.buffer.percentdefault 0.7),這個參數其實是一個百分比,意思是說,shuffilereduce內存中的數據最多使用內存量為:0.7 * maxHeap of reduce task。也就是說,如果該reduce task的最大heap使用量(通常通過mapred.child.java.opts來設置,比如設置為-Xmx1024m)的一定比例用來緩存數據。默認情況下,reduce會使用其heap size70%來在內存中緩存數據。如果reduceheap由於業務原因調整的比較大,相應的緩存大小也會變大,這也是為什么reduce用來做緩存的參數是一個百分比,而不是一個固定的值了。

 

假設mapred.job.shuffle.input.buffer.percent0.7reduce taskmax heapsize1G,那么用來做下載數據緩存的內存就為大概700MB左右,這700M的內存,跟map端一樣,也不是要等到全部寫滿才會往磁盤刷的,而是當這700M中被使用到了一定的限度(通常是一個百分比),就會開始往磁盤刷。這個限度閾值也是可以通過job參數來設定的,設定參數為:mapred.job.shuffle.merge.percentdefault 0.66)。如果下載速度很快,很容易就把內存緩存撐滿,那么調整一下這個參數有可能會對reduce的性能有所幫助。

 

reduce將所有的map上對應自己partition的數據下載完成后,就會開始真正的reduce計算階段(中間有個sort階段通常時間非常短,幾秒鍾就完成了,因為整個下載階段就已經是邊下載邊sort,然后邊merge的)。當reduce task真正進入reduce函數的計算階段的時候,有一個參數也是可以調整reduce的計算行為。也就是:mapred.job.reduce.input.buffer.percentdefault 0.0)。由於reduce計算時肯定也是需要消耗內存的,而在讀取reduce需要的數據時,同樣是需要內存作為buffer,這個參數是控制,需要多少的內存百分比來作為reduce讀已經sort好的數據的buffer百分比。默認情況下為0,也就是說,默認情況下,reduce是全部從磁盤開始讀處理數據。如果這個參數大於0,那么就會有一定量的數據被緩存在內存並輸送給reduce,當reduce計算邏輯消耗內存很小時,可以分一部分內存用來緩存數據,反正reduce的內存閑着也是閑着。

----------------------------------------------------------------------------------------------------------------------------------------------

 

map輸出文件位於運行map任務機器的本地磁盤上(注:reduce輸出不是這樣的)

 

每一個分區數據都會啟動一個reduce任務,同一分區數據可能來自於不同機器上的好幾個不同的文件。所以一個reduce任務需所要的數據可能來自於好幾個不同機器上的map 任務輸出文件。

 

每個map任務完成的時間不相同,但只要有一個任務完成輸出,reduce就會使用復制線程(默認為5個,可以修改mapred.reduce.parallel.copies)將map任務輸出復制過來

 

如果從map任務復制到reducer端的數據能足夠存放在JVM里的緩存(這個內存是通過mapred.job.shuffle.input.buffer.percent來配置,值是一個百分比,默認為0.7,即划出70%JVM heap堆內存用來在shuffle過程中存放復制過來的map輸出)中時,就不直接復制到reducer所在機器上的磁盤中,而是直接放在其內存中;當緩沖達到閾值大小(可由mapred.job.shuffle.merge.percent屬性設定),或者存放的文件數達到閾值(由mapred.inmem.merge.threshold指定,默認為1000)時,就開始合並,並溢寫到reducer磁盤中;如果指定了combiner,則還會在合並期間運行以減少寫入硬盤的數據量

當不斷有文件溢寫到磁盤時,后台線程就開合並,而不是等到所有map輸出都復制過來時才開始,這樣可以為后面的合並節約時間

 

合並時,如果復制過來的數據是壓縮的,會先解壓

 

如果某個redue所需的map任務輸出全部復制過來后,reduce task就會進入排序階段,該階段進行map輸出合並,並維護其排序順序。合並是循環進行的,假如有50map輸出,而且合並因子為10(即可同時打開10 I/O進行文件操作,可由io.sort.factor配置,默認為10),則合並將進行5趟,每趟將10個文件合並成一個文件,因此最后有5個中間文件。為了避免再次寫磁盤,這5個文件並不會再次合並成一個文件,而是直接將這些文件輸入到reduce函數。

 

當所有map任務輸出數據都復制到reducer端並合並完成后,就進入了reduce階段,對於每個輸入的鍵值對就會調用一次reduce函數,並且reduce階段的輸出會直接寫入到HDFS中,由於tasktracker node(或MapReduce 2 中的node manager)與data node在同一機器上,所以reduce輸出第一個副本會放tasktracker node所在的data node中存儲

 

每趟(round)合並的文件數與前面提供的實例可能有所不同,為了最終產生的文件個數最少為目的(這樣可以減少寫入到磁盤上的數據量,因為文件數越少,合並力度越大),例如40個文件,就會采用如下圖合並的方式,第1趟只合並4個文件,第234趟合並10個文件,這樣經過4趟后合並后,就會合並成4個文件,還剩6個文件,這樣最后一趟(第5趟)將這最后10 = 4 + 6個文件合並成一個文件,這樣總比經過4趟,每趟10個文件的要好,因為這最終會生成4個文件,不是最優合並法:

 

16.3.4   shuffle配置調優

通過調整shuffle過程的配置,可以提高MapReduce的性能,下面兩個表總結了相應配置與默認值,這些設置都是以作業為單位的(除非特別說明)

配置項

類型

默認值

說明

io.sort.mb

int

100

Map任務輸出內存緩沖區,單位M

io.sort.record.percent

float

0.05

io.sort.mb中用來保存map output記錄邊界(即索引數據部分)的百分比,其他緩存用來保存數據。1.X版本后不在使用

io.sort.spill.percent

float

0.80

緩沖區存儲達到這個百分比時,開始溢寫

io.sort.factor

int

10

排序合並文件時,一次可合並處理的文件數,即同時可以打開10文件流,該配置項還用於reduce

min.num.spills.for.combine

int

3

運行combiner所需最小spill溢寫文件

mapred.compress.map.output

boolean

false

壓縮map輸出

mapred.map.output.compression.codec

Class name

org.apache.hadoop.io.compress.DefaultCodec

map輸出壓縮算法

tasktracker.http.threads

int

40

每個tasktracker上所開的http線程數,該HTTP服務線程用於將map輸出傳輸到reduce端。該配置項是針對整個集群的,不是單個作業。在MapReduce 2中已不適用

總的原則是給shuffle過程盡量多提供內存空間。然而,有一個平衡問題,也就是要確保map函數和reduce函數能得到足夠的內存來運行。這就是為什么編寫map函數和reduce函數時盡量少用內存原因,它們不應該無限使用內存,例如應避免在map中堆積數據

 

運行map任務和reduce任務的JVM,其大小由mapred.child.java.opts屬性設置,任務節點上的內存大小應該盡量大

 

map端,可以通過避免多次溢寫磁盤來獲得最佳性能;一次是最佳的情況。如果估算map輸出大小,就可以合理地設置ip.sort.*.屬性來盡可能減少溢寫的次數,具體而言,如增加io.sort.mb的值。MapReduce計數器(“Spilled records”)計算在作業運行整個階段中溢定磁盤的記錄數,這對於調優很有幫助,注:這個計數器包括mapreduce兩端的溢寫

 

reduce端,中間數據全部駐留在內存時,就能獲得最佳性能。在默認情況下,這是不可能發生的,因為所有內存一般都預留給reduce函數。但如果reduce函數的內存需求不大,把mapred.inmem.merge.threshold設置為0,把mapred.job.reduce.input.buffer.percent設置為1(或更低的值)就可以提升性能

 

配置項

類型

默認值

說明

mapred.reduce.parallel.copies

int

5

用於把map輸出復制到reducer的線程數

mapred.reduce.copy.backoff

int

300

map輸出復制到reducer過程中,由於種種原因可能失敗,失敗后reducer會自動重試,直到超過這個時間

io.sort.factor

int

10

這個參數也在map端使用,請參數map

mapred.job.shuffle.input.buffer.percent

float

0.70

reduce端的shuffle階段,將堆內存的百分之多少用來緩存從map端復制過來的數據

mapred.job.shuffle.merge.percent

float

0.66

reducer端用來接收map輸出的緩沖(由mapred.job.shuffle.input.buffer.percent配置)使用空間達到此百分比時,就會將緩沖的內容合並溢寫到磁盤

mapred.inmem.merge.threshold

int

1000

與前面mapred.job.shuffle.merge.percent參數一樣,也是用來控制合並溢寫的,如果從map節點取過來的map輸出數,當達到這個數之后,也會進行合並溢寫。如果為0,則不會生效,則合並溢寫觸發只能上面參數來控制了

mapred.job.reduce.input.buffer.percent

float

0.0

reduce時,用來存儲map輸出緩沖所占堆內存的最大百分比。在reduce階段,內存緩沖存放map輸出不會大於此大小。在默認情況下,為了reducer有更多可用的內存,reduce開始前所有map輸出都會被merge到磁盤。但是,如果reducer需要較少內存時可以增加該值來減少寫磁盤的次數

 

Hadoop讀取文件時使用默認為4KB的緩沖區,這是很低的,因此應該在集群中增加這個值(通過設置io.file.buffer.size

 

16.4        hadoop 配置項的調優

l  dfs.block.size

文件塊大小,由它決定HDFS文件block數量的多少(文件個數,塊越大,文件個數越小),它會間接的影響Job Tracker的調度和內存的占用(更影響內存的使用)

l  mapred.map.tasks.speculative.execution

l  mapred.reduce.tasks.speculative.execution

這是兩個推測式執行的配置項,默認是true

所謂的推測執行,就是當所有task都開始運行之后,Job Tracker會統計所有任務的平均進度,如果某個task所在的task node機器配

置比較低或者CPU load很高(原因很多),導致任務執行比總體任務的平均執行要慢,此時Job Tracker會啟動一個新的任務(duplicate task),原有任務和新任務哪個先執行完就把另外一個kill掉,這也是我們經常在Job Tracker頁面看到任務執行成功,但是總有些任務被kill,就是這個原因。

l  mapred.child.java.opts

默認值-Xmx200m

運行map任務和reduce任務的JVM,其大小由mapred.child.java.opts屬性設置,任務節點上的內存大小應該盡量大

l  mapred.compress.map.output

壓縮Map的輸出,這樣做有兩個好處:

a)壓縮是在內存中進行,所以寫入map本地磁盤的數據就會變小,大大減少了本地IO次數

b) Reduce從每個map節點copy數據,也會明顯降低網絡傳輸的時間

l  io.sort.mb

MB為單位,默認100M,這個值比較小

map節點沒運行完時,內存的數據過多,要將內存中的內容寫入磁盤,這個設置就是設置內存緩沖的大小,在suffle之前這個選項定義了map輸出結果在內存里占用buffer的大小,buffer達到某個閾值(后面那條配置),會啟動一個后台線程來對buffer的內容進行排序然后寫入本地磁盤(一個spill文件)

根據map輸出數據量的大小,可以適當的調整buffer的大小,注意是適當的調整,並不是越大越好,假設內存無限大,

io.sort.mb=1024(1G), io.sort.mb=300 (300M),前者未必比后者快:

11G的數據排序一次

2)排序3次,每次300MB

一定是后者快(歸並排序)

l  io.sort.spill.percent

這個值就是上面提到的buffer的閾值,默認是0.8,既80%buffer中的數據達到這個閾值,后台線程會起來對buffer中已有的數

據進行排序,然后寫入磁盤,此時map輸出的數據繼續往剩余的20% buffer寫數據,如果buffer的剩余20%寫滿,排序還沒結束,

map taskblock等待。

如果你確認map輸出的數據基本有序,排序時間很短,可以將這個閾值適當調高,更理想的,如果你的map輸出是有序的數據,那

么可以把buffer設的更大,閾值設置為1.

l  io.sort.factor

同時打開的文件句柄的數量,默認是10

當一個map task執行完之后,本地磁盤上(mapred.local.dir)有若干個spill文件,map task最后做的一件事就是執行merge sort,把這些spill文件合成一個文件(partitioncombine階段)。

執行merge sort的時候,每次同時打開多少個spill文件,就是由io.sort.factor決定的。打開的文件越多,不一定merge sort就越快,也要根據數據情況適當的調整。

注:merge排序的結果是兩個文件,一個是index,另一個是數據文件index文件記錄了每個不同的key在數據文件中的偏移量(即partition)。

map節點上,如果發現map所在的子節點的機器io比較重,原因可能是io.sort.factor這個設置的比較小,io.sort.factor設置小的話,如果spill文件比較多,merge成一個文件要很多輪讀取操作,這樣就提升了io的負載。io.sort.mb小了,也會增加io的負載。

如果設置了執行combine的話,combine只是在merge的時候,增加了一步操作,不會改變merge的流程,所以combine不會減少或者增加文件個數。另外有個min.num.spills.for.combine的參數,表示執行一個merge操作時,如果輸入文件數小於這個數字,就不調用combiner如果設置了combiner,在寫spill文件的時候也會調用,這樣加上merge時候的調用,就會執行兩次combine

 

提高Reduce的執行效率,除了在Hadoop框架方面的優化,重點還是在代碼邏輯上的優化.比如:對Reduce接受到的value可能有重

復的,此時如果用JavaSet或者STLSet來達到去重的目的,那么這個程序不是擴展良好的(non-scalable),受到數據量的限制,當數據膨脹,內存勢必會溢出

l  mapred.reduce.parallel.copies

Reduce copy數據的線程數量,默認值是5

Reduce到每個完成的Map Task 拷貝數據(通過RPC調用),默認同時啟動5個線程到map節點取數據。這個配置還是很關鍵的,如果你的map輸出數據很大,有時候會發現map早就100%了,reduce卻在緩慢的變化,那就是copy數據太慢了,比如5個線程copy 10G的數據,確實會很慢,這時就要調整這個參數,但是調整的太大,容易造成集群擁堵,所以 Job tuning(調優)的同時,也是個權衡的過程,要熟悉所用的數據!

l  mapred.job.shuffle.input.buffer.percent

當指定了JVM的堆內存最大值以后,上面這個配置項就是Reduce用來存放從Map節點復制過來的數據所用的內存占堆內存的比例,默認是0.7,既70%,通常這個比例是夠了,但是對於大數據的情況,這個比例還是小了一些,0.8-0.9之間比較合適。(前提是你的reduce函數不會瘋狂的吃掉內存)

l  mapred.job.shuffle.merge.percent(默認值0.66)

l  mapred.inmem.merge.threshold(默認值1000)

第一個指的是從Map節點取數據過來,放到內存,當達到這個閾值之后,后台啟動線程(通常是Linux native process)把內存中的

數據merge sort,寫到reduce節點的本地磁盤;

第二個指的是從map節點取過來的文件個數,當達到這個個數之后,也進行merger sort,然后寫到reduce節點的本地磁盤;這兩個配置項第一個優先判斷,其次才判斷第二個thresh-hold

從實際經驗來看,mapred.job.shuffle.merge.percent默認值偏小,完全可以設置到0.8左右;第二個默認值1000,完全取決於map輸出數據的大小,如果map輸出的數據很大,默認值1000反倒不好,應該小一些,如果map輸出的數據不大(light weight),可以設置2000或者以上。

l  mapred.reduce.slowstart.completed.maps

map完成多少百分比時,開始shuffle

map運行慢,reduce運行很快時,如果不設置mapred.reduce.slowstart.completed.maps會使jobshuffle時間變的很長,map運行完很早就開始了reduce,導致reduceslot一直處於被占用狀態。mapred.reduce.slowstart.completed.maps 這個值是和“運行完的map數除以總map數”做判斷的,當后者大於等於設定的值時,開始reduceshuffle。所以當mapreduce的執行時間多很多時,可以調整這個值(0.75,0.80,0.85及以上),默認為0.05

 

16.5        MapReduce作業的默認配置

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.fs.Path;

import org.apache.hadoop.mapreduce.Job;

import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

publicclass T {

    publicstaticvoid main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        conf.set("mapred.job.tracker", "hadoop-master:9001");

        Job job = Job.getInstance(conf, "XXX");

        job.setJarByClass(T.class);

 

        FileInputFormat.addInputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/all/1901.all"));

        FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/output2"));

        System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

上面這個示例什么與沒有指定,就指定了輸入也輸出位置,輸入結果:

每一行以整數開始,表示每行中Value所在原輸入文件的行起始偏移量,值就是原輸入文件的每行內容。雖然這個程序不是很有用的程序,但對於理解它如何產生輸出是很有用的

 

上面示例代碼沒有顯示的設置輸入輸出參數類型以及mapreduce類,那是因為使用了默認設置,下面粗體就是默認設置,只不過通過程序顯示指定了:

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.fs.Path;

import org.apache.hadoop.io.LongWritable;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.mapreduce.Job;

import org.apache.hadoop.mapreduce.Mapper;

import org.apache.hadoop.mapreduce.Reducer;

import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;

import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;

import org.apache.hadoop.mapreduce.lib.partition.HashPartitioner;

publicclass T {

    publicstaticvoid main(String[] args) throws Exception {

 

        Configuration conf = new Configuration();

        conf.set("mapred.job.tracker", "hadoop-master:9001");

        Job job = Job.getInstance(conf, "XXX");

        job.setJarByClass(T.class);

       

       job.setInputFormatClass(TextInputFormat.class);

       job.setMapperClass(Mapper.class);

       job.setMapOutputKeyClass(LongWritable.class);

       job.setMapOutputValueClass(Text.class);

       job.setPartitionerClass(HashPartitioner.class);

       job.setNumReduceTasks(1);

       job.setReducerClass(Reducer.class);

       job.setOutputKeyClass(LongWritable.class);

       job.setOutputValueClass(Text.class);

       job.setOutputFormatClass(TextOutputFormat.class);

 

        FileInputFormat.addInputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/all/1901.all"));

        FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/output2"));

        System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

默認的輸入文件格式為TextInputFormat,它產生的鍵類型是LongWritable(文件中每行起始偏移量),值類型是Text(文本行)

默認的mapperorg.apache.hadoop.mapreduce.Mapper我們實現的map()方法就是繼承這個類然后重寫的)類,它將輸入的鍵和值原封不動地寫到輸出中:

publicclass Mapper<KEYIN, VALUEIN, KEYOUT, VALUEOUT> {

  protectedvoid map(KEYIN key, VALUEIN value,

                     Context context) throws IOException, InterruptedException {

    context.write((KEYOUT) key, (VALUEOUT) value);

  }

}

默認的partitionerHashPartitioner,它對每條記錄的鍵進行哈希操作以決定該記錄(鍵值對)應該屬於哪個分區。每個分區對應一個reducer任務,所以分區數等於作業的reducer個數:

publicclass HashPartitioner<K, V> extends Partitioner<K, V> {

  publicint getPartition(K key, V value,

                          int numReduceTasks) {

    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;

  }

}

鍵的哈希碼被轉換為一個非負整數,它由哈希值與最大的整型值做一次按位與操作而獲得,然后用分區數進行取模操作,來決定該記錄屬於哪個分區索引(即該鍵值對應該復制到哪個reduce任務上去)。

默認情況下,只有一個reducer,因此也就只有一個分區,在這種情況下,由於所有數據都放入同一個分區,partitioner操作將變得無關緊要了。然后,如果有很多reducer,了解HashPartitioner的作用就非常重要了。假設基於鍵的散列函數足夠好,那么記錄將被均勻分到若干個reduce任務中,這樣,具有相同鍵的記錄將由同一個reduce任務進行處理

 

注:map任務數據並不能設置,該數量等於輸入文件被划分成的分塊數,這取決於輸入文件的大小以及文件塊的大小

 

默認的reducerorg.apache.hadoop.mapreduce.Reducer我們實現的reduce()方法就是繼承這個類然后重寫的)類型,它簡單地將所有的輸入寫到輸出中:

publicclass Reducer<KEYIN,VALUEIN,KEYOUT,VALUEOUT> {

  protectedvoid reduce(KEYIN key, Iterable<VALUEIN> values, Context context

                        ) throws IOException, InterruptedException {

    for(VALUEIN value: values) {

      context.write((KEYOUT) key, (VALUEOUT) value);

    }

  }

}

默認的輸出格式為TextOutputFormat,它將鍵和值轉換成字符串並用制表符分隔開,然后一條記錄一行地進行輸出。這就是為什么輸出文件是用制表符(Tab)分隔的,這是TextOutputFormat的特點

 

16.6        輸入格式

16.6.1   輸入分片與記錄

一個分片(split)對應一個map任務處理,即每個map只處理一個輸入分片。每個分片被划分為若干個記錄,每條記錄就是一個鍵值對,map是一個個處理記錄的。輸入分片和記錄都是邏輯概念。

 

publicabstractclass InputSplit {

  /**

   * Get the size of the split, so that the input splits can be sorted by size.

   * @return the number of bytes in the split

   */

  publicabstractlong getLength() throws IOException, InterruptedException;

  /**

   * Get the list of nodes by name where the data for the split would be local.

   * The locations do not need to be serialized.

   * @return a new array of the node nodes.

   */

  publicabstract String[] getLocations() throws IOException, InterruptedException;

}

InputSplit包括一個以字節為單位的長度和一組存儲位置(即一組主機名)。注:分片並不數據本身,而是指向數據的引用(reference)。可以根據存儲位置將map任務盡量放在分片數據附近。可以根據分片大小用來排序分片,以便優先處理最大的分片,從而最小化作業運行時間

我們不必直接處理InputSplit,因為它是由InputFormat創建的。InputFormat負責產生輸入分片並將它們分割成記錄

publicabstractclass InputFormat<K, V> {

 

  /**

   * Logically split the set of input files for the job. 

   * <p><i>Note</i>: The split is a <i>logical</i> split of the inputs and the

   * input files are not physically split into chunks. For e.g. a split could

   * be <i>&lt;input-file-path, start, offset&gt;</i> tuple. The InputFormat

   * also creates the {@link RecordReader} to read the {@link InputSplit}.

   */

  publicabstract

    List<InputSplit> getSplits(JobContext context) throws IOException, InterruptedException;

 

  /**

   * Create a record reader for a given split. The framework will call

   * {@link RecordReader#initialize(InputSplit, TaskAttemptContext)} before

   * the split is used.

   * @param split the split to be read

   */

  publicabstract RecordReader<K,V> createRecordReader(InputSplit split, TaskAttemptContext context ) throws IOException,

                                                 InterruptedException;

}

運行作業的客戶端通過調用getSplits()獲取邏輯分片列表,然后將它們發送到jobtrackerjobtracker使用其存儲位置信息來調度map任務從而在tasktracker上處理這些分片數據。在tasktracker上,map任務把輸入分片傳給InputFormatcreateRecordReader()方法獲得這個分片的RecordReaderRecordReader就是記錄上的迭代器,map任務通用RecordReader來生成記錄的鍵值對,然后再傳給map函數。從org.apache.hadoop.mapreduce.Mapper我們實現的map()方法就是繼承這個類然后重寫的)的run()方法可以看到這一過程:

  /**

   * Expert users 專家用戶可以重寫這個run方法 can overridethis method for more complete control over the execution of the Mapper.

   */

  publicvoid run(Context context) throws IOException, InterruptedException {

    setup(context);

    try {

      while (context.nextKeyValue()) {

        map(context.getCurrentKey(), context.getCurrentValue(), context);

      }

    } finally {

      cleanup(context);

    }

  }

運行完setup()方法之后,再循環調用Context上的nextKeyValue()方法,該方法會委托給RecordReader的同名方法來為mapper產生Keyvalue,如果未講到stream的尾部時,借助於context委托調用同名方法(getCurrentKey(),getCurrentValue())將讀取出來的keyvalue傳給map()方法,這樣我們就可以在map()方法里來處理一個個記錄(鍵值對)了

 

通常情況下我們的mapper是從org.apache.hadoop.mapreduce.Mapper繼承過來然后重寫map()方法來實現,它有很多的子類:

MultithreadedMapperMapper的一個子類,它能以並行的方式來運行mapper,即啟動一個線程來運行map()方法,MultithreadedMapper能啟動多少個線程是可以通過mapreduce.mapper.multithreadedmapper.threads來配置的。在通常情況下我們繼承org.apache.hadoop.mapreduce.Mapper就可以了,但如果map()方法需要花很長一段時間來處理每一條記錄時(如需要連接外部服務器),就可以繼承這個類,針對一個分片就可以實例化多個Mapper,讓記錄之間的處理是並行的,而不是串行的

16.6.1.1           FileInputFormat

FileInputFormat是使用文件作為數據源的基類,一切基於文件格式輸入的實現都是從此類繼承的:

FileInputFormat它有兩個功用:一是提供作業輸入文件的位置,二是將輸入文件分片的實現代碼;而將分片分割成記錄則是由其子類來實現的

16.6.1.1.1     FileInputFormat文件路徑

FileInputFormat提供了四種靜態方法來設定Job的輸入路徑:

publicstaticvoid addInputPath(Job job, Path path)

publicstaticvoid addInputPaths(Job job, String commaSeparatedPaths)

publicstaticvoid setInputPaths(Job job, Path... inputPaths)

publicstaticvoid setInputPaths(Job job, String commaSeparatedPaths)

addInputPathaddInputPaths可以多次調用,它們是附加路徑,而setInputPaths則是一次性設置完成的路徑列表,並且以前設置的都會被替換掉

路徑可以是一個文件、一個目錄、或文件通配,如果是目錄的話則包含這個目錄下所有文件(默認情況下是不包含子目錄,並且如果有子目錄也會被當作文件看,這樣運行時會產生錯誤,處理這個問題就是使用文件通配或過濾器來選擇目錄中的文件;但如果將mapred.input.dir.recursive設置為true時,就會遞歸讀取子目錄)

 

addset方法指定了要包含的文件,如果要排除,可以使用setInputPathFilter()方法來設置一個過濾器:

publicstaticvoid setInputPathFilter(Job job, Class<? extends PathFilter> filter)

PathFilter過濾器的使用請參考前面

即使不使用過濾器,FileInputFormat也會使用一個默認的過濾器來排除隱藏文件(名稱中以“.”和“_”開頭的文件),如果通過setInputPathFilter方法設置了過濾器,它會在默認過濾器的基礎上進行過濾

路徑和過濾器也可以通過配置屬性來設置:

16.6.1.1.2     InputSplit

InputSplit是指分片,在MapReduce當中作業中,作為map task最小輸入單位。分片是基於文件基礎上出來的而來的概念,通俗的理解一個文件可以切分為多少個片段(一個分片可包含的多個block,一個分片也可以來自多個文件——如CombineFileInputFormat輸入)每個片段包括了“文件名,開始位置,長度,位於哪些主機”等信息。在MapTask拿到這些分片后,會知道從哪開始讀取該map任務所處理的數據。注:InputSplit本身並沒有存儲數據,而只是記錄了數據所在的地方

 

通過調用map()方法參數對象Context上的getInputSplit()方法,返回InputSplit對象,如果輸入的文件是FileInputFormat,則是可以將InputSplit強轉為FileSplit類型,然后就可以調用以下方法獲取相應信息:

  /** Constructs a split with host information

   * @param file the file name 文件分片所在的文件路徑,從這里來看,不是路徑數組,只是單個路徑,這說明一個FileSplit文件分片只對應一個文件,即FileSplit文件分片不會跨文件(但可能包含多個數據塊),但CombineFileSplit分片是可以跨的文件的

   * @param start the position of the first byte in the file to process 分片所在文件中的起始位置

   * @param length the number of bytes in the file to process 分片長度

   * @param hosts the list of hosts containing the block, possibly null 主機名是一個數組,因為一個文件是分塊存放在不同的DataNode上的

   */

  public FileSplit(Path file, long start, long length, String[] hosts) {

    this.file = file;

    this.start = start;

    this.length = length;

    this.hosts = hosts;

  }

 

publicclass CombineFileSplit extends InputSplit implements Writable {

  private Path[] paths;//該分片數據來自哪些文件(CombineFileSplit是跨文件的)。paths[]startoffset[]lengths[]這三個數組中的元素是一一對應的,這可以理解 CombineFileSplit是由很多個FileSplit組成的一樣

  privatelong[] startoffset;

  privatelong[] lengths;

  private String[] locations;

  privatelongtotLength;//分片長度

 

FileInputFormat輸入格式對應的是FileSplitCombineFileInputFormat輸入格式對應的是CombineFileSplit

16.6.1.1.3     FileInputFormat文件分片算法

FileInputFormatgetSplits()方法為邏輯分片算法,即如何將文件分成輸入片斷,為map任務提供輸入數據

FileInputFormatgetSplits()返回的是FileSplit列表

 

下面三個配置屬性可以用來控制分片大小:

mapred.min.split.sizemapred.max.split.size是在mapred-site.xml配置的,dfs.block.size是在hdfs-site.xml中配置的;其中mapred.max.split.size屬性在舊APIorg.apache.hadoop.mapred包下面的FileInputFormat)中沒有使用(新API——org.apache.hadoop.mapreduce包下的FileInputFormat才會使用),所以如果使用的是舊API,則配置了mapred.max.split.size也不管用;如果配置文件中沒有配置mapred.min.split.sizemapred.max.split.size這兩屬性,則它們在程序中會有相應默認值:1與最大的long;除了通過配置文件進行配置外,mapred.min.split.sizemapred.max.split.size還可以通過FileInputFormat的方法:setMinInputSplitSize()setMaxInputSplitSize()方法分別進行設置。

 

FileInputFormat文件格式輸入的分片大小(splitSize)由以下公式計算:

splitSize = max(minSize, min(maxSize, blockSize))

由於在默認情況下:

minSize < blockSize < maxSize

所以默認分片大小就是blockSize

 

計算公式可以查看FileInputFormat.computeSplitSize()方法:

  protectedlong computeSplitSize(long blockSize, long minSize, long maxSize) {

    return Math.max(minSize, Math.min(maxSize, blockSize));

  }

該方法會被getSplits()方法調用:

longsplitSize= computeSplitSize(blockSize, minSize, maxSize);//splitSize即為分片大小,分片時就會根據此大小進行分片

 

上面計算公式具體為三步:

1、  minSize取值邏輯:minSize = Math.max(FormatMinSplitSize, MinSplitSize)

1)         FormatMinSplitSize,本Format設置的最小Split大小,通過getFormatMinSplitSize()獲取,程序里寫死的是1個字節

2)         MinSplitSize,通過配置文件屬性mapred.min.split.size設置或通過程序方法setMinInputSplitSize()設置,通過getMinSplitSize()獲取,未設置則為1

3)         取兩者較大值。由於FormatMinSplitSizeMinSplitSize都為1,所以最終minSize還是為1

2、  Math.min(maxSize, blockSize)取值邏輯:

1)         maxSize,通過配置文件屬性mapred.max.split.size設置或通過程序方法setMaxInputSplitSize()設置,通過getMaxSplitSize()獲取,無設置則取Long.MAX_VALUE

2)         文件塊大小blockSize,由配置文件屬性dfs.block.size設置

3)         取兩者較小值

3、  再取 minSizeMath.min(maxSize, blockSize)的較大值

 

從上圖可以看出mapred.min.split.size可以改變最小分片大小,如果將該值設置成比HDFS文件系統塊大的話,這樣就可以強迫分片比HDFS塊大。這樣做不是很好,因為這樣的話,分片中的數據就可能跨不同的機器,這樣map任務的數據就不是本地化數據了

16.6.1.1.3.1           BlockSplit

1、  blockhdfs存儲文件的單位(默認是64M);這是物理的划分,大文件上傳到HDFS時,就會切分成若干塊后分塊存儲

2、  InputSplitMapReduce對文件進行處理和運算的輸入單位,只是一個邏輯概念,每個InputSplit並沒有對文件實際的切割,只是記錄了要處理的數據的位置(包括文件的pathhosts)和長度(由startlength決定)。

 

BlockSplit沒有必然的關系,在分片時不是以Block為單位的,即一個Split可能包含了某個Block一部分

 

兩個問題:

1、  Hadoop的一個Block默認是64M,在上傳文件到HDFS中存儲時,會不會從一行的中間進行切開?即一行會分到兩個不同的Block中?

2、  map任務處理前,會將輸入文件進行邏輯分片,這會不會造成一行記錄被分成兩個InputSplit,如果被分成兩個InputSplit,這樣一個InputSplit里面就有一行不完整的數據,那么處理這個InputSplitMapper會不會得出不正確的結果?

答案:以行記錄為輸入單位的文本,是可能存在一行記錄被划分到不同的Block,甚至不同的DataNode上去;再通過分析FileInputFormat里面的getSplits()方法,可以得出,某一行記錄同樣也可能被划分到不同的InputSplit

        long bytesRemaining = length;//整個文件大小

        while (((double) bytesRemaining) / splitSize > 1.1) {//不是只要文件大小大於了分片大小,這個文件就立馬進行分片的,而要求該文件大小超過分片大小的10%以上才會進行分片

          int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);

          splits.add(new FileSplit(path, length - bytesRemaining, splitSize,

                                   blkLocations[blkIndex].getHosts()));//FileSplit即為一個邏輯分片,它記錄了數據片斷所對應的文件路徑、起始偏量(length - bytesRemaining),以及長度(splitSize)等信息;從這里可以看出,一個FileSplit是不可能跨文件的這可能與CombineFileSplit是不一樣的

          bytesRemaining -= splitSize; //剩下的部分再次循環分片

        }

        if (bytesRemaining != 0) {//最后不足一個分片大小時,也會創建一個分片

          splits.add(new FileSplit(path, length-bytesRemaining, bytesRemaining,

                     blkLocations[blkLocations.length-1].getHosts()));

        }

從上面的分片代碼可以看出:

1、  一個FileSplit分片不能跨文件

2、  FileInputFormat對文件的切分是嚴格按照偏移量來的,所以會出現下后兩種情況:

3、  一個FileSplit分片可以跨Block,甚至只包含某個Block的一部分(即一個Block可能被分割到幾個FileSplit中)

4、  完全可能從行中間進行分片,這會造成同一行被分片到了不同的FileSplit分片中

 

盡管一行記錄可能被拆分到不同的InputSplit,但是與FileInputFormat關聯的RecordReader被設計的足夠健壯,當一行記錄跨InputSplit時,其能夠到讀取不同的InputSplit,直到把這一行記錄讀取完成;在Hadoop里,以行形式的輸入文本,通常采用默認的TextInputFormat類來進行格式輸入,TextInputFormat關聯的是LineRecordReader,而LineRecordReader最終又是借助於LineReader來讀取行的,而LineReaderreadLine()方法解決了跨分片讀取整行的問題(因為行是可以跨分片的,即同一行被分片到了不同的FileSplit分片中);另外,行跨分片會帶來另一問題:該行數據會不會被重復讀取的問題,其實重復讀取問題是不會存在的,原因是LineRecordReaderinitialize()初始化方法里解決了這個問題——會自動跳過最前面的半行

 

16.6.1.1.4     CombineFileInputFormat

CombineFileInputFormat適合處理大批量的小文件,它把多個文件打包到一個分片中以便每個mapper可以處理更多的數據,決定哪些文件放入同一分片時,CombineFileInputFormat會考慮到節點和機架因素,將相近的數據盡量打在一個包中

注:這里的小文件是指文件的大小是小於Block的,即每個文件只有一個Block,如果超一個Block,則就不太適用了

 

CombineFileInputFormat分片即不是以文件為單位,也不是以Block塊為單位進行划分的FileInputFormat切分時與文件及Block都是沒關系,它直接是以字節為單位進行的,所以FileInputFormat切分出來的分片都是一樣的——除最后一個分片外),而是在真正開始分片之前,預先將每個文件邏輯切分成若干個小的碎片(前提的文件是可切分的,即isSplitable),碎片切分算法如下:

            long left = locations[i].getLength();//文件大小

            long myOffset = locations[i].getOffset();//初始值固定為0

            long myLength = 0;

            while (left > 0) {

              if (maxSize == 0) {

                myLength = left;

              } else {

                if (left > maxSize && left < 2 * maxSize) {

                  // if remainder is between max and 2*max - then

                  // instead of creating splits of size max, left-max we

                  // create splits of size left/2 and left/2. This is

                  // a heuristic to avoid creating really really small

                  // splits.

                  myLength = left / 2;

                } else {

                  myLength = Math.min(maxSize, left);

                }

              }

//文件碎片,關鍵信息為起始位置及碎片長度

              OneBlockInfo oneblock = new OneBlockInfo(path, myOffset,

                  myLength, locations[i].getHosts(), locations[i].getTopologyPaths());

              left -= myLength;

              myOffset += myLength;

 

              blocksList.add(oneblock);

            }

預先將文件分成若干小碎片的好處是便於后面在真正分片時,是以這些小碎片為單位進行分片,這樣可以將每個分片的大小分得更接近,但相對於FileInputFormat來說還是粒度粗了很多,所以導致CombineFileInputFormat分出的片斷應該只能是大小相近

 

 

CombineFileInputFormat涉及到三個重要的屬性:

mapreduce.input.fileinputformat.split.maxsize(或通過setMaxSplitSize()方法進行設置):同一節點或同一機架的數據塊形成切片時,切片大小的最大值;

mapreduce.input.fileinputformat.split.minsize.per.node(或通過setMinSplitSizeNode()方法進行設置):同一節點的數據塊形成切片時,切片大小的最小值;

mapreduce.input.fileinputformat.split.minsize.per.rack(或通過setMinSplitSizeRack()方法進行設置):同一機架的數據塊形成切片時,切片大小的最小值。

注:這些屬性都有相應的方法進行設置(CombineFileInputFormat.setMaxSplitSize()CombineFileInputFormat.setMinSplitSizeNode()CombineFileInputFormat.setMinSplitSizeRack ()),通過方法進行設置要優先於通過配置屬性進行的設置

 

CombineFileInputFormat切片形成過程(具體可參考CombineFileInputFormatgetSplits()方法調用的getMoreSplits()方法):

1)逐個節點形成切片;

a、遍歷並累加這個節點上的數據塊,如果累加數據塊(這里的數據塊就是上面預先分的碎片)大小大於或等於mapred.max.split.size,則將這些數據塊形成一個切片,繼承該過程,直到剩余數據塊累加大小小於mapred.max.split.size,則進行下一步;

b、如果剩余數據塊累加大小大於或等於mapred.min.split.size.per.node,則將這些剩余數據塊形成一個切片,如果剩余數據塊累加大小小於mapred.min.split.size.per.node,則這些數據塊留待后續處理。

2)逐個機架形成切片;

a、遍歷並累加這個機架上的數據塊(這些數據塊即為上一步遺留下來的數據塊),如果累加數據塊大小大於或等於mapred.max.split.size,則將這些數據塊形成一個切片,繼承該過程,直到剩余數據塊累加大小小於mapred.max.split.size,則進行下一步;

b、如果剩余數據塊累加大小大於或等於mapred.min.split.size.per.rack,則將這些剩余數據塊形成一個切片,如果剩余數據塊累加大小小於mapred.min.split.size.per.rack,則這些數據塊留待后續處理。

3)遍歷並累加所有機架上剩余數據塊,如果數據塊大小大於或等於mapred.max.split.size,則將這些數據塊形成一個切片,繼承該過程,直到剩余數據塊累加大小小於mapred.max.split.size,則進行下一步;

4)剩余數據塊形成一個切片。

注:如果maxSplitSizeminSizeNodeminSizeRack三個都沒有設置,那是所有輸入整合成一個分片

總結:

CombineFileInputFormat形成切片過程中考慮數據本地性(同一節點、同一機架),首先處理同一節點的數據塊,然后處理同一機架的數據塊,最后處理剩余的數據塊,可見本地性是逐步減弱的

 

CombineFileInputFormat是一個抽象類,需要實現createRecordReader()方法

 

16.6.1.1.4.1           示例

1、實現CombineFileInputFormat

import org.apache.hadoop.io.BytesWritable;

import org.apache.hadoop.io.LongWritable;

import org.apache.hadoop.mapreduce.InputSplit;

import org.apache.hadoop.mapreduce.RecordReader;

import org.apache.hadoop.mapreduce.TaskAttemptContext;

import org.apache.hadoop.mapreduce.lib.input.CombineFileInputFormat;

import org.apache.hadoop.mapreduce.lib.input.CombineFileRecordReader;

import org.apache.hadoop.mapreduce.lib.input.CombineFileSplit;

 

publicclass CombineSmallfileInputFormat extendsCombineFileInputFormat<LongWritable, BytesWritable> {

    @Override//使用CombineFileInputFormat時,需要實現createRecordReader()方法

    public RecordReader<LongWritable, BytesWritable> createRecordReader(InputSplit split,

            TaskAttemptContext context) throws IOException {

        CombineFileSplit combineFileSplit = (CombineFileSplit) split;

        //CombineSmallfileRecordReader為自定義的RecordReader

        CombineFileRecordReader<LongWritable, BytesWritable> recordReader = new CombineFileRecordReader<LongWritable, BytesWritable>(combineFileSplit, context, CombineSmallfileRecordReader.class);

        try {

            recordReader.initialize(combineFileSplit, context);

        } catch (InterruptedException e) {

            new RuntimeException("Error to initialize CombineSmallfileRecordReader.");

        }

        return recordReader;

    }

}

 

2、自定義RecordReader

import org.apache.hadoop.fs.Path;

import org.apache.hadoop.io.BytesWritable;

import org.apache.hadoop.io.LongWritable;

import org.apache.hadoop.mapreduce.InputSplit;

import org.apache.hadoop.mapreduce.RecordReader;

import org.apache.hadoop.mapreduce.TaskAttemptContext;

import org.apache.hadoop.mapreduce.lib.input.CombineFileSplit;

import org.apache.hadoop.mapreduce.lib.input.FileSplit;

import org.apache.hadoop.mapreduce.lib.input.LineRecordReader;

 

publicclass CombineSmallfileRecordReader extendsRecordReader<LongWritable, BytesWritable> {

 

    private CombineFileSplit combineFileSplit;

    /*

     * 該自定義的RecordReader實質上借助於LineRecordReader來實現的,免去了實現細節,除了構造與初始化

     * 其它操作都可直接委派到相應的方法。但是LineRecordReader返回的KeyLongWritable,即文本的行號,

     * 如果Key是自定義鍵(如復合Key),我們可以將LongWritable拷貝過來,然后修改它的nextKeyValue()

     * 法,將key使用自定義的Key類型即可

     */

    private LineRecordReader lineRecordReader = new LineRecordReader();

    private Path[] paths;//當前分片所有文件路徑

    privateinttotalLength;//所有文件數

    privateintcurrentIndex;//當前正處理的數據塊索引

    privatefloatcurrentProgress = 0;//處理進度

    private LongWritable currentKey;//

    private BytesWritable currentValue = new BytesWritable();//

 

    /*

     * 該自定義RecordReader類一定要帶有如下參數的構造函數,否則運行出錯

     */

    public CombineSmallfileRecordReader(CombineFileSplit combineFileSplit, TaskAttemptContext context,

            Integer index) throws IOException {

        super();

        this.combineFileSplit = combineFileSplit;

        this.currentIndex = index; // 當前要處理的小文件BlockCombineFileSplit中的索引

    }

    @Override

    publicvoid initialize(InputSplit split, TaskAttemptContext context) throws IOException,

            InterruptedException {

        this.combineFileSplit = (CombineFileSplit) split;

        // 處理CombineFileSplit中的一個小文件Block,因為使用LineRecordReader,需要構造一個FileSplit對象,然后才能夠讀取數據

        FileSplit fileSplit = new FileSplit(combineFileSplit.getPath(currentIndex),

                combineFileSplit.getOffset(currentIndex), combineFileSplit.getLength(currentIndex),

                combineFileSplit.getLocations());

        lineRecordReader.initialize(fileSplit, context);

 

        this.paths = combineFileSplit.getPaths();

        totalLength = paths.length;

    }

    @Override

    public LongWritable getCurrentKey() throws IOException, InterruptedException {

        currentKey = lineRecordReader.getCurrentKey();

        returncurrentKey;

    }

    @Override

    public BytesWritable getCurrentValue() throws IOException, InterruptedException {

        byte[] content = lineRecordReader.getCurrentValue().getBytes();

        currentValue.set(content, 0, content.length);

        returncurrentValue;

    }

    @Override

    publicboolean nextKeyValue() throws IOException, InterruptedException {

        if (currentIndex >= 0 && currentIndex < totalLength) {

            returnlineRecordReader.nextKeyValue();

        } else {

            returnfalse;

        }

    }

    @Override

    publicfloat getProgress() throws IOException {

        if (currentIndex >= 0 && currentIndex < totalLength) {

            currentProgress = (float) currentIndex / totalLength;

            returncurrentProgress;

        }

        returncurrentProgress;

    }

    @Override

    publicvoid close() throws IOException {

        lineRecordReader.close();

    }

}

 

3、Job中使用

job.setInputFormatClass(CombineSmallfileInputFormat.class);

 

 

16.6.1.1.5     避免文件切分

有些情況下可能不希望文件被切分,而是用一個mapper完整處理每一個輸入文件,例如,檢查一個文件中所有記錄是否有序,這種情況就不能切分,需要以文件為單位進行處理

 

有兩種方法。第一種:增加最小分片大小屬性的值,將它設置成大於要處理的最大文件大小,如設置為最大long;第二種方法就是重寫FileInputFormatisSplitable()方法,把返回值設置為false

16.6.1.1.6     示例:將整個文件作為一條記錄處理

為了避免在HDFS中產生很多小文件,在產生很多小文件之前,可以使用SequenceFile將它們合成一個大的文件后放在HDFS中:可以將文件名作為鍵(如果不需要鍵,可以使用NullWritable代替),文件的內容作為值

 

先定義一個FileInputFormat

//由於沒有用到Key,所以使用NullWritable 代替,值是BytesWritable,存儲整個文件內容

publicclass WholeFileInputFormat extends FileInputFormat<NullWritable, BytesWritable> {

    @Override

    protectedboolean isSplitable(JobContext context, Path file) {

        returnfalse;//不切分文件

    }

    @Override

    public RecordReader<NullWritable, BytesWritable> createRecordReader(InputSplit split,

            TaskAttemptContext context) throws IOException, InterruptedException {

        WholeFileRecordReader reader = new WholeFileRecordReader();

        reader.initialize(split, context);

        return reader;

    }

}

 

再定義一個RecordReader負責將FileSplit轉換成一條記錄

//不使用鍵,所以為NullWritable;值為BytesWritable類型,存儲文件內容

class WholeFileRecordReader extends RecordReader<NullWritable, BytesWritable> {

 

    private FileSplit fileSplit;

    private Configuration conf;

    private BytesWritable value = new BytesWritable();

    privatebooleanprocessed = false;//記錄是否已經被處理過

 

    @Override

    publicvoid initialize(InputSplit split, TaskAttemptContext context) throws IOException,

            InterruptedException {

        this.fileSplit = (FileSplit) split;

        this.conf = context.getConfiguration();

    }

 

    @Override

    publicboolean nextKeyValue() throws IOException, InterruptedException {

        if (!processed) {

            //緩沖用來存放文件內容,長度為分片大小(這里實為整個文件大小,因為isSplitablefalse

            byte[] contents = newbyte[(int) fileSplit.getLength()];

            Path file = fileSplit.getPath();

            FileSystem fs = file.getFileSystem(conf);

            FSDataInputStream in = null;

            try {

                in = fs.open(file);//打開文件

                //讀取文件

                IOUtils.readFully(in, contents, 0, contents.length);

                //將讀取出來的文件字節數組轉換為Writable對象

                value.set(contents, 0, contents.length);

            } finally {

                IOUtils.closeStream(in);

            }

            processed = true;

            returntrue;

        }

        returnfalse;

    }

    @Override

    public NullWritable getCurrentKey() throws IOException, InterruptedException {

        return NullWritable.get();

    }

    @Override

    public BytesWritable getCurrentValue() throws IOException, InterruptedException {

        returnvalue;

    }

    @Override

    publicfloat getProgress() throws IOException {

        returnprocessed ? 1.0f : 0.0f;

    }

    @Override

    publicvoid close() throws IOException {

        // do nothing 由於只有一條記錄,讀取完后nextKeyValue()方法自己就已將其關閉了,所以這里不需要再關閉

    }

}

 

下面來使用上面自建的FileInputFormat,將多個小文件合並成一個大的序列文件(鍵為文件名,值為文件內容):

publicclass SmallFilesToSequenceFileConverter {

    staticclass SequenceFileMapper extends Mapper<NullWritable, BytesWritable, Text, BytesWritable> {

        private Text filenameKey;

        @Override

        protectedvoid setup(Context context) throws IOException, InterruptedException {

            InputSplit split = context.getInputSplit();

            Path path = ((FileSplit) split).getPath();

            filenameKey = new Text(path.toString());//文件名做為序列文件的Key

        }

        @Override

        protectedvoid map(NullWritable key, BytesWritable value, Context context) throws IOException,

                InterruptedException {

            context.write(filenameKey, value);//文件名做為序列文件的Key

        }

    }

 

    publicstaticvoid main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        conf.set("mapred.job.tracker", "hadoop-master:9001");

        Job job = Job.getInstance(conf, "XXX");

        job.setJarByClass(T.class);

        //使用前面自定義的FileInputFormat

        job.setInputFormatClass(WholeFileInputFormat.class);//使用前面自定義的FileInputFormat

        job.setOutputFormatClass(SequenceFileOutputFormat.class);

        job.setOutputKeyClass(Text.class);

        job.setOutputValueClass(BytesWritable.class);

        job.setMapperClass(SequenceFileMapper.class);

 

        FileInputFormat.addInputPath(job, new Path("hdfs://hadoop-master:9000/smallfile"));

        FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop-master:9000/smallfile/output"));

        System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

a.txt~f.txt,每個文件中的內容為文件名相應的兩字母,除e.txt沒有內容,下面是合並成一個序列文件的結果:

至少有一種方法可以改進程序,一個mapper處理一個小文件的方法低效,所以較好的方法是繼承CombineFileInputFormat而不是FileInputFormat

16.6.2   文本輸入

16.6.2.1           TextInputFormat

在作業中未通過job.setInputFormatClass()方法指定InputFormat時,默認會使用TextInputFormat

 

TextInputFormat格式輸入文件里的內容:每一行文本就是一條記錄鍵值對,即每行文本都會調用一次map()方法處理一次。鍵是LongWritable類型,鍵代表每一行文本在文件中的起始字節偏移量,第一行為0;值的類型是Text,存儲的內容為這行的文本內容,不包括任何行終止符(換行和回車符)。所以,包含如下文本的文件被切分為包含4條記錄的一個分片:

每條記錄以鍵值對形式出現:

很明顯,鍵並不是行號,因為文件按字節而不是按行切分為分片的,所以很難取得行號,每個分片又是單獨不分先后順序處理的,行號又是一個連續的標記,在分片內知道行號是可能的,但文件分成多個分片后就不可能了,所以Key是行文本的偏移量而不是行號的原因

 

文本行是完全有可能跨塊Block與跨分片Split的,但FileInputFormat實現已解決了行的讀取問題

16.6.2.2           KeyValueTextInputFormat

我們知道TextInputFormat的鍵是每一行在文件中的字節偏移量,通常沒有什么用。如果輸入文件中的每行都是由 / 構造,並且健與值之間使用某個分界符進行分隔,比如Tab,則可以使用KeyValueTextInputFormat進行輸入,以下為例,其中 --> 表示制表符:

則使用KeyValueTextInputFormat格式化輸入后,后形成的輸入分片數據如下:

此時的鍵為每一行Tab制表符前在的字符串,而非行字節偏移量

 

可以通過mapreduce.input.keyvaluelinerecordreader.key.value.separator 屬性 (API中為 key.value.separator.in.input.line)來指定分隔符,它默認為一個制表符

16.6.2.3           NLineInputFormat

NLineInputFormatTextInputFormat是相似:健也是行字節偏移量,只是在分片時,根據N值來划分,即每個mapper會收到固定行數N的輸入。N的默認值為1

可以通過mapreduce.input.lineinputformat.linespermap屬性(舊API中為mapred.line.input.format.linespermap)對N進行設置

 

如果有以下四行的文本:

N2時,則每個輸入分片包含兩行,其中一個mapper收到的輸入分片為:

另一個mapper眉目到的輸入分片則為:

鍵和值與TextInputFormat生成的一樣,不同的是輸入分片的構造方式

16.6.2.4           XML

如果要處理XML類型的文件,則可以把輸入格式設置為StreamInputFormat,把stream.recordreader.class屬性設置為org.apache.hadoop.streaming.StreamXmlRecordReader

import java.io.IOException;

import java.util.Iterator;

 

import org.apache.hadoop.fs.Path;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.mapred.FileOutputFormat;

import org.apache.hadoop.mapred.JobClient;

import org.apache.hadoop.mapred.JobConf;

import org.apache.hadoop.mapred.MapReduceBase;

import org.apache.hadoop.mapred.Mapper;

import org.apache.hadoop.mapred.OutputCollector;

import org.apache.hadoop.mapred.Reducer;

import org.apache.hadoop.mapred.Reporter;

import org.apache.hadoop.mapred.TextOutputFormat;

import org.apache.hadoop.mapred.lib.MultipleInputs;

import org.apache.hadoop.streaming.StreamInputFormat;

import org.apache.hadoop.streaming.StreamXmlRecordReader;

 

publicclass XmlMR {

    publicstaticclass MyMap extends MapReduceBase implements Mapper<Text, Text, Text, Text> {

        @Override

        publicvoid map(Text key, Text value, OutputCollector<Text, Text> ctx, Reporter r) throws IOException {

            System.out.println("map :::::: " + key.toString() + " value = " + value); //注:value一直是空的

            ctx.collect(key, key);

        }

    }

 

    publicstaticclass MyReduce extends MapReduceBase implements Reducer<Text, Text, Text, Text> {

        @Override

        publicvoid reduce(Text key, Iterator<Text> value, OutputCollector<Text, Text> ctx, Reporter r)

                throws IOException {

            StringBuffer sb = new StringBuffer();

            while (value.hasNext()) {

                Text v = value.next();

                System.out.println("reduce :::::: " + v.toString() + " key = " + key);

                sb.append(v.toString());

            }

            ctx.collect(new Text(key.getLength() + ""), new Text(sb.toString()));

        }

    }

    //自定義StreamInputFormat,不讓切分文件,因為經測試分片后,輸入有重復,是因為分片造成的

    publicstaticclass MyStreamInputFormat extends StreamInputFormat {

        @Override

        protectedboolean isSplitable(FileSystem fs, Path file) {

            returnfalse;

        }

    }

    publicstaticvoid main(String[] args) throws Exception {

 

        String begin = "<property>";

        String end = "</property>";

 

        JobConf conf = new JobConf(XmlMR.class);

        conf.setJobName("xmlMR");

 

        conf.setOutputKeyClass(Text.class);

        conf.setOutputValueClass(Text.class);

 

        conf.setMapperClass(MyMap.class);

        conf.setReducerClass(MyReduce.class);

 

        conf.setInputFormat(MyStreamInputFormat.class);

        conf.setOutputFormat(TextOutputFormat.class);

 

        conf.setJarByClass(XmlMR.class);

 

        MyStreamInputFormat.addInputPath(conf, new Path("hdfs://hadoop-master:9000/xml/core-site.xml"));  FileOutputFormat.setOutputPath(conf, new Path("hdfs://hadoop-master:9000/xml/output"));

 

        conf.set("stream.recordreader.class", StreamXmlRecordReader.class.getName());

// hadoop會把包含起始結束標簽之間的內容(包括起始標簽)作為一個record傳給map函數

        conf.set("stream.recordreader.begin", begin);

        conf.set("stream.recordreader.end", end);

        conf.set("mapred.job.tracker", "hadoop-master:9001");

        JobClient.runJob(conf);

    }

}

目前好像只支持舊API

 

由於要使用hadoop-streaming-1.2.1.jar包,所以需要將它加入到類路中,修改hadoop-env.sh

 

解析出的XML如下:

16.6.3   二進制輸入

16.6.3.1           SequenceFileInputFormat

MapReduce除了可以處理文本文件外,還可以處理二進制的SequenceFile數據文件,sequence file格式的文件里存儲的就是         二進制的鍵值對記錄,sequence file相關詳細信息請參考前面章節的SequenceFile。由於SequenceFile文件里存儲了一定量的同步特殊標記點,該標記能快速定義到下一個記錄的邊界,所以這類文件也是可分片的(splittable),這類文件很適合MapReduce來處理,並且它還支持壓縮,它還可以使用序列化技術存儲任意類型的數據

 

如果要用順序文件SequenceFile作為MapReduce的輸入,應該使用SequenceFileInputFormat,鍵和值的類型由順序文件決定

 

注:SequenceFileInputFormat除了可以讀取SequenceFile外,還可以讀取MapFiles,因為在處理順序文件時如果遇到目錄,SequenceFileInputFormat類會認為自己正在讀取MapFile,此時則會讀取其數據文件,所以這也就是為什么沒有MapFileInputFormat類的原因

16.6.3.2           SequenceFileAsTextInputFormat

SequenceFileAsTextInputFormatSequenceFileInputFormat的子類,它將順序文件的鍵和值都轉換為Text對象,即在鍵和值上調toString()方法來實現的

16.6.3.3           SequenceFileAsBinaryInputFormat

SequenceFileAsBinaryInputFormat也是SequenceFileInputFormat的子類,它將獲取順序文件的鍵和值作為二進制對象,它們被封裝為BytesWritable對象。這些順序文件可以由SequenceFile.WriterappendRaw()或者是SequenceFileAsBinaryOutputFormat來生成

16.6.4   多個輸入

MultipleInputs可以為每個輸入路徑指定個InputFormatMapper,如果輸入是多個路徑且數據格式不相同時,則不能使用同一個Mapper來解析輸入,這時可以使用MultipleInputs來設定每個各自路徑所對應的Mapper

  /**

   * Add a {@link Path} with a custom {@link InputFormat} and

   * {@link Mapper} to the list of inputs for the map-reduce job.

   *

   * @param job The {@link Job}

   * @param path {@link Path} to be added to the list of inputs for the job

   * @param inputFormatClass {@link InputFormat} class to use for this path

   * @param mapperClass {@link Mapper} class to use for this path

   */

  @SuppressWarnings("unchecked")

  publicstaticvoid addInputPath(Job job, Path path,

      Class<? extendsInputFormat> inputFormatClass,

      Class<? extendsMapper> mapperClass) {

比英國Met OfficeNCDC的氣象數據放在一起來分析最高氣溫,則可以通過以下方式來設置輸入路徑:

MultipleInputs.addInputPath(job, ncdcInputPath,TextInputFormat.class, MaxTemperatureMapper.class);

MultipleInputs.addInputPath(job, metOfficeInputPath,TextInputFormat.class, MetOfficeMaxTemperatureMapper.class);

這段代碼取代了對以前對FileInputFormat.addInputPath()job.setMapperClass()調用,Met OfficeNCDC都是文本數據,所以兩者都使用TextInputFormat數據類型,但這兩個數據源的行格式不同,所以我們使用了兩個不一樣的mapperMaxTemperatureMapper讀取NCDC的輸入數據並抽取年份和氣溫了字段的值;MetOfficeMaxTemperatureMapper讀取Met Office的輸入數據,抽取年份和氣溫字段的值。

 

MultipleInputs類有一個重載版的addInputPath(),它沒有mapper參數:

  publicstaticvoid addInputPath(Job job, Path path, Class<? extends InputFormat> inputFormatClass)

如果有多種輸入格式而只有一個mapper,則通過JobsetMapperClass()方法來統一設定mapper

16.6.5   數據庫輸入

DBInputFormat輸入格式用於使用JDBC關系數據庫中讀取數據。

注意:DBInputFormat內部實現機制沒使用連接池,所以使用時要小心,不建議將Mapper任務數(mapred.map.tasks,如果不設置,默認Mapper任務數會是1)設置過多,因為每一個Mapper產生一個物理數據庫連接;但如果查詢的數據條數很多,可以設置適當設置Mapper任務個數,這樣多個Mapper並行去取部分數據最后整合起來,這樣查詢速度快。如果不設置mapred.map.tasks,默認就是1,數據量大時也要注意!

 

在關系數據庫和HDFS之間抽取數據的另一個方法是:使用Sqoop

 

另外,MapReduce可以通過HBaseTableInputFormat讀取HBase非關系性數據庫)表中的數據,MapReduce通過TableOutputFormat把數據輸出到HBase表中

 

為了方便MapReduce直接訪問關系型數據庫(MySQL,Oracle),Hadoop提供了DBInputFormatDBOutputFormat兩個類,通過DBInputFormat類把數據庫表數據讀入到HDFS,根據DBOutputFormat類把MapReduce產生的結果集導入到數據庫表中。

 

16.6.5.1           MySql安裝

1、在 /etc/yum.repos.d/ 下建立 MariaDB.repo,內容如下:

$ cd /etc/yum.repos.d

$ vi MariaDB.repo

輸入以下內容保存:

# MariaDB 5.5 CentOS repository list - created 2016-05-19 11:18 UTC

# http://mariadb.org/mariadb/repositories/

[mariadb]

name = MariaDB

baseurl = http://yum.mariadb.org/5.5/centos7-amd64

gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB

gpgcheck=1

其它各版本及各操作系統下載源配置見https://downloads.mariadb.org/mariadb/repositories/#mirror=neusoft

 

查看是否已安裝過MySQL

rpm -qa|grep MySQL                       -----以前版本MySQL

rpm -qa|grep mysql                        

rpm -qa|grep MariaDB                   ----新版本MySQL

 

rpm -e --nodeps MariaDB-common-5.5.49-1.el7.centos.x86_64                  ---上面查出什么包就卸載什么包

 

2、使用YUM安裝MariaDB

[root@hadoop-slave2 /etc/yum.repos.d]# yum install MariaDB-server MariaDB-client

 

3、啟動數據庫

$ service mysql start

 

4、修改Root的密碼

$ mysqladmin -u root password 'AAAaaa111'

 

5、配置遠程訪問,MariaDB為了安全起見,默認情況下綁定ip 127.0.0.1),限制其他IP遠程訪問。

$ mysql -u root -p

Enter password:

Welcome to the MariaDB monitor.  Commands end with ; or \g.

Your MariaDB connection id is 4

Server version: 10.0.4-MariaDB MariaDB Server

Copyright (c) 2000, 2013, Oracle, Monty Program Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

 

MariaDB [(none)]>GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY 'AAAaaa111' WITH GRANT OPTION;

MariaDB [(none)]>flush privileges;

 

並可以使用 mysql -h localhost -u root -p 驗證是否能遠程登錄到本地

 

grant權限1,權限2,…權n ON 數據庫名稱.表名稱 TO用戶名@用戶地址 IDENTIFIED BY '連接口令';

權限1,權限2,…權限n代表select,insert,update,delete,create,drop,index,alter,grant,references,reload,shutdown,process,file14個權限。

當權限1,權限2,…權限nALL PRIVILEGES或者ALL代替,表示賦予用戶全部權限。

當數據庫名稱.表名稱被 *.*代替,表示賦予用戶操作服務器上所有數據庫所有表的權限。

用戶地址可以是localhost,也可以是ip地址、機器名字、域名。也可以用 '%'表示從任何地址連接。"%"表示任何主機都可以遠程登錄到該服務器上訪問

'連接口令'不能為空,否則創建失敗。

 

flush privileges:第二句表示從mysql數據庫的grant表中重新加載權限數據。因為MySQL把權限都放在了cache中,所以在做完更改后需要重新加載。

 

5、  禁止防火牆

 

7、大小寫敏感配置

root帳號登錄后,在/etc/my.cnf 中的[mysqld]后添加添加lower_case_table_names=1,重啟MYSQL服務,這時已設置成功:不區分表名的大小寫;

[mysqld]

lower_case_table_names = 1

其中 0:區分大小寫,1:不區分大小寫

 

8、  重啟服務

# service mysql stop

$ service mysql start

 

9、  查看數據庫字符集

MariaDB [(none)]> show variables like 'character%';

+--------------------------+----------------------------+

| Variable_name            | Value                      |

+--------------------------+----------------------------+

| character_set_client     | utf8                       |

| character_set_connection | utf8                       |

| character_set_database   | latin1                     |

| character_set_filesystem | binary                     |

| character_set_results    | utf8                       |

| character_set_server     | latin1                     |

| character_set_system     | utf8                       |

| character_sets_dir       | /usr/share/mysql/charsets/ |

+--------------------------+----------------------------+

 

10、              修改字符集

注:請在創建數據庫之前修改,否則以前創建的庫不會隨之修改

#vi /etc/my.cnf

[client]

default-character-set=utf8

[mysqld]

character-set-server=utf8

[mysql]

default-character-set=utf8

 

MariaDB [hadoopdb]> show variables like 'character%';

+--------------------------+----------------------------+

| Variable_name            | Value                      |

+--------------------------+----------------------------+

| character_set_client     | utf8                       |

| character_set_connection | utf8                       |

| character_set_database   | utf8                       |

| character_set_filesystem | binary                     |

| character_set_results    | utf8                       |

| character_set_server     | utf8                       |

| character_set_system     | utf8                       |

| character_sets_dir       | /usr/share/mysql/charsets/ |

+--------------------------+----------------------------+

 

11、              創建庫

$ mysql -u root -p

輸入登錄密碼

MariaDB [(none)]> create database hadoopdb;

 

12、              顯示數據庫

MariaDB [(none)]> show databases;

+--------------------+

| Database   |

+--------------------+

| information_schema  |

| hadoopdb          |

| mysql              |

| performance_schema |

| test                |

+----------------------------------+

 

13、              連接數據庫

MariaDB [(none)]> use hadoopdb;

Database changed

 

14、              創建數據表

MariaDB [hadoopdb]> CREATE TABLE t_stud (

    -> id INTEGER NOT NULL PRIMARY KEY,

    -> name VARCHAR(32) NOT NULL);

 

15、              查看庫中有哪些表

MariaDB [hadoopdb]> show tables;

+--------------------+

| Tables_in_hadoopdb |

+--------------------+

| t_stud             |

+--------------------+

 

16、              退出:

MariaDB [hadoopdb]> exit

Bye

 

下載驅動並上傳到hadoop-master主機上:/root/hadoop-1.2.1/lib/mysql-connector-java-5.1.22-bin.jar

配置hadoop-env.sh

注:多路徑Linux使用冒號分隔,而不是分號

再將配置及驅動復制到其他主機上

 

連接異常

com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Communication link failure,  message from server: "Can't get hostname for your address"

修改配置文件:vi /etc/my.cnf

[mysqld]

skip-name-resolve

16.6.5.2           從數據庫中讀寫數據

如果要將文本文件中的數據數據導入到數據庫中,則這個文本文件格式要是不帶BOM頭信息的UTF-8格式,否則三個字節的BOM頭也會被讀入,Hadoop好像沒有考慮BOM

下面將Hadoop中的數據插入到MySql數據庫中,並從MySql中讀取數據到Hadoop中:

 

import org.apache.hadoop.io.Text;

import org.apache.hadoop.io.Writable;

import org.apache.hadoop.mapreduce.lib.db.DBWritable;

/*

 * 鍵值對應記錄,出入庫都通過它

 */

publicclass StudentinfoRecord implements Writable, DBWritable {

    intid;

    String name;

 

    // 反序列化:從文件讀取時調用

    publicvoid readFields(DataInput in) throws IOException {

        this.id = in.readInt();

        this.name = Text.readString(in);

        // 或直接讀取

        // this.name = in.readUTF();

    }

 

    // 序列化:寫入文件時調用

    publicvoid write(DataOutput out) throws IOException {

        out.writeInt(this.id);

        // Text.writeString(out, this.name);

        // 或直接寫

        out.writeUTF(this.name);

    }

 

    // 反序列化:從數據庫中讀取時調用

    publicvoid readFields(ResultSet result) throws SQLException {

        this.id = result.getInt(1);

        this.name = result.getString(2);

    }

 

    // 序列化:寫入數據庫時調用

    publicvoid write(PreparedStatement stmt) throws SQLException {

        stmt.setInt(1, this.id);

        stmt.setString(2, this.name);

    }

 

    public String toString() {

        returnthis.id + " " + this.name;

    }

}

 

 

 

import org.apache.hadoop.conf.Configuration;

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.Job;

import org.apache.hadoop.mapreduce.Reducer;

import org.apache.hadoop.mapreduce.lib.db.DBConfiguration;

import org.apache.hadoop.mapreduce.lib.db.DBOutputFormat;

import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

/*

 * 將數據寫入數據庫

 */

publicclass DBOutput {

    /*

     * 如果輸入文件是GBK,可以使用此方法將GBK轉換為UTF-8的字符串

     * 即使從文本行中某個字符中間被split也沒關系,因為TextInputFormat是以行為單位進行讀取的,

     * 所以這里也是以行為單位進行編解碼,所以沒有問題

     */

    publicstatic Text transformTextToUTF8(Text text, String encoding) {

        String value = null;

        try {

            value = new String(text.getBytes(), 0, text.getLength(), encoding);

        } catch (UnsupportedEncodingException e) {

            e.printStackTrace();

        }

        returnnew Text(value);

    }

 

    /*

     * 注:需要定義成靜態的內部類,否則框架在利用反射實例化時會拋異常: java.lang.RuntimeException:

     * java.lang.NoSuchMethodException: DBOutput$MyReducer.<init>()

     */

    publicstaticclass MyReducer extends Reducer<LongWritable, Text, StudentinfoRecord, NullWritable> {

 

        protectedvoid reduce(LongWritable key, Iterable<Text> values, Context context) throws IOException,

                InterruptedException {

            // 如果是GBK,則可以重新編碼成UTF8

            // Text t = transformTextToUTF8(values.iterator().next(), "GBK");

            // String[] splits = t.toString().split("\t");

            String[] splits = values.iterator().next().toString().split("\t");

            StudentinfoRecord studt = new StudentinfoRecord();

            studt.id = Integer.parseInt(splits[0]);

            studt.name = splits[1];

            // 好像只會將Key寫入數據庫,即Value會忽略

            context.write(studt, NullWritable.get());

            // context.write(studt, new Text(studt.name));

        };

    }

 

    publicstaticvoid main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {

        Configuration conf = new Configuration();

        conf.set("mapred.job.tracker", "hadoop-master:9001");

 

        DBConfiguration.configureDB(conf, "com.mysql.jdbc.Driver",

                "jdbc:mysql://hadoop-slave1:3306/hadoopdb", "root", "AAAaaa111");

 

        Job job = Job.getInstance(conf, "DBOutput");

        job.setJarByClass(DBOutput.class);

        job.setOutputFormatClass(DBOutputFormat.class);

        DBOutputFormat.setOutput(job, "t_stud", "id", "name");

 

        job.setReducerClass(MyReducer.class);

 

        FileInputFormat.setInputPaths(job, new Path("hdfs://hadoop-master:9000/db/input"));

        System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

 

 

 

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.fs.Path;

import org.apache.hadoop.io.LongWritable;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.mapreduce.Job;

import org.apache.hadoop.mapreduce.Mapper;

import org.apache.hadoop.mapreduce.lib.db.DBConfiguration;

import org.apache.hadoop.mapreduce.lib.db.DBInputFormat;

import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import org.apache.hadoop.mapreduce.lib.output.TextOutputFormat;

/*

 * 從數據庫中讀取數據

 */

publicclass DBInput {

    /*

     * 注:需要定義成靜態的內部類,否則框架在利用反射實例化時會拋異常: java.lang.RuntimeException:

     * java.lang.NoSuchMethodException: DBInput$DBInputMapper.<init>()

     */

    publicstaticclass DBInputMapper extends Mapper<LongWritable, StudentinfoRecord, LongWritable, Text> {

        protectedvoid map(LongWritable key, StudentinfoRecord value, Context context) throws IOException,

                InterruptedException {

            //這里的key為數據庫記錄索引,從0開始

            context.write(key, new Text(value.toString()));

        };

    }

 

    publicstaticvoid main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {

 

        Configuration conf = new Configuration();

        conf.set("mapred.job.tracker", "hadoop-master:9001");

 

        DBConfiguration.configureDB(conf, "com.mysql.jdbc.Driver",

                "jdbc:mysql://hadoop-slave1:3306/hadoopdb", "root", "AAAaaa111");

 

        Job job = Job.getInstance(conf, "DBInput");

        job.setJarByClass(DBInput.class);

 

        // 默認是TextInputFormat,所以需要修改

        job.setInputFormatClass(DBInputFormat.class);

 

        String tableName = "t_stud";

        String conditions = "";

        String orderBy = "id";

        String[] fieldNames = { "id", "name" };

        /**

         * @param job The map-reduce job

         * @param inputClass the class object implementing DBWritable, which is the Java object holding tuple fields.

         * @param tableName The table to read data from

         * @param conditions The condition which to select data with, eg. '(updated > 20070101 AND length > 0)'

         * @param orderBy the fieldNames in the orderBy clause.

         * @param fieldNames The field names in the table

         */

        DBInputFormat.setInput(job, StudentinfoRecord.class, tableName, conditions, orderBy, fieldNames);

 

        // String inputQuery = "select id, name from t_stud";

        // String inputCountQuery = "select count(*) from t_stud";

        // DBInputFormat.setInput(job, StudentinfoRecord.class, inputQuery, inputCountQuery);

 

        job.setOutputFormatClass(TextOutputFormat.class);

        FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop-master:9000/db/output"));

 

        job.setMapperClass(DBInputMapper.class);

 

        job.setMapOutputKeyClass(LongWritable.class);

        job.setMapOutputValueClass(Text.class);

        job.setOutputKeyClass(LongWritable.class);

        job.setOutputValueClass(Text.class);

 

        System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

 

16.7        輸出格式

16.7.1   文本輸出

Job沒有指定輸出格式時,默認就是TextOutputFormat,它把每條記錄寫為文本行,它的鍵和值可以是任意類型,因為TextOutputFormat調用toString()方法把它們轉換為字符串。每個鍵值對默認使用Tab進行分隔,當然也可以通過設置mapreduce.output.textoutputformat.separator屬性(舊APImapred.textoutputformat.separator)來改變默認的分隔符

 

TextOutputFormat對應的輸入格式是KeyValueTextInputFormat,它也是通過可配置的分隔符將鍵值對文本行分隔

 

如果不想輸出鍵或值,則可以將鍵或值的類型設定為NullWritable(如果鍵和值都不需要輸出,則等效於NullOutputFormat),當只有鍵或者值某一個輸出時,就不會有鍵與值分隔符輸出,則輸出的這樣類型的文件適合用TextInputFormat讀取

16.7.2   二進制輸出

16.7.2.1           SequenceFileOutputFormat

如果要以SequenceFile順序文件來存儲reduce的輸出,則使用SequenceFileOutputFormatSequenceFile順序文件支持壓縮,存儲了鍵值對類型,其結構更適合於MapReduce的輸入,具體請參考前面章節SequenceFile順序文件的特性

16.7.2.2                                                                                                                                                                                               SequenceFileAsBinaryOutputFormat  

SequenceFileAsBinaryOutputFormatSequenceFileAsBinaryInputFormat相對應,它把鍵值對作為二進制格式寫到SequenceFile順序文件

16.7.2.3                                                                                                                                                                                               MapFileOutputFormat

MapFileOutputFormatMapFile作為輸出。MapFile中的鍵必須是順序添加,所以必須確保reducer輸出的鍵是已經排好序

 

reduce輸入的鍵一定是有序的,但輸出的鍵由reduce函數控制,MapReduce框架中沒有硬性規定reduce輸出鍵必須是有序的。所以要使用MapFileOutputFormat,就需要額外的限制來保證reduce輸出的鍵是有序的

 

16.7.3   多個輸出

一般情況下,每個reducer只會輸出一個文件,名稱格式如下:part-r-00000, part-r-00001等,是以分區號(0000000001)來區分(part是可以通過MultipleOutputs修改的,即多文件輸出時才可修改)。

如果一個reducer需要輸出多個文件,則需要MultipleOutputs

 

一個reducer對應一個分區,使用MultipleOutputs要以將屬於同一分區的數據可以輸出到不同的文件中

16.7.3.1           示例:分區數據

每個氣象站單獨輸出一個文件

 

第一種方法:每個氣象站對應一個reducer。這需要兩步:第一步,寫一個partitioner,把同一個氣象站的數據放到同一個分區;第二步驟,把作業的reducer數據設為氣象站的個數。partitioner如下:

publicclass StationPartitioner extends Partitioner<LongWritable, Text> {

    private NcdcRecordParser parser = new NcdcRecordParser();

    @Override

    publicint getPartition(LongWritable key, Text value, int numPartitions) {

        String stationId = value.toString().substring(4, 10) + "-" + value.toString().substring(10, 15);      

return getPartition(stationId);

    }

    privateint getPartition(String stationId) {//將氣象ID轉換為相應的分區號

        ...

    }

}

上面這種方式有兩個缺點:第一,需要在作業運行前知道氣象站的個數;第二,如果每個氣象站對應一個reducer任務,如果任務處理數據量差別很大,少量的數據也要開啟一個reducer,整體完成時間不能平衡,而且reducer任務開啟也會需要開銷的。

 

第二方法:由於第一種方法的明顯缺陷,最好的作法是讓集群決定分區數據,這樣每個分區就可能包括多個氣象站的數據,由於默認情況下同一分區的數據只會輸出一個文件,因此,如果要實現同一分區的每個氣象站一個輸出文件的話,這就需要MultipleOutputs

16.7.3.1.1     NcdcRecordParser氣溫數據解析器

publicclass NcdcRecordParser {

    privatestaticfinalintMISSING_TEMPERATURE = 9999;

    privatestaticfinal DateFormat DATE_FORMAT = new SimpleDateFormat( "yyyyMMddHHmm");

    private String stationId;//氣象站ID

    private String observationDateString;//觀測日期字符串

    private String year;//

    private String airTemperatureString;//溫度字符串

    privateintairTemperature;//溫度

    privatebooleanairTemperatureMalformed;//

    private String quality;//空氣質量

 

    // 傳入的為一行氣象數據文本

    publicvoid parse(String record) {

       stationId = record.substring(4, 10) + "-" + record.substring(10, 15);

       observationDateString = record.substring(15, 27);

       year = record.substring(15, 19);

       airTemperatureMalformed = false;

       // Remove leading plus sign as parseInt doesn't like them

       if (record.charAt(87) == '+') {

           airTemperatureString = record.substring(88, 92);

           airTemperature = Integer.parseInt(airTemperatureString);

       } elseif (record.charAt(87) == '-') {

           airTemperatureString = record.substring(87, 92);

           airTemperature = Integer.parseInt(airTemperatureString);

       } else {//溫度字符串前不帶正負號

           airTemperatureMalformed = true;

       }

       airTemperature = Integer.parseInt(airTemperatureString);

       quality = record.substring(92, 93);

    }

 

    publicvoid parse(Text record) {

       parse(record.toString());

    }

 

    publicboolean isValidTemperature() {

       return !airTemperatureMalformed

              && airTemperature != MISSING_TEMPERATURE

              && quality.matches("[01459]");

    }

 

    publicboolean isMalformedTemperature() {

       returnairTemperatureMalformed;

    }

 

    publicboolean isMissingTemperature() {

       returnairTemperature == MISSING_TEMPERATURE;

    }

 

    public String getStationId() {

       returnstationId;

    }

 

    public Date getObservationDate() {

       try {

           returnDATE_FORMAT.parse(observationDateString);

       } catch (ParseException e) {

           thrownew IllegalArgumentException(e);

       }

    }

 

    public String getYear() {

       return year;

    }

 

    publicint getYearInt() {

       return Integer.parseInt(year);

    }

 

    publicint getAirTemperature() {

       returnairTemperature;

    }

 

    public String getAirTemperatureString() {

       returnairTemperatureString;

    }

 

    public String getQuality() {

       returnquality;

    }

}

16.7.3.2           MultipleOutputs

MultipleOutputs類可以將同一分區的數據寫到多個文件(即每個reducer可以產生多個輸出文件),並且還可以設置輸出文件的名稱

 

使用MultipleOutputs將同一分區的數據輸出到不同的文件(同一氣象站數據輸出到時同一文件中):

import org.apache.hadoop.conf.Configuration;

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.Job;

import org.apache.hadoop.mapreduce.Mapper;

import org.apache.hadoop.mapreduce.Reducer;

import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import org.apache.hadoop.mapreduce.lib.output.MultipleOutputs;

 

publicclass PartitionByStationUsingMultipleOutputs {

 

    staticclass StationMapper extends Mapper<LongWritable, Text, Text, Text> {

        @Override

        protectedvoid map(LongWritable key, Text value, Context context) throws IOException,

                InterruptedException {

            String stationId = value.toString().substring(4, 10) + "-" + value.toString().substring(10, 15);

            // Key為氣象站ID

            context.write(new Text(stationId), value);

        }

    }

 

    staticclass MultipleOutputsReducer extends Reducer<Text, Text, NullWritable, Text> {

        private MultipleOutputs<NullWritable, Text> multipleOutputs;

        @Override

        protectedvoid setup(Context context) throws IOException, InterruptedException {

            // 初始化,不輸出Key,只輸出值

            multipleOutputs = new MultipleOutputs<NullWritable, Text>(context);

        }

 

        @Override

        protectedvoid reduce(Text key, Iterable<Text> values, Context context) throws IOException,

                InterruptedException {

            for (Text value : values) {

                /*

                 * Key不輸出,所以KeyNullWritable的實例:這里NullWritable.get()

                 * 獲取就是NullWritable的單例

                 *

                 * 第三個參數是輸出文件名,文件名相同的自然就會輸出到同一文件,這里為 key.toString()

                 * Key為氣象站ID。這個文件名可以包含路徑,但不要以 / 開頭,文件名(以及帶路徑的文件名)都是相對於

                 * 作業設置的輸出路徑: FileOutputFormat.setOutputPath(),即這里都會輸出到

                 * hdfs://hadoop-master:9000/ncdc/output1(2)這個目錄下

                 *

                 * 在這里是通過MultipleOutputs實例來輸出的,而不再是是Context.write()

                 *

          * 輸出的文件名形式為:name-r-nnnnnname就是這里的 key.toString(),即氣象站ID,輸出文件名形式為

          * station_identifier-r-nnnnn

                 * 1/  2/  為子路徑,相對於hdfs://hadoop-master:9000/ncdc/output1(2)這個目錄

                 */

//              multipleOutputs.write(NullWritable.get(), value, "1/" + key.toString());

                multipleOutputs.write(NullWritable.get(), value, "2/" + key.toString());

            }

        }

 

        @Override

        protectedvoid cleanup(Context context) throws IOException, InterruptedException {

            multipleOutputs.close();

        }

    }

 

    publicstaticvoid main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        conf.set("mapred.job.tracker", "hadoop-master:9001");

 

        Job job = new Job(conf);

        job.setJarByClass(PartitionByStationUsingMultipleOutputs.class);

        FileInputFormat.addInputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/all/1901.all"));

//      FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/output1"));

        FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/output2"));

 

        job.setMapperClass(StationMapper.class);

        job.setOutputKeyClass(NullWritable.class);

        // 由於Mapper的輸出與Reducer的輸出類型不同,所以這里單設置mapper的輸出類型

        job.setMapOutputKeyClass(Text.class);

        // 由於前面使用了job.setMapOutputKeyClass()設置了mapper輸出類型,所以這里只是設置reducer的輸出類型

        job.setReducerClass(MultipleOutputsReducer.class);

 

//      job.setNumReduceTasks(1);// reducer數量設置為1

        job.setNumReduceTasks(2);

 

        System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

下面分下是將reduce數量設為12時的輸出文件情況,當reducer數據為2時,有5個氣象站的數據都分到的1區,只有1個氣象站分到了0區,在通常情況下(未使用MultipleOutputs情況下),那5個氣象站會輸出到一個文件中,但這里因為使用了MultipleOutputs,讓每個氣象站輸出到單獨一個文件中(文件名為氣象站ID):

圖中有3空文件(注:只有在使用MultipleOutputs時出現),是因為名為 part­-r-xxxxx 的文件沒有數據(如果在沒有使用MultipleOutputs情況下,數據才會輸出到這3個文件)

16.7.4   禁止空文件輸出

FileOutputFormat子類會輸出文件名格式為part-r-nnnnn的文件,即使是空的(如上面的多文件輸出示例中就會產生)。如果不想要這些空文件,則可以使用LazyOutputFormat,它是對所有的OutputFormat(如TextOutputFormatSequenceFileOutputFormat等)的一個wrapper,使用了LazyOutputFormatjob,在輸出output的時候,會在第一條記錄輸出時才創建文件,如果沒有輸出就不會被創建;它不像現在所有outputFormat的機制一樣,task以開始運行不管有沒有輸出先把輸出文件給創建了再說。這種延遲創建的方式有利於避免job創建過多的空輸出文件而占據過多的namendoe內存。

LazyOutputFormat.setOutputFormatClass(job,TextOutputFormat.class);

注:由於reducer默認輸出是TextOutputFormat,所以這里為TextOutputFormat,但不能是FileOutputFormat,因為是抽象類,不能被實例化。另外,如果有job.setOutputFormatClass(TextOutputFormat.class);,則要注掉

 

將上面的代碼加到上面示例中,就不會輸出空文件了

16.7.5   數據庫輸出

請參考前面章節數據庫輸入

 

16.8        Counters計數器

計數器即作業執行后的一些統計信息,根據這些信息可反應出作業的執行情況

16.8.1   任務計數器

任務計數器:該類計數器在任務處理過程中不斷更新,同一作業里的所有任務上統計

作業計數器:該類計數器在作業處理過程中不斷更新,所有不同作業上統計

 

任務計數器由其相應的任務自己維護,並定期的發送給tasktracker,再由tasktracker發送給jobtracker,最后在jobtracker上合計

 

任務計數器的值每次都是完整傳輸,即不只是傳輸增加值,而是完成可以覆蓋以前的傳輸過來的值,這樣避免了某一次由於傳輸計數器值失敗而引起計數器值最終不准的問題。

另外,如果一個任務在作業執行期間失敗,則相關的計數器的值也會隨之減小

 

每個任務運行完后,可以通過http://hadoop-master:50030來查看這些計數器統計值,若任務是直接前台運行,則在任務包括完后,即可看到各計數器統計值,粗體為計數器的組名,其下方列出的就是各個計數器統計值:

16/05/23 09:55:59 INFO mapred.JobClient: Running job: job_201605201002_0044

16/05/23 09:56:00 INFO mapred.JobClient:  map 0% reduce 0%

16/05/23 09:56:07 INFO mapred.JobClient:  map 40% reduce 0%

16/05/23 09:56:09 INFO mapred.JobClient:  map 60% reduce 0%

16/05/23 09:56:10 INFO mapred.JobClient:  map 100% reduce 0%

16/05/23 09:56:15 INFO mapred.JobClient:  map 100% reduce 33%

16/05/23 09:56:17 INFO mapred.JobClient:  map 100% reduce 100%

16/05/23 09:56:17 INFO mapred.JobClient: Job complete: job_201605201002_0044

16/05/23 09:56:17 INFO mapred.JobClient: Counters: 29

16/05/23 09:56:17 INFO mapred.JobClient:   Job Counters

16/05/23 09:56:17 INFO mapred.JobClient:     SLOTS_MILLIS_MAPS=27746

16/05/23 09:56:17 INFO mapred.JobClient:     Launched reduce tasks=1

16/05/23 09:56:17 INFO mapred.JobClient:     Total time spent by all reduces waiting after reserving slots (ms)=0

16/05/23 09:56:17 INFO mapred.JobClient:     Total time spent by all maps waiting after reserving slots (ms)=0

16/05/23 09:56:17 INFO mapred.JobClient:     Launched map tasks=5

16/05/23 09:56:17 INFO mapred.JobClient:     Data-local map tasks=5

16/05/23 09:56:17 INFO mapred.JobClient:     SLOTS_MILLIS_REDUCES=10157

16/05/23 09:56:17 INFO mapred.JobClient:   File Output Format Counters

16/05/23 09:56:17 INFO mapred.JobClient:     Bytes Written=45

16/05/23 09:56:17 INFO mapred.JobClient:   FileSystemCounters

16/05/23 09:56:17 INFO mapred.JobClient:     FILE_BYTES_READ=360619

16/05/23 09:56:17 INFO mapred.JobClient:     HDFS_BYTES_READ=4439538

16/05/23 09:56:17 INFO mapred.JobClient:     FILE_BYTES_WRITTEN=1078053

16/05/23 09:56:17 INFO mapred.JobClient:     HDFS_BYTES_WRITTEN=45

16/05/23 09:56:17 INFO mapred.JobClient:   File Input Format Counters

16/05/23 09:56:17 INFO mapred.JobClient:     Bytes Read=4438998

16/05/23 09:56:17 INFO mapred.JobClient:   Map-Reduce Framework

16/05/23 09:56:17 INFO mapred.JobClient:     Map output materialized bytes=360643

16/05/23 09:56:17 INFO mapred.JobClient:     Map input records=32827

16/05/23 09:56:17 INFO mapred.JobClient:     Reduce shuffle bytes=360643

16/05/23 09:56:17 INFO mapred.JobClient:     Spilled Records=65566

16/05/23 09:56:17 INFO mapred.JobClient:     Map output bytes=295047

16/05/23 09:56:17 INFO mapred.JobClient:     Total committed heap usage (bytes)=644698112

16/05/23 09:56:17 INFO mapred.JobClient:     CPU time spent (ms)=7960

16/05/23 09:56:17 INFO mapred.JobClient:     Combine input records=0

16/05/23 09:56:17 INFO mapred.JobClient:     SPLIT_RAW_BYTES=540

16/05/23 09:56:17 INFO mapred.JobClient:     Reduce input records=32783

16/05/23 09:56:17 INFO mapred.JobClient:     Reduce input groups=5

16/05/23 09:56:17 INFO mapred.JobClient:     Combine output records=0

16/05/23 09:56:17 INFO mapred.JobClient:     Physical memory (bytes) snapshot=958246912

16/05/23 09:56:17 INFO mapred.JobClient:     Reduce output records=5

16/05/23 09:56:17 INFO mapred.JobClient:     Virtual memory (bytes) snapshot=11630010368

16/05/23 09:56:17 INFO mapred.JobClient:     Map output records=32783

16.8.1.1           MapReduce任務計數器

16.8.1.2           文件系統任務計數器

BYTES_READmapreduce任務讀取的每一種文件系統的字節數,每一個文件系統都有一個計數器,文件系統可以是:LocalHDFSS3KFS

BYTES_WRITTENmapreduce任務往每一種文件系統寫的字節數

16.8.1.3           FileInputFormat任務計數器

map任務通過FileInputFormat讀取的字節數

16.8.1.4           FileOutputFormat任務計數器

map任務(針對僅含map的作業)或者reduce任務通過FileOutputFormat寫出的字節數

16.8.2   作業計數器

作業計數器由jobtracker(或Yarn中的application master)維護,不需要在網絡上傳送,這與其他(包括用戶自定義的計數器)不一樣

這些計數器都是在作業級別統計得到的,其值不會隨着任務運行而改變

 

16.8.3   自定義計數器

 

一般用戶使用枚舉(enum)來自定一組相關的計數器,枚舉類型名即為計數器組名,枚舉類型里定義的成員字段就是計數器名稱

 

這里計數器是全局的,即在所有mapreduce中進行計數累加,並在作業結束時產生一個最終的結果

 

publicclass MaxTemperatureWithCounters extends Configured implements Tool {

    // 定義了兩個計數器。一般會將自定義枚舉計數器定義成內部類

    enum Temperature {

        MISSING, MALFORMED

    }

 

    staticclass MaxTemperatureMapperWithCounters extends Mapper<LongWritable, Text, Text, IntWritable> {

        private NcdcRecordParser parser = new NcdcRecordParser();

        @Override

        protectedvoid map(LongWritable key, Text value, Context context) throws IOException,

                InterruptedException {

            parser.parse(value);

            if (parser.isValidTemperature()) {//如果是有效氣溫,則輸出

                int airTemperature = parser.getAirTemperature();

                context.write(new Text(parser.getYear()), new IntWritable(airTemperature));

            } elseif (parser.isMalformedTemperature()) {//如果數據問題,MALFORMED計數器累加

                context.getCounter(Temperature.MALFORMED).increment(1);

            } elseif (parser.isMissingTemperature()) {//如果沒有氣溫值,MISSING計數器累加

                context.getCounter(Temperature.MISSING).increment(1);

            }

 

            // dynamic counter動態計數器:即未使用枚舉來定義計數器,而是在程序中直接指定計數器組名以及計數器名

            //下面 TemperatureQuality 即組名,parser.getQuality()即計數器名。與枚舉靜態計數器定義不同的時,在獲取時

            //需要明確的指定計數器組名

            context.getCounter("TemperatureQuality", parser.getQuality()).increment(1);

        }

    }

    . . . . . .

 

如果計數器是采用枚舉類型來定義的,則計數器默認名稱就是枚舉類型的完全限定類名,如這里的默認計數器組名為:MaxTemperatureWithCounters$Temperature(注:由於上面示例類在默認包括里,所以沒有包前綴信息),而每個成員的名稱就是默認計數器名稱,如這里的MISSING MALFORMED,下面是上面作業運行后輸出的計數器統計信息:

枚舉類型定義的計數器的默認名稱不易讀,可以為這個枚舉定義相應的屬性資源文件,如果枚舉是內部類,則屬性文件名中使用下畫線分隔:MaxTemperatureWithCounters_Temperature.properties

屬性文件中內容:

CounterGroupName配置的是計數器組名稱

成功字段名.name配置的是具體某個計數器名稱

 

如果是中文翻譯,則對應的屬性資源文件名為:

MaxTemperatureWithCounters_Temperature_zh_CN.properties

 

16.8.4   獲取計數器

除了通過Web界面和命令行(hadoop job -counter),還可以通過Java API來獲取計數器的值

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.mapred.Counters;

import org.apache.hadoop.mapred.JobClient;

import org.apache.hadoop.mapred.JobConf;

import org.apache.hadoop.mapred.JobID;

import org.apache.hadoop.mapred.RunningJob;

import org.apache.hadoop.mapred.Task;

 

publicclass MissingTemperatureFields {

    publicstaticvoid main(String[] args) throws Exception {

        Configuration conf = new Configuration();

        conf.set("mapred.job.tracker", "hadoop-master:9001");

 

        JobClient jobClient = new JobClient(new JobConf(conf));

        // 根據作業ID來獲取作業,一般jobtracker上默認保存最近100個作業,可以通過

        // mapred.jobtracker.completeuserjobs.maximum 配置屬性配置,所以有可能找不到作業

        RunningJob job = jobClient.getJob(JobID.forName("job_201605201002_0046"));

 

        // 獲取作業上所有的計數器

        Counters counters = job.getCounters();

        // 獲取具體某個計數器

        long missing = counters.getCounter(MaxTemperatureWithCounters.Temperature.MISSING);

        long total = counters.getCounter(Task.Counter.MAP_INPUT_RECORDS);

 

        System.out.printf("Records with missing temperature fields: %.2f%%\n", 100.0 * missing / total);

    }

}

 

上面示例代碼片斷使用的是舊版API,通過新版API獲取計數器的方法並不適用於Hadoop 1.X版本(但使用舊版編寫的獲取計數器的代碼可以運行在新版API上)。新舊版本API主要差別是在於是新版本API中使用Cluster對象來獲取一個Job對象(而非一個RunningJob對象),然后再調用它的getCounters()方法:

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.mapreduce.Cluster;

import org.apache.hadoop.mapreduce.Counters;

import org.apache.hadoop.mapreduce.Job;

import org.apache.hadoop.mapreduce.JobID;

import org.apache.hadoop.mapreduce.TaskCounter;

       Cluster cluster = new Cluster(conf);

       Job job = cluster.getJob(JobID.forName("job_201605201002_0046"));

       Counters counters = job.getCounters();

       longmissing = counters.findCounter(MaxTemperatureWithCounters.Temperature.MISSING).getValue();

       longtotal = counters.findCounter(TaskCounter.MAP_INPUT_RECORDS).getValue();

另外一個區別,新API使用org.apache.hadoop.mapreduce.TaskCounter枚舉類型,則舊API里使用org.apache.hadoop.mapred.Task.Counter枚舉類型

16.9        排序

16.9.1   氣象數據轉換為順序文件

為了按氣溫對天氣數據進行排序,需要以天氣為Key寫進sequence 文(在Map輸出時就會自動按Key進行排序),所以在排序之前,需要先將天氣數據文件轉換為SequenceFile格式的文件:

 

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.fs.Path;

import org.apache.hadoop.io.IntWritable;

import org.apache.hadoop.io.LongWritable;

import org.apache.hadoop.io.SequenceFile.CompressionType;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.io.compress.GzipCodec;

import org.apache.hadoop.mapreduce.Job;

import org.apache.hadoop.mapreduce.Mapper;

import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;

 

publicclass SortDataPreprocessor {

    staticclass CleanerMapper extends Mapper<LongWritable, Text, IntWritable, Text> {

       private NcdcRecordParser parser = new NcdcRecordParser();

 

       @Override//這里只有map,沒有reduce

       protectedvoid map(LongWritable key, Text value, Context context)

              throws IOException, InterruptedException {

           parser.parse(value);

           if (parser.isValidTemperature()) {

              context.write(new IntWritable(parser.getAirTemperature()), value);

           }

       }

    }

 

    publicstaticvoid main(String[] args) throws Exception {

       Configuration conf = new Configuration();

       conf.set("mapreduce.framework.name", "yarn");// 指定使用yarn框架

       conf.set("yarn.resourcemanager.address", "hadoop-master:8032"); // 遠程提交任務需要用到

       conf.set("fs.defaultFS", "hdfs://hadoop-master:9000");// 指定namenode

       conf.set("mapred.remote.os", "Linux");

       conf.set("mapred.jar", new File("").getAbsolutePath()

              + "\\SortDataPreprocessor.jar");

 

       Job job = Job.getInstance(conf, "weather");

       job.setJarByClass(SortDataPreprocessor.class);

 

       job.setMapperClass(CleanerMapper.class);

       // 由於氣溫是按帶符號的,所以不能簡單以字符串來比,所以要轉換為數字后才能比,所以Key為整型

       job.setOutputKeyClass(IntWritable.class);

       job.setOutputValueClass(Text.class);// 值就是行文本

       // 不需要 reducer 任務,所以可設置為0。由於reducer任務個數設置為0,每個文件(還是 每數據塊?)

       // 輸出一個Map輸出,輸出文件名形式為part-m-xxxxx,沒有reduce輸出。注:由於沒有Reduce任務,

       //所以這里的Map輸出數據是未分區的,只是簡單的將普通的Text輸入文件轉換為SequenceFile文件

       job.setNumReduceTasks(0);

       job.setOutputFormatClass(SequenceFileOutputFormat.class);// 以鍵值對順序文件格式輸出

       SequenceFileOutputFormat.setCompressOutput(job, true);// 啟用壓縮

       SequenceFileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);// 壓縮方法

       SequenceFileOutputFormat.setOutputCompressionType(job,

              CompressionType.BLOCK);// 壓縮類型:塊級別的壓縮

 

       FileInputFormat.addInputPath(job, new Path( "hdfs://hadoop-master:9000/ncdc/all"));

       FileOutputFormat.setOutputPath(job, new Path( "hdfs://hadoop-master:9000/ncdc/all-seq/"));

 

       System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

16.9.2   部分排序

將順序文件按溫度排序,由於采用是默認Hash分區,每個分區輸出到一個文件,這個文件里的數據是按溫度排序的,但是分區(文件)之間不是排序的,即一個文件里的所以數據不一定全部大於或小於另一文件里的數據,所以這是部分排序

 

下面這個程序是對前面無序的順序文件進行默認排序(采用默認分區、並使用默認的mapreduce),此作業輸出的單個文件里的數據是有序的,但文件之間是無序的,所以是部分排序:

publicclass SortByTemperatureUsingHashPartitioner {

    publicstaticvoid main(String[] args) throws Exception {

       Configuration conf = new Configuration();

       conf.set("mapreduce.framework.name", "yarn");

       conf.set("yarn.resourcemanager.address", "hadoop-master:8032");

       conf.set("fs.defaultFS", "hdfs://hadoop-master:9000");

       conf.set("mapred.remote.os", "Linux");

       conf.set("mapred.jar", new File("").getAbsolutePath()

              + "\\SortByTemperatureUsingHashPartitioner.jar");

 

 

       Job job = Job.getInstance(conf, "weather");

       job.setJarByClass(SortByTemperatureUsingHashPartitioner.class);

 

       /*

        * 該類沒有指定MapReducer,所以使用默認的mapreducer,由於在shuffle過程中,

        * 默認就會對分區中的數據里進行排序,所以每個輸出文件中的數據是按Key(溫度)排序的

        */

       job.setInputFormatClass(SequenceFileInputFormat.class);//輸入的為順序文件

       job.setOutputKeyClass(IntWritable.class);

       job.setOutputFormatClass(SequenceFileOutputFormat.class);//還是以順序文件格式輸出

       job.setNumReduceTasks(5);//會輸出5個已排序的輸出文件

       //輸出還是會使用塊級別的壓縮

       SequenceFileOutputFormat.setCompressOutput(job, true);

       SequenceFileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);

       SequenceFileOutputFormat.setOutputCompressionType(job, CompressionType.BLOCK);

 

       FileInputFormat.addInputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/all-seq/"));

       FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/output-hashsort/"));

 

       System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

 

上面產生的文件是部分排序的順序文件SequenceFile(文件里是排序的,但文件之間無序),為了根據溫度Key在文件中進行查找,需要這些文件為MapFile,哪怕這些文件是之間是無序的。下面程序與上面示例一樣,唯一不同的是上面輸出格式為SequenceFileOutputFormat,而這個為MapFileOutputFormat

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.fs.Path;

import org.apache.hadoop.io.IntWritable;

import org.apache.hadoop.io.SequenceFile.CompressionType;

import org.apache.hadoop.io.compress.GzipCodec;

import org.apache.hadoop.mapreduce.Job;

import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import org.apache.hadoop.mapreduce.lib.input.SequenceFileInputFormat;

import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import org.apache.hadoop.mapreduce.lib.output.MapFileOutputFormat;

import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;

 

publicclass SortByTemperatureToMapFile {

    publicstaticvoid main(String[] args) throws Exception {

       Configuration conf = new Configuration();

       conf.set("mapreduce.framework.name", "yarn");

       conf.set("yarn.resourcemanager.address", "hadoop-master:8032");

       conf.set("fs.defaultFS", "hdfs://hadoop-master:9000");

       conf.set("mapred.remote.os", "Linux");

       conf.set("mapred.jar", new File("").getAbsolutePath()

              + "\\SortByTemperatureToMapFile.jar");

 

       Job job = Job.getInstance(conf, "weather");

 

       job.setInputFormatClass(SequenceFileInputFormat.class);

       job.setOutputKeyClass(IntWritable.class);

       job.setOutputFormatClass(MapFileOutputFormat.class);

       job.setNumReduceTasks(5);

       SequenceFileOutputFormat.setCompressOutput(job, true);

       SequenceFileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);

       SequenceFileOutputFormat.setOutputCompressionType(job, CompressionType.BLOCK);

 

       FileInputFormat.addInputPath(job, new Path( "hdfs://hadoop-master:9000/ncdc/all-seq"));

       FileOutputFormat.setOutputPath(job, new Path( "hdfs://hadoop-master:9000/ncdc/output-hashmapsort"));

       System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

 

如果按默認reduce輸出,則會產生一個 _SUCCESS 空的標記文件,但這個文件會導致

       Reader[] readers = MapFileOutputFormat.getReaders(path, conf);獲取MapFile.Reader出錯(請看后面示例):

Exception in thread "main" java.io.FileNotFoundException: File does not exist: hdfs://hadoop-master:9000/ncdc/output-hashmapsort/_SUCCESS/data

原因是該方法的path參數輸入的要求輸入的路徑是分區文件夾的上一級目錄,這里即為output-hashmapsort MapFile.Reader的特點會自動定位到dataindex文件,所以如果讀取MapFile時,只需到目錄即可。所以設以下這個配置屬性,不讓其輸出:

conf.setBoolean("mapreduce.fileoutputcommitter.marksuccessfuljobs", false);

設置后輸入結果文件中就不會產生_SUCCESS 空的標記文件了:

16.9.2.1           在部分排序的MapFile文件集中查找

MapFileOutputFormat類提供了兩個便利的靜態方法,用來在mapreduce輸入為MapFileOutputFormat格式的文件集中進行查找:

  publicstatic MapFile.Reader[]getReaders(Path dir, Configuration conf) throws IOException {

    FileSystem fs = dir.getFileSystem(conf);

    Path[] names = FileUtil.stat2Paths(fs.listStatus(dir));

 

    // sort names, so that hash partitioning works

    Arrays.sort(names);

   

    MapFile.Reader[] parts = new MapFile.Reader[names.length];

    for (inti = 0; i < names.length; i++) {

      parts[i] = new MapFile.Reader(fs, names[i].toString(), conf);

    }

    returnparts;

  }

該方法讀取mapreduce作業輸出的MapFileOutputFormat格式的MapFile文件,由於作業輸出的mapfile為多個(如上面的output-hashmapsort輸出文件夾),返回的為數組MapFile.Reader[]Mapreducer會為每個Mapfile輸出文件打開一個MapFile.ReaderMapFile.Reader可以參考前面章節

  publicstatic <K extends WritableComparable<?>, V extends Writable> WritablegetEntry(MapFile.Reader[] readers, Partitioner<K, V> partitioner, K key, V value) throws IOException {

    intpart = partitioner.getPartition(key, value, readers.length);

    returnreaders[part].get(key, value);

  }

根據指定的key及分區算法,找到該key所對應的value。實現過程:為了得到鍵值keyreaders數組中的哪一個Reader對象中,我們需要構建一個HashPartitioner類的對象partitioner。在創建了對象partitioner后,我們可以根據要查找的鍵key(如果存在時)該所在分區的索引(readers數組的下標)。得到數組的下標后,我們就可以得到要查找的鍵Key所在的輸出reader,通過readerget方法,就可以得到指定key所對應的value

 

 

下面基於上面產生的MapFile,在文件中查找指定溫度的氣象數據,注:這些文件之間是無序的,文件里是有序的,即這些文件是部分排序的。

 

示例:從MapFile文件集中查找第一條符合指定溫度的氣象數據

publicclass LookupRecordByTemperature {

    publicstaticvoid main(String[] args) throws Exception {

       Configuration conf = new Configuration();

 

       Path path = new Path("hdfs://hadoop-master:9000/ncdc/output-hashmapsort");

       IntWritable key = new IntWritable(Integer.parseInt("-39"));

 

       Reader[] readers = MapFileOutputFormat.getReaders(path, conf);

       Partitioner<IntWritable, Text> partitioner = new HashPartitioner<IntWritable, Text>();

       Text val = new Text();

       //根據分區方式,在readers數組中找到指定的第一個key所對應的val,返回的值entry實質上也是val

       Writable entry = MapFileOutputFormat.getEntry(readers, partitioner, key, val);

       if (entry == null) {

           System.err.println("Key not found: " + key);

           System.exit(0);

       }

       NcdcRecordParser parser = new NcdcRecordParser();

       parser.parse(val.toString());

       //entryval是一樣的

       //parser.parse(entry.toString());

       System.out.printf("%s\t%s\n", parser.getStationId(), parser.getYear());

    }

}

028690-99999  1909

 

示例:從MapFile文件集中查找所有符合指定溫度的氣象數據

publicclass LookupRecordsByTemperature {

    publicstaticvoid main(String[] args) throws Exception {

       Configuration conf = new Configuration();

 

       Path path = new Path("hdfs://hadoop-master:9000/ncdc/output-hashmapsort");

       IntWritable key = new IntWritable(Integer.parseInt("-39"));

 

       Reader[] readers = MapFileOutputFormat.getReaders(path, conf);

       Partitioner<IntWritable, Text> partitioner = new HashPartitioner<IntWritable, Text>();

       Text val = new Text();

 

       //找到指定Key所對應的MapFile.Reader

       Reader reader = readers[partitioner.getPartition(key, val, readers.length)];

       Writable entry = reader.get(key, val);

       if (entry == null) {

           System.err.println("Key not found: " + key);

           System.exit(0);

       }

       NcdcRecordParser parser = new NcdcRecordParser();

       IntWritable nextKey = new IntWritable();

       do {

           parser.parse(val.toString());

           System.out.printf("%s\t%s\t%s\n", key, parser.getStationId(), parser.getYear());

       } while (reader.next(nextKey, val) && key.equals(nextKey));//如果還未到文件尾,且鍵為指定的鍵時

    }

}

-39 029600-99999  1905

-39 029600-99999  1905

-39 029810-99999  1905

-39 029810-99999  1905

-39 029810-99999  1905

-39 029600-99999  1905

-39 029440-99999  1907

-39 228920-99999  1907

16.9.3   全排序

如何讓mapreducer作業產生一個全局排序的文件?最簡單方法就是使用一個分區,即只輸出一個文件,這樣的方法跟單機沒什么區別,完全沒有利用分布式計算的優勢,數據量稍大時,一個reduce的處理效率極低;如果是多個分區,則會輸出的是多個文件,多文件輸出的情況下要是全排序的話,除了文件內部是排序外(Map默認就是按Key升序輸出給Reducer,所以Reducer輸出默認也是升序的,所以文件內部排序默認情況下不需要我們去編碼實現),文件之間也是排序的,即前一分區里的所有Key全大於或小於或等於后一分區里的所有Key

 

默認情況下,會使用HashPartitioner類對Map輸出進行Hash分區,但是, Hash分區算法可能會導致分區之間的記錄分配不均,如可能會出現下面分配不均的問題:

這時我們可以使用TotalOrderPartitioner全排序分區算法,對Map輸出數據進行均衡分區。使用該類進行分區前,需要先對數據進行采樣得到一系列的采樣數據,並能這些采樣數據進行排序,然后通過一定的算法並將這些排序過的采樣數據划分為多個范圍(數量為Reducer個數減一),然后取這些范圍的起始Key作為抽樣點,最后TotalOrderPartitioner類會根據這些分區范圍對數據進行分區。

所以現在使用TotalOrderPartitioner進行全排序分區的問題,轉換為如何對數據進行采樣生成分區范圍抽樣點的問題

 

如果我們預先就知道所有溫度數據,則可以通過自定義分區算法,將所有記錄很好的均勻分配到各分區中,但對於大數據量輸入來說遍歷整個數據集是不可取的,這時我們只能通過對鍵分布空間進行采樣,才可能較為均勻的划分數據集。采樣的思想就是針對一部分的數據獲得其分布情況,由此來構建分區,並且Hadoop已提供了這樣內置的采樣器,無需自己編寫

16.9.3.1           數據抽樣

為什么要使用采樣器?

簡單的來說就是解決"How to automatically find good partitioning function",即創建大小近似相等的分區

 

采樣的目的是為了創建大小近似相等的一系統分區

 

如何對大數據進行高效地全局排序且排序過程中reducer要數據均衡?首先,創建一系列排序好的文件;其次,串聯這些文件;最后生成一個全局排序的文件。

主要思路是使用一個partitioner來描述全局排序的輸出。

由此我們可以歸納出這樣一個用hadoop對大量數據排序的步驟:

1  對待排序數據進行抽樣;

2  對抽樣數據進行排序,產生標尺;

3  Map對輸入的每條數據計算其處於哪兩個標尺之間;將數據發給對應區間IDreduce

4  Reduce將獲得數據直接輸出。

 

InputSampler類內部實現了三種采樣方法:SplitSamplerRandomSamplerIntervalSamplerRandomSampler最耗時),它們都實現了InputSampler的內部接口Sampler接口:

  publicinterface Sampler<K,V> {

    //根據job的配置信息以及輸入獲取抽樣數據

    K[] getSample(InputFormat<K,V> inf, Job job) throws IOException, InterruptedException;

  }

這個接口通常不直接由客戶端調用,而是由InputSampler類的靜態方法writePartitionFile() 調用,目的是創建一個順序文件SequenceFile(該文件即為分區文件,根據該文件對Map輸出數據進行分區)來存儲抽樣點數據,根據這些抽樣點(即具體某個key)就可以知道每個分區所存儲的數據范圍(區間),抽樣點的個數為分區數減一,即如果分區數為n,則該順序文件會記入 n - 1 個抽樣點,就靠這 n - 1 個分隔點(抽樣點)將mapreduce輸出數據均衡分成n個分區,這樣多個reducer任務最終產生的多個輸出文件之間的數據就是有序並且均衡。具體如何從抽樣數據中獲取抽樣點,可以參考publicstatic <K,V> void writePartitionFile(Job job, Sampler<K,V> sampler)這個方法,訪方法是將采樣的結果數據進行排序,然后從這份樣本數據中取出 n - 1n為分區數)個抽樣點key)寫入到順序文件中,具體抽樣點的取法如下:

NullWritable nullValue = NullWritable.get();

// numPartitions:分區數,samples:樣本數據數組。stepSize即為將樣本數據分成numPartitions份,每份約多少個(可能為小數)?即步長

    float stepSize = samples.length / (float) numPartitions;

int last = -1;//上一次被抽中樣本元素索引,即最近一次寫入到順序文件的樣本數據所在samples樣本數組里的索引

//注:如果分區數為1,則是不會對抽樣數據進行抽樣取點,即下面循環不會執行

    for(int i = 1; i < numPartitions; ++i) {//執行 numPartitions - 1次,即需要抽取的樣本元素個數為numPartitions - 1

      int k = Math.round(stepSize * i);//四舍五入,如 k = 4.3則取samples[4], k = 4.5則取samples[5]

     //防止上一次元素再次被抽中,按理的話k不太可能小於last,最多只能相等

      while (last >= k && comparator.compare(samples[last], samples[k]) == 0) {

        ++k;

      }

      writer.append(samples[k], nullValue);//將抽樣點寫入SequenceFileNullWritable表示值不寫入

      last = k;

}

 

1、  SplitSampler只采樣分片中的前N條記錄,並且只對隨機幾個分片中抽取(而不是所有分片),所以該采樣器並不適合於已經排好序的數據,這樣會造成取樣太偏。SplitSampler類有兩個屬性:

  publicstaticclass SplitSampler<K,V> implements Sampler<K,V> {

    privatefinalintnumSamples;//從所有選中的分片中獲取的最大抽樣數

    privatefinalintmaxSplitsSampled;//用來選作抽樣的最大分片數。只要 numSamplesmaxSplitsSampled任一條件滿足,即停止抽樣

通過其構造函數對這兩屬性進行設置:

    public SplitSampler(int numSamples, int maxSplitsSampled) {

 

2、  IntervalSampler根據一定的間隔從分片中采樣數據,非常適合對排好序的數據采樣SplitSampler類有兩個屬性也有兩個屬性:

  publicstaticclass IntervalSampler<K,V> implements Sampler<K,V> {

    privatefinaldoublefreq;

    privatefinalintmaxSplitsSampled;

通過其構造函數對這兩屬性進行設置:

    public IntervalSampler(double freq, int maxSplitsSampled) {

如果當前樣本數與已經讀取的記錄數的比值小於freq,則將這條記錄添加到樣本集合,否則讀取下一條記錄

 

3、  RandomSampler隨機地從輸入數據中抽取Key,是一個優秀的通用采樣器。RandomSampler類有三個屬性:

  publicstaticclass RandomSampler<K,V> implements Sampler<K,V> {

    privatedoublefreq;// Key被選中的概率

    privatefinalintnumSamples;//從所有選中的分片中獲取的最大抽樣數

    privatefinalintmaxSplitsSampled;//用來選作抽樣的最大分片數

通過其構造函數對這三個屬性進行設置:

    public RandomSampler(double freq, int numSamples, int maxSplitsSampled) {

取出一條記錄,判斷得到的隨機浮點數是否小於等於采樣頻率freq,如果大於則放棄這條記錄

 

采樣方式對比表:

類名稱

采樣方式

構造方法

效率

特點

SplitSampler<K,V>

對前n個記錄進行采樣

采樣總數,取樣最大分片數

最高

 

RandomSampler<K,V>

遍歷所有數據,隨機采樣

采樣頻率,采樣總數,取樣最大分片數

最低

 

IntervalSampler<K,V>

固定間隔采樣

采樣頻率,取樣最大分片數

對有序的數據十分適用

16.9.3.2           根據抽樣數據進行全排序,並讓數據均衡分布

默認的Hash分區算法類HashPartitioner可能會導致分區數據不均,可以使用全排序分區算法TotalOrderPartitioner並接合隨機抽樣RandomSampler一起使用,可以輸出數據均衡的全排序文件

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.MapFile;

import org.apache.hadoop.io.NullWritable;

import org.apache.hadoop.io.SequenceFile.CompressionType;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.io.compress.GzipCodec;

import org.apache.hadoop.mapreduce.Job;

import org.apache.hadoop.mapreduce.filecache.DistributedCache;

import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import org.apache.hadoop.mapreduce.lib.input.SequenceFileInputFormat;

import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import org.apache.hadoop.mapreduce.lib.output.SequenceFileOutputFormat;

import org.apache.hadoop.mapreduce.lib.partition.InputSampler;

import org.apache.hadoop.mapreduce.lib.partition.TotalOrderPartitioner;

 

publicclass SortByTemperatureUsingTotalOrderPartitioner {

    publicstaticvoid main(String[] args) throws Exception {

       Configuration conf = new Configuration();

       conf.set("mapreduce.framework.name", "yarn");

       conf.set("yarn.resourcemanager.address", "hadoop-master:8032");

       conf.set("fs.defaultFS", "hdfs://hadoop-master:9000");

       conf.set("mapred.remote.os", "Linux");

       conf.set("mapred.jar", new File("").getAbsolutePath()

              + "\\SortByTemperatureUsingTotalOrderPartitioner.jar");

 

       Job job = Job.getInstance(conf, "weather");

       job.setJarByClass(SortByTemperatureUsingTotalOrderPartitioner.class);

       // 該程序將輸入5個內部已排好序的分區,且分區i中所有的鍵都小於i+1中的鍵

       job.setNumReduceTasks(5);

       job.setInputFormatClass(SequenceFileInputFormat.class);

       job.setOutputKeyClass(IntWritable.class);

       job.setOutputFormatClass(SequenceFileOutputFormat.class);

       SequenceFileOutputFormat.setCompressOutput(job, true);

       SequenceFileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);

       SequenceFileOutputFormat.setOutputCompressionType(job,

              CompressionType.BLOCK);

 

       FileInputFormat.addInputPath(job, new Path(

              "hdfs://hadoop-master:9000/ncdc/all-seq"));

       FileOutputFormat.setOutputPath(job, new Path(

              "hdfs://hadoop-master:9000/ncdc/output-totalsort2"));

       // 指定分區方式為全排序的分區類TotalOrderPartitioner,而非默認的Hash分區類HashPartitioner

       // 即分區ii為分區編號,即part-r-nnnnn中的nnnnn)中所有的鍵都小於i+1中的鍵

//:使用TotalOrderPartitioner的場景中,不要讓分區數(reducer數)比采樣得到的非重復Key還要多,否則可能會出問題

       job.setPartitionerClass(TotalOrderPartitioner.class);

 

       // 一定要再次通過job來獲取conf,而不能直接使用上面定義的conf,否則會找不到分布式緩存文件路徑

       conf = job.getConfiguration();

       // 分區文件需要存放到分布式緩存中與其他任務共享,TotalOrderPartitioner需要用到抽樣文件

       String partitionFile = TotalOrderPartitioner.getPartitionFile(conf);

       URI partitionUri = new URI(partitionFile + "#" + TotalOrderPartitioner.DEFAULT_PATH);

       DistributedCache.addCacheFile(partitionUri, conf);//注:使用job.addCacheFile(partitionUri);來代替

       DistributedCache.createSymlink(conf);//老版本中使用的,現在不用了,可以注掉

 

       // 使用RandomSampler采樣器抽樣。采樣概率為0.110000為最大抽樣數,即最多抽取10000個;10為選作抽樣的

       // 最大分片數只要最大抽樣數與選作抽樣的最大分片數任一條件滿足,即停止抽樣

       InputSampler.Sampler<IntWritable, Text> sampler = new InputSampler.RandomSampler<IntWritable, Text>(

              0.1, 10000, 10);

       //訪方法是將采樣的結果數據進行排序,然后從這份樣本數據中取出 n - 1n為分區數)個抽樣點(key)寫入到順序文件中(根據該文件中的 n - 1 KeyMap輸出進行分區),生成的該分區文件會被TotalOrderPartitioner全排序分區類使用。

       // 這里輸出文件會存放到分布式緩存中,具體路徑是通過上面DistributedCache配置的

       InputSampler.writePartitionFile(job, sampler);

 

       System.exit(job.waitForCompletion(true) ? 0 : 1);

    }

}

最終產生的抽樣分區文件如下,由於要分成5個分區,所以分區文件中存放了4個抽樣點(key),將所有數據划分為5個范圍([-,-33)、[-33,11)[11,61)[61,122)[122,+)),同一范圍內的數據會輸出到同一reducer中:

 

下面是使用默認HashPartitioner分區與TotalOrderPartitioner分區結果對比,發現采用TotalOrderPartitioner類進行分區的輸出文件數據分布是比較均衡的:

上面output-hashsort文件夾里的是通過HashPartitioner分區出來的數據,而output-totalsort是通過TotalOrderPartitioner分區出來的數據,可以看出output-totalsort里的文件大小比較均衡一致,即數據分布均衡,並且分區ii為分區編號,即part-r-nnnnn中的nnnnn)中所有的鍵都小於i+1中的鍵

 

 

如果都將reducer任務數調為10,更明顯:

16.9.3.3           分區Partitioner

分區算法都從Partitioner繼續:

publicabstractclass Partitioner<KEY, VALUE> {

  /**

   * 根據指定的Key,返回它所在的分區的分區號

   * @param numPartitions 總共分區數.

   */

  publicabstractint getPartition(KEY key, VALUE value, int numPartitions);

}

 

Partition主要作用就是將map的結果發送到相應的reduce。這就對partition有兩個要求:

1均衡負載,盡量的將工作均勻的分配給不同的reduce

2)效率,分配速度一定要快。

 

Mapreduce提供的Partitioner

 

1、HashPartitioner<k,v>mapreduce默認partitioner。源代碼如下:

  publicint getPartition(K key, V value, int numReduceTasks) {

// numReduceTasks reducer任務數。(key.hashCode() & Integer.MAX_VALUE)作用是去掉負號

    return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;

  }

 

2、BinaryPartitioner:僅對鍵值K的二進制值的[rightOffsetleftOffset]這個區間的二進制數據取hash

一個數組的元素索引偏移兩的兩種表達方式:leftOffset從前往后來標記,第一個元素為0rightOffset從后向前標記,第一個元素為-1

*  +---+---+---+---+---+

 *  | B | B | B | B | B |

 *  +---+---+---+---+---+

 *    0   1   2   3   4

 *   -----1

// lefHash開始的起始索引,采用從前往后標記,索引號為正,即通常的數組元素編號;

// rightHash終止的結束索引,采用從后向前標記,索引號為負

  publicstaticvoid setOffsets(Configuration conf, int left, int right) {

    conf.setInt("mapred.binary.partitioner.left.offset", left);

    conf.setInt("mapred.binary.partitioner.right.offset", right);

  }

  privateintleftOffset, rightOffset

  publicvoid setConf(Configuration conf) {

this.conf = conf;

//如果left.offsetright.offset都未設置,則將會對整個Key所對應的二進制數組進行Hash運算

    leftOffset = conf.getInt("mapred.binary.partitioner.left.offset", 0);//如果未設置,則取0

    rightOffset = conf.getInt("mapred.binary.partitioner.right.offset", -1); //如果未設置,則取-1

  }

  publicint getPartition(BinaryComparable key, V value, int numPartitions) {

    int length = key.getLength();//Key所對應的二進制數數據字節長度

    int leftIndex = (leftOffset + length) % length;//起始索引

int rightIndex = (rightOffset + length) % length;//結束索引

// leftIndex :開始Hash的起始偏移量,rightIndex - leftIndex + 1為需要Hash的二進制數據長度

    int hash = WritableComparator.hashBytes(key.getBytes(), leftIndex, rightIndex - leftIndex + 1);

    return (hash & Integer.MAX_VALUE) % numPartitions;

  }

 

3、KeyFieldBasedPartitioner也是基於hash的個partitioner。和BinaryPatitioner不同,它提供了多個區間用於計算hash。當區間數為0KeyFieldBasedPartitioner退化成HashPartitioner

 

4、TotalOrderPartitioner這個類可以實現輸出的全排序。不同於以上3partitioner,這個類並不是基於hash的。

 

16.9.4   第二排序(復合Key

前面的示例都是找最高或最低氣溫,都是以溫度來作為Key的,如果是以年為維度,找每年的最高最低氣溫呢?這可以先按年排序,然后按溫度進行排序來實現,但是,由於只能按Key進行排序,所以需要進行排序的字段都要做為Key的一部分,所以這時需要將Key設置成復合Key:年+溫度,而不是年或者溫度作為Key了。

 

初看起來,復合鍵是可以解決這種混排的問題,但是,要注意的是,由於是復合鍵(鍵中除年外,還有溫度信息)會導致同一年的數據會被分區到不同Reducer,這樣輸出還是會有問題的(同一年找出的最高或最低溫度就會有多個,並且分布在不同的Reducer輸出文件中)。為了使同一年的數據送往同一Reducer,這需要設計一個只按年進行分區的partitioner,由它來確保同一年的數據發送到同一Reducer。但這樣還是有一個問題就是,雖然通過自定義的分區算法,可以將同一年的數據發送到同一Reducer中,但是,由於Key是由 +溫度 的復合鍵,這會導致同一年的數據還是不能合並在一起(即分組到一起):

),如會出下現在的問題(以整個復合鍵來分組,即使年相同但溫度不同時,還是會分成多個組,這樣會導致每年的最高氣溫不只一個):

其實這個問題也可以像自定義分區那樣,也可以自定義分組的,我們可以按年來分組,這樣同一年的數據在發送Reducer后,就會自分在一組(即只按年來分組,溫度不參與分組——默認情況下是參與的,所以還需要自定義分組算法):

所以,總之,對於復合Key,一般需要自定義分區與自定義分組才能很好的工作

 

 

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.conf.Configured;

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.io.WritableComparable;

import org.apache.hadoop.io.WritableComparator;

import org.apache.hadoop.mapreduce.Job;

import org.apache.hadoop.mapreduce.Mapper;

import org.apache.hadoop.mapreduce.Partitioner;

import org.apache.hadoop.mapreduce.Reducer;

import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import org.apache.hadoop.util.Tool;

import org.apache.hadoop.util.ToolRunner;

 

import ch02.NcdcRecordParser;

 

publicclass MaxTemperatureUsingSecondarySort extends Configured implements Tool {

//如果是字符類型,則可以參考前面的TextPair  注:該Int整型Key並未實現原生比較,如果要實現請參考這里IntWritable.Comparator來實現

    publicstaticclass IntPair implements WritableComparable<IntPair> {

       privateintfirst;//

       privateintsecond;// 溫度

 

       public IntPair() {

       }

 

       public IntPair(intfirst, intsecond) {

           set(first, second);

       }

 

       publicvoid set(intfirst, intsecond) {

           this.first = first;

           this.second = second;

       }

 

       publicint getFirst() {

           returnfirst;

       }

 

       publicint getSecond() {

           returnsecond;

       }

 

       @Override

       publicvoid write(DataOutput out) throws IOException {

           out.writeInt(first);

           out.writeInt(second);

       }

 

       @Override

       publicvoid readFields(DataInput in) throws IOException {

           first = in.readInt();

           second = in.readInt();

       }

 

       @Override

       publicint hashCode() {

           returnfirst * 163 + second;

       }

 

       @Override

       publicboolean equals(Object o) {

           if (oinstanceof IntPair) {

              IntPair ip = (IntPair) o;

              returnfirst == ip.first && second == ip.second;

           }

           returnfalse;

       }

 

       @Override

       public String toString() {

           returnfirst + "\t" + second;

       }

 

       @Override//WritableComparator里自定義比較方法 compare(WritableComparable a, WritableComparable b) 會回調此方法

       publicintcompareTo(IntPair ip) {

           intcmp = compare(first, ip.first);

           if (cmp != 0) {

              returncmp;

           }

           return compare(second, ip.second);

       }

 

       /**

        * Convenience method for comparing two ints.

        */

       publicstaticint compare(inta, intb) {

           return (a < b ? -1 : (a == b ? 0 : 1));

       }

    }

 

 

 

 

    staticclass MaxTemperatureMapper extends Mapper<LongWritable, Text, IntPair, NullWritable> {

       private NcdcRecordParser parser = new NcdcRecordParser();

       @Override

       protectedvoid map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

           parser.parse(value);

           if (parser.isValidTemperature()) {

              // 鍵為復合主鍵,值為空

              context.write(new IntPair(parser.getYearInt(), parser.getAirTemperature()), NullWritable.get());

           }

       }

    }

 

    staticclass MaxTemperatureReducer extends Reducer<IntPair, NullWritable, IntPair, NullWritable> {

       @Override

       protectedvoid reduce(IntPair key, Iterable<NullWritable> values, Context context) throws IOException,

              InterruptedException {

           context.write(key, NullWritable.get());

       }

    }

 

    // 自定義分區算法,

    publicstaticclass FirstPartitioner extends Partitioner<IntPair, NullWritable> {

       @Override

       publicint getPartition(IntPair key, NullWritable value, intnumPartitions) {

           // 只讓年參與計算。這里乘127后再Hash會更好的打散分區,防止數據傾斜

           return Math.abs(key.getFirst() * 127) % numPartitions;

       }

    }

 

    //自定排序比較器

    publicstaticclass KeyComparator extends WritableComparator {

       protected KeyComparator() {

           super(IntPair.class, true);

       }

 

       @Override //重寫父類WritableComparator中的compare(WritableComparable a, WritableComparable b)方法

       publicint compare(WritableComparable w1, WritableComparable w2) {

           IntPair ip1 = (IntPair) w1;

           IntPair ip2 = (IntPair) w2;

           intcmp = IntPair.compare(ip1.getFirst(), ip2.getFirst());

           if (cmp != 0) {//先按年升序排,如果不同則不再需要比較溫度

              returncmp;

           }

           //在年相同的情況下,降序排序溫度,這樣同一年第一行溫度即為最高溫度

           return -IntPair.compare(ip1.getSecond(), ip2.getSecond()); // reverse

       }

    }

 

    // 自定義分組比較器

    publicstaticclass GroupComparator extends WritableComparator {

       protected GroupComparator() {

           super(IntPair.class, true);

       }

 

       @Override

       publicint compare(WritableComparable w1, WritableComparable w2) {

           IntPair ip1 = (IntPair) w1;

           IntPair ip2 = (IntPair) w2;

           // 只按年來進行比較分組,只要年份相同就會分為一組,雖然Key是年+溫度

           //由於Job設置了自定義排序(先按年升序,再按溫度降序),所以最后分組

           //只取溫度最高的復合Key,即數據發送到Reducer前,每一年只有一記錄,該

           //記錄就是溫度最高的記錄,其他都會因為分組給忽略掉

           return IntPair.compare(ip1.getFirst(), ip2.getFirst());

       }

    }

 

    @Override

    publicint run(String[] args) throws Exception {

       Configuration conf = new Configuration();

       conf.set("mapreduce.framework.name", "yarn");

       conf.set("yarn.resourcemanager.address", "hadoop-master:8032");

       conf.set("fs.defaultFS", "hdfs://hadoop-master:9000");

       conf.set("mapred.remote.os", "Linux");

       conf.set("mapred.jar", new File("").getAbsolutePath()

              + "\\MaxTemperatureUsingSecondarySort.jar");

 

       Job job = Job.getInstance(conf, "weather");

       job.setJarByClass(SortByTemperatureUsingHashPartitioner.class);

      

       FileInputFormat.addInputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/all"));

       FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop-master:9000/ncdc/output-secondarysort"));

      

       job.setMapperClass(MaxTemperatureMapper.class);

       // 1、先分區:設置自定義分區算法

       job.setPartitionerClass(FirstPartitioner.class);

       //2、再排序:設置自定義排序比較器,用於Map輸出分區后,發往Reducer前,在分區類進行排序進調用

       job.setSortComparatorClass(KeyComparator.class);

       //3、最后分組:設置自定義分組器,在數據分區且排序完成發送Reducer前,會調用此算法進行分組:同一年分到同一組

       job.setGroupingComparatorClass(GroupComparator.class);

       job.setReducerClass(MaxTemperatureReducer.class);

       job.setOutputKeyClass(IntPair.class);

       job.setOutputValueClass(NullWritable.class);

 

       returnjob.waitForCompletion(true) ? 0 : 1;

    }

 

    publicstaticvoid main(String[] args) throws Exception {

       intexitCode = ToolRunner.run(new MaxTemperatureUsingSecondarySort(), args);

       System.exit(exitCode);

    }

}

16.10  連接

如果要像關系數據庫那樣,可以對兩張表進行關聯操作,在MapReducer里是可以實現的,但很麻煩的,可以考慮使用PigHive

 

前面氣象記錄數據中只有氣象站的ID,如果有一份文件存放了氣象站更多信息:如氣象站名,如何將氣象記錄數據與氣象站數據通過ID進行關聯,輸出溫度時也輸出氣象站名稱呢?

 

將數據錄入到測試文本文件中:

 

 

 

JOIN操作可以在Map端,也可以在Reducer端完成,至於采用哪種,則要看數據的組織方式。

16.10.1            Map端連接

之所以存在reduce side join,是因為在map階段不能獲取所有需要的join字段,即:同一個key對應的字段可能位於不同map中,如兩個表進行關聯,表數據來自於不同的數據文件中。Reduce side join是非常低效的,因為shuffle階段要進行大量的數據傳輸。

 

Map side join是針對以下場景進行的優化:兩個待連接表中,有一個表非常大,而另一個表非常小,這樣小表可以直接存放到內存中,這樣就可以將該小表緩存到讀取大表的Map任務中,在Map中就可以完成聯接。這樣,我們可以將小表復制多份,讓每個map task內存中存在一份(比如存放到hash table中),然后只掃描大表:對於大表中的每一條記錄key/value,在hash table中查找是否有相同的key的記錄,如果有,則連接后輸出即可。

 

為了支持文件的復制,Hadoop提供了一個類DistributedCache,使用該類的方法如下:

(1)       Jobmain()方法中使用靜態方法DistributedCache.addCacheFile()指定要復制的文件(一般是主數據文件,如用戶、商品等數據量小的主數據文件),它的參數是文件的URI(如果是HDFS上的文件,可以這樣:hdfs://namenode:9000/home/XXX/file,其中9000是自己配置的NameNode端口號)。JobTracker在作業啟動之前會獲取這個URI列表,並將相應的文件拷貝到各個TaskTracker本地磁盤上。

(2)       map()方法中使用DistributedCache.getLocalCacheFiles()方法獲取文件目錄,並使用標准的文件讀寫API讀取相應的文件,然后與業務數據文件(一般是大文件,如訂單業務交易數據文件,這類文件通過map方法去分布讀取)進行關聯。

 

具體查看后面的分布式緩存

16.10.2            Reducer端連接

reduce side join是一種最簡單的join方式,其主要思想如下:

map階段,map函數同時讀取兩個文件File1File2,為了區分兩種來源的key/value數據對,對每條數據打一個標簽(tag,比如:tag=0表示來自文件File1tag=2表示來自文件File2。即:map階段的主要任務是對不同文件中的數據打標簽。

reduce階段,reduce函數獲取key相同的來自File1File2文件的value list 然后對於同一個key,對File1File2中的數據進行join(笛卡爾乘積)。即:reduce階段進行實際的連接操作。

 

import java.io.DataInput;

import java.io.DataOutput;

import java.io.IOException;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.io.WritableComparable;

import org.apache.hadoop.io.WritableComparator;

import org.apache.hadoop.io.WritableUtils;

publicclassTextPairimplements WritableComparable<TextPair> {

    private Text first;

    private Text second;

 

    public TextPair() {

       set(new Text(), new Text());

    }

 

    public TextPair(String first, String second) {

       set(new Text(first), new Text(second));

    }

 

    public TextPair(Text first, Text second) {

       set(first, second);

    }

 

    publicvoid set(Text first, Text second) {

       this.first = first;

       this.second = second;

    }

 

    public Text getFirst() {

       returnfirst;

    }

 

    public Text getSecond() {

       returnsecond;

    }

 

    @Override

    publicvoid write(DataOutput out) throws IOException {

       first.write(out);

       second.write(out);

    }

 

    @Override

    publicvoid readFields(DataInput in) throws IOException {

       first.readFields(in);

       second.readFields(in);

    }

 

    @Override

    publicint hashCode() {

       returnfirst.hashCode() * 163 + second.hashCode();

    }

 

    @Override

    publicboolean equals(Object o) {

       if (oinstanceof TextPair) {

           TextPair tp = (TextPair) o;

           returnfirst.equals(tp.first) && second.equals(tp.second);

       }

       returnfalse;

    }

 

    @Override

    public String toString() {

       returnfirst + "\t" + second;

    }

 

    @Override

    publicint compareTo(TextPair tp) {

       intcmp = first.compareTo(tp.first);

       if (cmp != 0) {

           returncmp;

        }

       returnsecond.compareTo(tp.second);

    }

 

    // 自定義排序:用來對分區里的數據進行排序,除了按第一主鍵氣象站ID來排序外,第二輔助標記鍵也參與排序

    publicstaticclass Comparator extends WritableComparator {

       privatestaticfinal Text.Comparator TEXT_COMPARATOR = new Text.Comparator();

       // 也可以這樣獲取比較實例

       privatestaticfinal WritableComparator TEXT_COMPARATOR2 = WritableComparator.get(Text.class);

 

       public Comparator() {

           super(TextPair.class);

       }

 

       @Override//具體實現過程請參考前面的TextPair

       publicint compare(byte[] b1, ints1, intl1, byte[] b2, ints2, intl2) {

           try {

              intfirstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1);

              intfirstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2);

              intcmp = TEXT_COMPARATOR2.compare(b1, s1, firstL1, b2, s2, firstL2);

              if (cmp != 0) {

                  returncmp;

              }

              // 當第一主鍵相同時,對第二輔助鍵進行排序

              returnTEXT_COMPARATOR.compare(b1, s1 + firstL1, l1 - firstL1, b2, s2 + firstL2, l2 - firstL2);

           } catch (IOException e) {

              thrownew IllegalArgumentException(e);

           }

       }

    }

 

    static {

       // 注:這里靜態注冊的是分區數據排序算法,而不是下面分組排序。該排序類使用不需在JOB里特別設置,會自動使用

       WritableComparator.define(TextPair.class, new Comparator());

    }

 

    // 自定義排序:用來分組,只根據第一個主鍵進行分組,即同一氣象站數據(氣象站元數據與氣溫數據)

    // 傳給Reducer前會分在一組里,而忽略第二輔助標記鍵

    publicstaticclass FirstComparator extends WritableComparator {

       privatestaticfinal Text.Comparator TEXT_COMPARATOR = new Text.Comparator();

 

       public FirstComparator() {

           super(TextPair.class);

       }

 

       @Override

       publicint compare(byte[] b1, ints1, intl1, byte[] b2, ints2, intl2) {

 

           try {

              // 只根據氣象站ID進行排序

              intfirstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1);

              intfirstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2);

              returnTEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2);

           } catch (IOException e) {

              thrownew IllegalArgumentException(e);

           }

       }

 

       @Override

       @SuppressWarnings("rawtypes")

       publicint compare(WritableComparable a, WritableComparable b) {

           if (ainstanceof TextPair && binstanceof TextPair) {

              return ((TextPair) a).first.compareTo(((TextPair) b).first);

           }

           returnsuper.compare(a, b);

       }

    }

}

 

 

import java.io.IOException;

import org.apache.hadoop.io.LongWritable;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.mapreduce.Mapper;

//Mapper讀取氣象站元數據並標記

publicclassJoinStationMapperextends Mapper<LongWritable, Text, TextPair, Text> {

    @Override

    protectedvoid map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

       /*

        * 輔助鍵為0,用來在Reducer中標記數據是來自於氣象站數據文件stations.txt

        * 當輔助鍵為1時表示數據來自於氣溫數據文件records.txt。 不管數據是氣象站還是氣溫數據,

        * Key都是 氣象站ID + 數據來源標記

        * 組成,將氣象站 標記為0,而氣溫數據標記為1是為了按輔助鍵升序排序時,讓氣象數據排在分區的第一條,

        * 除第一條外,后面的數據都是來自於氣溫文件

        */

       context.write(new TextPair(value.toString().split("\t")[0], "0"),// =氣象站ID+標記

              new Text(value.toString().split("\t")[1]));// =氣象站名

    }

}

 

 

import org.apache.hadoop.io.LongWritable;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.mapreduce.Mapper;

//Mapper讀取氣溫數據並標記

publicclassJoinRecordMapperextends Mapper<LongWritable, Text, TextPair, Text> {

    @Override

    protectedvoid map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

       // 輔助鍵為1,用來在Reducer中標記數據是來自於氣溫數據文件records.txt

       context.write(new TextPair(value.toString().split("\t")[0], "1"), // =氣象站ID+標記

              value);// 值為氣溫記錄

    }

}

 

 

import java.io.IOException;

import java.util.Iterator;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.mapreduce.Reducer;

//Reducer用來完成數據的Join操作,數據來自於氣象站與氣溫兩個數據文件

publicclassJoinReducerextends Reducer<TextPair, Text, Text, Text> {

    @Override

    protectedvoid reduce(TextPair key, Iterable<Text> values, Context context) throws IOException,

           InterruptedException {

       Iterator<Text> iter = values.iterator();

       // map傳過來的分區文件中的第一行為氣象站元數據,這是由於特定的排序來決定的

       // (這里假設每條氣溫都有一條氣象點元數據,否則這里第一條數據需要特殊處理一下)

       Text stationName = new Text(iter.next());

       // 從分區數據的第二條開始往后就都是氣溫數據了

       while (iter.hasNext()) {

           Text record = iter.next();

           Text outValue = new Text(stationName.toString() + "\t" + record.toString().split("\t", 2)[1]);

           context.write(key.getFirst(), outValue);

       }

    }

}

 

 

import java.io.File;

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.conf.Configured;

import org.apache.hadoop.fs.Path;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.mapreduce.Job;

import org.apache.hadoop.mapreduce.Partitioner;

import org.apache.hadoop.mapreduce.lib.input.MultipleInputs;

import org.apache.hadoop.mapreduce.lib.input.TextInputFormat;

import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import org.apache.hadoop.util.Tool;

import org.apache.hadoop.util.ToolRunner;

publicclassJoinRecordWithStationNameextends Configured implements Tool {

    // 自定義分區類

    publicstaticclass KeyPartitioner extends Partitioner<TextPair, Text> {

       @Override

       publicint getPartition(TextPair key, Text value, intnumPartitions) {

           //只根據第氣象ID進行分區,即同一氣象站數據會放到同一分區中,&是為了去掉負號

           return (key.getFirst().hashCode() & Integer.MAX_VALUE) % numPartitions;

       }

    }

 

    @Override

    publicint run(String[] args) throws Exception {

       Configuration conf = new Configuration();

       conf.set("mapreduce.framework.name", "yarn");

       conf.set("yarn.resourcemanager.address", "hadoop-master:8032");

       conf.set("fs.defaultFS", "hdfs://hadoop-master:9000");

       conf.set("mapred.remote.os", "Linux");

       conf.set("mapred.jar", new File("").getAbsolutePath() + "\\JoinRecordWithStationName.jar");

 

       Job job = Job.getInstance(conf, "weather");

       job.setJarByClass(JoinRecordWithStationName.class);

 

       Path stationInputPath = new Path("hdfs://hadoop-master:9000/join/stations.txt");

       Path ncdcInputPath = new Path("hdfs://hadoop-master:9000/join/records.txt");

       Path outputPath = new Path("hdfs://hadoop-master:9000/join/output");

       //使用MultipleInputs類來添加多文件源輸入

       MultipleInputs.addInputPath(job, ncdcInputPath, TextInputFormat.class, JoinRecordMapper.class);

       MultipleInputs.addInputPath(job, stationInputPath, TextInputFormat.class, JoinStationMapper.class);

       FileOutputFormat.setOutputPath(job, outputPath);

 

       job.setReducerClass(JoinReducer.class);

      

       //設置自定義分區類:只根據第一主鍵:氣象站ID進行分區

       job.setPartitionerClass(KeyPartitioner.class);

       //設置自定義分組:只根據第一主鍵:氣象站ID進行分組

       job.setGroupingComparatorClass(TextPair.FirstComparator.class);

 

       //由於Map的輸出與Reducer的輸出類型不一樣,所以需要使用下面兩行分別指定

       job.setMapOutputKeyClass(TextPair.class);//設置Map輸出類型

       job.setOutputKeyClass(Text.class);//設置Reducer輸出類型

 

       returnjob.waitForCompletion(true) ? 0 : 1;

    }

 

    publicstaticvoid main(String[] args) throws Exception {

       intexitCode = ToolRunner.run(new JoinRecordWithStationName(), args);

       System.exit(exitCode);

    }

}

 

16.10.3            自連接

有以下父子關系:

child     parent

Tom          Lucy

Tom          Jack

Jone         Lucy

Jone          Jack

Lucy          Mary

Lucy         Ben

Jack          Alice

Jack          Jesse

Terry         Alice

Terry         Jesse

Philip         Terry

Philip         Alma

Mark         Terry

Mark         Alma

 

 

要求找出子孫關系。設計思路:將左表的parent與右表的child進行關聯。在Map中將上面輸出兩次,第一次輸出為左表,以parentKey,值為1表示數據來自於左表;第二次輸出為右表,以childKey,值為2表示數據來自於右表。這當數據傳到Reducer后,由於相同的Key會分組合並,這樣可以在Reducer中取出每個分組,然后取出值列表,將自來左表的數據與來自右表的數據進行笛卡爾積輸出即可

 

import java.io.DataInput;

import java.io.DataOutput;

import java.io.IOException;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.io.Writable;

publicclass ValueText implements Writable {

    private Text personName;//人名

    private Text from;//數據來自左表還是右表:1—左,2—右

 

    public ValueText() {

       set(new Text(), new Text());

    }

 

    public ValueText(String personName, String from) {

       set(new Text(personName), new Text(from));

    }

 

    public ValueText(Text personName, Text from) {

       set(personName, from);

    }

 

    publicvoid set(Text personName, Text from) {

       this.personName = personName;

       this.from = from;

    }

 

    public Text getPersonName() {

       returnpersonName;

    }

 

    public Text getFrom() {

       returnfrom;

    }

 

    @Override

    publicvoid write(DataOutput out) throws IOException {

       personName.write(out);

       from.write(out);

    }

 

    @Override

    publicvoid readFields(DataInput in) throws IOException {

       personName.readFields(in);

       from.readFields(in);

    }

   

    @Override

    public String toString() {

       returnpersonName + "\t" + from;

    }

}

 

 

 

import java.io.File;

import java.io.IOException;

import java.util.ArrayList;

import org.apache.hadoop.conf.Configuration;

import org.apache.hadoop.conf.Configured;

import org.apache.hadoop.fs.Path;

import org.apache.hadoop.io.LongWritable;

import org.apache.hadoop.io.Text;

import org.apache.hadoop.mapreduce.Job;

import org.apache.hadoop.mapreduce.Mapper;

import org.apache.hadoop.mapreduce.Reducer;

import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;

import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;

import org.apache.hadoop.util.Tool;

import org.apache.hadoop.util.ToolRunner;

 

publicclass JoinSelf extends Configured implements Tool {

    staticclass SelfMapper extends Mapper<LongWritable, Text, Text, ValueText> {

       @Override

       protectedvoid map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {

           if (value.toString().length() != 0) {

              // 輸出左表數據

              context.write(new Text(value.toString().split("\t")[1]),// Keyparent

                     new ValueText(value.toString().split("\t")[0], "1"));// 值為child,使用1來標記,表示是左表

              // 輸出右表數據

              context.write(new Text(value.toString().split("\t")[0]),// Keychild

                     new ValueText(value.toString().split("\t")[1], "2"));// 值為parent,用2來標記,表示是右表

           }

       }

    }

 

    staticclass SelfReducer extends Reducer<Text, ValueText, ValueText, Text> {

       @Override

       protectedvoid reduce(Text key, Iterable<ValueText> values, Context context) throws IOException,

              InterruptedException {

           ArrayList<Text> lefTab = new ArrayList<Text>();

           ArrayList<Text> rightTab = new ArrayList<Text>();

           for (ValueText vt : values) {

              if (vt.getFrom().toString().equals("1")) {

                  /*

                   * keyvalue相關的對象只有兩個,reduce會反復重用這兩個對象(每次清除里面的內容,然后重用

                   * KeyValue對象本身)所以如果要保存key或者value的結果,只能將其中的值取出另存或者重新clone

                   * 一個對象(例如Text store = new Text(value) 或者String a = value.toString()),而不能

                   * 直接賦引用,否則集合中存儲的將是最后一次KeyValue

                   */

                  lefTab.add(new Text(vt.getPersonName()));

              } elseif (vt.getFrom().toString().equals("2")) {

                  rightTab.add(new Text(vt.getPersonName()));

              }

           }

 

           for (Text textL : lefTab) {

              for (Text textR : rightTab) {

                  context.write(new ValueText(textL,key), textR);

              }

           }

       }

    }

 

    @Override

    publicint run(String[] args) throws Exception {

       Configuration conf = new Configuration();

       conf.set("mapreduce.framework.name", "yarn");

       conf.set("yarn.resourcemanager.address", "hadoop-master:8032");

       conf.set("fs.defaultFS", "hdfs://hadoop-master:9000");

       conf.set("mapred.remote.os", "Linux");

       conf.set("mapred.jar", new File("").getAbsolutePath() + "\\JoinSelf.jar");

 

       Job job = Job.getInstance(conf, "selfjoin");

       job.setJarByClass(JoinSelf.class);

 

       FileInputFormat.addInputPath(job, new Path("hdfs://hadoop-master:9000/join/self/self.txt"));

       FileOutputFormat.setOutputPath(job, new Path("hdfs://hadoop-master:9000/join/self/output"));

 

       job.setMapperClass(SelfMapper.class);

       job.setReducerClass(SelfReducer.class);

 

       // 由於MapReducer的輸出值類型不一樣,所在需要以下兩行設置

       job.setOutputKeyClass(Text.class);

       job.setMapOutputValueClass(ValueText.class);

        // 由於MapRducer的輸出健類型不一樣,所在需要以下兩行設置

       job.setMapOutputKeyClass(Text.class);

       job.setOutputKeyClass(ValueText.class);

 

       returnjob.waitForCompletion(true) ? 0 : 1;

    }

 

    publicstaticvoid main(String[] args) throws Exception {

       intexitCode = ToolRunner.run(new JoinSelf(), args);

       System.exit(exitCode);

    }

}

 

16.11  mapreduce函數參數傳遞

 

MapReduce程序通常要傳遞各種各樣的參數到map()reduce()方法中,最直接的方式就是使用Configuration的各種set(String name, String value)方法,但這要求值為字符串類型,即如果傳遞的是某個對象,此時需要將對象先轉換為字符串才能傳遞,一般我們是調用其對象的toString()進行轉換,但這可能會出問題:如果數字對象的精度丟失與字節內容的膨脹,這就要用到org.apache.hadoop.io.DefaultStringifier這個類,通過這個類,將對象的反序列化的出來的字節內容通過Base64進行編碼成字符串,然后再進行傳遞,這種方法的好處是保證了原始的內容,內容也不會膨脹

 

 

下面是不通過Configuration進行傳遞時,由於任務跑在不同的JVM上,所以很多時候會出問題,如下面:

1.  Java編寫MapReduce程序時,如何向mapreduce函數傳遞參數

先做個實驗:

在主類中聲明兩個靜態變量, 然后在 main 函數中給變量賦值, 試圖在 mapreduce函數中獲得變量的值。

代碼結構類似如下:

說明: [MapReduce] <wbr>如何向map和reduce腳本傳遞參數,加載文件和目錄《轉載》

提交到集群運行發現在 map reduce函數中, 靜態變量MaxScore的值始終是初值1

於是試圖在主類的靜態區(圖中的2)中給變量賦值 (因為靜態區中的代碼比main中的代碼要先執行), 仍是不成功, MaxScore的值始終是初值1

將上述代碼在 單機hadoop上運行, 結果正常, map 函數中能獲得變量的值。

思考是這個原因: 在提交作業到hadoop集群后,mapper類和reducer類就到各個 tasktracker上去運行了, 與主類獨立, 不能交互

因此,上述往 map reduce 函數傳參數的方法實在太天真。

於是想到其它一些方法: 例如將參數寫入hdfs文件中, 然后在 mapper reducer 類的 run方法中讀取文件, 並將值讀到相應變量,這是可行的,但是方法較復雜,代碼如下:

說明: [MapReduce] <wbr>如何向map和reduce腳本傳遞參數,加載文件和目錄《轉載》

MapperReducer類請參考這里

 

上述方法盡管可用, 但是不是常規方法, 下面介紹常用的方法:

16.11.1            通過 Configuration 傳遞

main函數中調用set方法設置參數, 例如:

說明: [MapReduce] <wbr>如何向map和reduce腳本傳遞參數,加載文件和目錄《轉載》

mapper中通過上下文context來獲取當前作業的配置, 並獲取參數, 例如:

說明: [MapReduce] <wbr>如何向map和reduce腳本傳遞參數,加載文件和目錄《轉載》

: context 很有用, 能獲取當前作業的大量信息,例如上面就獲取了任務ID.

16.11.2            通過DefaultStringifier

直接將對象轉換為字符串(調用其toString())方法,然后使用Configuration.set(String name, String value)傳遞這個字符串到mapreduce方法中進行解析,這種方法有些缺點:如將對象變成字符串會有精度上的損失,如 double類型轉換成字符串,不僅精度有損失,而且8字節的空間用字符串來表示可能會變成幾十字節。正確的方法是,讓這個對象實現Writable接口,使它具有序列化的能力,然后使用org.apache.hadoop.io.DefaultStringifierstore(Configuration conf, K item, String keyName)load(Configuration conf, String keyName, Class<K> itemClass)靜態方法設置和獲取這個對象。他的主要思想就是將這個對象序列化成一個字節數組后,用Base64編碼成一個字符串,然后傳遞給 conf, 解析的時候與之類似。

 

           Configuration conf = new Configuration();

           Text maxscore = new Text("12989");

           DefaultStringifier.store(conf, maxscore, "maxscore");

Job中這樣設置后,Text對象maxscore就以“maxscore”作為key存儲在conf對象中了,然后在mapreduce方法中調用load的方法便可以把對象讀出:

           Configuration conf = context.getConfiguration();

           Text out = DefaultStringifier.load(conf, "maxscore", Text.class);

 

需要說明的是,這個需要傳遞的對象必須要先實現序列化的接口Writable

 

下面看看DefaultStringifierstore()方法原型:

  /**

   * Stores the item in the configuration with the given keyName.

   *

   * @param<K>  the class of the item

   * @param conf the configuration to store

   * @param item the object to be stored

   * @param keyName the name of the key to use

   */  publicstatic <K> void store(Configuration conf, K item, String keyName) throws IOException {

    DefaultStringifier<K> stringifier = new DefaultStringifier<K>(conf, GenericsUtil.getClass(item));

    conf.set(keyName, stringifier.toString(item));//存儲到conf對象中的類型最后是字符串,但這是如何將對象轉換為字符串,請看后面DefaultStringifiertoString()方法

    stringifier.close();

  }

DefaultStringifiertoString()方法原型:

  @Override

  public String toString(T obj) throws IOException {

    outBuf.reset();

    serializer.serialize(obj);//最終調用了writable.write(dataOut);方法來完成序列化

    byte[] buf = newbyte[outBuf.getLength()];

    System.arraycopy(outBuf.getData(), 0, buf, 0, buf.length);

    returnnew String(Base64.encodeBase64(buf), Charsets.UTF_8);//將序列化出來的二進制轉換為Base64編碼格式的字符串,所以最終存儲到conf對象中的就是這個Base64格式的字符串

  }

 

再看看DefaultStringifierload ()方法:

  /**

   * Restores the object from the configuration.

   *

   * @param<K> the class of the item

   * @param conf the configuration to use

   * @param keyName the name of the key to use

   * @param itemClass the class of the item

   * @return restored object

   */

  publicstatic <K> K load(Configuration conf, String keyName, Class<K> itemClass) throws IOException {

    DefaultStringifier<K> stringifier = new DefaultStringifier<K>(conf, itemClass);

    try {

      String itemStr = conf.get(keyName);

      returnstringifier.fromString(itemStr);//先調用fromString()(具體實現請看下面)方法將Base64編碼格式的字符串轉換成二進制字節數組,最后將該二進制字節數組反序列化成原始對象

    } finally {

      stringifier.close();

    }

  }

  @Override

  public T fromString(String str) throws IOException {

    try {

      byte[] bytes = Base64.decodeBase64(str.getBytes("UTF-8"));//Base64編碼格式的字符串轉換成二進制字節數組

      inBuf.reset(bytes, bytes.length);

      T restored = deserializer.deserialize(null);//最終通過writable.readFields(dataIn);進行反序列化

      returnrestored;

    } catch (UnsupportedCharsetException ex) {

      thrownew IOException(ex.toString());

    }

  }

 

16.11.2.1      第三方JavaBean

此方法需要注意一點是obj這個對象需要實現Writable接口,使它具有序列化的能力。此對象的Writable接口可以自己實現也可以將此obj轉化為BytesWritable類型的(因為DefaultStringifiersetload方法參數需要的類型要是WritableBytesWritable就像InteWritable一樣,是實現了Writable接口的),這樣在從conf中取出的時候還得進行反轉,轉化方法可以這樣寫:

    privatestatic BytesWritable transfer(Object patterns) {

       ByteArrayOutputStream baos = null;

       ObjectOutputStream oos = null;

       try {

           oos = new ObjectOutputStream(newByteArrayOutputStream());//借助於Java標准的序列化來實現

           oos.writeObject(patterns);

           oos.flush();

           returnnew BytesWritable(baos.toByteArray());//最后再將這個BytesWritable對應傳遞給DefaultStringifier即可,這樣這個patterns類就不需要實現Writable接口了,因為這個類可能是第三方提供的JavaBean,我們沒法讓去實現Writable

       } catch (Exception e) {

       } finally {

           IoUtils.close(baos);

           IoUtils.close(oos);

       }

       returnnull;

    }

 

反轉方法為:

    privatestatic Object transferMRC(byte[] bytes) {

       ObjectInputStream is = null;

       try {

           is = new ObjectInputStream(new ByteArrayInputStream(bytes));

           returnis.readObject();

       } catch (Exception e) {

       } finally {

           IoUtils.close(is);

       }

       returnnull;

    }

 

但是如果遇到更大的參數呢?比如分詞用的語料庫等等,這時就應該用到Hadoop的緩存機制Distributed Cache了。

16.12  Distributed Cache分布式緩存

如果傳遞的參數數據量比較大,就不適合通過conf對象來傳遞了,因為這些參數是直接放在內存中的,這時可以考慮Distributed Cache比如前面的抽樣分區示例

 

DistributedCacheHadoop提供的文件緩存工具,它能夠自動將指定的文件分發到各個節點上,緩存到任務執行節點的本地磁盤中,供用戶程序(多為map()reduce()方法)讀取使用。它具有以下幾個特點:緩存的文件是只讀的,修改這些文件內容沒有意義;用戶可以調整文件可見范圍(比如只能用戶自己使用,所有用戶都可以使用等),進而防止重復拷貝現象;按需拷貝,文件是通過HDFS作為共享數據中心分發到各節點的,且只發給任務被調度到的節點。本文將介紹DistributedCacheHadoop 1.02.0中的使用方法及實現原理。

 

Hadoop DistributedCache有以下幾種典型的應用場景:

1)分發字典文件,一些情況下Mapper或者Reducer需要用到一些外部字典,比如黑白名單、詞表等;

2map-side join:當多表連接時,一種場景是一個表很大,一個表很小,小到足以加載到內存中,這時可以使用DistributedCache將小表分發到各個節點上,以供Mapper加載使用;

3)分發第三方庫(jar,so)

 

注:需要分發的文件,必須提前放到hdfs上,默認的路徑前綴是hdfs://,不是file://

 

Hadoop提供了兩種DistributedCache使用方式,一種是通過API,在程序中設置文件路徑,另外一種是通過命令行-files-archives-libjars)參數告訴Hadoop,個人建議使用第二種方式,該方式可使用以下三個參數設置文件:

1-files:將指定的本地/hdfs文件分發到各個Task的工作目錄(hadoop jar ... .jar包所在的目錄?)下,不對文件進行任何處理;

2-archives:將指定文件分發到各個Task的工作目錄下,並對名稱后綴為“.jar”、“.zip”,“.tar.gz”、“.tgz”的文件自動解壓,比如壓縮包為dict.zip,則解壓后內容存放到目錄dict.zip中。為此,你可以給文件起個別名(軟鏈接),比如dict.zip#dict,這樣,壓縮包會被解壓到目錄dict中。

3-libjars:指定待分發的jar包,Hadoop將這些jar包分發到各個節點上后,會將其自動添加到任務的CLASSPATH環境變量中

 

前面提到,DistributedCache分發的文件是有可見范圍的,有的文件可以只對當前程序可見,程序運行完后,直接刪除;有的文件只對當前用戶可見(該用戶所有程序都可以訪問);有的文件對所有用戶可見DistributedCache會為每種資源(文件)計算一個唯一ID,以識別每個資源,從而防止資源重復下載,舉個例子,如果文件可見范圍是所有用戶,則在每個節點上,第一個使用該文件的用戶負責緩存該文件,之后的用戶直接使用即可,無需重復下載。那么,Hadoop是怎樣區分文件可見范圍的呢?

Hadoop 1.0版本中,Hadoop是以HDFS文件的屬性作為標識判斷文件可見性的,需要注意的是,待緩存的文件即使是在Hadoop提交作業的客戶端(Linux服務器,安裝了Hadoop)上,也會首先上傳到HDFS的某一目錄下,再分發到各個節點上的,因此,HDFS是緩存文件的必經之路。對於經常使用的文件或者字典,建議放到HDFS上,這樣可以防止每次重復下載,做法如下:

比如將數據保存在HDFS/dict/public目錄下,並將/dict/dict/public兩層目錄的可執行權限全部打開(在Hadoop中,可執行權限的含義與linux中的不同,該權限只對目錄有意義,表示可以查看該目錄中的子目錄),這樣,里面所有的資源(文件)便是所有用戶可用的,並且第一個用到的應用程序會將之緩存到各個節點上,之后所有的應用程序無需重復下載,可以在提交作業時通過以下命令指定:

-files hdfs:///dict/public/blacklist.txt, hdfs:///dict/public/whilelist.txt

如果有多個HDFS集群可以指定namenode的對外rpc地址:

-files hdfs://host:port/dict/public/blacklist.txt, hdfs://host:port/dict/public/whilelist.txt

DistributedCache會將blacklist.txtwhilelist.txt兩個文件緩存到各個節點的一個公共目錄下,並在需要時,在任務的工作目錄下建立一個指向這兩個文件的軟連接。

 

如果可執行權限沒有打開,則默認只對該應用程序的擁有者可見,該用戶所有應用程序可共享這些文件。

一旦你對/dict/public下的某個文件進行了修改,則下次有作業用到對應文件時,會發現文件被修改過了,進而自動重新緩存文件

 

對於一些頻繁使用的字典,不建議存放在客戶端,每次通過-files指定這樣的文件,每次都要經歷以下流程:上傳到HDFS上—》緩存到各個節點上—》之后不再使用這些文件,直到被清除,也就是說,這樣的文件,只會被這次運行的應用程序使用,如果再次運行同樣的應用程序,即使文件沒有被修改,也會重新經歷以上流程,非常耗費時間,尤其是字典非常多,非常大時。

 

DistributedCache內置緩存置換算法,一旦緩存(文件數目達到一定上限或者文件總大小超過某一上限)滿了之后,會踢除最久沒有使用的文件。

 

Hadopo 2.0中,自帶的MapReduce框架仍支持1.0的這種DistributedCache使用方式,但DistributedCache本身是由YARN實現的,不再集成到MapReduce中。YARN還提供了很多相關編程接口供用戶調用,有興趣的可以閱讀源代碼。

 

下面介紹Hadoop 2.0中,DistributedCache通過命令行分發文件的基本使用方式:

1)運行Hadoop自帶的example例子, dict.txt會被緩存到各個Task的工作目錄下,因此,直接像讀取本地文件一樣,在MapperReducer中,讀取dict.txt即可:

bin/Hadoop jar \

share/hadoop/mapreduce/hadoop-mapreduce-examples-2.2.0.jar \

wordcount \

-files hdfs:///dict/public/dict.txt \

/test/input \

/test/output

3)接下給出一個緩存壓縮文件的例子,假設壓縮文件為dict.zip,里面存的數據為:

data/1.txt

data/2.txt

mapper.list

reducer.list

通過-archives參數指定dict.zip后,該文件被解壓后,將被緩存(實際上是軟連接)到各個Task的工作目錄下的dict.zip目錄下,組織結構如下:

dict.zip/

    data/

        1.txt

        2.txt

    mapper.list

    reducer.list

你可以在MapperReducer程序中,使用類似下面的代碼讀取解壓后的文件:

File file2 = read(“dict.zip/data/1.txt”, “r”);

…….

File file3 = read(“dict.zip/mapper.list”, “r”);

 

疑問:為什么MapReduce不直接通過API讀取HDFS中的文件呢?為什么一定得用DistributedCache分發到Task所在的節點本地呢?

因為MapReduce依賴的外部資源大部分是本地資源,比如jar包,可執行文件等,這些資源,必須在本地才能使用,比如jar包必須加到環境變量CLASSPATH中,而CLASSPATH是不能識別HDFS文件的,JVM不支持;另外,HDFS上的文件是不可以直接執行的,必須放到本地,這個除非支持遠程執行或者遠程調用,這個在默認情況下,操作系統是不支持的。 除了上面這些原因,還有一個是,文件放到本地后程序讀取數據更快,因為數據不需要在網絡中傳輸了。

 

16.12.1            MR1

1、通過配置

可以配置這三個屬性值:

mapred.cache.files,

mapred.cache.archives,

 

mapred.create.symlink (值設為yes 如果要建link的話)注:在由於在2.X版本后,沒有該類設置,如連接中有#,則會自動創建,所以2.0后不再使用

 

也可以通過設置配置文檔里的屬性 mapred.job.classpath.{files|archives}

 

如果要分發的文件有多個的話,要以逗號分隔(貌似在建link的時候,逗號分隔前后還不能有空,,否則會報錯)

 

2、使用命令行

pipesstreaming里面可能會用到

-files  Specify comma-separated files to be copied to the Map/Reduce cluster

-libjars  Specify comma-separated jar files to include in the classpath

-archives  Specify comma-separated archives to be unarchived on the compute machines

例如:

-files hdfs://host:fs_port/user/testfile.txt

-files hdfs://host:fs_port/user/testfile.txt#testfile

-files hdfs://host:fs_port/user/testfile1.txt,hdfs://host:fs_port/user/testfile2.txt

-archives hdfs://host:fs_port/user/testfile.jar

-archives hdfs://host:fs_port/user/testfile.tgz#tgzdir

 

注:只要命令行中(hadoo jar ...)運行Job時指定了上面某個參數,則就會將指定的文件分發到Task任務節點的本地,如下面命令:

% hadoop jar hadoop-examples.jar MaxTemperatureByStationNameUsingDistributedCacheFile \

-files input/ncdc/metadata/stations-fixed-width.txt input/ncdc/all output

上面運行MaxTemperatureByStationNameUsingDistributedCacheFile作業時,會先將input/ncdc/metadata/stations-fixed-width.txt文件分發到任務執行節點的本地input/ncdc/all output分別為輸入輸出目錄

上面的命令將本地文件stations-fixed-width.txt(未指定文件系統,從而被自動解析為本地文件)復制到任務節點,從而可以查找氣象站名稱,下面是在reducer中使用這個分發的緩存文件代碼:

       protectedvoid setup(Context context) throws IOException, InterruptedException {

           metadata = new NcdcStationMetadata();

           //使用命令方式運行Job時會自動創建symlink符號軟連接,軟連接名默認就是文件名自身,除非使用#URI里指定

           metadata.initialize(new File("stations-fixed-width.txt"));

       }

 

命令工作機制:

當用戶啟動一個作業,Hadoop會把由-files-archives-libjars等選項所指定的文件復制到分布式文件系統(一般是HDFS)之中,接着會在任務運行前,tasktracker將文件從HDFS系統復制到本地磁盤緩存起來使任務能夠訪問文件。此外,由-libjars指定的文件會在任務啟動前添加到任務的類路徑(classpath)中

 

3、代碼調用

       DistributedCache.addCacheFile(URI,conf) / DistributedCache.addCacheArchive(URI,conf)

       DistributedCache.setCacheFiles(URIs,conf) / DistributedCache.setCacheArchives(URIs,conf)

DistributedCache.addArchiveToClassPath(Path, Configuration, FileSystem) //將文件加載到jvmclasspath

DistributedCache.addFileToClassPath //將文件加載到jvmclasspath

如果要建link,需要增加DistributedCache.createSymlink(Configuration)注:在由於在2.X版本后,不需要手動來創建,如連接中有#,則會自動創建,所以2.0后不再使用

 

其中URI的形式是 hdfs://host:port/absolute-path#link-name

hdfs://namenode:port/lib.so.1#lib.so,則在task當前工作目錄會有名為lib.so的連接文件, 它指向了本地緩存目錄(${hadoop.tmp.dir}/mapred/local)下的lib.so.1

 

獲取cache文件可以使用

DistributedCache.getLocalCacheFiles(Configuration conf)

DistributedCache.getLocalCacheArchives(Configuration conf)

public Path[] getLocalCacheFiles() throws IOException;

public Path[] getLocalCacheArchives() throws IOException;

public Path[] getFileClassPaths();

public Path[] getArchiveClassPaths();

 

代碼調用常常會有各樣的問題,一般我比較傾向於通過createSymlink的方式來使用,就把cache當做當前目錄的文件來操作,簡單很多。常見的通過代碼來讀取cache文件的問題如下:

DistributedCache.getLocalCacheFiles在偽分布式情況下,常常返回null.

DistributedCache.getLocalCacheFiles其實是把DistributedCache中的所有文件都返回(可以使用軟連接解決,參考下面符號連接)。需要自己篩選出所需的文件.archives也有類似的問題

DistributedCache.getLocalCacheFiles返回的是tt機器本地文件系統的路徑,使用的時候要注意,因為很多地方默認的都是hdfs://,可以自己加上file://來避免這個問題

 

4symlink符號連接

給分發的文件,task運行的當前工作目錄建立軟連接(如同Linux下的ln -s命令),在使用起來的時候會更方便,沒有上面的各種麻煩

    conf.set("mapred.cache.files", "/data/data#mData");

    conf.set("mapred.cache.archives", "/data/data.zip#mDataZip");//會在Task工作目錄創建名為mDataZip的軟連接指向本地緩存目錄(${hadoop.tmp.dir}/mapred/local)下的data.zip文件

    @Override

    protectedvoid setup(Context context) throws IOException,InterruptedException { 

        super.setup(context); 

        FileReader reader = new FileReader(new File("mData")); 

        BufferedReader bReader = new BufferedReader(reader); 

        // TODO 

    }

在使用symlink之前,需要告知hadoop,如下:

    conf.set("mapred.create.symlink", "yes"); // 通過配置設置。是yes,不是true

    DistributedCache.createSymlink(Configuration)//通過API設置。注:在由於在2.X版本后,不需要手動來創建,如連接中有#,則會自動創建,所以2.0后不再使用

 

5、緩存在本地的存儲目錄:

<property>

  <name>mapreduce.cluster.local.dir</name>

  <value>${hadoop.tmp.dir}/mapred/local</value>

  <description>The local directory where MapReduce stores intermediate

  data files.  May be a comma-separated list of

  directories on different devices in order to spread disk i/o.

  Directories that do not exist are ignored.

  </description>

</property>

 

Jobmain()方法中,將HDFS文件添加到distributed cache中:

       Configuration conf = job.getConfiguration();

       DistributedCache.addCacheFile(new URI(inputFileOnHDFS), conf);  // add file to distributed cache

其中,inputFileOnHDFS是一個HDFS文件的路徑。在mappersetup()方法中使用:

       Configuration conf = context.getConfiguration();

       Path[] localCacheFiles = DistributedCache.getLocalCacheFiles(conf);

       readCacheFile(localCacheFiles[0]);

其中,readCacheFile()是我們自己的讀取cache文件的方法,可能是這樣做的(僅舉個例子):

    privatestaticvoid readCacheFile(Path cacheFilePath) throws IOException {

       BufferedReader reader = new BufferedReader(new FileReader(cacheFilePath.toUri().getPath()));

       String line;

       while ((line = reader.readLine()) != null) {

           //TODO: your code here

       }

       reader.close();

    }

 

 

要獲得cache數據,就得在map/reduce task中的setup方法中取得cache數據,再進行相應操作:

    protectedvoid setup(Context context) throws IOException, InterruptedException {

       super.setup(context);

       URI[] uris = DistributedCache.getCacheFiles(context.getConfiguration());

       Path[] paths = DistributedCache.getLocalCacheFiles(context.getConfiguration());

       // TODO

    }

而三方庫的使用稍微簡單,只需要將庫上傳至hdfs,再用代碼添加至classpath即可:

    DistributedCache.addArchiveToClassPath(new Path("/data/test.jar"), conf);

16.12.2            MR2

上面的代碼中,addCacheFile() 方法和 getLocalCacheFiles() 都已經被Hadoop 2.x標記為 @Deprecated 了。

因此,有一套新的API來實現同樣的功能,如下面將HDFS文件添加到distributed cache中:

       job.addCacheFile(new Path(inputFileOnHDFS).toUri());

mappersetup()方法中:

       Configuration conf = context.getConfiguration();

       URI[] localCacheFiles = context.getCacheFiles();

       readCacheFile(localCacheFiles[0]);

其中,readCacheFile()是我們自己的讀取cache文件的方法,參考上面

 

 

我用的是Hadoop 2.5.1版,這一版里面DistributedCache已經被Deprecated了。最好是用標准命令行參數-files <file1>,<file2>...來上傳DistributedCache文件。上傳的文件會被自動拷貝到data node的本地文件系統中,並被強制建立符號鏈接,符號鏈接的文件名就是#號后面的部分,缺省是上傳前本地文件名。

指定-files參數時估計可以帶#號指定符號鏈接文件名,不過我沒試過。

16.12.3            相關配置

16.12.3.1     MR1

屬性名

默認值

備注

mapred.local.dir

${hadoop.tmp.dir}/mapred/local

The local directory where MapReduce stores intermediate data files. May be a comma-separated list of directories on different devices in order to spread disk i/o. Directories that do not exist are ignored.

local.cache.size

10737418240(10G)

The number of bytes to allocate in each local TaskTracker directory for holding Distributed Cache data.

mapreduce.tasktracker.cache.local.numberdirectories

10000

The maximum number of subdirectories that should be created in any particular distributed cache store. After this many directories have been created, cache items will be expunged regardless of whether the total size threshold has been exceeded.

mapreduce.tasktracker.cache.local.keep.pct

0.95(作用於上面2個參數)

It is the target percentage of the local distributed cache that should be kept in between garbage collection runs. In practice it will delete unused distributed cache entries in LRU order until the size of the cache is less than mapreduce.tasktracker.cache.local.keep.pct of the maximum cache size. This is a floating point value between 0.0 and 1.0. The default is 0.95.

16.12.3.2     MR2

yarn.nodemanager.local-dirs

yarn.nodemanager.delete.debug-delay-sec

yarn.nodemanager.local-cache.max-files-per-directory

yarn.nodemanager.localizer.cache.cleanup.interval-ms

yarn.nodemanager.localizer.cache.target-size-mb

 

 

 

 

 

 

 

 

 

附件列表

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM