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        经典的mapreduce