HDFS的写入数据过程细节上比较复杂,我们先来看一看具体代码是怎样实现写入数据过程的,然后再根据代码进行分析:
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
public class HDFSCreateFile {
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);
//创建输出流对象
FSDataOutputStream os = fs.create(file);
//写入数据
byte[] buff = "Hello World".getBytes();
os.write(buff, 0, buff.length);
System.out.println("Create:"+fileName);
//关闭输出流和文件系统
os.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对象的create()方法,将文件实例作为参数传入,该方法会创建并返回一个输出流FSDataOutputStream对象,在HDFS文件系统中,具体的输出流就是DFSOutputStream,换句话说,就是FSDataOutputStream对象在创建以后,里面封装了一个DFSOutputStream对象:
FSDataOutputStream os = fs.create(file);
在FSDataOutputStream对象被创建时,调用了DFSOutputStream类的构造方法,在该构造方法中,DitributedFileSystem对象通过RPC远程调用名称节点,在文件系统的命名空间中创建一个新的文件,而名称节点会执行一些检查,比如文件是否已经存在、客户端是否有权限创建文件等,检查通过之后,名称节点会构造一个新文件,并添加文件信息,远程方法调用结束后,DistributedFileSystem对象会利用DFSOutputStream对象来实例化FSDataOutputStream类,并返回给客户端,客户端使用这个输出流写入数据;
-
写入数据:创建字节数组,将字符串转换为字节形式装入字节数组,并通过调用输出流的write()方法向HDFS文件系统中对应的文件写入数据:
byte[] buff = "Hello World".getBytes(); os.write(buff, 0, buff.length);
在以上代码中,其实是在执行一段循环过程,客户端向输出流FSDataOutputStream对象中写入的数据会首先被分成一个个的分包,这些分包被放入DFSOutputStream对象的内部队列,输出流FSDataOutputStream对象会向名称节点申请保存文件和副本数据块的若干个数据节点,这些数据节点形成一个数据流管道,队列中的分包最后会被打包成数据包,由客户端发往数据流管道中的第一个数据节点,第一个数据节点将数据包发送给第二个数据节点,第二个数据节点将数据包发送给第三个数据节点,这样,数据包会流经管道上的各个数据节点,这就是HDFS采用的流水线复制策略,当最后文件写入完成时,数据备份也同时完成,不过需要注意的是,因为各个数据节点位于不同的机器上,数据需要通过网络发送,因此,为了保证所有数据节点的数据都是准确的,接收到数据的数据节点要向发送者发送“确认包”(ACK Packet),确认包沿着数据流管道逆流而上,从数据流管道依次经过各个数据节点并最终发往客户端,当客户端收到应答时,它将对应的分包从内部队列移除,如此循环执行上述过程,直到数据全部写完;
-
关闭实例对象:调用close()方法关闭输出流和文件系统:
os.close(); fs.close();
在关闭输出流后,客户端不会再向输出流中写入数据,所以,当DFSOutputStream对象内部队列中的分包都收到应答以后,就可以使用ClientProtocol.complete()方法通知名称节点关闭文件,完成一次正常的写文件过程。
至此,HDFS的写入数据过程大概介绍完毕,如果其中我的理解有错误,请各位大佬帮忙纠正,至于HDFS的数据读取过程,具体内容请参照:初步理解HDFS数据读取过程 + Java代码实现。
最后再附上一张大致的流程图: