Learning Hadoop (2)

来源:互联网 发布:mysql 1054错误 编辑:程序博客网 时间:2024/06/06 07:04

3. HDFS和Hadoop I/O

HDFS适合于:大型文件的流式读写(一次写入,多次访问),但是不太适合:低延迟、大量小文件、多次读写的场合。

3.1 HDFS的文件系统接口

我们可以执行所有常用的文件系统操作,例如,读取文件,创建目录,移动文件,删除数据,列出目录,等等。可以输入
hadoop fs -help 
命令获取所有命令的详细帮助文件。
首先从本地文件系统将一个文件复制到HDFS:
% hadoop fs -copyFromLocal input/docs/quangle.txt hdfs://localhost/user/tom/quangle.txt
该命令调用Hadoop 文件系统的shell 命令fs,该命令提供了一系列子命令,在这里的例子中,我们执行的是copyFromLocal 。本地文件quangle.txt 被复制到运行在localhost 上的HDFS 实例中,路径为/user/tom/quangle.txt 。
事实上,我们可以简化命令格式以省略主机的URI 并使用默认设置,即省略hdfs://localhost ,因为该项已在core-site.xml 中指定。
% hadoop fs -copyFromLocal input/docs/quangle.txt /user/tom/quangle.txt
我们也可以使用相对路径,并将文件复制药j HDFS 的home 目录中,本例中为/user/tom·
% hadoop fs -copyFromLocal input/docs/quangle.txt quangle.txt
我们把文件复制回本地文件系统,并检查是否一致:
% hadoop fs -copyToLocal quangle.txt quangle.copy.txt% md5 input/docs/quangle.txt quangle.copy.txt
由于MD5 键值相同,表明这个文件在HDFS 之旅中得以幸存并保存完整。
最后,我们看一下HDFS 文件列表。我们首先创建一个目录着它在列表中是如何显示的:
% hadoop fs -mkdir books% hadoop fs -ls
要想列出本地文件系统中根目录下的文件,可以使用如下的命令:
% hadoop fs -ls file:///

3.2 HDFS的Java接口(org.apache.hadoop.fs)

HDFS的java接口即为hadoop中的FileSystem类——与Hadoop的某一文件系统进行交互的API。FileSystem是一个抽象类,对于HDFS和本地FS会有相关的implementation。
相关的API在:http://hadoop.apache.org/docs/r1.2.1/api/

3.2.1 Hadoop的URL读取(在Hadoop 2.2之后的版本已经剔除对这个的支持)

要从Hadoop 文件系统中读取文件,最简单的方陆是使用java.net.URL 对象打开数据流,进而从中读取数据。具体格式如下:
InputStream in = null;try {in = new URL("hdfs://host/path").openStream();// process in} finally {IOUtils.closeStream(in);}
让Java 程序能够识别Hadoop 的hdfs。还需要一些额外的工作。这里是通过 FsUrlStreamHandlerFactorγ 实例调用URL 中的setURLStreamHandlerFactory方法。由于Java 虚拟机只能调用一次上述方法,因此通常在静态方法中调用上述方法。这个限制意味着如果程序的其他组件——如不受你控制的第三方组件——已经声明了一个URLStreamHandlerFactorγ 实例,你将无法再使用上述方法从Hadoop 中读取数据,这个比较尴尬。
例 3-1 举一个例子,通过URLStreamHandler实例显示HDFS内的文件:
public class URLCat {static {URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory());}public static void main(String[] args) throws Exception {InputStream in = null;try {in = new URL(args[0]).openStream();IOUtils.copyBytes(in, System.out, 4096, false);} finally {IOUtils.closeStream(in);}}}
从0.21.0 版本开始:加入了一个名为FileContext 的文件系统接口,该接口能够更好地处理多文件系统问题(例如,单个FileContext 接口能够解决多文件系统方案),并且该接口更简明、一致。

3.2.2 Hadoop的FileSystem API读取

正如前一小节所解释的,有时无法在应用中设置URLStreamHandlerFactorγ 实例。这种情况下,需要使用FileSystem API来打开一个文件的输入流。
Hadoop 文件系统中通过Hadoop Path 对象来代表文件(而非java.io.File 对象)。你可以将一条路径视为一个Hadoop 文件系统URL ,如hdfs://localhost/user/tom/quangle.txt 。
FileSystem 是一个通用的文件系统API,所以第一步是检索我们需要使用的文件系统实例,这里是HDFS 。获取FileSystem 实例有两种静态工厂方法:
public static FileSystem get(Configuration conf) throws IOExceptionpublic static FileSystem get(URI uri, Configuration conf) throws IOExceptionpublic static FileSystem get(URI uri, Configuration conf, String user) throws IOException
Configuration 对象封装了客户端或服务器的配置,通过设置配置文件读取类路径来实现(如conf/core-site.xml)。
  • 第一个方法返回的是默认文件系统(在conf/core-site.xml 中指定的,如果没有指定,则使用默认的本地文件系统)。
  • 第二个方法通过给定的URI 方案和权限来确定要使用的文件系统,如果给定URI 中没有指定方案,则返回默认文件系统。
