Hadoop IO基于文件的数据结构详解【列式和行式数据结构的存储策略】

Charles所有关于hadoop的文章参考自hadoop权威指南第四版预览版 大家可以去safari免费阅读其英文预览版。本人也上传了PDF版本在我的资源中可以免费下载,不需要C币,点击这里下载。

对于某些应用,需要一个特殊的数据结构来存储数据。针对运行基于MapReduce 的进程,将每个二进制数据块放入它自己的文件,这样做不易扩展, 所以Hadoop 为此开发了一系列高级容器。我们可以想象一下,mapreduce遇到的文件可能是日志文件,文本文件等等,mapreduce 拆分之后变成一条条数据记录,会转化为IntWritable,Text等等可以序列化的对象,然后序列化输出到网络或者硬盘,每一种类型的输出都会放入自己的文件,这样是很不经济的,因为我们期望的是所有的数据可以用同一个容器就最好了,那么hadoop就提供了一系列高级容器,用来存放这些数据。

SequenceFile和 MapFile两个代表。


假设有一个日志文件,色的每一个日志记录都是一行新的文本。如果想记录二进制 类型,纯文本并不是一个合适的格式。Hadoop 的SequenceFile 类很适合这个情 况,它为二进制键/值对提供一个持久化的数据结构。如果想将它用作日 志文件格式,需要选择一个键(例如LongWriteable 表示的时间戳)和一个值(是一个 Writable表示日志记录的数量)。

SequenceFile

如何编写SequenceFile呢?怎么使用呢?
写入SequenceFile
要想新建一个SequenceFile 类. 使用色的其中一个createWriter() 静态方 法,该方法会返回一个SequenceFile.Writer 实例。有几个重载版本,但是它们 都需要指定一个要写入的流FSDataOutputStream 或成对的文件系统和路径,一 个Configuration 对象和键/值类型。可选参数包括压缩的类型和编码/解码器、 一个将由写进度来唤醒的Progressable 回调和一个将存储在SequenceFile 类头 部的Metadata 实例。 存储在SequenceFile 类中的键和值不一定必须是Writable 。可以被 SequenceFile 类序列化和反序列的任何类型都可以使用。 有SequenceFile.Writer 之后,就用append ()方能写入键/值对。然后在结束的 时候调用close() 方法。(SequenceFile .Writer 实现了java.io.Closeable) 。
总结如下:  
1)创建Configuration 
2)获取FileSystem 
3)创建文件输出路径Path 
4)调用SequenceFile.createWriter得到SequenceFile.Writer对象 
5)调用SequenceFile.Writer.append追加写入文件 
6)关闭流 
代码实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public  class  SequenceFileWriteDemo {
   
   private  static  final  String[] DATA = {
     "One, two, buckle my shoe" ,
     "Three, four, shut the door" ,
     "Five, six, pick up sticks" ,
     "Seven, eight, lay them straight" ,
     "Nine, ten, a big fat hen"
   };
   
   public  static  void  main(String[] args)  throws  IOException {
     String uri = args[ 0 ];
     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  {
       writer = SequenceFile.createWriter(fs, conf, path,
//   SequenceFile.Writer writer = SequenceFile.createWriter(fs,conf,path,key.getClass(),value.getClass(),CompressionType.RECORD,new BZip2Codec());  采用压缩,用BZip2压缩算法
           key.getClass(), value.getClass());
       for  ( int  i =  0 ; i <  100 ; i++) {
         key.set( 100  - i);
         value.set(DATA[i % DATA.length]);
         System.out.printf( "[%s]\t%s\t%s\n" , writer.getLength(), key, value);//getLength获取的是当前文件的位置
         writer.append(key, value);
       }
     finally  {
       IOUtils.closeStream(writer);
     }
   }
}
% hadoop SequenceFileWriteDemo numbers.seq
[128]   100     One, two, buckle my shoe
[173]   99      Three, four, shut the door
[220]   98      Five, six, pick up sticks
[264]   97      Seven, eight, lay them straight
[314]   96      Nine, ten, a big fat hen
[359]   95      One, two, buckle my shoe
[404]   94      Three, four, shut the door
[451]   93      Five, six, pick up sticks
[495]   92      Seven, eight, lay them straight
[545]   91      Nine, ten, a big fat hen
...
[1976]  60      One, two, buckle my shoe
[2021]  59      Three, four, shut the door
[2088]  58      Five, six, pick up sticks
[2132]  57      Seven, eight, lay them straight
[2182]  56      Nine, ten, a big fat hen
...
[4557]  5       One, two, buckle my shoe
[4602]  4       Three, four, shut the door
[4649]  3       Five, six, pick up sticks
[4693]  2       Seven, eight, lay them straight
[4743]  1       Nine, ten, a big fat hen


读取SequenceFile
从头到尾读取序列文件,需要创建一个SequenceFile . Reader 实例,反复调用next ()方法之一遍历记录。使用哪一个方法取决于所用的序列化框架。如果使用Writable 类型,则可以使用取一个键和一个值作为参数的next ( )方怯,然后将数据流中的下一个键/值对读入这些变量:public boolean next(Writable key , Writable val)如果读取的是一个键/值对,则返回值为true ,如果读取的是文件末尾,返回值为false 。
总结如下:
1)创建Configuration 
2)获取FileSystem 
3)创建文件输出路径Path 
4)new一个SequenceFile.Reader进行读取 
5)得到keyClass和valueClass 
6)关闭流
代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public  class  SequenceFileReadDemo {
   
   public  static  void  main(String[] args)  throws  IOException {
     String uri = args[ 0 ];
     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);//获取key的数据类型 是从reader中获取的
       Writable value = (Writable)
         ReflectionUtils.newInstance(reader.getValueClass(), conf);
       long  position = reader.getPosition();
       while  (reader.next(key, value)) {
         String syncSeen = reader.syncSeen() ?  "*"  "" ;//同步点,那就*标记
         System.out.printf( "[%s%s]\t%s\t%s\n" , position, syncSeen, key, value);
         position = reader.getPosition();  // beginning of next record
       }
     finally  {
       IOUtils.closeStream(reader);
     }
   }
}

