文章目录
- HDFS读数据
- HDFS读数据的简要过程
- HDFS读数据的详细过程
- HDFS通信协议
- HDFS常用的Java API
- 编程实现
- HDFS写数据
- HDFS写数据的简要过程
- HDFS写数据的详细过程
- 编程实现
- HDFS读写数据综合实现
HDFS读数据
HDFS读数据的简要过程
- 客户端将要读取的文件路径发送给namenode;
- namenode获取文件的元信息(主要是block的存放位置信息)返回给客户端;
- 客户端根据返回的信息找到相应datanode逐个获取文件的block;
- 客户端对block进行追加合并从而获得整个文件;
HDFS读数据的详细过程
- 客户端通过FileSystem.open()打开文件(在HDFS文件系统中DistributedFileSystem具体实现了FileSystem),创建输入流FSDataInputStream(对于HDFS而言,具体的输入流就是DFSInputStream);
- 在DFSInputStream的构造函数中,输入流通过ClientProtocal.getBlockLocations()远程调用名称节点,获得文件开始数据块的保存信息。对于该数据块,名称节点返回保存该数据块的所有数据节点的地址,同时根据客户端的远近对数据节点进行排序,返回给客户端;
- 获得输入流FSDataInputStream后,客户端调用read()函数开始读取数据,输入流根据前面的排序结果,选择距离客户端最近的数据节点建立连接并读取数据;
- 数据从该数据节点读取到客户端,当该数据块读取完毕时,FSDataInputStream关闭和该数据节点的连接;
- 输入流通过getBlockLocations()方法向名称节点查找下一个数据块,重复2、3、4步;
- 当客户端读取完毕数据的时候,调用FSDataInputStream的close函数,关闭输入流;
注意:在读取数据的过程中,如果客户端与数据节点通信出现错误,就会尝试连接包含此数据块的下一个数据节点。
HDFS通信协议
上面的HDFS读数据的详细过程中,客户端与名称节点,客户端与数据节点等都存在远程调用和信息交互,这都是基于HDFS提供的通信协议实现的。
HDFS中提供两种通信协议
- Hadoop RPC接口:HDFS中基于Hadoop RPC框架实现的接口
- 流式接口:HDFS中基于TCP或者HTTP实现的接口
我们主要介绍Hadoop RPC接口
Hadoop RPC调用使得HDFS进程能够像本地调用一样调用另一个进程中的方法,目前Hadoop RPC调用基于Protobuf实现,接口定义在org.apache.hadoop.hdfs.protocol和org.apache.hadoop.hdfs.server.protocol包中,其中包括以下几个接口
- ClientProtocol:客户端和名字节点间的接口;
- ClientDatanodeProtocol:客户端与数据节点的接口
- DatanodeProtocol:数据节点与名字节点通信接口
- InterDatanodeProtocol:数据节点与数据节点间的通信接口
- NamenodeProtocol:第二名字节点与名字节点间的接口
HDFS常用的Java API
在下面的HDFS读数据的编程实现的代码中,我们会用到一些HDFS常用的Java API,在编程实现HDFS读数据流程之前,我们对这些Java API进行一下简单的了解
- org.apache.hadoop.conf.Configuration:该类封装了客户端或者服务端的配置
- org.apache.hadoop.fs.FileSystem:一个通用文件系统的抽象基类,可以被分布式文件系统继承
- org.apache.hadoop.fs.Path:用于表示Hadoop文件系统中的一个文件或者一个目录的路径
- org.apache.hadoop.fs.FSDataInputStream:文件输入流,用于读取Hadoop文件
编程实现
import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FSDataInputStream;
public class ReadFile {
public static void main(String[] args) {
try {
//访问工程目录下的core-site.xml和hdfs-site.xml配置项,把相关配置项加载进来
Configuration conf = new Configuration();
conf.set("fs.defaultFS","hdfs://Master:9000");
conf.set("fs.hdfs.impl","org.apache.hadoop.hdfs.DistributedFileSystem");
//通过FileSystem的静态方法get获得DistributedFileSystem对象
FileSystem fs = FileSystem.get(conf);
//要读取的文件路径
Path file = new Path("/user/hadoop/input/wordfile2.txt");
//调用DistributedFileSystem.open(),创建输入流DFSInputStream
FSDataInputStream getIt = fs.open(file);
BufferedReader d = new BufferedReader(new InputStreamReader(getIt));
String content = d.readLine(); //读取文件一行
System.out.println(content);
d.close(); //关闭文件
fs.close(); //关闭hdfs
} catch (Exception e) {
e.printStackTrace();
}
}
运行结果
HDFS写数据
HDFS写数据的简要过程
1)客户端向namenode请求上传文件,namenode检查目标文件是否已存在,客户端是否有权限创建文件,检 查通过后,namenode返回是否可以上传;
2)客户端请求第一个 block上传到哪几个datanode服务器上;
3)namenode返回3个datanode节点,分别为dn1、dn2、dn3;
4)客户端请求dn1上传数据,dn1收到请求会继续调用dn2,然后dn2调用dn3,将这个通信管道建立完成, dn1、dn2、dn3逐级应答客户端;
5)客户端开始往dn1上传第一个block(先从磁盘读取数据放到一个本地内存缓存),以packet为单位,dn1收到一个packet就会传给dn2,dn2传给dn3,dn3返回确认包应答,逐级上传给客户端;
6)当一个block传输完成之后,客户端再次请求namenode上传第二个block的服务器(重复执行2—5步);
7)当数据全部写完之后,客户端调用close方法关闭输出流,当客户端收到写入分包的应答以后,就可以通知名称节点关闭文件,完成一次正常的写文件过程;
HDFS写数据的详细过程
- 客户端通过FileSystem.create()创建文件(在HDFS文件系统中DistributedFileSystem具体实现了FileSystem),创建输出流FSDataOutputStream(对于HDFS而言,具体的输入流就是DFSOutputStream);
- DistributedFileSystem对象通过RPC远程调用名称节点,在文件系统的命名空间中新建一个文件 名称节点会执行一些检查(文件是否存在,客户端权限),检查通过之后,名称节点会构造一个新文件,并添加文件信息。远程方法调用结束后,DistributedFileSystem会利用DFSOutputStream来实例化FSDataOutputStream,返回给客户端,客户端使用这个输出流写入数据;
- 获得输出流后,客户端调用输出流的write()方法向HDFS中对应的文件写入数据;
- 数据被分成一个个分包 分包被放入DFSOutputStream对象的内部队列 DFSOutputStream向名称节点申请保存数据块的若干数据节点 ,这些数据节点形成一个数据流管道。队列中的分包最后被打包成数据包 发往数据流管道中的第一个数据节点 第一个数据节点将数据包发送到第二个节点 依此类推,形成“流水线复制”;
- 为了保证节点数据准确,接收到数据的数据节点要向发送者发送“确认包” 确认包沿着数据流管道逆流而上,经过各个节点最终到达客户端 客户端收到应答时,它将对应的分包从内部队列移除;
- 完成向文件写入数据,Client在文件输出流(FSDataOutputStream)对象上调用close方法,关闭输出流,当客户端收到写入分包的应答以后,就可以调用DistributedFileSystem对象的complete方法,通知NameNode文件写入成功;
编程实现
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.Path;
public class Chapter3 {
public static void main(String[] args) {
try {
Configuration conf = new Configuration();
conf.set("fs.defaultFS","hdfs://Master:9000");
FileSystem fs = FileSystem.get(conf);
byte[] buff = "xiao tuo hao shuai\n".getBytes(); // 要写入的内容
String filename = "test"; //要写入的文件名
FSDataOutputStream os = fs.create(new Path(filename));
os.write(buff,0,buff.length);
System.out.println("Create:"+ filename);
os.close();
fs.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果
HDFS读写数据综合实现
上面分别介绍了HDFS的读数据和写数据的过程解析及编程实现,下面我们用一个综合小案例把HDFS中读数据和写数据综合起来。
该案例要实现的功能是
- 给定指定目录,从指定目录中过滤出所有后缀名不为“.abc”的文件;
- 然后,对过滤的文件进行读取;
- 最后,将这些文件的内容合并到指定的文件中
下面来直接看代码(个别地方已给上注释)
import java.io.IOException;
import java.io.PrintStream;
import java.net.URI;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.*;
/**
* 过滤掉文件名满足特定条件的文件
* 通过实现接口PathFilter中的方法accept(Path path),对Path指定的文件进行过滤
*/
class MyPathFilter implements PathFilter {
String reg = null;
MyPathFilter(String reg) {
this.reg = reg;
}
public boolean accept(Path path) {
if (!(path.toString().matches(reg)))
return true;
return false;
}
}
/***
* 利用FSDataOutputStream和FSDataInputStream合并HDFS中的文件
*/
public class MergeFile {
Path inputPath = null; //待合并的文件所在的目录的路径
Path outputPath = null; //输出文件的路径
//MergeFile类的构造函数
public MergeFile(String input, String output) {
this.inputPath = new Path(input);
this.outputPath = new Path(output);
}
public void doMerge() throws IOException {
Configuration conf = new Configuration();
conf.set("fs.defaultFS","hdfs://Master:9000");
conf.set("fs.hdfs.impl","org.apache.hadoop.hdfs.DistributedFileSystem");
//两个文件系统的实例,一个用于读数据,一个用于写数据
FileSystem fsSource = FileSystem.get(URI.create(inputPath.toString()), conf);
FileSystem fsDst = FileSystem.get(URI.create(outputPath.toString()), conf);
//下面过滤掉输入目录中后缀为.abc的文件
FileStatus[] sourceStatus = fsSource.listStatus(inputPath,
new MyPathFilter(".*\\.abc"));
FSDataOutputStream fsdos = fsDst.create(outputPath);
PrintStream ps = new PrintStream(System.out);
//下面分别读取过滤之后的每个文件的内容,并输出到同一个文件中
for (FileStatus sta : sourceStatus) {
//下面打印后缀不为.abc的文件的路径、文件大小
System.out.print("路径:" + sta.getPath() + " 文件大小:" + sta.getLen()
+ " 权限:" + sta.getPermission() + " 内容:");
FSDataInputStream fsdis = fsSource.open(sta.getPath());
byte[] data = new byte[1024];
int read = -1;
while ((read = fsdis.read(data)) > 0) {
ps.write(data, 0, read);
fsdos.write(data, 0, read);
}
//关闭输入流
fsdis.close();
}
//关闭打印流
ps.close();
//关闭输出流
fsdos.close();
}
//main函数,程序入口
public static void main(String[] args) throws IOException {
MergeFile merge = new MergeFile(
"hdfs://Master:9000/user/hadoop/input",
"hdfs://Master:9000/user/hadoop/output/merge.txt");
merge.doMerge();
}
}
运行结果
查看merge.txt,数据已写入