HDFS的读取数据过程细节上比较复杂,我们先来看一看具体代码是怎样实现读取数据过程的,然后再根据代码进行分析:
import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
public class HDFSReadFile {
public static void main(String[] args) {
try {
//加载配置项
Configuration conf = new Configuration();
conf.set("fs.defaultFS", "hdfs://localhost:9000");
conf.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem");
//创建文件系统实例
FileSystem fs = FileSystem.get(conf);
//创建文件实例
String fileName = "test";
Path file = new Path(fileName);
//创建输入流对象
FSDataInputStream in = fs.open(file);
//读取数据
BufferedReader d = new BufferedReader(new InputStreamReader(in));
String content = d.readLine();
System.out.println(content);
//关闭缓存流、输入流以及文件系统
d.close();
in.close();
fs.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
下面我们根据以上代码来一步步分析HDFS读取数据的过程:
-
加载配置项:Configuration对象封装了客户端或服务器的配置,通过set()方法在程序里面进行传参,fs.defaultFS参数用于指定HDFS文件系统的访问地址,fs.hdfs.impl参数则是用于指定HDFS文件系统的具体实现类,在这里,HDFS文件系统的访问地址被指定为hdfs://localhost:9000,其中9000是端口号,HDFS文件系统的具体实现类则被指定为DistributedFileSystem类,对这个类在下面会有解释:
Configuration conf = new Configuration(); conf.set("fs.defaultFS", "hdfs://localhost:9000"); conf.set("fs.hdfs.impl", "org.apache.hadoop.hdfs.DistributedFileSystem");
-
创建文件系统实例:将Configuration对象作为参数传入get()方法,get()方法返回的是默认文件系统,该默认文件系统由在Configuration对象中封装的地址所指定,我们这里的默认文件系统就是上一步中指定的HDFS文件系统,如果没有指定,则使用默认的本地文件系统:
FileSystem fs = FileSystem.get(conf);
以上代码中的FileSystem类是一个通用文件系统的抽象基类,可以被分布式文件系统继承,所有可能使用Hadoop文件系统的代码都要使用这个类,Hadoop为FileSystem这个抽象类提供了多种具体实现子类,在HDFS文件系统中,就是DistributedFileSystem类具体实现了这个抽象类,也就是说,在上面的代码中,创建的其实是一个引用类型为FileSystem的DistributedFileSystem对象,如果在上面传入配置参数的时候,没有传入fs.hdfs.impl参数,我们也可以使用以下代码通过强制类型转换创建文件系统实例:
DistributedFileSystem fs = (DistributedFileSystem) FileSystem.get(conf);
-
创建文件实例:被创建的文件的名称是test,这里并没有给出路径全称,表示采用了相对路径,实际上该文件就是当前登录Linux系统的用户在HDFS中对应的用户目录下的test文件,比如我的当前登录用户名是hadoop,那么绝对路径就是hdfs://localhost:9000/user/hadoop/test:
String fileName = "test"; Path file = new Path(fileName);
-
创建输入流对象:调用引用类型为FileSystem的DistributedFileSystem对象的open()方法,将文件实例作为参数传入,该方法会创建并返回一个输入流FSDataInputStream对象,在HDFS文件系统中,具体的输入流就是DFSInputStream,换句话说,就是FSDataInputStream对象在创建以后,里面封装了一个DFSInputStream对象:
FSDataInputStream in = fs.open(file);
在FSDataInputStream对象被创建时,调用了DFSInputStream类的构造方法,在该构造方法中,输入流通过ClientProtocal.getBlockLocations()方法远程调用名称节点,获得文件开始部分数据块的保存位置,对于该数据块,名称节点返回保存该数据块的所有数据节点的地址,同时根据距离客户端的远近对数据节点进行排序,然后,DistributedFileSystem对象会利用DFSInputStream对象来实例化FSDataInputStream类,并返回给客户端,同时返回数据块的数据节点地址列表;
-
读取数据:使用缓冲类获得输入流FSDataInputStream对象,并通过调用readLine()方法开始读取数据,输入流根据前面的排序结果,选择距离客户端最近的数据节点,建立连接并读取数据,:
BufferedReader d = new BufferedReader(new InputStreamReader(in)); String content = d.readLine();
在以上代码中,其实是在执行一段循环过程,数据从数据节点读到客户端,当当前数据块读取完毕时,FSDataInputStream关闭和当前数据节点的连接,并通过ClientProtocal.getBlockLocations()方法查找下一数据块,但如果客户端缓存中已经包含了下一数据块的位置信息,就不需要调用该方法,找到下一数据块的最佳数据节点,开始读取数据,如此循环直到读完所有数据,不过需要注意的是,在读取数据的过程中,如果客户端与数据节点通信时出现错误,就会尝试连接包含当前数据块的下一个数据节点;
-
关闭实例对象:调用close()方法关闭缓存流、输入流以及文件系统:
d.close(); in.close(); fs.close();
至此,HDFS的读取数据过程大概介绍完毕,如果其中我的理解有错误,请各位大佬帮忙纠正,至于HDFS的数据写入过程,具体内容请参照:初步理解HDFS数据写入过程 + Java代码实现。
最后再附上一张大致的流程图: