hadoop SequenceFile与MapFile、Writable、FileSystem
一、SequenceFile简介
SequenceFile文件是Hadoop用来存储二进制形式的key-value对而设计的一种平面文件(Flat File)。
目前,也有不少人在该文件的基础之上提出了一些HDFS中小文件存储的解决方案,他们的基本思路就是将小文件进行合并成一个大文件,同时对这些小文件的位置信息构建索引。不过,这类解决方案还涉及到hadoop的另一种文件格式——MapFile文件。SequenceFile文件并不保证其存储的key-value数据是按照key的某个顺序存储的,同时不支持append操作。
在SequenceFile文件中,每一个key-value被看做是一条记录(Record),因此基于Record的压缩策略,SequenceFile文件可支持三种压缩类型(SequenceFile.CompressionType):
NONE: 对records不进行压缩;
RECORD: 仅压缩每一个record中的value值;
BLOCK: 将一个block中的所有records压缩在一起;
那么,基于这三种压缩类型,Hadoop提供了对应的三种类型的Writer:
SequenceFile.Writer 写入时不压缩任何的key-value对(Record);
SequenceFile.RecordCompressWriter写入时只压缩key-value对(Record)中的value;
SequenceFile.BlockCompressWriter 写入时将一批key-value对(Record)压缩成一个Block;
二、MapFile
MapFile是排序后的SequenceFile,通过观察其目录结构可以看到MapFile由两部分组成,分别是data和index。
index作为文件的数据索引,主要记录了每个Record的key值,以及该Record在文件中的偏移位置。在MapFile被访问的时候,索引文件会被加载到内存,通过索引映射关系可迅速定位到指定Record所在文件位置,因此,相对SequenceFile而言,MapFile的检索效率是高效的,缺点是会消耗一部分内存来存储index数据。
需注意的是,MapFile并不会把所有Record都记录到index中去,默认情况下每隔128条记录存储一个索引映射。当然,记录间隔可人为修改,通过MapFIle.Writer的setIndexInterval()方法,或修改io.map.index.interval属性;
另外,与SequenceFile不同的是,MapFile的KeyClass一定要实现WritableComparable接口,即Key值是可比较的。
三、自定义Writable的TextPair
Hadoop自带一系列有用的Writable实现,可以满足绝大多数用途。但有时,我们需要编写自己的自定义实现。通过自定
义Writable,我们能够完全控制二进制表示和排序顺序。Writable是MapReduce数据路径的核心,所以调整二进制表示对其性
能有显著影响。现有的Hadoop Writable应用已得到很好的优化,但为了对付更复杂的结构,最好创建一个新的Writable类型,
而不是使用已有的类型。
编写一个表示一对字符串的实现,名为TextPair:
import java.io.*; import org.apache.hadoop.io.*; public class TextPair implements WritableComparable<textpair> { private Text first; private Text second; public TextPair() { set(newText(),newText()); } public TextPair(String first, String second) { set(newText(first),newText(second)); } public TextPair(Text first, Text second) { set(first, second); } public void set(Text first, Text second) { this.first = first; this.second = second; } public Text getFirst() { return first; } public Text getSecond() { return second; } @Override public void write(DataOutput out)throws IOException { first.write(out); second.write(out); } @Override public void readFields(DataInput in)throwsIOException { first.readFields(in); second.readFields(in); } @Override public int hashCode() { return first.hashCode() *163+ second.hashCode(); } @Override public boolean equals(Object o) { if(o instanceof TextPair) { TextPair tp = (TextPair) o; return first.equals(tp.first) && second.equals(tp.second); } return false; } @Override public String toString() { return first +"\t"+ second; } @Override public int compareTo(TextPair tp) { int cmp = first.compareTo(tp.first); if(cmp !=0) { return cmp; } return second.compareTo(tp.second); } }
此实现的第一部分直观易懂:有两个Text实例变量(first和second)和相关的构造函数、get方法和set方法。所有的Writable实现都必须有一个默认的构造函数,以便MapReduce框架能够对它们进行实例化,进而调用readFields()方法来填充它们的字段。 Writable实例是易变的、经常重用的,所以我们应该尽量避免在write()或readFields()方法中分配对象。
通过委托给每个Text对象本身,TextPair的write()方法依次序列化输出流中的每一个Text对象。同样,也通过委托给Text对象本身,readFields()反序列化输人流中的字节。DataOutput和DataInput接口有丰富的整套方法用于序列化和反序列化Java基本类型,所以在一般情况下,我们能够完全控制Writable对象的数据传输格式。
正如为Java写的任意值对象一样,我们会重写java.lang.Object的hashCode()方法,equals()方法和toString()方法。HashPartitioner使用hashCode()方法来选择reduce分区,所以应该确保写一个好的哈希函数来确保reduce函数的分区在大小上是相当的TextPair是WritableComparable的实现,所以它提供了compareTo()方法的实现,加入我们希望的顺序:它通过一个一个
String逐个排序。请注意,TextPair不同于前面的TextArrayWritable类(除了它可以存储Text对象数之外),因为TextArrayWritable只是一个Writable,而不是WritableComparable。
实现一个快速的RawComparator
上例中所示代码能够有效工作,但还可以进一步优化。正如前面所述,在MapReduce中,TextPair被用作键时,它必须被反序列化为要调用的compareTo()方法的对象。是否可以通过查看其序列化表示的方式来比较两个TextPair对象。
事实证明,我们可以这样做,因为TextPair由两个Text对象连接而成,二进制Text对象表示是一个可变长度的整型,包UTF-8表示的字符串中的字节数,后跟UTF-8字节本身。关键在于读取开始的长度。从而得知第一个Text对象的字节表示有多长,然后可以委托Text对象的RawComparator,然后利用第一或者第二个字符串的偏移量来调用它。下面例子给出了具体方法(注意,该代码嵌套在TextPair类中)。
用于比较TextPair字节表示的RawComparator:
public static class Comparator extends WritableComparator { private static final Text.Comparator TEXT_COMPARATOR =new Text.Comparator(); public Comparator() { super(TextPair.class); } @Override public int compare(byte[] b1,int s1,int l1, byte[] b2,int s2,int l2) { try { int firstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1); int firstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2); int cmp = TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2); if(cmp != 0) { return cmp; } return TEXT_COMPARATOR.compare(b1, s1 + firstL1, l1 - firstL1, b2, s2 + firstL2, l2 - firstL2); } catch(IOException e) { throw new IllegalArgumentException(e); } } } static { WritableComparator.define(TextPair.class,newComparator()); }
事实上,我们一般都是继承WritableComparator,而不是直接实现RawComparator,因为它提供了一些便利的方法和默认实现。这段代码的精妙之处在于计算firstL1和firstL2,每个字节流中第一个Text字段的长度。每个都由可变长度的整型(由WritableUtils的decodeVIntSize()返回)和它的编码值(由readVInt()返问)组成。
静态代码块注册原始的comparator以便MapReduce每次看到TextPair类,就知道使用原始comparator作为其默认comparator。
自定义comparator
从TextPair可知,编写原始的cornparator比较费力,因为必须处理字节级别的细节。如果需要编写自己的实现,org.apache.hadoop.io包中Writable的某些前瞻性实现值得研究研究。WritableUtils的有效方法也比较非常方便。
如果可能,还应把自定义comparator写为RawComparators。这些comparator实现的排序顺序不同于默认comparator定义的自然排序顺序。下面的例子显示了TextPair的comparator,称为First Comparator。只考虑了一对Text对象中的第一个字符串。请注意,我们重写了compare()方法使其使用对象进行比较,所以两个compare()方法的语义是相同的。
自定义的RawComparator,用于比较TextPair字节表示中的第一字段:
public static class FirstComparator extends WritableComparator { private static final Text.Comparator TEXT_COMPARATOR =newText.Comparator(); public FirstComparator() { super(TextPair.class); } @Override public int compare(byte[] b1,ints1,intl1, byte[] b2,ints2,intl2) { try { int firstL1 = WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1); int firstL2 = WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2); return TEXT_COMPARATOR.compare(b1, s1, firstL1, b2, s2, firstL2); } catch(IOException e) { throw new IllegalArgumentException(e); } } @Override public int compare(WritableComparable a, WritableComparable b) { if(a instanceof TextPair && b instanceof TextPair) { return((TextPair) a).first.compareTo(((TextPair) b).first); } return super.compare(a, b); } }
四、Hadoop序列化
import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; public interface Writable { void write(DataOutput out) throws IOException; void readFields(DataInput in)throws IOException; }
五、FileSystem 对象
取得FileSystem实例有两种静态方法:
(1)public static FileSystem get(Configuration conf)
Configuration对象封装了一个客户端或服务器的配置,这是用类路径读取而来的
返回默认文件系统(在conf/core-site.xml中设置,如果没有设置过,则是默认的本地文件系统)
(2)public static FileSystem get (URI uri,Configuration conf)
参数URI指定URI方案及决定所用文件系统的权限,如果没有指定方案,则退回默认的文件系统
(3)open()方法
public FSDataInputStream open(path f)
该方法默认使用4kb的缓冲大小
public abstract FSDataInputStream open(Path f, int bufferSize)
第二个参数指定缓冲大小
open方法实际返回的是一个FSDataInputStream,这个类是java.io.DataInputStream的一个子类,支持随机访问,能从流中任意位置对其数据。
A、FSDataInputStream类实现了Seekable接口
Seekable接口允许在文件中定位seek()方法,提供一个查询方法,用于查询当前位置相对于文件开始处的偏移量getPos()。
seekToNewSource(long targetPos)在应用程序中并不常用,此方法用来切换到数据的另一个副本并在新的副本中找寻targetPos指定的位置。HDFS内部就采用这样的方法在数据节点故障时为客户端提供可靠的数据输入流。
B、FSDataInputStream类实现了PositionedReadable接口
(1)public int read(long position,byte[] buffer,int offset,int length)
read()方法指定position读取指定长度的字节放入缓冲区buffer的指定偏移量offset,返回值是读到的字节数。
(2)public int readFully(long position,byte[] buffer,int offset,int length)
该方法读出字节length指定的数据到buffer。
(3)public int readFully(long position,byte[] buffer)
只接受buffer字节数组的版本中,再读取buffer.length字节。
所有这些方法会保留文件当前位置并且是线程安全的,因此它们提供了在读取文件(可能是元数据)的主要部分时访问其他部分的便利方法。
注意,seek()是相对高开销的操作,需要慎重使用。
C、创建文件的方法
(1)public FSDataOutputStream create(Path f)
该方法如果没有文件,就会自动创建文件路径,所以最好用exists()检查父目录是否存在。
hadoop.util中有个Progressable的接口中,有个progress()方法,可以被告知数据写入数据节点的进度。
(2)public FSDataOutputStream append(Path f)
该append()方法允许一个写入者打开已有文件并在其末尾写入数据。HDFS支持,但S3不支持。
D、创建目录
public boolean mkdirs(Path f)
E、文件元数据:Filestatus
(1)FileStatus类封装了文件系统镇南关文件的目录和元数据,包括文件长度、块大小、副本、修改时间、所有者以及许可信息。
(2)getFileStatus()提供了一个获取一个文件或目录的状态对象的方法。大家可以通过看源代码看看它的信息参数。
(3)listSatus()能够列出目录的内容。
(4)globStatus(Path pathPattern,PathFilter filter)使用通配符在一个表达式中核对多个文件。
(5)PathFilter对象 ,通配格式不是总能描述我们想要访问的文件集合。listStatus()和globStatus()方法提供了可选PathFilter对象,使我们可以通过编程方式控制匹配。
(6)delete(Path f,boolean recursive)永久性删除文件或目录。

浙公网安备 33010602011771号