有了FileSystem 实例之后,我们调用open()函数来获取文件的输入流:
public FSDataInputStream open(Path f) throws IOExceptionpublic abstract FSDataInputStream open(Path f, int bufferSize) throws IOException
例3-2. 直接使用FileSystem 以标准输出格式显示Hadoop 文件系统的文件
public class FileCopyWithProgress {public static void main(String[] args) throws Exception {String localSrc = args[0];String dst = args[1];InputStream in = new BufferedInputStream(new FileInputStream(localSrc));Configuration conf = new Configuration();FileSystem fs = FileSystem.get(URI.create(dst), conf);OutputStream out = fs.create(new Path(dst), new Progressable() {public void progress() {System.out.print(".");}});IOUtils.copyBytes(in, out, 4096, true);}}
FSDatainputStream 
实际上, FileSystem 对象中的open()方法返回的是FSDatainputStream 对象,而不是标准的java.io 类对象。这个类是继承了java.io.DatainputStream 接口的一个特殊类,并支持随机访问,由此可以从流的任意位置读取数据。
package org.apache.hadoop.fs;public class FSDataInputStream extends DataInputStreamimplements Seekable, PositionedReadable {// implementation elided}
Seekable 接口支持在文件中找到指定位置,并提供一个查询当前位置相对于文件起始位置偏移量(getpos())的查询方法:
public interface Seekable {void seek(long pos) throws IOException;long getPos() throws IOException;}
调用seek()来定位大于文件长度的位置会导致IOException 异常。与java.io.InputStream 中的skip()不同, seek()可以移到文件中任意一个绝对位置,skip()则只能相对于当前位置定位到另一个新位置。
例3-3 为例3-2 的简单扩展,它将一个文件写入标准输出两次:在一次写完之后,定位到文件的起始位置再次以流方式读取该文件。
例3-3. 使用seek()方法,将Hadoop 文件系统中的一个文件在标准输出上显示两次
public class FileSystemDoubleCat {public static void main(String[] args) throws Exception {String uri = args[0];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);in.seek(0); // go back to the start of the fileIOUtils.copyBytes(in, System.out, 4096, false);} finally {IOUtils.closeStream(in);}}}
FSDatainputStream 类也实现了Positioned Readable 接口,从一个指定偏移量处读取文件的一部分:
public interface PositionedReadable {public int read(long position, byte[] buffer, int offset, int length)throws IOException;public void readFully(long position, byte[] buffer, int offset, int length)throws IOException;public void readFully(long position, byte[] buffer) throws IOException;}
read()方法从文件的指定position 处读取至多为length 字节的数据井存入缓冲区buffer 的指定偏离量offset 处。返回值是实际读到的字节数:调用者需要检查这个值,它有可能小于指定的length 长度。readFully()方法将指定length长度的字节数数据读取到buffer 中(或在只接受buffer、字节数组的版本中,读取buffer、.length 长度字节数据),除非已经读到文件末尾,这种情况下将抛出EOFException 异常。
所有这些方法会保留文件当前偏移量,并且是线程安全的,因此它们提供了在读取文件——可能是元数据——的主体时访问文件的其他部分的便利方法。事实上,这只是按照以下模式实现的Seekable 接口。
最后务必牢记, seek()也是一个相对高开销的操作,需要慎重使用。建议用流数据来构建应用的访问模式(如使用MapReduce),而非执行大量的seek()方法。

3.2.3 写入数据

FileSystem 类有一系列创建文件的方法。最简单的方越是给准备创建的文件指定一个Path 对象,然后返回一个用于写入数据的输出流:
public FSDataOutputStream create(Path f) throws IOException
上述方陆有多个重载版本,允许我们指定是否需要强制覆盖已有的文件、文件备份数量、写入文件时所用缓冲区大小、文件块大小以及文件权限。
TIPS: create()方法能够为需要写入且当前不存在的文件创建父目录。尽管这样很方便,但有时并不希望这样。如果你希望不存在父目录就发生文件写入失败,则应该先调用exists()方法检查父目录是否存在。
还有一个重载方法Progressable,用于传递回调接口,如此一来,可以把数据写人数据节点的进度通知到你的应用:
package org.apache.hadoop.util;public interface Progressable {public void progress();}
另一种新建文件的方法,是使用append()方法在一个已有文件末尾追加数据(还存在一些其他重载版本):
public FSDataOutputStream append (Path f) throws IOException
该追加操作允许一个writer 打开文件后在访问该文件的最后偏移量处追加数据。有了这个API ,某些应用可以创建无边界文件。例如,日志文件可以在机器重启后在已有文件后面继续追加数据。该追加操作是可选的,井非所有Hadoop 文件系统都实现了该操作。例如, HDFS 支持追加,但S3 文件系统就不支持。
例3-4 显示了如何将本地文件复制到Hadoop 文件系统。每次Hadoop 调用Progress()方法时——也就是每次将64 KB 数据包写入datanode 管线后一一打印一个时间点来显示整个运行过程。注意,这个操作井不是通过API 实现的,因此Hadoop 后续版本能否执行该操作,取决于该版本是否修改过上述操作。API 仅能让你知道到“正在发生什么事情”。
例3-4. 将本地文件复制到Hadoop 文件系统
public class FileCopyWithProgress {public static void main(String[] args) throws Exception {String localSrc = args[0];String dst = args[1];InputStream in = new BufferedInputStream(new FileInputStream(localSrc));Configuration conf = new Configuration();FileSystem fs = FileSystem.get(URI.create(dst), conf);OutputStream out = fs.create(new Path(dst), new Progressable() {public void progress() {System.out.print(".");}});IOUtils.copyBytes(in, out, 4096, true);}}
目前,其他Hadoop 文件系统写入文件时均不调用progress()方法。你将在后续章节中看到进度对于MapReduce 应用的重要性。
FSDataOutputStream 对象
FileSystem 实例的create()方法返回FSDataOutputStream 对象,与FSDatalnputStream 类相似,它也有一个查询文件当前位置的方法:
package org.apache.hadoop.fs;public class FSDataOutputStream extends DataOutputStream implements Syncable {public long getpos() throws IOException {// implementation elided}// implementation elided}
但与FSDatalnputStream 类不同的是, FSDataOutputStream 类不允许在文件中定位。这是因为HDFS 只允许对一个己打开的文件顺序写入,或在现有文件的末尾追加数据。

3.2.4 目录

Filesystem 实例提供了创建目录的方法:
public boolean mkdirs(Path f) throws IOException
这个方法可以一次性新建所有必要但还没有的父目录,就像java.io.File 类的mkdirs()方法。如果目录(以及所有父目录)都已经创建成功,则返回true 。
通常,你不需要显式创建一个目录,因为调用create()方法写入文件时会自动创建父目录。

3.2.5 查询文件系统

文件元数据: FileStatus
任何文件系统的一个重要特征都是提供其目录结构浏览和检索它所存文件和目录相关信息的功能。FileStatus 类封装了文件系统中文件和目录的元数据,包括文件长度、块大小、备份、修改时间、所有者以及权限信息。
FileSystem 的get FileStatus()方法用于获取文件或目录的FileStatus 对象。
例3-5 显示了它的用法,旨在展示文件状态信息
public class ShowFileStatusTest {private MiniDFSCluster cluster; // use an in-process HDFS cluster for testingprivate FileSystem fs;@Beforepublic void setUp() throws IOException {Configuration conf = new Configuration();if (System.getProperty("test.build.data") == null) {System.setProperty("test.build.data", "/tmp");}cluster = new MiniDFSCluster(conf, 1, true, null);fs = cluster.getFileSystem();62 | Chapter 3: The Hadoop Distributed FilesystemOutputStream out = fs.create(new Path("/dir/file"));out.write("content".getBytes("UTF-8"));out.close();}@Afterpublic void tearDown() throws IOException {if (fs != null) { fs.close(); }if (cluster != null) { cluster.shutdown(); }}@Test(expected = FileNotFoundException.class)public void throwsFileNotFoundForNonExistentFile() throws IOException {fs.getFileStatus(new Path("no-such-file"));}@Testpublic void fileStatusForFile() throws IOException {Path file = new Path("/dir/file");FileStatus stat = fs.getFileStatus(file);assertThat(stat.getPath().toUri().getPath(), is("/dir/file"));assertThat(stat.isDir(), is(false));assertThat(stat.getLen(), is(7L));assertThat(stat.getModificationTime(),is(lessThanOrEqualTo(System.currentTimeMillis())));assertThat(stat.getReplication(), is((short) 1));assertThat(stat.getBlockSize(), is(64 * 1024 * 1024L));assertThat(stat.getOwner(), is("tom"));assertThat(stat.getGroup(), is("supergroup"));assertThat(stat.getPermission().toString(), is("rw-r--r--"));}@Testpublic void fileStatusForDirectory() throws IOException {Path dir = new Path("/dir");FileStatus stat = fs.getFileStatus(dir);assertThat(stat.getPath().toUri().getPath(), is("/dir"));assertThat(stat.isDir(), is(true));assertThat(stat.getLen(), is(0L));assertThat(stat.getModificationTime(),is(lessThanOrEqualTo(System.currentTimeMillis())));assertThat(stat.getReplication(), is((short) 0));assertThat(stat.getBlockSize(), is(0L));assertThat(stat.getOwner(), is("tom"));assertThat(stat.getGroup(), is("supergroup"));assertThat(stat.getPermission().toString(), is("rwxr-xr-x"));}}
如果文件或目录均不存在,则会抛出FileNotFoundException 异常。但是,如果只需检查文件或目录是否存在,那么调用exists()方法会更方便:
public boolean exists(Path f) throws IOException
列出文件
查找一个文件或目录的信息很实用,但通常你还需要能够列出目录的内容。这就是FileSystem 的listStatus()方法的功能:
public FileStatus[] listStatus(Path f) throws IOExceptionpublic FileStatus[] listStatus(Path f, PathFilter filter) throws IOExceptionpublic FileStatus[] listStatus(Path[] files) throws IOExceptionpublic FileStatus [] listStatus(Path [] files, Path Filter、filter‘) throws IOException
当传入的参数是一个文件时,它会简单转变成以数组方式返回长度为1 的FileStatus 对象。当传人参数是一个目录时,则返回0或多个FileStatus 对象,表示此目录中包含的文件和目录。
一种重载方单是允许使用Path Filter 来限制匹配的文件和目录。最后,如果指定一组路径,其执行结果相当于依次轮流传递每条路径井对其调用listStatus()方法,再将FileStatus 对象数组累积存入同一数组中,但该方法更为方便。这从文件系统树的不同分支构建输入文件列表时,这是很有用的。例3-6 简单显示了这种方法。注意FileUtil 中stat2Paths()方法的使用,它将一个FileStatus 对象数组转换为Path 对象数组。
例3-6. 显示Hadoop 文件系统申一组蹈径的文件信息
public class ListStatus {public static void main(String[] args) throws Exception {String uri = args[0];Configuration conf = new Configuration();FileSystem fs = FileSystem.get(URI.create(uri), conf);Path[] paths = new Path[args.length];for (int i = 0; i < paths.length; i++) {paths[i] = new Path(args[i]);}FileStatus[] status = fs.listStatus(paths);Path[] listedPaths = FileUtil.stat2Paths(status);for (Path p : listedPaths) {System.out.println(p);}}}
我们可以通过这个程序显示一组路径集目录列表的井集:
% hadoop ListStatus hdfs://localhost/ hdfs://localhost/user/tomhdfs://localhost/userhdfs://localhost/user/tom/bookshdfs://localhost/user/tom/quangle.txt
文件模式
在单个操作中处理一批文件,这是一个常见要求。举例来说,处理日志的MapReduce 作业可能需要分析一个月内包含在大量目录中的日志文件。在一个表达式中使用通配符来匹配多个文件是比较方便的,无需列举每个文件和目录来指定输入,该操作称为“通配”(globbing)。Hadoop 为执行通配提供了两个FileSystem方法:
public FileStatus[] globStatus(Path pathPattern) throws IOExceptionpublic FileStatus[] globStatus(Path pathPattern, PathFilter filter) throws IOException
globStatus()方法返回与路径相匹配的所有文件的FileStatus 对象数组,井按路径排序。PathFilter、命令作为可选项可以进一步对匹配进行限制。
Hadoop 支持的通配符与Unix bash 相同(见表3-2 )。
PathFilter对象
通配符模式并不总能够精确地描述我们想要访问的文件集。比如,使用通配格式排除一个特定的文件就不太可能。FileSystem 中的listStatus()和globStatus()方法提供了可选的Path Filter 对象,使我们能够通过编程方式控制通配符:
package org.apache.hadoop.fs;public interface PathFilter {boolean accept(Path path);}
PathFilter‘与java.io.FileFilter 一样,是Path 对象而不是File 对象。
例3-7 显示了用于排除匹配正则表达式路径的PathFilter。
public class RegexExcludePathFilter implements PathFilter {private final String regex;public RegexExcludePathFilter(String regex) {this.regex = regex;}public boolean accept(Path path) {return !path.toString().matches(regex);}}
这个过滤器只传递不匹配正则表达式的文件。
过滤器由Path 表示,只能作用于文件名。不能针对文件的属性(例如创建时间)来构建过撞器。但是,通配符模式和正则表达式同样无法对文件属性进行匹配。例如,如果你将文件存储在按照日期排列的目录结构中(如同前一节中讲述的那样),则可以根据Pathfilter、在给定的时间范围内选出文件。
删除数据
使用FileSystem 的delete()方法可以永久性删除文件或目录。
public boolean delete(Path f, boolean recursive) throws IOException
如果是一个文件或空目录,那么recursive 的值就会被忽略。只有在recrusive值为true 时,一个非空目录及其内容才会被删除(否则会抛出IOException 异常)。

0 0