深入理解hadoop之HDFS
刚刚才写完关于mapreduce的一篇博文,趁热打铁接下来聊聊HDFS。本博文参考资料为HADOOP权威指南第3版完版,博文如有错漏之处,敬请指正。
HDFS即Hadoop Distributed FileSystem,是hadoop旗舰机的文件系统。HDFS以流式数据访问模式来存储超大文件。有如下几个特点:超大文件;流式数据访问模式,即一次写入多次读取的访问模式;商用硬件,hadoop不需要运行在昂贵的商用硬件上面,对于庞大的集群来说,节点的故障概率是非常高的,而HDFS是为了让系统继续运行而不让用户感受到明显的中断;HDFS不适合地数据延迟的应用;不适宜大量的小文件;不支持多用户写入,任意修改文件的 操作。
一、HDFS的一些概念
1.block(块):文件在物理上是分块存储(block),块的大小可以通过配置参数( dfs.blocksize)来规定,默认大小在hadoop2.x版本中是128M,之前的版本中是64M。分块的好处有二:1.文件的大小可以大于网络中任意一个磁盘的容量;2.使用抽象块而非整个文件作为存储单元。
2.名称节点namenode,管理文件系统的命名空间,维护整个系统树以及整个系统树内的文件目录,这些信息就以镜像文件和编辑日志的形式存储在本地磁盘上。
3.数据节点DataNode,存储并检索数据块,定期向namenode发送它们存储的块的列表
4.DistributedFileSystem
如上图所示, HDFS 也是按照 Master 和 Slave 的结构。分NameNode、 SecondaryNameNode、 DataNode 这几个角色。NameNode:是 Master 节点,是大领导。管理数据块映射;处理客户端的读写请求;配置副本策略;管理 HDFS 的名称空间;SecondaryNameNode:是一个小弟,分担大哥 namenode的一部分工作量;是 NameNode 的冷备份;合并 fsimage 和fsedits 然后再发给 namenode。DataNode: Slave 节点,奴隶,干活的。负责存储 client 发来的数据块 block;执行数据块的读写操作。热备份: b 是 a 的热备份,如果 a 坏掉。那么 b 马上运行代替a 的工作。冷备份: b 是 a 的冷备份,如果 a 坏掉。那么 b 不能马上代替a 工作。但是 b 上存储 a 的一些信息,减少 a 坏掉之后的损失。fsimage:元数据镜像文件(文件系统的目录树。)edits:元数据的操作日志(针对文件系统做的修改操作记录)namenode 内存中存储的是=fsimage+edits。SecondaryNameNode 负责定时默认 1 小时,从namenode 上,获取 fsimage 和 edits 来进行合并,然后再发送给 namenode。减少 namenode 的工作量。
二、java API接口访问
在进行剖析HDFS文件系统读写之前,我们先来看看java API来访问hdfs的实例(hadoop2.x版本)。
import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLConnection; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.FsUrlStreamHandlerFactory; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IOUtils; import org.junit.Test; /** * 完成hdfs操作 */ public class TestHDFS { /** * 读取hdfs文件 */ @Test public void readFile() throws Exception{ //注册url流处理器工厂(hdfs) URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory()); URL url = new URL("hdfs://192.168.231.201:8020/user/centos/hadoop/index.html"); URLConnection conn = url.openConnection(); InputStream is = conn.getInputStream(); byte[] buf = new byte[is.available()]; is.read(buf); is.close(); String str = new String(buf); System.out.println(str); } /** * 通过hadoop API访问文件 */ @Test public void readFileByAPI() throws Exception{ Configuration conf = new Configuration(); conf.set("fs.defaultFS", "hdfs://192.168.231.201:8020/"); FileSystem fs = FileSystem.get(conf) ; Path p = new Path("/user/centos/hadoop/index.html"); FSDataInputStream fis = fs.open(p); byte[] buf = new byte[1024]; int len = -1 ; ByteArrayOutputStream baos = new ByteArrayOutputStream(); while((len = fis.read(buf)) != -1){ baos.write(buf, 0, len); } fis.close(); baos.close(); System.out.println(new String(baos.toByteArray())); } /** * 通过hadoop API访问文件 */ @Test public void readFileByAPI2() throws Exception{ Configuration conf = new Configuration(); conf.set("fs.defaultFS", "hdfs://192.168.231.201:8020/"); FileSystem fs = FileSystem.get(conf) ; ByteArrayOutputStream baos = new ByteArrayOutputStream(); Path p = new Path("/user/centos/hadoop/index.html"); FSDataInputStream fis = fs.open(p); IOUtils.copyBytes(fis, baos, 1024); System.out.println(new String(baos.toByteArray())); } /** * mkdir */ @Test public void mkdir() throws Exception{ Configuration conf = new Configuration(); conf.set("fs.defaultFS", "hdfs://192.168.231.201:8020/"); FileSystem fs = FileSystem.get(conf) ; fs.mkdirs(new Path("/user/centos/myhadoop")); } /** * putFile */ @Test public void putFile() throws Exception{ Configuration conf = new Configuration(); conf.set("fs.defaultFS", "hdfs://192.168.231.201:8020/"); FileSystem fs = FileSystem.get(conf) ; FSDataOutputStream out = fs.create(new Path("/user/centos/myhadoop/a.txt")); out.write("helloworld".getBytes()); out.close(); } /** * removeFile */ @Test public void removeFile() throws Exception{ Configuration conf = new Configuration(); conf.set("fs.defaultFS", "hdfs://192.168.231.201:8020/"); FileSystem fs = FileSystem.get(conf) ; Path p = new Path("/user/centos/myhadoop"); fs.delete(p, true); } }
三、HDFS剖析文件读取
1.客户端首先通过FileSystem客户端的Open()方法来打开要进行读取的文件,然后DistributedFileSystem通过使用RPC来调用namenode以确定文件起始块的位置对于每一个块,namenode存有该块副本的datanode 的地址,DistributedFileSystem类返回一个FSDataInputStream对象给client并进行数据的读取,接下来客户端通过输入流来对这些数据节点来反复调用read()方法,第一个block读取完毕之后,寻找下一个block的最佳datanode,来读取数据,最后数据读取完毕关流,这些DataNode根据与客户端的距离来进行排序。具体过程如下图1:
(图1客户端读取HDFS数据)
2.在读数据过程中,如果与Datanode的通信发生错误,DFSInputStream对象会尝试从下一个最佳节点读取数据,并且记住该失败节点, 后续Block的读取不会再连接该节点 读取一个Block之后,DFSInputStram会进行检验和验证,如果Block损坏,尝试从其他节点读取数据,并且将损坏的block汇报给Namenode。 客户端连接哪个datanode获取数据,是由namenode来指导的,这样可以支持大量并发的客户端请求,namenode尽可能将流量均匀分布到整个集群。Block的位置信息是存储在namenode的内存中,因此相应位置请求非常高效,不会成为瓶颈。
四、HDFS剖析文件写入
1.对于文件写入的过程,我们要了解的是创建文件,写入文件,最后关闭文件。
2.客户端通过DistributedFileSystem来调用create函数来创建新的文件;namenode来检查该文件是否存在以及是否有权限来创建该文件,检查通过就会创建文件,否则就会抛出异常,DisTributedFileSystem向客户端返回一个FSDataOutputStream对象,由此客户端就可以开始写入数据;客户端写入数据时DFSOutputStream将数据分为一个个的数据包,并将其写入数据队列,DataStreamer处理数据队列,根据DataNode列表的要求namenode来分配合适的新块来存储数据副本,这些节点存放同一个Block的副本,构成一个管道。 DataStreamer将packet写入到管道的第一个节点,第一个节点存放好packet之后,转发给下一个节点,下一个节点存放 之后继续往下传递。DFSOutputStream同时维护一个ack queue队列,等待来自datanode确认消息。当管道上的所有datanode都确认之后,packet从ack队列中移除;数据写入完毕,客户端close输出流。
暂时写这么多吧,写累了,休息下。