注意:
   Writable key = (Writable)  ReflectionUtils.newInstance(reader.getKeyClass(), conf);//获取key的数据类型 是从reader中获取的
   Writable value = (Writable)  ReflectionUtils.newInstance(reader.getValueClass(), conf);
    通过以上两行代码,我们可以找到任何reader的数据类型,也就是说我们可以处理任何数据类型,只要是writable实现的。

此程序的另一个特征是它显示了序列文件中同步点的位置。 同步点是流中的一个 点,如果reader "迷失" ,同步点就可用于重新同步记录边界,例如在查找流中任 意一个位置之后.同步点由SequenceFile.Reader 来记录,当序列文件被写入的 时候, 它会每隔几个记录就插入一个特殊的项来标记此同步点.插入的这种项非常 小,通常开销小于存储大小的1 % 。同步点通常与记录边界重合。
% badoop SequencepileReadDemo numbers. seq
% hadoop SequenceFileReadDemo numbers.seq
[128]   100     One, two, buckle my shoe
[173]   99      Three, four, shut the door
[220]   98      Five, six, pick up sticks
[264]   97      Seven, eight, lay them straight
[314]   96      Nine, ten, a big fat hen
[359]   95      One, two, buckle my shoe
[404]   94      Three, four, shut the door
[451]   93      Five, six, pick up sticks
[495]   92      Seven, eight, lay them straight
[545]   91      Nine, ten, a big fat hen
[590]   90      One, two, buckle my shoe
...
[1976]  60      One, two, buckle my shoe
[2021*] 59      Three, four, shut the door
[2088]  58      Five, six, pick up sticks
[2132]  57      Seven, eight, lay them straight
[2182]  56      Nine, ten, a big fat hen
...
[4557]  5       One, two, buckle my shoe
[4602]  4       Three, four, shut the door
[4649]  3       Five, six, pick up sticks
[4693]  2       Seven, eight, lay them straight
[4743]  1       Nine, ten, a big fat hen


