在现代的企业环境中,单机容量每每没法存储大量数据,须要跨机器存储。统一管理分布在集群上的文件系统称为分布式文件系统。而一旦在系统中,引入网络,就不可避免地引入了全部网络编程的复杂性,例如挑战之一是若是保证在节点不可用的时候数据不丢失。java
传统的网络文件系统(NFS)虽然也称为分布式文件系统,可是其存在一些限制。因为NFS中,文件是存储在单机上,所以没法提供可靠性保证,当不少客户端同时访问NFS Server时,很容易形成服务器压力,形成性能瓶颈。另外若是要对NFS中的文件进行操做,须要首先同步到本地,这些修改在同步到服务端以前,其余客户端是不可见的。某种程度上,NFS不是一种典型的分布式系统,虽然它的文件的确放在远端(单一)的服务器上面。node
从NFS的协议栈能够看到,它事实上是一种VFS(操做系统对文件的一种抽象)实现。web
HDFS,是Hadoop Distributed File System的简称,是Hadoop抽象文件系统的一种实现。Hadoop抽象文件系统能够与本地系统、Amazon S3等集成,甚至能够经过Web协议(webhsfs)来操做。HDFS的文件分布在集群机器上,同时提供副本进行容错及可靠性保证。例如客户端写入读取文件的直接操做都是分布在集群各个机器上的,没有单点性能压力。正则表达式
若是你从零开始搭建一个完整的集群,参考[Hadoop集群搭建详细步骤(2.6.0)](http://blog.csdn.net/bingduanlbd/article/details/51892750)shell
HDFS设计之初就很是明确其应用场景,适用与什么类型的应用,不适用什么应用,有一个相对明确的指导原则。apache
存储很是大的文件:这里很是大指的是几百M、G、或者TB级别。实际应用中已有不少集群存储的数据达到PB级别。根据Hadoop官网,Yahoo!的Hadoop集群约有10万颗CPU,运行在4万个机器节点上。更多世界上的Hadoop集群使用状况,参考Hadoop官网.编程
采用流式的数据访问方式: HDFS基于这样的一个假设:最有效的数据处理模式是一次写入、屡次读取数据集常常从数据源生成或者拷贝一次,而后在其上作不少分析工做
分析工做常常读取其中的大部分数据,即便不是所有。 所以读取整个数据集所需时间比读取第一条记录的延时更重要。数组
有些场景不适合使用HDFS来存储数据。下面列举几个:缓存
1) 低延时的数据访问
对延时要求在毫秒级别的应用,不适合采用HDFS。HDFS是为高吞吐数据传输设计的,所以可能牺牲延时HBase更适合低延时的数据访问。安全
2)大量小文件
文件的元数据(如目录结构,文件block的节点列表,block-node mapping)保存在NameNode的内存中, 整个文件系统的文件数量会受限于NameNode的内存大小。
经验而言,一个文件/目录/文件块通常占有150字节的元数据内存空间。若是有100万个文件,每一个文件占用1个文件块,则须要大约300M的内存。所以十亿级别的文件数量在现有商用机器上难以支持。
3)多方读写,须要任意的文件修改
HDFS采用追加(append-only)的方式写入数据。不支持文件任意offset的修改。不支持多个写入器(writer)。
物理磁盘中有块的概念,磁盘的物理Block是磁盘操做最小的单元,读写操做均以Block为最小单元,通常为512 Byte。文件系统在物理Block之上抽象了另外一层概念,文件系统Block物理磁盘Block的整数倍。一般为几KB。Hadoop提供的df、fsck这类运维工具都是在文件系统的Block级别上进行操做。
HDFS的Block块比通常单机文件系统大得多,默认为128M。HDFS的文件被拆分红block-sized的chunk,chunk做为独立单元存储。比Block小的文件不会占用整个Block,只会占据实际大小。例如, 若是一个文件大小为1M,则在HDFS中只会占用1M的空间,而不是128M。
HDFS的Block为何这么大?
是为了最小化查找(seek)时间,控制定位文件与传输文件所用的时间比例。假设定位到Block所需的时间为10ms,磁盘传输速度为100M/s。若是要将定位到Block所用时间占传输时间的比例控制1%,则Block大小须要约100M。
可是若是Block设置过大,在MapReduce任务中,Map或者Reduce任务的个数 若是小于集群机器数量,会使得做业运行效率很低。
Block抽象的好处
block的拆分使得单个文件大小能够大于整个磁盘的容量,构成文件的Block能够分布在整个集群, 理论上,单个文件能够占据集群中全部机器的磁盘。
Block的抽象也简化了存储系统,对于Block,无需关注其权限,全部者等内容(这些内容都在文件级别上进行控制)。
Block做为容错和高可用机制中的副本单元,即以Block为单位进行复制。
整个HDFS集群由Namenode和Datanode构成master-worker(主从)模式。Namenode负责构建命名空间,管理文件的元数据等,而Datanode负责实际存储数据,负责读写工做。
Namenode存放文件系统树及全部文件、目录的元数据。元数据持久化为2种形式:
可是持久化数据中不包括Block所在的节点列表,及文件的Block分布在集群中的哪些节点上,这些信息是在系统重启的时候从新构建(经过Datanode汇报的Block信息)。
在HDFS中,Namenode可能成为集群的单点故障,Namenode不可用时,整个文件系统是不可用的。HDFS针对单点故障提供了2种解决机制:
1)备份持久化元数据
将文件系统的元数据同时写到多个文件系统, 例如同时将元数据写到本地文件系统及NFS。这些备份操做都是同步的、原子的。
2)Secondary Namenode
Secondary节点按期合并主Namenode的namespace image和edit log, 避免edit log过大,经过建立检查点checkpoint来合并。它会维护一个合并后的namespace image副本, 可用于在Namenode彻底崩溃时恢复数据。下图为Secondary Namenode的管理界面:
Secondary Namenode一般运行在另外一台机器,由于合并操做须要耗费大量的CPU和内存。其数据落后于Namenode,所以当Namenode彻底崩溃时,会出现数据丢失。 一般作法是拷贝NFS中的备份元数据到Second,将其做为新的主Namenode。
在HA(High Availability高可用性)中能够运行一个Hot Standby,做为热备份,在Active Namenode故障以后,替代原有Namenode成为Active Namenode。
数据节点负责存储和提取Block,读写请求可能来自namenode,也可能直接来自客户端。数据节点周期性向Namenode汇报本身节点上所存储的Block相关信息。
DataNode一般直接从磁盘读取数据,可是频繁使用的Block能够在内存中缓存。默认状况下,一个Block只有一个数据节点会缓存。可是能够针对每一个文件能够个性化配置。
做业调度器能够利用缓存提高性能,例如MapReduce能够把任务运行在有Block缓存的节点上。
用户或者应用能够向NameNode发送缓存指令(缓存哪一个文件,缓存多久), 缓存池的概念用于管理一组缓存的权限和资源。
咱们知道NameNode的内存会制约文件数量,HDFS Federation提供了一种横向扩展NameNode的方式。在Federation模式中,每一个NameNode管理命名空间的一部分,例如一个NameNode管理/user目录下的文件, 另外一个NameNode管理/share目录下的文件。
每一个NameNode管理一个namespace volumn,全部volumn构成文件系统的元数据。每一个NameNode同时维护一个Block Pool,保存Block的节点映射等信息。各NameNode之间是独立的,一个节点的失败不会致使其余节点管理的文件不可用。
客户端使用mount table将文件路径映射到NameNode。mount table是在Namenode群组之上封装了一层,这一层也是一个Hadoop文件系统的实现,经过viewfs:协议访问。
在HDFS集群中,NameNode依然是单点故障(SPOF: Single Point Of Failure)。元数据同时写到多个文件系统以及Second NameNode按期checkpoint有利于保护数据丢失,可是并不能提升可用性。
这是由于NameNode是惟一一个对文件元数据和file-block映射负责的地方, 当它挂了以后,包括MapReduce在内的做业都没法进行读写。
当NameNode故障时,常规的作法是使用元数据备份从新启动一个NameNode。元数据备份可能来源于:
启动新的Namenode以后,须要从新配置客户端和DataNode的NameNode信息。另外重启耗时通常比较久,稍具规模的集群重启常常须要几十分钟甚至数小时,形成重启耗时的缘由大体有:
1) 元数据镜像文件载入到内存耗时较长。
2) 须要重放edit log
3) 须要收到来自DataNode的状态报告而且知足条件后才能离开安全模式提供写服务。
采用HA的HDFS集群配置两个NameNode,分别处于Active和Standby状态。当Active NameNode故障以后,Standby接过责任继续提供服务,用户没有明显的中断感受。通常耗时在几十秒到数分钟。
HA涉及到的主要实现逻辑有
1) 主备需共享edit log存储。
主NameNode和待命的NameNode共享一份edit log,当主备切换时,Standby经过回放edit log同步数据。
共享存储一般有2种选择
QJM是专门为HDFS的HA实现而设计的,用来提供高可用的edit log。QJM运行一组journal node,edit log必须写到大部分的journal nodes。一般使用3个节点,所以容许一个节点失败,相似ZooKeeper。注意QJM没有使用ZK,虽然HDFS HA的确使用了ZK来选举主Namenode。通常推荐使用QJM。
2)DataNode须要同时往主备发送Block Report
由于Block映射数据存储在内存中(不是在磁盘上),为了在Active NameNode挂掉以后,新的NameNode可以快速启动,不须要等待来自Datanode的Block Report,DataNode须要同时向主备两个NameNode发送Block Report。
3)客户端须要配置failover模式(失效备援模式,对用户透明)
Namenode的切换对客户端来讲是无感知的,经过客户端库来实现。客户端在配置文件中使用的HDFS URI是逻辑路径,映射到一对Namenode地址。客户端会不断尝试每个Namenode地址直到成功。
4)Standby替代Secondary NameNode
若是没有启用HA,HDFS独立运行一个守护进程做为Secondary Namenode。按期checkpoint,合并镜像文件和edit日志。
若是当主Namenode失败时,备份Namenode正在关机(中止 Standby),运维人员依然能够从头启动备份Namenode,这样比没有HA的时候更省事,算是一种改进,由于重启整个过程已经标准化到Hadoop内部,无需运维进行复杂的切换操做。
NameNode的切换经过代failover controller来实现。failover controller有多种实现,默认实现使用ZooKeeper来保证只有一个Namenode处于active状态。
每一个Namenode运行一个轻量级的failover controller进程,该进程使用简单的心跳机制来监控Namenode的存活状态并在Namenode失败时触发failover。Failover能够由运维手动触发,例如在平常维护中须要切换主Namenode,这种状况graceful(优雅的) failover,非手动触发的failover称为ungraceful failover。
在ungraceful failover的状况下,没有办法肯定失败(被断定为失败)的节点是否中止运行,也就是说触发failover后,以前的主Namenode可能还在运行。QJM一次只容许一个Namenode写edit log,可是以前的主Namenode仍然能够接受读请求。Hadoop使用fencing来杀掉以前的Namenode。Fencing经过收回以前Namenode对共享的edit log的访问权限、关闭其网络端口使得原有的Namenode不能再继续接受服务请求。使用STONITH技术也能够将以前的主Namenode关机。
最后,HA方案中Namenode的切换对客户端来讲是不可见的,前面已经介绍过,主要经过客户端库来完成。
HDFS提供了各类交互方式,例如经过Java API、HTTP、shell命令行的。命令行的交互主要经过hadoop fs来操做。例如:
Hadoop中,文件和目录的权限相似于POSIX模型,包括读、写、执行3种权限:
每一个文件或目录都有owner,group,mode三个属性,owner指文件的全部者,group为权限组。mode
由全部者权限、文件所属的组中组员的权限、非全部者非组员的权限组成。下图表示其全部者root拥有读写权限,supergroup组的组员有读权限,其余人有读权限。
文件权限是否开启经过dfs.permissions.enabled属性来控制,这个属性默认为false,没有打开安全限制,所以不会对客户端作受权校验,若是开启安全限制,会对操做文件的用户作权限校验。特殊用户superuser是Namenode进程的标识,不会针对该用户作权限校验。
最后看一下ls命令的执行结果:
这个返回结果相似于Unix系统下的ls命令,第一栏为文件的mode,d表示目录,紧接着3种权限9位。 第二栏是指文件的副本数,这个数量经过dfs.replication
配置,目录则使用-表示没有副本一说。其余诸如全部者、组、更新时间、文件大小跟Unix系统中的ls命令一致。
若是须要查看集群状态或者浏览文件目录,能够访问Namenode暴露的Http Server查看集群信息,通常在namenode所在机器的50070端口。
前面Hadoop的文件系统概念是抽象的,HDFS只是其中的一种实现。Hadoop提供的实现以下图:
简单介绍一下,Local是对本地文件系统的抽象,hdfs就是咱们最多见的,两种web形式(webhdfs,swebhdfs)的实现经过HTTP提供文件操做接口。har是Hadoop体系下的压缩文件,当文件不少的时候能够压缩成一个大文件,能够有效减小元数据的数量。viewfs就是咱们前面介绍HDFS Federation张提到的,用来在客户端屏蔽多个Namenode的底层细节。ftp顾名思义,就是使用ftp协议来实现,对文件的操做转化为ftp协议。s3a是对Amazon云服务提供的存储系统的实现,azure则是微软的云服务平台实现。
前面咱们提到了使用命令行跟HDFS交互,事实上还有不少方式来操做文件系统。例如Java应用程序可使用org.apache.hadoop.fs.FileSystem来操做,其余形式的操做也都是基于FileSystem进行封装。咱们这里主要介绍一下HTTP的交互方式。
WebHDFS和SWebHDFS协议将文件系统暴露HTTP操做,这种交互方式比原生的Java客户端慢,不适合操做大文件。经过HTTP,有2种访问方式,直接访问和经过代理访问
直接访问
直接访问的示意图以下:
Namenode和Datanode默认打开了嵌入式web server,即dfs.webhdfs.enabled默认为true。webhdfs经过这些服务器来交互。元数据的操做经过namenode完成,文件的读写首先发到namenode,而后重定向到datanode读取(写入)实际的数据流。
经过HDFS代理
采用代理的示意图如上所示。 使用代理的好处是能够经过代理实现负载均衡或者对带宽进行限制,或者防火墙设置。代理经过HTTP或者HTTPS暴露为WebHDFS,对应为webhdfs和swebhdfs URL Schema。
代理做为独立的守护进程,独立于namenode和datanode,使用httpfs.sh脚本,默认运行在14000端口
除了FileSystem直接操做,命令行,HTTTP外,还有C语言API,NFS,FUSER等方式,这里不作过多介绍。
实际的应用中,对HDFS的大多数操做仍是经过FileSystem来操做,这部分重点介绍一下相关的接口,主要关注HDFS的实现类DistributedFileSystem及相关类。
可使用URL来读取数据,或者直接使用FileSystem操做。
java.net.URL类提供了资源定位的统一抽象,任何人均可以本身定义一种URL Schema,并提供相应的处理类来进行实际的操做。hdfs schema即是这样的一种实现。
为了使用自定义的Schema,须要设置URLStreamHandlerFactory,这个操做一个JVM只能进行一次,屡次操做会致使不可用,一般在静态块中完成。下面的截图是一个使用示例:
1) 首先获取FileSystem实例,通常使用静态get工厂方法
若是是本地文件,经过getLocal获取本地文件系统对象:
public static LocalFileSystem getLocal(COnfiguration conf) thrown IOException
2)调用FileSystem的open方法获取一个输入流:
默认状况下,open使用4KB的Buffer,能够根据须要自行设置。
3)使用FSDataInputStream进行数据操做
FSDataInputStream是java.io.DataInputStream的特殊实现,在其基础上增长了随机读取、部分读取的能力
随机读取操做经过Seekable接口定义:
seek操做开销昂贵,慎用。
部分读取经过PositionedReadable接口定义:
在HDFS中,文件使用FileSystem类的create方法及其重载形式来建立,create方法返回一个输出流FSDataOutputStream,能够调用返回输出流的getPos方法查看当前文件的位移,可是不能进行seek操做,HDFS仅支持追加操做。
建立时,能够传递一个回调接口Peofressable,获取进度信息
append(Path f)方法用于追加内容到已有文件,可是并非全部的实现都提供该方法,例如Amazon的文件实现就没有提供追加功能。
下面是一个例子:
使用mkdirs()方法,会自动建立没有的上级目录
HDFS中元数据封装在FileStatus类中,包括长度、block size,replicaions,修改时间、全部者、权限等信息。使用FileSystem提供的getFileStatus方法获取FileStatus。exists()方法判断文件或者目录是否存在;
列出文件(list),则使用listStatus方法,能够查看文件或者目录的信息
Path是个文件的时候,返回长度为1的数组。FileUtil提供的stat2Paths方法用于将FileStatus转化为Path对象。
globStatus则使用通配符对文件路径进行匹配:
public FileStatus[] globStatus(Path pathPattern) throws IOException
PathFilter用于自定义文件名过滤,不能根据文件属性进行过滤,相似于java.io.FileFilter。例以下面这个例子排除到给定正则表达式的文件:
使用FileSystem的delete()方法
public boolean delete(Path f , boolean recursive) throws IOException;
recursive参数在f是个文件的时候被忽略。若是f是文件而且recursice为true,则删除整个目录,不然抛出异常.
接下来详细介绍HDFS读写数据的流程,以及一致性模型相关的一些概念。
大体读文件的流程以下:
1)客户端传递一个文件Path给FileSystem的open方法
2)DFS采用RPC远程获取文件最开始的几个block的datanode地址。Namenode会根据网络拓扑结构决定返回哪些节点(前提是节点有block副本),若是客户端自己是Datanode而且节点上恰好有block副本,直接从本地读取。
3)客户端使用open方法返回的FSDataInputStream对象读取数据(调用read方法)
4)DFSInputStream(FSDataInputStream实现了改类)链接持有第一个block的、最近的节点,反复调用read方法读取数据
5)第一个block读取完毕以后,寻找下一个block的最佳datanode,读取数据。若是有必要,DFSInputStream会联系Namenode获取下一批Block 的节点信息(存放于内存,不持久化),这些寻址过程对客户端都是不可见的。
6)数据读取完毕,客户端调用close方法关闭流对象
在读数据过程当中,若是与Datanode的通讯发生错误,DFSInputStream对象会尝试从下一个最佳节点读取数据,而且记住该失败节点, 后续Block的读取不会再链接该节点
读取一个Block以后,DFSInputStram会进行检验和验证,若是Block损坏,尝试从其余节点读取数据,而且将损坏的block汇报给Namenode。
客户端链接哪一个datanode获取数据,是由namenode来指导的,这样能够支持大量并发的客户端请求,namenode尽量将流量均匀分布到整个集群。
Block的位置信息是存储在namenode的内存中,所以相应位置请求很是高效,不会成为瓶颈。
步骤分解
1)客户端调用DistributedFileSystem的create方法
2)DistributedFileSystem远程RPC调用Namenode在文件系统的命名空间中建立一个新文件,此时该文件没有关联到任何block。 这个过程当中,Namenode会作不少校验工做,例如是否已经存在同名文件,是否有权限,若是验证经过,返回一个FSDataOutputStream对象。 若是验证不经过,抛出异常到客户端。
3)客户端写入数据的时候,DFSOutputStream分解为packets(数据包),并写入到一个数据队列中,该队列由DataStreamer消费。
4)DateStreamer负责请求Namenode分配新的block存放的数据节点。这些节点存放同一个Block的副本,构成一个管道。 DataStreamer将packet写入到管道的第一个节点,第一个节点存放好packet以后,转发给下一个节点,下一个节点存放 以后继续往下传递。
5)DFSOutputStream同时维护一个ack queue队列,等待来自datanode确认消息。当管道上的全部datanode都确认以后,packet从ack队列中移除。
6)数据写入完毕,客户端close输出流。将全部的packet刷新到管道中,而后安心等待来自datanode的确认消息。所有获得确认以后告知Namenode文件是完整的。 Namenode此时已经知道文件的全部Block信息(由于DataStreamer是请求Namenode分配block的),只需等待达到最小副本数要求,而后返回成功信息给客户端。
Namenode如何决定副本存在哪一个Datanode?
HDFS的副本的存放策略是可靠性、写带宽、读带宽之间的权衡。默认策略以下:
这样选择很好滴平衡了可靠性、读写性能
一致性模型描述文件系统中读写操纵的可见性。HDFS中,文件一旦建立以后,在文件系统的命名空间中可见:
可是任何被写入到文件的内容不保证可见,即便对象流已经被刷新。
“`java
Path p = new Path(“p”);
OutputStream out = fs.create(p);
out.write(“content”.getBytes(“UTF-8”));
out.flush();
assertTaht(fs.getFileStatus(p).getLen,0L); // 为0,即便调用了flush
关闭对象流时,内部会调用hflush方法,可是hflush不保证datanode数据已经写入到磁盘,只是保证写入到datanode的内存, 所以在机器断电的时候可能致使数据丢失,若是要保证写入磁盘,使用hsync方法,hsync类型与fsync()的系统调用,fsync提交某个文件句柄的缓冲数据。
使用hflush或hsync会致使吞吐量降低,所以设计应用时,须要在吞吐量以及数据的健壮性之间作权衡。
另外,文件写入过程当中,当前正在写入的Block对其余Reader不可见。
在读取和写入的过程当中,namenode在分配Datanode的时候,会考虑节点之间的距离。HDFS中,距离没有
采用带宽来衡量,由于实际中很难准确度量两台机器之间的带宽。
Hadoop把机器之间的拓扑结构组织成树结构,而且用到达公共父节点所需跳转数之和做为距离。事实上这是一个距离矩阵的例子。下面的例子简明地说明了距离的计算:
同一数据中心,同一机架,同一节点距离为0
同一数据中心,同一机架,不一样节点距离为2
同一数据中心,不一样机架,不一样节点距离为4
不一样数据中心,不一样机架,不一样节点距离为6
Hadoop集群的拓扑结构须要手动配置,若是没配置,Hadoop默认全部节点位于同一个数据中心的同一机架上。
前面的关注点都在于单线程的访问,若是须要并行处理文件,须要本身编写应用。Hadoop提供的distcp工具用于并行导入数据到Hadoop或者从Hadoop导出。一些例子:
distcp是底层使用MapReduce实现,只有map实现,没有reduce。在map中并行复制文件。 distcp尽量在map之间平均分配文件。map的数量能够经过-m参数指定:
hadoop distcp -update -delete -p hdfs://master1:9000/foo hdfs://master2/foo
这样的操做经常使用于在两个集群之间复制数据,update参数表示只同步被更新过的数据,delete会删除目标目录中存在,可是源目录不存在的文件。p参数表示保留文件的全校、block大小、副本数量等属性。
若是两个集群的Hadoop版本不兼容,可使用webhdfs协议:
hadoop distcp webhdfs://namenode1:50070/foo webhdfs://namenode2:50070/foo
在distcp工具中,若是咱们指定map数量为1,不只速度很慢,每一个Block第一个副本将所有落到运行这个惟一map的节点上,直到磁盘溢出。所以使用distcp的时候,最好使用默认的map数量,即20. HDFS在Block均匀分布在各个节点上的时候工做得最好,若是没有办法在做业中尽可能保持集群平衡,例如为了限制map数量(以便其余节点能够被别的做业使用),那么可使用balancer工具来调整集群的Block分布