这章节的内容重理论,所以有些枯燥,不过对整个分布式文件系统的构建还是讲的蛮详细的。
这章其实主要可以分为三大部分:基本概念介绍,接口,数据流。
一、基本概念中,介绍了数据块、namenode、datanode。这些概念在好多地方都有讲,而且讲得也很好,在这里我只稍微提一下。namenode可以认为是记录了任何一个文件所对应的datanode(其实是文件对应块所在的datanode的信息),datanode用于存储检索本节点的数据块,数据块是真真正正的存储文件内容。这里需要强调的是,HDFS中小于一个块的文件不会占据一个块,HDFS数据块之所以设置那么大,是为了是为了减少寻址开销,但是块也不应该设置的太大,因为MapReduce中的map通常每次处理一个块大小的数据,所以如果每次处理的任务过少,集群上的结点不能充分利用,处理速度自然会慢下来。
二、接口中,书中先是讲了命令行接口,这一部分没有什么好说的,就是一些命令而已,和Linux下的命令很相似。类比的学习就很容易掌握。重要的是Java接口,
(1)用Hadoop URL读数据(本书源代码下载地址)
import java.io.InputStream;
import java.net.URL;
import org.apache.hadoop.fs.FsUrlStreamHandlerFactory;
import org.apache.hadoop.io.IOUtils;
// vv URLCat
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);
}
}
}
编译前要注意设置${hadoop-home}/conf/hadoop-env.sh,把HADOOP_HOME(文件中本来就有,只是被注释掉了,自己改过来就好)的值设为你创建.class的目录,编译时注意添加所需类的路径,也就是${hadoop-home}/hadoop-core-x.x.x.jar,编译语句是这样的javac -cp ${hadoop-home}/hadoop-core-x.x.x.jar 源文件(.java文件)
编译完后利用${hadoop-home}/bin/hadoop 类名(无.class) 输入文件就可以看到结果。这里不清楚就直接按书上做就可以。
这个函数仅仅是利用URL进行文件传输的,和java从任何一个网站上读信息没有区别,由于setURLStreamHandlerFactory()方法只能被java虚拟机调用一次,所以如果第三方创建了URLStreamHandlerFactory实例,那么这个类将无法从hadoop上读取信息。所以接口中不作为重点讲。
(2)用FileSystem API读取数据
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;
// vv FileSystemCat
public class FileSystemCat {
public static void main(String[] args) throws Exception {
String uri = args[0];
Configuration conf = new Configuration();
FileSystem fs = FileSystem.get(URI.create(uri), conf);
InputStream in = null;
try {
in = fs.open(new Path(uri));
IOUtils.copyBytes(in, System.out, 4096, false);
} finally {
IOUtils.closeStream(in);
}
}
}
重要API:<1>FileSystem该类提供了FileSystem.get来检索文件系统实例,conf中封装了客户端或者服务端的配置信息,URI用于确定使用的文件系统。FileSystem.open用来打开文件获取输入流。其实此处FileSystem.open返回的不是java.io而是FsDataInputStream,只是这个类继承了java.io.DataInputStream。书中在这部分还详细的讲了FsDataInputStream,着重讲到着各类中用于定位的接口,也就是说我们可以通过这个类从文件的任意位置读取数据。(3)写入数据
需要指明的是这里的写入数据是将数据写到HDFS上。
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
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;
import org.apache.hadoop.util.Progressable;
// vv FileCopyWithProgress
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);
}
}
重要API:<1>FsDataOutputStream,这个类是fs.create(fs.append也可以实现同样的效果)这一句返回的类型,相当于返回了一个用于写入的输出流。Progressable这个接口是用于返回进度的(输出进度)。但是与FsDataInputStream不同的是,写入操作是没有定位的,因为在hadoop中数据不能更改,也没有必要更改。
(4)查询文件系统
import java.net.URI;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FileUtil;
import org.apache.hadoop.fs.Path;
// vv ListStatus
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);
}
}
}
重要API:FileStatus封装了文件系统中文件和目录的元数据,包括文件长度,块大小,备份、修改时间,所有者及权限信息。FileSystem可以利用getFileStatus获取文件或目录的FileStatus实例,但这里有一个更好用的方法就是listStatus,该方法可以连同目录的内容一并列出来。最后就是stat2Paths这个方法,这个方法可以把FileStatus对象转化为Path数组。
(5)文件模式
该部分就是用单个操作实现处理一批文件,就是利用通配符实现批量处理,重要的API就是FileSystem.globStatus()这个函数,他会返回与变量地址相配的所有文件的FileStatus数组。globStatus有两个形式
public FileStatus[] globStatus(Path pathPattern) throws IOException
public FileStatus[] globStatus(Path pathPattern, PathFilter filter) throws IOException
第二个涉及到了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);
}
}
这里的accept里说明了不与给定的路径匹配,也就是说排除该文件。
最后,删除文件就用FileSystem.delete()就可以。
三、数据流
这一部分理论性很强,我不能保证我完全理解正确,如果有错误还请网友点出。
(1)文件读取
首先,客户端通过FileSystem对象的open函数来打开要读取的文件,然后DistributedFileSystem通过RPC调用namenode以确定文件前几个块的位置,然后namenode返回存有该块复本的datanode地址。DistributedFileSystem返回一个FsDataInputStream对象,FsDataInputStream封装有DFSInputStream(管理namenode和datanode I/O),存储着文件前几个块DFSInputStream然后连接距离最近的datanode,然后客户端反复调用read(),数据从datanode流入客户端,到达块末端的时候,关闭与该datanode的连接,寻求下一个最佳的datanode。当客户端读完一批数据块的时候,就需要再次向namenode请求检索一批datanode的位置了。当读取完毕的时候,客户端调用close()关闭FsDataInputStream。这样基本流程就介绍完了,下面是对流程中的一些细节作出的说明。
namenode返回的数据块复本的datanode的地址是按照datanode到客户端的距离来排序的,其实这里的距离是很模糊的概念,由于带宽限制了数据的传输速率,所以利用带宽来表示距离,而带宽的衡量也很难,所以我们从抽象的层面来构建树,说是这样构造的:叶子节点相当于进程,进程的父亲是节点,节点的父亲为机架,机架的父亲是数据中心。两个节点之间的距离是他们到最近的共同祖先的距离总和。
如果DFSInputStream在与datanode通信时遇到错误,他便会从离这个块最近的datanode读取数据,并且记录下datanode以保证以后不会反复地去读取了,同时他还会确认从这个datanode发来的信息是否有错,如果有错,就在从其他datanode读取这个数据块的复本前通知给namenode。
(2)文件写入
客户端通过对DistributedFileSystem对象调用create()函数来创建文件,然后DistributedFileSystem对Name弄得创建一个RPC调用,在文件系统的命名空间中创建一个新文件,此时该文件还没有对应的数据块。而且namenode执行各种检查来确保这个文件不存在。如果检查通过,namenode就会为创建新文件怎加一条记录,否则就会抛出异常。DistributedFileSystem返回一个FSDataOutputStream对象(客户端利用它来写数据)。FSDataOutputStream封装了一个DFSOutputStream对象,用于处理datanode和namenode 之间的通信。客户端写入数据时,DFSOutputStream将数据分为一个个的包,存入数据队列,DataStreamer利用数据队列,根据datanode列表来要求namenode分配合适的新块来存储数据备份。这些块构成一个管线,DataStreamer将数据块流式传给第一个datanode,第一个datanode在存储完数据包后再发送给第二个datanode,这样一直传给最后一个datanode。DFSOutputStream还维护这一个确认队列,用来等待datanode的确认回执,只有当管线中所有datanode确认收到数据后才会把数据包从确认队列中删除。
如果一个Datanode的数据被写入时失败,首先,该管道被关闭,并且确认队列中的所有数据包添加到数据队列的前面,因此,故障节点的下游数据节点不会丢掉数据包。在好的DataNode上的当前块被赋予了新的标示,并将该标示传递给namenode。从管线中把故障节点删除,然后把余下的数据写入剩下的datanode,如果namenode发现复本量不足时会在新的节点上创建复本。
客户端写完数据时会调用close(),此行为刷新Datanode管道的所有剩余的数据包,并在联系namenode和发送文件操作完成之前等待确认。namenode已经知道文件有哪些块组成,所以它在返回成功前只需要等待数据块进行最小量的复制。
加一条个人理解,namenode选定datanode之后就会把数据写到相应的datanode上,每一个datanode都会记录下该文件的所有信息,不同的复本只是用于冗余备份。