有两种方法可以查找序列文件中的指定位置。
第一种是seek ( )方法,它将reader 定位在文件中的指定点。例如,如预期的那样寻找一个记录边界:
reader.seek(359);assertThat(reader.next(key,value),is(true));assertThat(((IntWritable)key).get(),is(95));

但如果文件中的指定位置不是记录边界. reader 会在调用next ( )时失败。

reader.seek(360);reader.next(key,value);// fails with IOException

第二种查找记录边界的方格用到了同步点。SequenceFile.Reader 上的 sync(long position) 方能把reader 定位到下一个同步点。(如果在这之后没有同 步点, 那么此reader 会定位到文件末尾)。因此,我们可以用流中的任何位置来调 用sync () 一一如一个没有记录的边界一一且reader 将自己重定位到下一个同步点 继续读取:

reader.sync(360);assertThat(reader.getPosition(),is(2021L));assertThat(reader.next(key,value),is(true));assertThat(((IntWritable)key).get(),is(59));

在使用序列文件作为Map Reduce 输入的时候,同步点开始发挥作用,因为它们允 许文件分割,所以它的不同部分通过独立的map 任务得以单独处理。这一点是很重要的。

用命令行接口显示序列文件
Hadoop 文件系统命令有一个-text 选项显示文本格式的序列文件。它看起来像是 文件的魔法数字,使其能够尝试检测文件类型并相应地将其转换为文本。它可以识别  gzip 压缩文件和序列文件,否则便假设输入是纯文本。 对于序列文件,在键/值对有一个有意义的字符串表示时,此命令非常有用(如 toString ()方法定义的那样)。如果有自己的键/值对类,必须确定它们在Hadoop
的类路径上。 对前面创建的序列文件运行它,得到如下输出:
% hadoop fs -text numbers.seq | head
100     One, two, buckle my shoe
99      Three, four, shut the door
98      Five, six, pick up sticks
97      Seven, eight, lay them straight
96      Nine, ten, a big fat hen
95      One, two, buckle my shoe
94      Three, four, shut the door
93      Five, six, pick up sticks
92      Seven, eight, lay them straight
91      Nine, ten, a big fat hen

