Hadoop中的HDFS讲解
HDFS 性能详解
HDFS 天生是为大规模数据存储与计算服务的,而对大规模数据的处理目前还有没比较稳妥的解决方案。 HDFS 将将要存储的大文件进行分割,分割到既定的存储块(Block)中进行了存储,并通过本地设定的任务节点进行预处理,从而解决对大文件存储与计算的需求。在实际工作中,除了某些尺寸较大的文件要求进行存储及计算,更多时候是会产生并存储无数的小尺寸文件。而对于小尺寸文件的处理, HDFS 没有要求使用者进行特殊的优化,也就是说可以通过普通的编程与压缩方式进行解决。对于大部分的文件来说,一旦文件生成完毕,更多的是对文件进行读取而非频繁的修改。 HDFS 对于普通文件的读取操作来说,一般情况下主要分成两种。大规模的持续性读取与小型化随机读取。针对这两种读取方式, HFDS 分别采取了不同的对应策略。对于大规模的数据读取, HDFS 采用的是在存储时进行优化,也就是说在文件进入 HDFS 系统时候,就对较大体积的文件存储时就采用集中式存储的方式,使得未来的读取能够在一个文件一个连续的区域进行,从而节省寻址及复制时间。
而对于小数据的读取, HDFS更多的做法时在把小规模的随机读取操作合并并对读取顺序进行排序,这样可以在一定程度上实现按序读取,提高读取效率。因此可以说, HDFS 更多是考虑到数据读取的批处理,而不是对单独命令的执行。
架构与基本存储单元
对于 HDFS 架构来说,一个 HDFS 基本集群包括两大部分,即 NameNode 与 DataNode节点,其作用是将管理与工作进行分离。通常来说,一个集群中会有一个 NameNode 与若干个 DataNode。 NameNode 是一个集群的主服务器,主要是用于对 HDFS 中所有的文件及内容数据进行维护,并不断读取记录集群中 DataNode 主机情况与工作状态,并通过读取与写入镜像日志文件的方式进行存储。而 DataNode 是在 HDFS 集群中担任任务具体执行,是整个集群的工作节点,文件被分成若干个相同大小的数据块,分别存储在若干个 DataNode 上,DataNode 定时定期向集群内 NameNode 发送自己的运行状态与存储内容,并根据 NameNode发送的指令进行工作。
小提示:NameNode和DataNode可以工作在一台机器上,但是此种工作方式极大的限制了HDFS性能。
NameNode 负责接受客户端发送过来的信息,然后将文件存储信息位置发送给提交请求的客户端,由客户端直接与 DataNode 进行联系,进行部分文件的运算与操作。对于文件存储来说, HDFS 使用 Block(分块)来对文件的存储进行操作。对于传统磁盘存储来说,磁盘都有默认的存储单元,通常使用的是数据定义中的最小存储单元。 Block 是HDFS 的基本存储单元,默认大小是 64M,这个大小远远大于一般系统文件的默认存储大小。这样做的一个最大好处减少文件寻址时间。
除此之外,采用 Block 对文件进行存储,大大提高了文件的灾难生存与恢复能力, HDFS还对已经存储的 Block 进行多副本备份,将每个 Block 至少复制到 3 个相互独立的硬件上。这样做的好处就是确保在发生硬件故障的时候,能够迅速的从其他硬件中读取相应的文件数据。而具体复制到多少个独立硬件上也是可以设置的。
数据存储位置与复制详解
同一节点上的存储数据
同一机架上不同节点上的存储数据
同一数据中心不同机架上的存储数据
不同数据中心的节点
HDFS 数据存放策略就是采用同节点与同机架并行的存储方式。在运行客户端的当前节点上存放第一个副本,第二个副本存放在于第一个副本不同的机架上的节点,第三个副本放置的位置与第二个副本在同一个机架上而非同一个节点。
读取过程分析
以上是过程的一个图示,下面我们对具体过程进一步的分析:
1. 客户端或者用户通过调用FileSystem 对象的 open()方法打开需要读取的文件,这对 HDFS 来说是常见一个分布式文件系统的一个读取实例。
2. FileSystem 通过远程协议调用 NameNode 确定文件的前几个 Block 的位置。对于每一个 Block, NameNode 返回一含有那个 Block 拷贝的“元数据”,即文件基本信息;接下来,DataNode 按照上文定义的距离值进行排序,如果 Client 本身就是一个 DataNode,那么优先从本地 DataNode 节点读取数据。 HDFS 实例做完以上工作后,返回一个 FSDataInputStream给客户端,让其从 FSDataInputStream 中读取数据。 FSDataInputStream 接着包装一个DFSInputStream,用来管理 DataNode 和 NameNode 的 I/O。
3. NameNode 向客户端返回一个包含数据信息的地址,客户端根据地址创建一个FSDataInputStream 开始对数据进行读取。
4. FSDataInputStream 根据开始时存放的前几个 Blocks 的 DataNode 的地址,连接到最近的 DataNode 上对数据开始从头读取。客户端反复调用 read()方法,以流式方式从DataNode 读取数据。
5. 当读到 Block 的结尾的时候,FSDataInputStream 会关闭到当前 DataNode 的链接,然后查找能够读取下一个 Block 的最好的 DataNode。这些操作对客户端是透明的,客户端感觉到的是连续的流,也就说读取的时候就开始查找下一个块所在的地址。
6. 读取完成调用 close()方法,关闭FSDataInputStream。
以上就是 HDFS 对数据进行读取的整个流程。
对于错误处理来说,在读取期间,当 Client 与 DataNode 通信的时候如果发生错误的话,它会尝试读取下个紧接着的含有那个 Block 的 DataNode。 Client 会记住发生错误的 DataNode,这样它就不必在读取以后的块的时候再尝试这个 DataNode 了。 Client 也验证从 DataNode 传递过来的数据的 checksum。如果错误的 Block 被发现,它将尝试从另一个 DataNode 读取数据前被报告给 NameNode。
小提示: NameNode 中存放的是元数据,即将数据类型、大小、格式以对象的形式存放在 NameNode 内存中。便于加快读取速度。
写入过程分析
下面我们来分析下写入流:
1. Client 通过调用 FileSystem 的 create()方法来请求创建文件
2. FileSystem 通过对 NameNode 发出远程请求,在 NameNode 里面创建一个新的文件,但此时并不关联任何的块。 NameNode 进行很多检查来保证不存在要创建的文件已经存在于文件系统中,同时检查是否有相应的权限来创建文件。如果这些检查都完成了,那么NameNode 将记录下来这个新文件的信息。 FileSystem 返回一个 FSDataOutputStream 给客户端用来写入数据。和读的情形一样, FSDataOutputStream 将包装一个 DFSOutputStream 用于和 DataNode 及 NameNode 通信。而一旦文件创建失败,客户端会收到一个 IOExpection,标示文件创建失败,停止后续任务。
3. 客户端开始写数据。FSDataOutputStream 把要写入的数据分成包的形式,将其写入到中间队列中。其中的数据由 DataStreamer 来读取。 DataStreamer 的职责是让 NameNode分配新的块——通过找出合适的 DataNode——来存储作为备份而复制的数据。这些DataNode 组成一个流水线,我们假设这个流水线是个三级流水线,那么里面将含有三个节点。此时, DataStreamer 将数据首先写入到流水线中的第一个节点。此后由第一个节点将数据包传送并写入到第二个节点,然后第二个将数据包传送并写入到第三个节点。
4. FSDataOutputStream 维护了一个内部关于 packets 的队列,里面存放等待被DataNode 确认无误的 packets 的信息。这个队列称为等待队列。一个 packet 的信息被移出本队列当且仅当 packet 被流水线中的所有节点都确认无误
5. 当完成数据写入之后客户端调用流的 close 方法,在通知NameNode 完成写入之前,这个方法将 flush 残留的 packets,并等待确认信息( acknowledgement)。 NameNode 已经知道文件由哪些块组成(通过 DataStream 询问数据块的分配),所以它在返回成功前只需要等待数据块进行最小值复制。
关于写入数据的时候 DataNode 发生错误的处理过程如下:发现错误之后,首先关闭流水线,然后将没有被确认的数据放到数据队列的开头,当前的块被赋予一个新的标识,这信息将发给 NameNode,以便在损坏的数据节点恢复之后删除这个没有被完成的块。然后从流水线中移除损坏的 DataNode。之后将这个块剩下的数据写入到剩下的两个节点中。NameNode 注意到这个块的信息还没有被复制完成,他就在其他一个 DataNode 上安排复制。接下来的 Block 写入操作就和往常一样了。
访问权限
为以下四类:
只读权限 -r :最基本的文件权限设置,应用于所有可进入系统的用户,任意一个用户读取文件或列出目录内容时只需要只读权限。
写入权限 -w :用户使用命令行或者 API 接口对文件或文件目录进行生成以及删除等操作的时候需要写入权限。
读写权限 -rw :同时具备上述两种权限功能的一种更加高级的权限设置。
执行权限 -x :一种特殊的文件设置, HDFS 目前没有可执行文件,因此一般不对此进行设置,但是可将此权限用于对某个目录的权限设置以对用户群加以区分。
HDFS控制(Java)
hadoop中关于文件操作类基本上全部是在org.apache.hadoop.fs包中,这些api能够支持的操作包含:打开文件,读写文件,删除文件等。
FileSystem,该类是个抽象类,只能通过来类的get方法得到具体类。get方法存在几个重载版本,常用的是这个:
static FileSystem get(Configurationconf);
代码演示
packagecom.shawn.hdfs;
importjava.io.IOException;
importorg.apache.hadoop.conf.Configuration;
importorg.apache.hadoop.fs.FSDataInputStream;
importorg.apache.hadoop.fs.FSDataOutputStream;
importorg.apache.hadoop.fs.FileStatus;
importorg.apache.hadoop.fs.FileSystem;
importorg.apache.hadoop.fs.Path;
publicclassHadoopFSOperations {
publicstaticvoid main(String[] args) throws Exception {
// createNewHDFSFile("/tmp/create2.c","hello");
//System.out.println(readHDFSFile("/tmp/copy.c").toString());
// mkdir("/tmp/testdir");
// deleteDir("/tmp/testdir");
listAll("/tmp/");
}
/*
* upload the local file to the hds notice thatthe path is full like
* /tmp/test.c
*/
publicstaticvoid uploadLocalFile2HDFS(String s,String d) throws IOException {
Configurationconfig = new Configuration();
FileSystemhdfs = FileSystem.get(config);
Pathsrc = new Path(s);
Pathdst = new Path(d);
hdfs.copyFromLocalFile(src, dst);
hdfs.close();
}
/*
* create a new file in the hdfs. notice thatthe toCreateFilePath is the
* full path and write the content to the hdfsfile.
*/
publicstaticvoid createNewHDFSFile(String toCreateFilePath, String content) throwsIOException {
Configurationconfig = new Configuration();
FileSystemhdfs = FileSystem.get(config);
FSDataOutputStreamos = hdfs.create(new Path(toCreateFilePath));
os.write(content.getBytes("UTF-8"));
os.close();
hdfs.close();
}
/*
* delete the hdfs file notice that the dst isthe full path name
*/
publicstaticboolean deleteHDFSFile(String dst) throws IOException {
Configurationconfig = new Configuration();
FileSystemhdfs = FileSystem.get(config);
Pathpath = new Path(dst);
booleanisDeleted = hdfs.delete(path);
hdfs.close();
returnisDeleted;
}
/*
* read the hdfs file content notice that thedst is the full path name
*/
publicstaticbyte[] readHDFSFile(String dst) throws Exception {
Configurationconf = new Configuration();
FileSystemfs =FileSystem.get(conf);
// check if the file exists
Pathpath = new Path(dst);
if (fs.exists(path)) {
FSDataInputStreamis = fs.open(path);
// get the file info to create the buffer
FileStatusstat = fs.getFileStatus(path);
// create the buffer
byte[] buffer = newbyte[Integer.parseInt(String.valueOf(stat.getLen()))];
is.readFully(0,buffer);
// 多次读取
// int length = 0;
// while ((length = is.read(buffer, 0,128)) != -1) {
// System.out.println(new String(buffer, 0,length));
// }
is.close();
fs.close();
returnbuffer;
} else {
thrownew Exception("thefile is not found .");
}
}
/*
* make a new dir in the hdfs
* the dir may like '/tmp/testdir'
*/
publicstaticvoid mkdir(String dir) throws IOException {
Configurationconf = new Configuration();
FileSystemfs =FileSystem.get(conf);
fs.mkdirs(new Path(dir));
fs.close();
}
/*
* delete a dir in the hdfs
* dir may like '/tmp/testdir'
*/
publicstaticvoid deleteDir(String dir) throws IOException {
Configurationconf = new Configuration();
FileSystemfs =FileSystem.get(conf);
fs.delete(new Path(dir));
fs.close();
}
publicstaticvoid listAll(String dir) throws IOException {
Configurationconf = new Configuration();
FileSystemfs =FileSystem.get(conf);
FileStatus[]stats = fs.listStatus(new Path(dir));
for (inti =0; i<stats.length; ++i) {
if (stats[i].isFile()) {
// regular file
System.out.println(stats[i].getPath().toString());
}elseif (stats[i].isDirectory()) {
// dir
System.out.println(stats[i].getPath().toString());
}elseif (stats[i].isSymlink()) {
// is s symlink in linux
System.out.println(stats[i].getPath().toString());
}
}
fs.close();
}
}
FileSystem
我们知道,首先对于任何文件系统都是与当前环境变量紧密联系一起,对于当前 HDFS来说,在创建出当前文件系统实例之前,有必要获得当前的环境变量。代码见下:
Configuration conf = newConfiguration();
Configuration 类为用户提供了对当前环境变量的一个实例,其中封装了当前搭载环境的配置,这配置是在我们由 core-site.xml 设置,一般返回值默认的本地系统文件。
而对于 HDFS 为我们提供的 FileSystem,更进一步为我们提供了一套加载当前环境并建立读写路径的 API,使用的方法如下所示:
publicstatic FileSystem get(Configuration conf) throws IOException
publicstatic FileSystem get(URI uri, Configuration conf) throws IOException
第一个方法使用默认的 URI 地址获取当前对象中环境变量加载的文件系统,第二个方法使用传入的 URI 获取路径指定的文件系统。
FSDataInputStream
我们在对 FSDataInputStream 进行分析之前,将上面例子中代码替换为如下所示:
InputStream fis = fs.open(inPath);
程序依旧可以正常运行。
publicclassFSDataInputStream extends DataInputStream implementsSeekable, PositionedReadable {
publicsynchronizedvoid seek(longdesired) throwsIOException {
((Seekable)in).seek(desired);
}
publicint read(longposition, byte[] buffer, intoffset, intlength) throwsIOException {
return ((PositionedReadable) in).read(position, buffer, offset, length);
}
publicvoid readFully(longposition, byte[] buffer, intoffset, intlength) throwsIOException {
((PositionedReadable)in).readFully(position, buffer, offset, length);
}
publicvoid readFully(longposition, byte[] buffer) throwsIOException {
((PositionedReadable)in).readFully(position, buffer, 0, buffer.length);
}
}
其实read(byte[] b)方法和readFully(byte []b)都是利用InputStream中read()方法,每次读取的也是一个字节,只是读取字节数组的方式不同,查询jdk中源代码发现
read(byte[] b)方法实质是读取流上的字节直到流上没有字节为止,如果当声明的字节数组长度大于流上的数据长度时就提前返回,而readFully(byte[] b)方法是读取流上指定长度的字节数组,也就是说如果声明了长度为len的字节数组,readFully(byte[] b)方法只有读取len长度个字节的时候才返回,否则阻塞等待,如果超时,则会抛出异常 EOFException。
publicclassFSDSample {
publicstaticvoid main(String[] args) throws Exception {
Configurationconf = new Configuration(); // 获取环境变量
FileSystemfs =FileSystem.get(conf); // 获取文件系统
FSDataInputStreamfsin = fs.open(new Path("sample.txt")); //建立输入流
byte[] buff = newbyte[128]; // 建立辅助字节数组
intlength = 0;
while ((length = fsin.read(buff, 0, 128))!= -1) { // 将数据读入缓存数组
System.out.println(new String(buff, 0, length)); //打印数据
}
System.out.println("length = " + fsin.getPos()); // 打印输入流的长度
fsin.seek(0); // 返回开始处
while ((length = fsin.read(buff, 0, 128))!= -1) { // 将数据读入缓存数组
System.out.println(new String(buff, 0, length)); //打印数据
}
fsin.seek(0); // 返回开始处
byte[] buff2 = newbyte[128]; // 建立辅助字节数组
fsin.read(buff2, 0, 128);
System.out.println("buff2 =" + new String(buff2));
System.out.println(buff2.length);
}
}
上述代码是重复读取指定 HDFS 中文件的内容,第一次读取结束后,调用 seek(0)方法从而返回文件开始处重新进行数据读取。
FSDataOutputStream
public FSDataOutputStream create(Path f) throws IOException {
return create(f, true);
}
public FSDataOutputStream create(Path f, booleanoverwrite) throws IOException {
return create(f, overwrite, getConf().getInt("io.file.buffer.size", 4096), getDefaultReplication(),
getDefaultBlockSize());
}
FSDataOutputStream 也是继承自 OutoutStream 的一个子类,专用为 FileSystem 提供文件的输出流。然后可以使用 OutoutStream 中的 write 方法对字节数组进行写操作。
publicclassFSWriteSample {
publicstaticvoid main(String[] args) throws Exception {
Pathpath = new Path("writeSample.txt"); // 创建写路径
Configurationconf = new Configuration(); // 获取环境变量
FileSystemfs =FileSystem.get(conf); // 获取文件系统
FSDataOutputStreamfsout = fs.create(path); // 创建输出流
byte[] buff = "helloworld".getBytes(); // 设置输出字节数组
fsout.write(buff); //开始写出数组
IOUtils.closeStream(fsout); // 关闭写出流
}
}
public FSDataOutputStream create(Path f,Progressable progress) throwsIOException {
}
有一个是Progressable progress 的形参, Progressable 接口如下所示:
publicinterface Progressable {
publicvoid progress(); //调用 progress 方法
}
Progressable 接口中只有一个 progress 方法,每次在 64K 的文件写入既定的输入流以后,调用一次 progress 方法,这给我们提供了很好一次机会,可以将输出进度显示,代码如下:
publicclassFSWriteSample2 {
staticintindex = 0;
publicstaticvoid main(String[] args) throws Exception {
StringBuffersb = new StringBuffer(); // 创建辅助可变字符串
Randomrand = new Random();
for (inti =0; i<9999999; i++) { // 随机写入字符
sb.append((char) rand.nextInt(100));
}
byte[] buff = sb.toString().getBytes(); // 生成字符数组
Pathpath = new Path("writeSample.txt"); // 创建路径
Configurationconf = new Configuration(); // 获取环境变量
FileSystemfs =FileSystem.get(conf); // 获取文件系统
FSDataOutputStreamfsout = fs.create(path, new Progressable() { // 创建写出流
@Override
publicvoid progress() { //默认的实用方法
System.out.println(++index); // 打印出序列号
}
});
fsout.write(buff); //开始写出操作
IOUtils.closeStream(fsout); // 关闭写出流
}
}
注意: FSDataOutputStream 与 FSDataInputStream 类似,也有 getPos 方法,返回是文件内以读取的长度。但是不同之处在于, FSDataOutputStream 不能够使用 seek 方法对文件重新定位
文件同步与并发访问
HDFS 提供多用户共同访问当前文件的功能,但是我们知道,对于多用户同时访问来说,文件的并发处理是一个非常难以解决的问题。例如当多名用户同时请求访问一个数据,而此时恰有一名管理员在对数据进行更新,则有可能造成堵塞或者造成用户看到的是不是及时准确的数据。
对于文件在创建时,由于文件从创建到写入数据完毕有一个时间延迟,在此延迟时间内此文件对除创建者之外的所有用户来说是透明的,并不能查看到文件的存在。为了解决此问题, HDFS 专门设置了一个方法用来强制所有的 HDFS 文件体系内的缓存与数据点进行同步,即调用 sync()方法来处理,当 sync 调用完毕后,能够确保所有用户对文件的查看具有一致性。
publicclassSyncSample {
publicstaticvoid main(String[] args) throws Exception {
Configurationconf = new Configuration();
FileSystemfs =FileSystem.get(conf);
Pathpath = new Path("syncSample.txt");
System.out.println("文件是否存在: " + fs.isFile(path));
FSDataOutputStreamfsout = fs.create(path);
fsout.writeUTF("helloWorld");
fsout.flush(); // 将缓存内容输出
fsout.sync(); // 更新所有节点
}
}
提示: FSDataOutputStream 类中的 close 方法隐含地含有 sync 方法,因此可以通过执行close 方法来对节点进行更新。
Hadoop文件控制
文件压缩
在海量数据产生后,我们首先需要对大规模数据进行存储,其后才能开始处理,但大规模数据的传输与存储本身就是一项艰难的工作,在将文件存储到 Hadoop 文件存储系统之前,推荐使用压缩对数据进行预处理。其好处也是不言而喻的,第一是节省了大量存储空间,第二则是由于便于 Hadoop 的任务处理机制。
压缩种类
压缩格式 | 工具 | 算法 | 文件后缀名 | 可分割 |
DEFLATE | 无 | DEFLATE | .deflate | 不 |
gzip | gzip | DEFLATE | .gz | 不 |
bzip2 | bzip2 | bzip2 | .bz2 | 可 |
LZO | Lzop | LZO | .lzo | 不 |
LZ4 | 无 | LZ4 | ,lz4 | 不 |
Snappy | 无 | Snappy | .snappy | 不 |
小提示:所有的压缩算法都是在空间与时间上做出一种平衡,即牺牲时间换取压缩空间,或者牺牲压缩空间换取压缩时间。例如在对即时性要求比较高的数据库设计上,一般要求注重压缩与解压缩时间,则程序设计人员会选择压缩与解压缩速度快的压缩格式,而对一般大文件存储时,则更选用注重节省压缩空间的算法。
比较而言, gzip 在对时间与空间的处理问题上更加均衡一些,也是我们在日常使用中优先考虑的压缩格式, bzip2 相对 gzip 来说,其压缩效率更高,压缩后的文件占据的空间更小,但是其需要的时间更长。 LZO、 LZ4 以及 Snappy 均优化了压缩速度,相对于 gzip 更快,但是从压缩率来看,此三种压缩率更低一些。特别需要注意, Snappy 及 LZ4 此两种压缩方式在解压缩中,明显的比 LZO 要快,这也是我们需要了解的地方。
我们知道,在 HDFS 文件格式中,文件是在分割在一个个不同的 Block中进行存储,每个标准 Block 大小为 64M,如果 MapReduce 将使用一个 1G(1024M)大小的文件,那么该文件首先会被存储在 16 个标准 Block 中,而将每个标准数据块作为一个 Map 任务的输入。那么我们假设使用 gzip 作为文件压缩的工具,将一个大小为 1G( 1024M)的文件分成16 个标准 Block,你会发现 MapReduce 不能运行,究其原因是系统默认的 DEFLATE 算法( gzip真正核心算法)在压缩过程中将数据进行连续的非指向性排列,若从其中一个位置被分割,那么无法确保 MapReduce 在进行计算完毕一个单独的 Block 后,能够及时准确获取接下来一个连续的块的位置。现在我们知道,表中可分割的意思就是标注 MapReduce 任务是否可以从某块被分割后的单个文件的开始(也可能是任意) 位置开始读取。那么读者不禁会问, Gzip 格式是不是就不能使用了呢?
小提示:回想下我们在前面说的压缩文件无非就是时间换空间或者空间换时间, gzip压缩后的大文件,被 Hadoop 以连续的方式存储在某个连续的若干个相同节点的 Block 中,对需要的存储容量进行了压缩,但是由于其在计算时,若干个 Block 将会由同一个MapReduce 任务来处理,因而大大降低了整体运行速度
第一个压缩程序
压缩类的使用对于 Hadoop 来说是透明的, MapReduce 可以根据文件后缀名自动识别出压缩文件,从而对相关的文件提供对应的算法进行解压缩。下面代码演示了使用 Gzip 类将内存中的数据写到 HDFS 中的具体方法。
publicclass SimpleCompression {
publicstaticvoid main(String[] args) throws Exception {
Configurationconf = new Configuration();
PathoutputPath = new Path("helloworld.gz");
FileSystemfs =FileSystem.get(conf); // 创建文件系统
OutputStreamos = fs.create(outputPath); // 创建输出路径
CompressionCodeccodec = new GzipCodec(); // 生成压缩格式实例
byte[] buff = "helloworld".getBytes(); // 将字符串转为字符数组
CompressionOutputStreamcos = codec.createOutputStream(os); // 创建压缩环境地址
cos.write(buff); // 开始写入数据
cos.close(); // 写入完成后关闭输出流
}
}
CompressionCodec接口
Hadoop 中基本上所有常用的压缩类都是实现了 CompressionCodec 接口,表 3-2 为我们列举了所有 Hadoop 中的压缩类,当然要注意的是一些类的使用需要下载相对应的扩展包。 Hadoop 自带的压缩类库如表所示:
压缩格式 | 压缩类库 |
DEFLATE | org.apache.hadoop.io.compress.DefaultCode |
gzip | org.apache.hadoop.io.compress.GzipCode |
bzip2 | org.apache.hadoop.io.compress.Bzip2Code |
LZO | com.hadoop.compression.lzo.lzoCode |
LZ4 | org.apache.hadoop.io.compress.lz4Code |
Snappy | org.apache.hadoop.io.compress.SnappyCode |
小提示:目前来说常用的是 gzip 与 bzip2,而 LZO、 LZ4、 Snappy 因为版权关系并没有包含在目前版本中需要额外从单独下载。
CompressionCodec 提供了一个易用的方法便于我们对文件进行压缩,简单的说,如果要对某一个具体内容的数据进行压缩,首先应该采用的是createOutputStream(OutputStreamout)方法,其目的是创造一个 CompressionOutputStream,即一个压缩文件输出流,根据形参的 out 的设定,调用对应的 write( byte[] buff)方法,将数据内容以二进制数组中的形式写入 HDFS 中。
CompressionCodec 同时还提供了一个解压缩的方法,调用如下
方法:
CompressionInputStream cis =codec.createInputStream(in); // 创建压缩输入类
代码示例
publicclass SimpleUncompression {
publicstaticvoid main(String[] args) throws Exception {
CompressionCodeccodec = new GzipCodec(); // 创建压缩格式实例
Configurationconf = new Configuration(); // 设置环境变量
FileSystemfs =FileSystem.get(conf); // 创建文件系统
FSDataInputStreamin = fs.open(new Path("helloworld.gz")); // 根据地址创建输入流
CompressionInputStreamcis = codec.createInputStream(in); // 创建输入流
byte[] buff = newbyte[128]; // 使用数组存放输入数据
intlength = 0; // 设置帮助变量
while ((length = cis.read(buff, 0, 128))!= -1) { // 循环输入数据至数组中
System.out.println(new String(buff, 0, length)); // 将数组数据打印
}
}
CompressionCodecFactory类详解
上述代码中有一句:
CompressionCodec codec = new GzipCodec();
此语句的作用是对压缩的格式进行设置。例中我们知道我们所使用的 Gzip格式的压缩文件,但是在实际的程序设计中,往往并不能很直观或者很方便的获取压缩文件的格式,或者说明我们不能够直接使用压缩类,因此得通过一个规则去获取到文件的压缩类型,那就是后缀名。
Hadoop 为我们提供了解决办法,一个名为 CompressionCodecFactory 的类。在介绍之前,我们首先看一下这个类的构造源码:
List<Class<? extendsCompressionCodec>>codecClasses = getCodecClasses(conf); //根据环境变量产生压缩类列表
if (codecClasses == null) { // 对列表容量判读
addCodec(new GzipCodec()); // 若列表容量为空则添加
addCodec(new DefaultCodec()); // 若列表容量为空则添加
}
代码示例
publicclass CompressionFactoryDemo {
publicstaticvoid main(String[] args) throws Exception {
Pathpath = new Path(args[0]); // 获取文件输入路径
Configurationconf = new Configuration(); // 创建环境变量
FileSystemfs =FileSystem.get(conf); // 创建文件系统
CompressionCodecFactoryfactory = new CompressionCodecFactory(conf);// 使用工厂模式获取本地环境变量中压缩格式
CompressionCodeccodec = factory.getCodec(path); // 获取输入文件压缩格式
FSDataInputStreamin = fs.open(path); // 创建输入流
CompressionInputStreamcis = codec.createInputStream(in); // 创建压缩文件输入流
byte[] buff = newbyte[128];
intlength = 0;
while ((length = cis.read(buff, 0, 128))!= -1) {
System.out.println(new String(buff, 0, length));
}
}
}
小提示:根据后缀名判断文件格式是一个通用的准则。
压缩池
对于Hadoop而言,压缩与解压工作是一个重量级任务,特别在频繁的压缩与解压过程中,需要耗费大量的资源去满足需求,因此在进行大量的压缩或者解压缩任务时,可以考虑使用压缩池去帮我们完成任务。
org.apache.hadoop.io.compress.CodecPool
压缩池包含两个重要方法getCompressor(CompressionCodeccodec) 及returnCompressor(Compressor compressor),从名称上可以看到, getCompressor方法是从压缩池中获取一个闲置资源,如果没有就创建一个,而returnCompressor方法则是将已经调用完毕的压缩资源归还到压缩池中
代码示例
publicclass CodecsPoolDemo {
publicstaticvoid main(String[] args) {
Configurationconf = new Configuration(); // 获取环境变量
CompressionCodecgzipCodec = new GzipCodec(); // 获取 Gzip 压缩格式实例
CompressionCodecbzip2Codec = new BZip2Codec(); // 获取 Bzip 压缩格式实例
Compressorcompressor = null; // 压缩池对象
try {
compressor = CodecPool.getCompressor(gzipCodec); //对压缩池对象进行赋值
}finally {
CodecPool.returnCompressor(compressor); // 取消压缩池对象赋值
compressor = CodecPool.getCompressor(bzip2Codec); //重新对压缩池对象赋值
}
}
}
值得注意的是,部分压缩格式在java程序中本身就具备,因此可以获得更好的支持,例如Gzip文件,由于其本身具有开源性,建议在使用Hadoop进行压缩相关操作时,尽量应当首先尝试使用内置的压缩格式,以便获得JDK支持从而达到更快的响应速度。在对比测试中,使用JAVA内置的Gzip类可以获得比单纯使用Hadoop支持的Gzip类非减少10%左右的压缩时间以及50%左右的解压缩时间。
小提示:但是并非所有的压缩文件格式都有 Java 本身支持,例如 Bzip2,其并没有 Java支持,只能使用 Hadoop 自带的压缩类库进行操作。而某些类库,却只有 Java 的压缩文件支持而没有 Hadoop 压缩格式的支持,例如 LZO。
在 MapReduce 中使用压缩
对于 Hadoop 来说,压缩是透明的,可以根据程序的后缀名自动识别压缩从而产生对应的压缩对象和调用相关的格式,也就是说如果输入的是被压缩后的数据,那么 Hadoop 可以自动根据后缀名进行解压缩
小提示:这里用到了是 Java 中的反射。
输出端的压缩操作
有两种方式可以设置输出端的压缩操作,第一种是通过配置.xml 的方式对压缩进行配置,打开 $Hadoop_HOME/src/mapred/mapred-default.xml 文件,修改原配置如下:
<property>
<name>mapred.output.compress</name>
<value>true</value>
<description>Shouldthe job outputs be compressed?
</description>
</property>
<property>
<name>mapred.output.compression.codec</name>
<value>org.apache.hadoop.io.compress.GzipCodec</value>
<description>Ifthe job outputs are compressed, how should they be
compressed?
</description>
</property>
小提示: value 中的值要求填写全称,不能只写 GzipCodec,表 3-3 为我们提供了目前Hadoop 自带的所有压缩类的对应库名。
压缩属性名 | 所属类库 | 说明 |
DefaultCodec | org.apache.hadoop.io.compress.DefaultCodec | 所有压缩格式对应的类库名 |
GzipCodec | org.apache.hadoop.io.compress.GzipCodec | |
BZip2Codec | org.apache.hadoop.io.compress.BZip2Codec |
第二种方式是通过代码的形式,在main方法中通过调用 Contribution类中的set方法动态的实现对压缩类的确定,并设定相应的压缩格式对输出结果进行压缩,代码片段如下所示:
publicstaticvoid main(String[] args) throws Exception {
Jobjob = new Job(); // 生成工作任务
job.setJarByClass(MyFirstMapReduce.class); // 设置主文件类
FileInputFormat.addInputPath(job, new Path("a.txt")); // 设置输入文件地址
FileOutputFormat.setOutputPath(job, new Path("out")); // 设置输出文件地址
job.setOutputKeyClass(Text.class); // 定义输出格式 key类型
job.setOutputValueClass(IntWritable.class); // 定义输出格式 value类型
FileOutputFormat.setCompressOutput(job, true); // 确认打开压缩格式
FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);//设置压缩格式代码
job.setMapperClass(MyMap.class);
job.setReduceClass(MyReduce.class);
job.waitForCompletion(true);
}
输出过程中的压缩
输出过程中的压缩主要是对map结果的压缩,以便减少IO传输量。也有两种方式:
第一是修改Hadoop配置文件,打开$Hadoop_HOME/src/mapred/mapred-default.xml文件,修改原配置如下:
<property>
<name>mapred.compress.map.output</name>
<value>true</value>
<description>Shouldthe outputs of the maps be compressed before being
sentacross the network. Uses SequenceFile compression.
</description>
</property>
<property>
<name>mapred.map.output.compression.codec</name>
<value>org.apache.hadoop.io.compress.GzipCodec</value>
<description>Ifthe map outputs are compressed, how should they be
compressed?
</description>
</property>
第二种方法在main方法中设置参数,代码如下:
publicstaticvoid main(String[] args) throws Exception {
Configurationconf = 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);
job.setJarByClass(MyFirstMapReduce2.class);
FileInputFormat.addInputPath(job, new Path("a.txt"));
FileOutputFormat.setOutputPath(job, new Path("out"));
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
job.setMapperClass(MyMap.class);
job.waitForCompletion(true);
}
文件序列化
所谓序列化( Serialization)使用官方的定义就是指将数据采用流的形式存储,以便于数据在网络传输或者写入磁盘上。这可读者可能要问,什么叫将数据以流的形似存储,简单的理解就是将无法或者难以被传输或存储的数据改变其存储结构,使之便于存储或传递,此谓序列化。
小提示:你可以把序列化理解成将复杂的数据格式变形为便于传输和存储的文件格式。例如全部转化成二进制传输。
我们知道,目前 Hadoop 采用运算模型是多个分节点共同进行 Map 任务,然后将分节点上的已经计算完毕的任务发送给 Reduce 任务节点共同进行任务操作, Reduce 任务节点收到信息后开始计算。但是这里产生的问题就是有些数据无法进行远程传输,或者无法保证传输质量,因此,为了解决这些问题, Hadoop 提出了一种消息的序列化格式,将消息在发送前人为或者自动修改成特定格式进入传输,在收到消息后,根据传来的序列化格式自动对其进行反序列化。这里反序列化的意思就是将数据从流的形式转化为原始的数据格式。
Text类
Text类的诞生就是用于在Hadoop中替代String类,是用于对一般字符串的操作。对于大多数的操作API来说, Text的用法与String类型完全一样,但是对于某些方法,其输出结果却是不同
publicclassStringTest {
publicstaticvoid main(String[] args) {
Stringstr = "helloworld"; // 定义字符串
System.out.println(str.length()); // 获取输出字符长度
System.out.println(str.indexOf("lo")); // 获取”lo”对应的位置
System.out.println(str.indexOf("world")); // 获取”world”对应的位置
System.out.println(str.charAt(0)); // 获取第 0 个字符
System.out.println(str.codePointAt(0)); // 获取第 0 个字符量
}
}
importorg.apache.hadoop.io.Text;
publicclassTextTest {
publicstaticvoid main(String[] args) {
Texttext = new Text("hello world"); // 定义Text 类型
System.out.println(text.getLength()); // 获取 Text 类型长度
System.out.println(text.find("lo")); // /获取”lo”对应的位置
System.out.println(text.find("world")); // 获取”world”对应的位置
System.out.println(text.charAt(0)); // 获取第 0 个字符
}
}
Text除了继承自 String类型的某些特征外,其通过继承Java中StringBuffer方法的某些有点对其本身进行了更好的提升,例如继承了 StringBuffer的可变性,可以通过调用set(String str)对Text对象在构造完毕后重新进行赋值,也可以调用其append(String str)方法,在已经构成完毕的Text对象后面进行追加字符串,代码如下
Texttext = new Text();
text.set("had");
text.append("oop".getBytes(), 0, "oop".getBytes().length);
System.out.println(text);
常用方法:
返回值 | 方法名 | 说明与描述 |
void | append(byte[] buff, int start, int len) | 将数组中内容追加到Text对象后面, Start是 开始索引, len是数组长度 |
void | clear() | 清除当前Text中内容 |
String | decode(byte[] buff) | 将二进制数组转成一个String对象 |
ByteBuffer | encode(String string) | 将String类型转化成ByteBuff对象 |
int | find(String what) | 找到某个String对象,返回其在当前Text对象 中的偏移量 |
Byte[] | getBytes() | 将当前Text对象转换成byte[]对象 |
String | readString(DataInput in) | 从流中读取String对象 |
void | set(String string) | 对当前Text对象清空并重新赋值 |
String | toString() | 生成当前Text对象所对应的String对象 |
以上可以看到,虽然Text实现了类似String类型的对字符串的操作,但是Text相对String来说,并没有太多的方法可供使用,因此我们建议读者在对Text进行使用时,先对其进行转换,将之转换成String类型操作完毕后再生产Text进行存入,参考下面代码示例:
publicclassTextToUpperCase {
publicstaticvoid main(String[] args) {
Texttext = new Text("Hadoop is the best cloud language");
Stringstr = text.toString();
String[]temp = str.split(" ");
for (inti =0; i<temp.length; i++) {
temp[i] = temp[i].substring(0, 1).toUpperCase() + temp[i].substring(1);
}
StringBuffersb = new StringBuffer();
for (String string : temp) {
sb.append(string);
sb.append(" ");
}
text.set(sb.toString());
System.out.println(text.toString());
}
}
IntWritable类
IntWritable 可以理解成就是简单的 int 类型在 Hadoop 中的包装,下面是IntWritable中的部分代码:
public IntWritable(int value) {
set(value);
} // 设置Integer 中的取值
publicvoid set(intvalue) {
this.value = value;
} // 设置Integer 中取值
publicint get() {
return value;
} // 获取设定值
publicvoid readFields(DataInput in) throws IOException { //创建读取方法
value= in.readInt();
}
publicvoid write(DataOutput out) throws IOException { //创建写入方法
out.writeInt(value);
}
可以看到IntWritable 就是在其内部封装了一个 int 类型的数据,通过 set 与 get 方法对其进行值进行存取,通过 readFields(DataInput in) 与 write(DataOutput out)方法对其进行输出与输入操作。
小提示:请牢记,对 IntWritable 类型数据进行处理应该首先将其转化成 int 类型进行处理。
为了实现IntWritable 之间的比较操作, IntWritable 实现了两种比较,第一种是两个 IntWritable 之间相互的比较,直接对其中的取值进行比较,源代码如下:
publicint compareTo(Object o) {
intthisValue = this.value; // 对比较数据复制
intthatValue =((IntWritable) o).value; // 将待比较数据进行转型
return (thisValue<thatValue ? -1 : (thisValue == thatValue ? 0 : 1)); //进行比较处理
}
另外一种则是通过实现 WritableComparator 接口中的compare(byte[] b1, int s1, intl1,byte[] b2, int s2, int l2)方法,通过对流中序列化的数据进行直接比较而省去了创建具体对象的资源调度,通过从指定字节中某个位置( s1、 s2)获取指定长度(l1、l2)而直接进行比较,代码如下:
publicstaticclass Comparator extendsWritableComparator {
publicint compare(byte[] b1, ints1, intl1, byte[] b2, ints2, intl2) {
intthisValue =readInt(b1, s1); //从 int 数组中读取数值
intthatValue =readInt(b2, s2); ///从数组中读取比较数值
return (thisValue<thatValue ? -1 : (thisValue == thatValue ? 0 : 1)); //进行比较处理
}
}
ObjectWritable类
Hadoop在运行过程中需要不停的对各个节点进行通信,而不同的数据类型对于通信的影响很大,因此,在借鉴Java中Object类的基础上, Hadoop定义了一个新的类ObjectWritable。ObjectWritable的作用是对Java语言中各个通用基本类型的包装,同时在Hadoop中对于内部通信产生的某些特定类型的对象做一个通用的封装。
小提示:ObjectWritable 类中并不是对某些 Hadoop 的基本类型的封装,例如上文所说的 Text 与IntWritbale,而是直接对 Java 中的某些类型进行封装,例如 String, int, enum 等,Java常见的基本类型。
通过源码分析我们可以看到ObjectWritable在构造出来的时候自动传递了某个Object的对象作为其内部对象,参见下文部分代码:
public ObjectWritable(Class declaredClass, Object instance) {
this.declaredClass = declaredClass; // 获取类型
this.instance = instance; // 创建类型实例
}
写方法代码如下:
publicvoid write(DataOutput out) throws IOException { //创建写入方法
writeObject(out, instance, declaredClass, conf); // 调用Text 原生写入方法
}
ObjectWritable不过是在某个基本Object上进行了一层包装,观察其write(DataOutputout)方法,其在内部调用 writeObject(out, instance, declaredClass,conf)方法,部分源代码如下所示:
if (declaredClass == String.class) { // 如果为 String 类型
UTF8.writeString(out, (String) instance); // 使用 UTF8 默认写方法
} elseif (declaredClass == Byte.TYPE) { //如果为 byte 类型
out.writeByte(((Byte) instance).byteValue()); // 使用byte 写方法
}
ObjectWritable通过传递进来的declaredClass获取具体Class类型, ObjectWritable调用其特定的写方法,将数据输出到相应的流中,完成数据的写工作。
对于readObject(DataInputin, Configuration conf)方法,通过判断相应的declaredClass类型,将特定类型读取到相应的Object instance中,最终将其返回。代码如下所示:
publicvoid readFields(DataInput in) throws IOException { //创建读取方法
readObject(in, this, this.conf);
}
publicstatic Object readObject(DataInput in,ObjectWritable objectWritable, Configuration conf)
throws IOException {
Objectinstance; // 创建对象用于构造实例
StringclassName = UTF8.readString(in); // 使用 UTF8 格式在输入流读取字符
Class<?>declaredClass = PRIMITIVE_NAMES.get(className); // 获取输入类型
if (declaredClass == String.class) { // 如果为 String 类型
instance = UTF8.readString(in); // 使用UTF8 默认读方法
} elseif (declaredClass == Byte.TYPE) { //如果为 byte 类型
instance = Byte.valueOf(in.readByte());// 使用byte 默认读方法
returninstance; // 返回实例
}
}
NullWritable类
相对于ObjectWritable是对Java基本类型的封装而言, NullWritable并不是一个null的封装,而是一个单独的特殊的Hadoop基本类型。 NullWritable构造方法为空,没有任何数据的读写操作,其值为空,没有长度的计算。源码如下:
private NullWritable() {
}
public String toString() {
return"(null)";
}
publicint hashCode() {
return 0;
}
publicint compareTo(Object other) {
if ((otherinstanceof NullWritable)) {
return 0; // 返回值为 0
}
}
NullWritable类存在的目的就是用作占位符,如果在Hadoop中某个位置不需要使用一个具体的值,可以将之声明为NullWritable,结果是存储某个值为空的常量。
小提示:使用 NullWritable 时,建议使用 NullWritable.get()获得真正一个值为 null 的数据值。
ByteWritable类
ByteWritable是用来包装二进制数组的一个序列化类,其构造方法如下所示:
public BytesWritable(byte[] bytes){ // 使用byte 类构造方法
this.bytes = bytes; //对内部支持 byte 赋值
this.size = bytes.length; // 获取长度
}
由构造方法来看, ByteWritable 接受一个二进制数组。通过相应的 get()与 set()方法对其取值与赋值。但是值得注意的是,对于 ByteWritable 来说,其长度的方法 getLength()与获得返回数组值后取长度的值不一样,究其原因 BytesWritable 的实现中,保存了一个 byte[]存放内容,一个 int size 表示 byte 数组里面前多少位是有效的,后面的是无效的,但是ByteWritable 的 getBytes()方法返回的确实 byte 数组的全部内容(长度很可能大于 size),所以在 Mapper 中进行处理的时候应该只操纵 size 大小的内容,后面的应该无视掉。
实现自定义的 Writable 类型
Hadoop将Java本身的一些类型进行了二次包装产生了 Text、IntWritable、 ObjectWritable等类型。除此之外, Hadoop还对Java基本上所有的基本数据类型进行了封装,并提供了相应的set()与get()方法对封装的值进行赋值与读取。
Java基本类型 | Hadoop封装类 | Java基本类型 | Hadoop封装类 |
boolean | BooleanWritable | float | FloatWritable |
byte | ByteWritable | long | LongWritable |
int | IntWritable | double | DoubleWritable |
Writable接口
在Hadoop中所有基本数据类型都是由 Writable结尾,通过Hadoop官方文档我们可以知道, Hadoop在包装Java基本数据类型时,使用了一个名为Writable()的接口。Writable()接口是一个简单高效的,基于基本I/O的一种序列化对象,其包含两个基本方法,源码如下所示:
publicinterface Writable {
void write(DataOutput out) throws IOException;
void readFields(DataInput in) throws IOException;
}
由源码可见, Writable接口包含两个方法, write(DataOutput out)与readFields(DataInput in) ,其功能分别是将数据写入指定的流中和从指定的流中读取数据,在前面的源码分析中,我们也能看到, Text与IntWritable类均实现了自己的write与readFields方法。
下面的示例了 Text读写的方法,将一个字符写入path指定的路径,然后新建一个名为str的Text类型将之重新读取出来。
publicclassTextWithReadAndWrite {
publicstaticvoid main(String[] args) throws Exception {
FileSystemfs =FileSystem.get(new Configuration()); // 获取文件系统
Pathpath = new Path("sample.txt"); // 建立输入文件路径
Texttext = new Text("helloWorld"); // 建立 Text 对象
text.write(fs.create(path)); // 对输出量进行写出
Textstr = new Text(); // 创建一个辅助对象
str.readFields(new FSDataInputStream(fs.open(path))); // 辅助对象进行赋值
System.out.println(str.toString());// 打印结果
}
}
WritableComparable接口与RawComparator 接口
对于Hadoop的工作过程来说,某个工作类实现Comparable接口是非常重要的,因为在其工作过程中有个比较的过程,特别是对于键(key)来来说, Hadoop为我们提供了一个简单优化高效的接口来提供这种比较的服务,WritableComparable接口与RawComparator接口 .
小提示: WritableComparable 接口与 RawComparator 接口是两个非常相近的名称。实际上实现了 WritableComparable 接口的类则声明自己提供了 compare 服务。而 RawComparator的接口的作用是提供了 compare 服务的一个具体实现的方法。
请看IntWritable那一节, IntWritable使用了一个继承自 WritableComparator的一个内部类来完成compare工作,而WritableComparator又是继承自 RawComparator接口的一个通用实现,查看RawComparator源码如下:
publicinterface RawComparator<T>extendsComparator<T> {
publicint compare(byte[] b1, ints1, intl1, byte[] b2, ints2, intl2); // 默认的比较方法
}
我们可以通过重新定义其中的compare(byte[] b1, ints1, int l1, byte[] b2, int s2, int l2)方法来提供对比较的支持,此种方法通过比较字节数组中某个固定长度的整数(通过s1与l1确定)而对字节数组进行比较。
下面我们继续分析WritableComparable这个类,其在本质上相当于一个注册类,维护内部的一个HashMap,将目前Hadoop中所有类进行注册,源代码如下:
privatestatic HashMap<Class, WritableComparator>comparators = newHashMap<Class, WritableComparator>();
WritableComparable提供的两个重要方法,第一个就是上文所说的实现了 RawComparator中的compare方法。另外一个是可以将Writable实现类具体实例化的工具,源代码如下:
publicstaticsynchronized WritableComparator get(Class<? extends WritableComparable>c) {
WritableComparatorcomparator = comparators.get(c); //根据传递类型进行判断
if (comparator == null) // 对赋值结果判断
comparator = new WritableComparator(c, true); // 产生新对象
returncomparator;
}
自定义的 Writable 类
通过了解 Hadoop 中自带的某些类以及其公共父类 Writable 类,同时带有 compare 方法的 WritableComparable 类是继承自 Writable 类,因此我们自定义的 Writable 类应该直接继承自 WritableComparable 类,并提供相应的实现。
现在开始实现我们自定义的 Writable 类。为了更好的给读者演示自定义的 Writable 类,首先我们实现我们第一个包含一个Text与IntWritable类型的自定义变量 TextAndIntPair。代码如下:
publicclassTextAndIntPair implements WritableComparable<TextAndIntPair> {
private Text text; // 定义一个 Text 类型数据
private IntWritable ints; // 定义IntWritable 类型
public TextAndIntPair() { // 自定义的构造方法
set(new Text(), new IntWritable()); // 调用 set 方法完成构造
}
public TextAndIntPair(String text, intints) { // 自定义的构造方法
set(new Text(text), newIntWritable(ints)); // 调用set 方法完成构造
}
publicvoid set(Text text, IntWritable ints) { // 对数据进行赋值
this.set = set; //对 Text 进行赋值
this.ints = ints; //对 ints 进行赋值
}
@Override
publicvoid readFields(DataInput in) throws IOException { //调用默认的读方法
text.readFields(in);
ints.readFields(in);
}
@Override
publicvoid write(DataOutput out) throws IOException { // 调用默认的写方法
text.write(out);
ints.write(out);
}
public Text getText() { // 获取Text类型的值
returntext;
}
public IntWritable getIntWritable() { // 获取IntWritable值
returnints;
}
@Override
publicboolean equals(Object obj) { // 比较方法
if (objinstanceof TextAndIntPair) { // 判断待比较类型
TextAndIntPairtemp = (TextAndIntPair) obj; // 对判断结果进行强制类型转换
returntext.equals(temp.getText()) && (ints.equals(temp.getIntWritable())); // 返回比较结果
}
returnfalse;
}
@Override
publicint hashCode() { // 获取HashCode值
returntext.hashCode()* 28 + ints.hashCode();
}
@Override
public String toString() { // 重定义toString值
returntext.toString()+ "," + ints.get(); // 返回定义值
}
@Override
publicint compareTo(TextAndIntPair o) { // 实现普通比较方法
intcompareText = text.compareTo(o.getText()); // 先对第一个Text比较
if (compareText != 0) { // 判断第一个值结果
returncompareText; // 返回不同值
}
returnints.compareTo(o.getIntWritable()); // 对第二个值进行比较
}
}
这是我们定制的第一个 Writable 类,这个类的实现非常简单,首先使用 Hadoop 基本数据类型 Text 与 IntWritable 进行基础构建,并提供 set 与 get 方法对其进行赋值与取值。通过其构造方法的调用对之进行实例化。同时因为我们自定义的Writable类继承自WritableComparable类,必须实现几个抽象方法。
小提示:关于散列值
对于包含容器类型的程序设计语言来说,基本上都会涉及到hashCode。在Java中也一样,hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable等。
为什么这么说呢?考虑一种情况,当向集合中插入对象时,如何判别在集合中是否已经存在该对象了?(注意:集合中不允许重复的元素存在)
text.hashCode() * 28中28可以改成任何值。
加速比较
我们在实现自定义的 Writable 类的时候,为了提供同种自定义的Writable 相互比较的功能,还实现了 compareTo 方法,此种方法是利用传递的形参提取特定值进行比较,这种比较方法简单易懂,就不再多做复述。
下面回忆下IntWritable 类在实现了基本 compareTo 功能外,还实现了 WritableComparator 类,我们已经介绍过,这个类是是对 RawComparator 接口的一种实现,提供了两个抽象方法用于对序列化过程中,也就是对流的内容进行比较的功能。相对于 compareTo 必须要将内容重新构建成我们所需要的类型而言,其速度自然而然的是快多了。因此我们也应该尝去完成对序列化中数据比较的方法。
首先观察IntWritable的源代码,因为其在实现时候就已经继承了WritableComparable, Hadoop 在框架的设计上顺延了 Java 设计规范的单继承模式,因此我们只能尝试去构建一个内部类,使用内部类去完成我们所需要的方法。
小提示:观察我们自己的的 TextAndIntPair 类,其构成有两部分,分别是 Text 与IntWritable, Text 类型是我们自定义类中的 Key,而 IntWritable 是我们自定义类中的 Value,因此分开来做,分别实现对 Key 值与 Value 值得比较方法。
首先我们来实现对 Key 的比较,由自定义的类可知, Key 是由一个 Text 类型所构成,而 Text 在序列化的过程中由两部分构成,分别是其字节数及字节本身构成,而我们进行比较的内容是对 Text 中那部分自己本身进行比较,因此我们比较时更应首先提取用于比较的字符。 WritableUtils 为我们提供了两个方法,分别是 decodeVIntSize 与 readVInt,这两个方法的作用分别是分别获取一个 Text 类型的长度以及根据其类型返回的编码值(例如 UTF8与 Big5 具有不同的编码值。)根据提取出来的特征值,我们调用 Text 中的 RawComparator,可以很方便的对 Text 流中的数据进行比较。下面程序给出根据 Key 值(Text)进行比较的例子。
publicclassKeyComparatorUsingText extends WritableComparator {
privatestaticfinal Text.Comparator KEY_COMPARATOR = newText.Comparator();
// 调用Text 的比较方法
protected KeyComparatorUsingText() {
super(TextAndIntPair.class); // 调用父类构造方法
}
@Override
publicint compare(byte[] b1, ints1, intl1, byte[] b2, ints2, intl2) { // 从序列中直接取值进行比较
intresult = 0;
try {
intfirstLength =WritableUtils.decodeVIntSize(b1[s1]) + readVInt(b1, s1);
// 获取第一个值长度
intsecondLength =WritableUtils.decodeVIntSize(b2[s2]) + readVInt(b2, s2);
// 获取第二个值长度
result = KEY_COMPARATOR.compare(b1, s1, firstLength, b2, s2, secondLength);
// 比较结果
if (result != 0) {
returnresult;
}
result = KEY_COMPARATOR.compare(b1, s1 + firstLength, l1 - firstLength, b2, s2 + secondLength, l2
-secondLength); // 对第二个值进行比较
} catch (IOException e) {
e.printStackTrace();
}
returnresult; // 返回结果值
}
static { // 注册比较类
WritableComparator.define(TextAndIntPair.class, new KeyComparatorUsingText());
}
}
小提示:最后的 static 代码块用于向 WritableComparator 注册,以便每次在 Hadoop 使用TextAndIntPair 这个自定义类就知道使用 KeyComparatorUsingText 作为其默认的 comparator类。
publicclassKeyComparatorUsingInt extends WritableComparator {
privatestaticfinal IntWritable.Comparator KEY_COMPARATOR = newIntWritable.Comparator(); // 获取IntWritable
protected KeyComparatorUsingInt() { // 父类构造方法进行赋值
super(TextAndIntPair.class);
}
@Override
publicint compare(byte[] b1, ints1, intl1, byte[] b2, ints2, intl2) { // 对值进行比较
intfirstLength = 0;
intsecondLength =0;
try {
firstLength = WritableUtils.decodeVIntSize(b1[s1])+ readVInt(b1, s1); // 取得第一个值长度
secondLength = WritableUtils.decodeVIntSize(b2[s2])+ readVInt(b2, s2); // 取得第二个值长度
} catch (IOException e) {
e.printStackTrace();
}
intfirstValue =readInt(b1, s1 + firstLength); // 从序列中产生第一个值
intsecondValue =readInt(b2, s2 + secondLength); // 从序列中产生第二个值
return (firstValue<secondValue ? -1 : (firstValue == secondValue ? 0 : 1)); //进行比较
}
static { // 注册比较类
WritableComparator.define(TextAndIntPair.class, new KeyComparatorUsingInt());
}
}
Hadoop 中小文件处理详解
Hadoop 天生是为了处理大型文件所诞生的,但是并不意味这其不能够用于处理大量的小文件。这里小文件的意思是指文件的 size 小于 hdfs 中每个 Block 大小的文件。如果节点计算机中存储有大量的小文件时,往往能够成为影响 Hadoop 计算性能的瓶颈。如果小文件过多, Hadoop 在执行时,需要频繁的访问各个节点,同时还要不停地生成与释放相关对应的资源。这样的频繁调度非常耗费 Hadoop 已有资源。同时我们知道 Hadoop 的文件存储是以对象的形式存放在内存中,过多的小文件及时存放在同一个基点中,也严重影响单节点上性能。举例来说,某个单节点上用于存放 word 文档或者 txt 文档,平均大小为 500byte,如果有 10000000 个如此大小的文件,则此节点上为 Hadoop 提供的内存至少需要4.5个G。因此小文件的存储处理一直是 Hadoop 研究的重点之一。
本节就是采用 SequenceFile 的形式,通过将若干规模较小的文件序列化后直接存储到一个文件中,例如处理多条日记记录时,使用小文件的文件名作为 Key,而内容作为其 Value进行整合。这样做的好处主要有以下几点:
便于操作。因为其使用的是 Hadoop 中自带的工具,可以方便地进行后期更改。
支持定制大小。可以将文件输出为既定大小的文件,例如将若干个文件生成 Block 规格大小的文件,便于后期在每个节点上进行计算。
SequenceFile 中自带的压缩格式可以为输出结果制定相应的压缩格式便于存储。
SequenceFile详解
为了更好的理解所介绍的内容,我们首先来看一个程序:
packagecom.shawn.hdfs;
importjava.util.Random;
importorg.apache.hadoop.conf.Configuration;
importorg.apache.hadoop.fs.FileSystem;
importorg.apache.hadoop.fs.Path;
importorg.apache.hadoop.io.IOUtils;
importorg.apache.hadoop.io.IntWritable;
importorg.apache.hadoop.io.SequenceFile;
importorg.apache.hadoop.io.Text;
publicclassSequenceWriteSample {
privatestatic String meg = "hello world";
publicstaticvoid main(String[] args) throws Exception {
Configurationconf = new Configuration();
// FileSystem fs = FileSystem.get(conf); //获取文件系统
// Path path = new Path("sequenceFile");// 定义路径
// 定义hdfs路径
Pathpath = new Path("hdfs://localhost:9000/wufan/sequenceFile");
FileSystemfs = path.getFileSystem(conf);
Randomrand = new Random(); // 定义辅助对象
// 创建写入格式
SequenceFile.Writerwrite = SequenceFile.createWriter(fs, conf, path, IntWritable.class, Text.class);
for (inti =0; i<100; i++){
write.append(new IntWritable(rand.nextInt(100)),new Text(meg));
}
IOUtils.closeStream(write);
}
}
执行命令:$ hadoop jarSequenceWriteSample.jar SequenceWriteSample
我们在此程序中使用 SequenceFile.Writer 定义了一个内部类的对象 write 实例,此对象通过 SequenceFile 中的 createWriter 方法,通过传递本身的各个变量,获取相应的 key 与 value的具体类型,为其写入提供基本环境变量的一个初始化工作。写入方法源代码如下:
publicstatic Writer createWriter(FileSystem fs,Configuration conf, Path name, Class keyClass,
ClassvalClass) throws IOException {
returncreateWriter(fs, conf, name, keyClass, valClass, getCompressionType(conf));
}
而定义的 write 在获取初始化的环境变量后,通过 append 方法,将以建立完成的 key 与value 的对象实例写地追加到 path 指定的内容之中,最后通过 IOUtils.closeStream 方法将流关闭。
其中 createWriter 中设置了五个形参,前四个已经有所了解,这里就不做复述。CompressionType 类用以对 SequenceFile 写入的文件设定是否进行压缩处理,通过设定三个枚举值分别用来对压缩形式进行设定:
CompressionType.NONE:表示不对任何数据进行压缩从而直接进行存储。
CompressionType.RECORD:表示仅仅压缩 key 而对 value 不进行压缩。
CompressionType.BLOCK:表示对所有的 key 与 value 都进行压缩。
读取 SequenceFile的示例代码如下:
publicclassSequenceReadeSample {
publicstaticvoid main(String[] args) throws Exception {
Configurationconf = new Configuration();
FileSystemfs =FileSystem.get(conf);
Pathpath = new Path("cool.txt"); // 定义输入路径
SequenceFile.Readerreader = new SequenceFile.Reader(fs, path, conf); //创建实例
IntWritablekey = new IntWritable(); // 创建待读入 Key 实例
Textvalue = new Text(); // 创建待读入 value 实例
while (reader.next(key, value)) { // 判断是否存在下个键值
System.out.println(key + "======" + value); //打印已读取键值对
}
IOUtils.closeStream(reader); // 关闭读入流
}
}
SequenceFile.Reader 通过构造方法初始化其读取的环境变量,然后生成具体的 key 与value 实例,调用 next 给予键值对进行赋值。
MapFile详解
MapFile 继承自 SequenceFile,是一个可供对其进行查询操作的文件类,其提供一个继承了 WritableComparable 的类作为 key,以便在计算时进行排序。MapFile 是一种基于键值对的用于查找的 SequenceFile,通过分析源代码可知, MapFile在生成运算结果的时候一般会分别生成关联的索引与数据文件,索引文件中包含上文中所说的“偏移位置”,数据文件则包括需要存储的键值对,通过查找索引文件中记录的偏移位置,我们可以很方便的在数据文件找到关联数据,从而提供方便查找。示例代码如下:
publicclassMapFileSample {
publicstaticvoid main(String[] args) throws Exception {
Configurationconf = new Configuration();
Stringpath = "mapFile.map"; //设置路径
FileSystemfs =FileSystem.get(conf); // 获取文件系统
Stringmeg = "helloworld"; // 创建待输入数据
// 创建写入实例
MapFile.Writerwriter = new MapFile.Writer(conf, fs, path, IntWritable.class, Text.class);
for (inti =0; i<100; i++){ // 写入数据
writer.append(new IntWritable(i), new Text(meg));
}
IOUtils.closeStream(writer); // 关闭写入流
// 创建读取实例
MapFile.Readerreader = new MapFile.Reader(fs, path, conf);
IntWritablekey = new IntWritable(); // 设置存放实例
Textvalue = new Text(); // 设置存放实例
while (reader.next(key, value)) { // 将数据读入实例中
System.out.println(key.toString()+ "__" + value.toString());// 打印数据
}
IOUtils.closeStream(reader); // 关闭读入流
}
}
通过运行此代码可以获得如下结果:
drwxr-xr-x - 0 2013-04-14 07:03/user/mapFile.map
-rw-r--r-- 1 3348 2013-04-14 07:03/user/data
-rw-r--r-- 1 203 2013-04-14 07:03/user/index
小提示:对于普通的写读, MapFile 基本上和 SequenceFile 相类似,相信读者也对程序中需要替换的部分做了替换而重新运行,如果我们使用的未经排序的 key 值序列进行输入,则会抛出 IOException(key out oforder)。
首先 MapFile 将 index 文件读入,然后进行二进制查找,根据对应的值找到最近最小的一个偏移位置值,并从此 key 值再进行逐一查找,最终查找到我们需要的值并将此值赋予到我们指定的 value 对象中。
MapFile 还提供了getClosest(WritableComparable key,Writable val) 方法,其与 get方法的区别在于, getClosest 并不直接返回制定的 value 键,而是返回靠近 key 值对应的那个偏移位置所对应的 value 值,通过制定的布尔值可以确定此值是大于还是小于当前 key值。具体代码如下:
publicclassMapFileSearch {
publicstaticvoid main(String[] args) throws Exception {
Configurationconf = new Configuration(); // 获取环境变量
Stringpath = "mapFile.map"; //获取输入路径
FileSystemfs =FileSystem.get(conf); // 获取文件系统
Stringmeg = "helloworld"; // 设定数据格式
// 设置读取实例
MapFile.Readerreader = new MapFile.Reader(fs, path, conf);
Textvalue = (Text) ReflectionUtils.newInstance(reader.getValueClass(), conf);
// 设定 Text 实例
// 读取指定位置的值
reader.get(new IntWritable(28), value);
System.out.println(value.toString());// 打印结果
value.clear(); // 清空 value 实例
// 获取相近位置的值
reader.getClosest(new IntWritable(28), value);
System.out.println(value.toString());// 打印结果
}
}
使用 MapFile 或 SequenceFile 虽然可以解决 HDFS 中小文件的存储问题,但也有一定局限性,例如:
文件不支持复写操作,不能向已存在的 SequenceFile(MapFile) 追加存储记录
当 write 流不关闭的时候,没有办法构造 read 流。也就是在执行文件写操作的时候,该文件是不可读取的
小提示: SequenceFile 与 MapFile 不是万能的。
SetFile、 ArrayFile详解
SetFile 继承自 MapFile 类,只能对 key 值进行设置而 value 值为空。
其源码如下所示
publicvoid append(WritableComparable key) throws IOException { //追加方法
append(key, NullWritable.get()); // 追加数据
}
ArrayFile 内置一个计数器,每进行一次数据的添加,计数器加 1,计数值作为 key 值存储在 ArrayFile 中,形成一个形式类似数组的序列化文件。其源码如下所示:
publicsynchronizedvoid append(Writable value) throws IOException { //重载的追加方法
super.append(count, value); //追加数据
count.set(count.get()+ 1); // 计数器值增加
}
HDFS小结
I/O 是 Hadoop 中重要的一部分,其基本输入输出功能决定着 Hadoop 在执行 MapReduce任务速度,其中的 IntWritable、 Text 等类似与 Java 中的 int、 String 等类型,操作可以使用转化数据类型的方法获得更大的支持。
I/O 的序列化为我们提供了一系列便于在不同节点与对象间传递消息的机制,并方便我们对数据进行永久化存储。 Hadoop 的 HDFS 和 MapReduce 在设计之初主要是针对存储容量比较大的数据计算来设计的,在小文件的处理上效率十分低下,而且对内存的占用十分巨大,因为每个小文件都要求使用单独的 Block,而每个 Block 在使用时都要求读入计算节点的内存中,对运算非常耗费资源。 SequenceFile 为我们提供了一系列的方法帮助我们对小文
件进行存储和计算,基本思路就是将若干个文件合并成一条大的文件进行存储。 MapFile 在此基础上要求对存储的 key 值进行排序,必须要求 key 实现了 WritableComparable 接口。以便对某个值得查找提供方便快捷的支持。
除此之外,本章的重点是介绍了如何自定义我们需要的 Writable 类。这是我们以后经常使用的一个类,更多介绍和用法会在其后进行说明。