排序和合并序列文件
序列文件的排序和合并也是我们必须要熟悉的内容,因为mapreduce处理序列文件的时候,不可避免要对数据进行拆分,排序,处理,合并。 排序和合并一个或多个序列文件,最强的方式是使用MapReduce . MapReduce 固 有的并行方式,允许我们指定使用多少个reduce(此数量决定桌输出分区的个数〉。 例如,通过指定一个reducer. 我们会得到一个输出文件。通过指定输入和输出为 序列文件并通过设置键/值类型,我们可以使用Hadoop 自带的排序例子:
% hadoop jar \  $HADOOP_HOME/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.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 \  numbers.seq sorted
% hadoop fs -text sorted/part-r-00000 | head
1       Nine, ten, a big fat hen
2       Seven, eight, lay them straight
3       Five, six, pick up sticks
4       Three, four, shut the door
5       One, two, buckle my shoe
6       Nine, ten, a big fat hen
7       Seven, eight, lay them straight
8       Five, six, pick up sticks
9       Three, four, shut the door
10      One, two, buckle my shoe
以上虽然只是一个小小的demo,却值得花时间好好研究一下,mapreduce的排序和合并功能是hadoop的灵魂之一,因此在mapreduce中对这个合并排序有详细的讲解,在此不再赘述。我们只需要明白,经过mapreduce的排序合并之后顺序改变为从小到大就可以了。

作为使用MapRedu ce 进行排序/合并的替代方案. SequenceFile . Sorter 类有 sort ( ) 和merge ( )方法。这些函数的出现早于MapReduce. 但性能低于 MapReduce(例如,为了实现并行计算,需要对敬据进行浮动分区) .所以一般情况 下. MapReduce 是排序、合并序列文件的首选。反正这些老旧的方法不要再用是个正确的选择。

序列文件的格式

Hadoop IO基于文件的数据结构详解【列式和行式数据结构的存储策略】_第1张图片
先给出上图。可以看出 序列文件由一个头部header和一个或多个记录record组成 。 序列文件的前三位字节是SEQ 字节,作为幻数,后紧接一个字节代表版本号.头 部包括其他字段(包含键/值类的名称、压缩细节、用户定义的元数据和罔步标 记户。调用同步标记可以允许一个reader 同步一个字段中的任何一个记录边界。每 个文件有一个随机产生的同步记号,它的值存储在头部中。同步记号出现在序列文 件的记录中, 它们设计数量不超过存储的1 %. 所以它们并没有必要出现每一对记 录中(比如在一个很短的记录中) 。
我们可以查看SequenceFile类来找到文件格式的蛛丝马迹
Hadoop IO基于文件的数据结构详解【列式和行式数据结构的存储策略】_第2张图片
序列文件的压缩:

记录的内部格式取决于是否启用压缩,如果是, 要么记录压缩,要么块压缩。 如果没有启用压缩(默认设置) .那么每个记录都由他的记录长度(字节数)、键的长 度、键和值组成。
记录压缩
长度字段被作为四字节整型值写入,遵循java.io.DataOutput 中writelnt ()方法的规范。键/值的序列化是通过为正被写入序列文件中的类而定 义的Serialization 来完成的。 记录压缩格式与无压缩基本相同,不同的是值字节是用定义在头部的编码/解码器 来压缩的。注意,键是不压缩的。
块压缩
块压缩一次压缩多个记录,因此它比记录压缩更紧凑,且一般应优先选择,因为它 有机会利用记录之间的相似之处。记录直到到达字节的最小大小才会 被添加到块,该最小值是由io.seqfile.compress.blocksize 中的属性定义的, 它的默认值是1000000 字节。同步标记写在每个块开始之前。块的格式是一个字 段(指出块中的记录数) . 后跟四个字段(分别为键长度、键、值长度和值)。
Hadoop IO基于文件的数据结构详解【列式和行式数据结构的存储策略】_第3张图片

MapFile

MapFile 是经过排序的带索引的SequenceFile . 可以根据键进行查找. MapFile 可以被认为是java.util.Map 的一种持久化形式(虽然它没有实现这个接口),它 会增长乃至超过Map 在内存中占用的大小。

写一个MapFil e

(1)MapFile是经过排序的索引的SequenceFile,可以根据key进行查找
(2)MapFile的key 是WritableComparable类型的,而value是Writable类型的;
(3)可以使用MapFile.fix()方法来重建索引,把SequenceFile转换成MapFile
(4)它有两个静态成员变量:
static String DATA_FILE_NAME //数据文件名
static String INDEX_FILE_NAME //索引文件名
MapFile写过程: 
(1)创建Configuration;
(2)获取FileSystem;
(3)创建文件输出路径Path;
(4)new一个MapFile.Writer对象;
(5)调用MapFile.Writer.append追加写入文件;
(6)关闭流;
下面给出代码实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public  class  MapFileWriteDemo {
   
   private  static  final  String[] DATA = {
     "One, two, buckle my shoe" ,
     "Three, four, shut the door" ,
     "Five, six, pick up sticks" ,
     "Seven, eight, lay them straight" ,
     "Nine, ten, a big fat hen"
   };
   
   public  static  void  main(String[] args)  throws  IOException {
     String uri = args[ 0 ];
     Configuration conf =  new  Configuration();
     FileSystem fs = FileSystem.get(URI.create(uri), conf);
 
     IntWritable key =  new  IntWritable();
//MapFile的key 是WritableComparable类型的,而value是Writable类型的;用来实现key的索引就需要比较值
     Text value =  new  Text();
     MapFile.Writer writer =  null ;
     try  {
       writer =  new  MapFile.Writer(conf, fs, uri,
           key.getClass(), value.getClass());
       
       for  ( int  i =  0 ; i <  1024 ; i++) {
         key.set(i +  1 );
         value.set(DATA[i % DATA.length]);
         writer.append(key, value);
       }
     finally  {
       IOUtils.closeStream(writer);
     }
   }
}
使用此程序来创建一个MapFile :
%hadoop MapPi1eWriteDemo numbers.map
看看这个MapFile ,可以看出它确实是一个目录, 其中包含两个文件, 分别名为 data 和index:
Hadoop IO基于文件的数据结构详解【列式和行式数据结构的存储策略】_第4张图片
两个文件都是SequenceFile. 数据文件包括所有的输入,按顺序的:
Hadoop IO基于文件的数据结构详解【列式和行式数据结构的存储策略】_第5张图片

index 文件包括一小部分键, 并且包括键到data 文件中键偏移量的映射:
% hadoop  fs -text numbers.map/index
Hadoop IO基于文件的数据结构详解【列式和行式数据结构的存储策略】_第6张图片

从输出可以看出,默认情况下,只有每128 个键被包括在索引中,不过可以改变这 个数,具体做法是设置io.map.index.interval 属性或调用MapFile.Writer 实 例的setlndexlnterval() . 增加索引间隔的理由之一是减少MapFil e 存储 索引所需要的内存大小.反之,在牺牲内存的情况下,可以减少索引间隔以改善随 机时间(因为平均说来,需要跳过的记录更少)。 因为索引只是键的部分索引,所以MapFile 不能提供方法来枚举或计数它包含的 所有键.执行这些操作的唯一方法是该取整个文件.

读取一个MapFile

MapFile读过程:
(1)创建Configuration;
(2)获取FileSystem;
(3)创建文件输出路径Path;
(4)new一个MapFile.Reader对象;
(5)得到keyClass和valueClass;
(6)关闭流;

虽然不想再次赘述,但是还是值得再给出相关代码看看:
1
2
3
4
5
6
7
8
9
10
11
12
13
  public  static  void  main(String[] args)  throws  IOException {
   Configuration conf =  new  Configuration();
   FileSystem fs = FileSystem.get(URI.create(uri),conf);
   Path path =  new  Path( "/numbers.map" );
   MapFile.Reader reader =  new  MapFile.Reader(fs, path.toString(), conf);
   WritableComparable key = (WritableComparable)ReflectionUtils.newInstance(reader.getKeyClass(), conf);
   Writable value = (Writable)ReflectionUtils.newInstance(reader.getValueClass(), conf);
   while (reader.next(key, value)){
   System.out.println( "key = " +key);
   System.out.println( "value = " +value);
   }
   IOUtils.closeStream(reader);
   }
以上是遍历, 随机存取查找可以调用get()方法来执行,源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/** Return the value for the named key, or null if none exists. */
     public  synchronized  Writable get(WritableComparable key, Writable val)
       throws  IOException {
       if  (seek(key)) {//找key 二叉搜索
         data.getCurrentValue(val);//找值 遍历
         return  val;
       else
         return  null ;
     }
  /** Positions the reader at the named key, or if none such exists, at the
      * first entry after the named key.  Returns true iff the named key exists
      * in this map.
      */
     public  synchronized  boolean  seek(WritableComparable key)  throws  IOException {
       return  seekInternal(key) ==  0 ;
     }
  private  synchronized  int  seekInternal(WritableComparable key,
         final  boolean  before)
       throws  IOException {
       readIndex();                                 // make sure index is read读取索引文件
 
       if  (seekIndex != - 1                          // seeked before 查找之前的预处理
           && seekIndex+ 1  < count           
           && comparator.compare(key, keys[seekIndex+ 1 ])< 0  // before next indexed
           && comparator.compare(key, nextKey)
           >=  0 ) {                                  // but after last seeked
         // do nothing
       else  {
         seekIndex = binarySearch(key);
         if  (seekIndex <  0 )                         // decode insertion point解码插入点
           seekIndex = -seekIndex- 2 ;
 
         if  (seekIndex == - 1 )                       // belongs before first entry
           seekPosition = firstPosition;            // use beginning of file从文件开头开始
         else
           seekPosition = positions[seekIndex];     // else use index否则使用当前索引值
       }
       data.seek(seekPosition);//文件指针指向开始的位置接下来想向后面查找
 
       if  (nextKey ==  null )
         nextKey = comparator.newKey();
 
       // If we're looking for the key before, we need to keep track
       // of the position we got the current key as well as the position
       // of the key before it.
       long  prevPosition = - 1 ;
       long  curPosition = seekPosition;
 
       while  (data.next(nextKey)) {//读取下一个key到nextkey中去
         int  c = comparator.compare(key, nextKey);//比较之前的key和下一个key
         if  (c <=  0 ) {                              // at or beyond desired 表示nextkey比key大所以位置就已经超过了我们要找的那个key要从头再找
           if  (before && c !=  0 ) {//如果没有到key
             if  (prevPosition == - 1 ) {
               // We're on the first record of this index block
               // and we've already passed the search key. Therefore
               // we must be at the beginning of the file, so seek
               // to the beginning of this block and return c
               data.seek(curPosition);//重新找
             else  {
               // We have a previous record to back up to
               data.seek(prevPosition);//返回之前的记录点
               data.next(nextKey);
               // now that we've rewound, the search key must be greater than this key
               return  1 ;
             }
           }
           return  c;
         }
         if  (before) {//找到,获取当前的位置
           prevPosition = curPosition;
           curPosition = data.getPosition();
         }
       }
 
       return  1 ;
     }
 /**
     * Get the 'value' corresponding to the last read 'key'.
     * @param val : The 'value' to be read.
     * @throws IOException
     */
    public synchronized void getCurrentValue(Writable val) 
      throws IOException {
      if (val instanceof Configurable) {
        ((Configurable) val).setConf(this.conf);
      }

      // Position stream to 'current' value
      seekToCurrentValue();

      if (!blockCompressed) {
        val.readFields(valIn);

        if (valIn.read() > 0) {
          LOG.info("available bytes: " + valIn.available());
          throw new IOException(val+" read "+(valBuffer.getPosition()-keyLength)
                                + " bytes, should read " +
                                (valBuffer.getLength()-keyLength));
        }
      } else {
        // Get the value
        int valLength = WritableUtils.readVInt(valLenIn);
        val.readFields(valIn);

        // Read another compressed 'value'
        --noBufferedValues;

        // Sanity check
        if ((valLength < 0) && LOG.isDebugEnabled()) {
          LOG.debug(val + " is a zero-length value");
        }
      }

    }
Hadoop IO基于文件的数据结构详解【列式和行式数据结构的存储策略】_第7张图片
返回值用于是否在MapFile 中找到一个条目,如果返回值为nul l ,指定值没有 对应的值. 如果找到键.则此键的对应值被该取到 val ,就像从调用方法返回 一样. 理解它的实现方式可能有所帮助。这里是一小段代码.从前一小节创建的MapFile 进行检索:

对于这个操作, MapFie.Reader 将index 文件读取到内存中(这里是缓存的,使后 面的随机存储调用能使用相同的内存索引)。然后, reader 对内存内的索引执行二 叉搜索,试图在索引文件中找到小子或等于搜索键496 的那些键.
在这个例子中, 找到的索引键是385 ,对应的值是18030 ,后者是在data 文件中的偏移量.接着 reader 在data 文件中找到这个偏移量,然后读取数据直到这个键大于或者等于搜索 键496. 在这里,找到了匹配并且从data 文件中读取了数据。上面的代码块我给出了这个流程的索引,代码很简单易懂。
总体来说,查找选择的是一次磁盘寻址和一次遍历磁盘上128 项的磁盘扫描. 对于随机存储读取,这实际上是非常高效的。 getClosest() 方峰和get ( )相似, 不同的是它返回"更靠近"指定键的匹配,而 不是在没有匹配时返回null . 更精确地说, 如果MapFile 包括指定的键.那么它 就是返回值,否则返回这个MapFile 中直接在该指定键之后的键(或者之前,看布 尔参数的设置)。 大型MapFile 的索引会占很大的内存.相较于重建索引改变索引间隔,我们可以 通过设置io . map . index . skip 属性将MapFile 中的小部分键读入内存。这个属性 一般为0 ,意味着不跳过索引键,如果值为1 , 意味着隔一个键跳过一个键(所以 留下一半的键在索引中) 如果值为2 ,表示该取一个键跳过两个键(所以三分之一 的键留在索引中) , 以此类推.更大的跳过值可以节省更多内存空间,但消耗查找 时间,因为平均而言,需要在磁盘中扫描更多条目。

将SequenceFile 转换为MapFile

对于MapFile ,一种方式是将其视为经过索引和排序的SequenceFile ,所以我们 自然会想到把SequenceFile 转换为MapFile. 前面讨论了如何对SequenceFile 进行排序,所以这里来看如何创建SequenceFile的索引.下面的程序代码使用了 类似MapFile 的静态实用方法fix (),后者重建MapFile 的索引:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public  class  MapFileFixer {
 
   @SuppressWarnings ( "unchecked" )
   public  static  void  main(String[] args)  throws  Exception {
     String mapUri = args[ 0 ];
     
     Configuration conf =  new  Configuration();
     
     FileSystem fs = FileSystem.get(URI.create(mapUri), conf);
     Path map =  new  Path(mapUri);
     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在hadoop系统中是最古老的文件格式,但是他们不是仅有的。许多其他的文件结构可能比这两个还要更好。我们必须要考虑其他的结构。

Avro数据文件和sequencefile和mapfile一样,它们被设计用于大规模数据序列文件加工,紧凑和可分割,不同的编程语言都可以使用。存储在Avro数据文件的数据通过一种
schema描述数据结构,而不是在一个可以执行的Java代码(如序列文件,就是使用的java硬代码),使他们不在以java为中心。Avro的数据文件在Hadoop的生态系统得到广泛支持,对于二进制文件存储传输他们是一个很好的默认选择。

序列文件,映射文件,Avro数据文件都是面向行式的数据格式,这意味着,每行的值被连续地存储在文件中。

在面向列的数据格式,在一个文件中的行(在Hive的表)被成行拆分,然后每个分割以面向列的方式存储:在每一行的第一列的值是第一个存储的,随后是每一行的第二列,依此类推。可以参考下图:

一个面向列式的数据储存结构允许在查询访问的时候不需要访问的列被跳过,这在oracle,mysql等行式数据储存中是不能做到的,你要取数据就必须取出一行的数据。我们可以参考下面的图,在逻辑表视图中,我们只是想取出第二列的数据,文件存储在一个序列文件的时候,因为sequcenceFile是行式的,连续的,整个行(存储在序列文件的一个记录)都会被加载到存储器中,即使我们实际只是需要读取第二列。虽然我们可以只反序列化我们需要的那部分数据,从而节省了一些处理时间,但它不能避免从磁盘读取所有字节时间和性能消耗成本。

在面向列的存储结构中,只在第二列的数据(图中描黑边框)需要被读入内存。在一般情况下,查询表中小数目的列的时候,面向列的存储格式会更加高效。相反,需要查询一行的大量列的数据的时候,行式数据存储会更加有效。如何选择取决于我们的需求

Hadoop IO基于文件的数据结构详解【列式和行式数据结构的存储策略】_第8张图片

列式数据存储,需要更多的内存进行读取和写入因为它们在内存中缓冲行的分割的数据而不仅仅是一个单行数据此外,无法确定什么时间会写入数据(通过刷新或同步操作列式存储格式不适合流操作,因为读写过程失败之后文件数据是不能够恢复的另一方面,行式数据结构,如序列文件和Avro数据文件(Avro datafile),再发生IO错误之后,可以读取最后一个同步点正是出于这个原因,Flume使用面向行式数据结构

Hadoop中的第一个列式文件结构HiveRCFile,是
Record  Columnar File的 简称 。现在 它已经被Hive的ORCFile, Parquet 取代 Parquet 基于谷歌 Dremel的 通用列式文件数据结构 ,得到整个 的Hadoop 组件的 广泛支持。 Avro公司 有一个名为 Trevni列式格式

到此为止。明白了行式结构和列式结构对理解hive以及Hbase很有帮助,希望我的总结和翻译解读能够帮助大家。
Charles 2015-12-27于Phnom Phen




版权说明:
本文由Charles Dong原创,本人支持开源以及免费有益的传播,反对商业化谋利。
CSDN博客:http://blog.csdn.net/mrcharles
个人站:http://blog.xingbod.cn




转载于:https://www.cnblogs.com/mrcharles/p/5080736.html

你可能感兴趣的:(Hadoop IO基于文件的数据结构详解【列式和行式数据结构的存储策略】)