-
数据块
-
HDFS有数据块的概念,默认为128M,但与面向单一磁盘的文件系统不同的是,HDFS中小于块大小的文件不会占据整个块的空间;
-
第一个是减少了寻址开销。如果块足够大,从磁盘传输数据的时间会明显大于定位这个块开始位置所需的时间。因而,传输一个由多个块组成的大文件的时间取决于磁盘传输速率;
-
使用分布式文件系统中的块而非整个文件作为存储单元会带来很多好处
- 第一是一个文件的大小可以大于网络上任意一个磁盘的容量。文件的所有块并不需要存储在同一个磁盘上,它们可以利用集群上任意一个磁盘进行存储;
- 第二个好处大大简化了存储子系统的设计,由于块的大小是固定的,因此计算单个磁盘能存储多少个块就相对容易。
-
-
namenode和datanode
-
HDFS集群集群有两类节点以master-slaves模式运行,即一个namenode对应多个datanode。namenode管理文件系统的命名空间。它维护着文件系统树及整棵树内所有的文件和目录。这些信息以两个文件的形式被持久化到本地磁盘:FsImage和EditLog。namenode也记录着每个文件中各个块所在的数据节点信息,但它并不永久保存块的位置信息,因为这些信息会在系统启动时,根据datanode报告的信息重建;
-
namenode的容错机制
- 第一种机制是备份那些组成文件系统元数据持久状态的文件。Hadoop可以通过配置使namenode在多个文件系统上保存元数据的持久状态。这些写操作是实时同步的,且是原子操作。一般的配置是,将持久状态写入本地磁盘的同时,写入一个远程挂载的网络文件系统NFS;
- 另一种可行的方法是运行一个复制namenode,但它不能被用作namenode。辅助namenode的作用是定期合并和保存编辑日志和命名空间镜像,一般在另一台单独的机器上运行,因为它需要占用大量CPU时间,并且需要和namenode一样多的内存来执行合并操作。但由于辅助namenode保存的状态总是滞后于主节点,难免会丢失部分数据。在这种情况下,一般把存储在NFS上的namenode元数据复制到辅助namenode并作为新的namenode运行。
-
块缓存
- 对于访问频繁的文件,其对应的块可能被显式地缓存在datanode的内存中,以堆外块缓存的形式存在,可以提高读操作的性能。用户或应用通过在缓存池中增加一个cache directive来告诉namenode需要缓存哪些文件及缓存多久。缓存池是一个用于管理缓存权限和资源使用的管理性分组。
-
联邦HDFS
- namenode在内存中保存文件系统中每个文件和每个数据块的引用关系。因此对一个超大集群来说,内存将成为限制系统横向扩展的瓶颈。在2.X版本引入的联邦HDFS允许系统通过添加namenode实现横向扩展,其中每个namenode管理文件系统命名空间中的一部分。
- 在联邦环境下,每个namenode维护一个命名空间卷,由命名空间的元数据和一个数据块池组成。数据块池包含该命名空间下文件的所有数据块。命名空间卷之间是相互独立的,两两之间并不互相通信,一个namenode的失效也不会影响其他namenode的可用性。数据块池不再进行切分,因此集群中的datanode需要注册到每个namenode,并且存储来自多个数据块池的数据块。
-
HDFS的高可用性
-
通过联合使用在多个文件系统中备份namenode和通过备用namenode创建监测点能防止数据丢失,但是依然无法实现HDFS的高可用,HDFS依旧存在单点失效问题,且冷启动恢复的时间较长,影响到日常维护;
-
Hadoop2针对上述问题,配置了一对活动-备用(active-standby)namenode。当活动namenode失效,备用namenode就会接管它的任务并开始服务于来自客户端的请求。实现这一目标需要在架构上做如下修改:
- namenode之间需要通过高可用共享存储实现编辑日志的共享。当备用namenode接管工作后,它将通读共享日志来实现与活动namenode的状态同步,并继续读取由活动namenode写入的新条目,可以从两种高可用性共享存储中做出选择NFS过滤器或群体日志管理器(QJM)
- QJM是一个专用的HDFS实现,为提供一个高可用的编辑日志而设计。QJM以一组日志节点的形式运行,所以系统能够忍受其中任何一个的丢失
- datanode需要同时向两个namenode发送数据块处理报告,因为数据块的映射信息存在namenode的内存而非磁盘中;
- 辅助namenode的角色被备用namenode所包含,备用namenode为活动的namenode命名空间设置检查点。
-
在活动namenode失效后,备用namenode能在几十秒的时间内实现任务接管,因为最新状态存储在内存中:包括最新的编辑日志条目和最新的数据块映射信息;
-
在活动namenode和备用namenode都失效的情况下,还可以指定一个备用namenode实现冷启动。
-
故障切换与规避
- 系统中有一个称为故障转移控制器的实体,管理着活动namenode转为备用namenode的过程。有多种故障转移控制器,默认一种的是使用了zookeeper来确保有且仅有一个活动namenode。每一个namenode运行着一个轻量级的故障转移控制器,其工作就是监视宿主namenode是否失效(通过心跳机制),并在namenode失效时进行故障切换;
- 在网速非常慢的情况下,同样有可能激发故障转移,但是先前活动的namenode依然运行着并且是活动namenode。为了确保先前活动的namenode不会执行危害系统并导致系统崩溃的操作,高可用采用了规避的方法:
- 同一时间QJM仅允许一个namenode向编辑日志中写入数据。然而,对于先前活动的namenode而言,仍然有可能相应并处理客户端的读请求,因此设置一个SSH规避命令用于杀死namenode的进程是一个好主意,或者对namenode所在的主机进行断电操作。
-
-
-
Hadoop文件系统:Hadoop有一个抽象的文件系统概念,HDFS只是其中的一个实现。还有Local、HDFS、WebHDFS、SecureHDFS、HAR和VIEW等
-
Java接口
-
从Hadoop URL读取数据
- 要从Hadoop文件系统读取文件,最简单的方法是使用java.net.URL对象打开数据流。但是让Java程序识别Hadoop的hdfs URL还需要一些额外的工作。可以通过FsUrlStreamHandlerFactory实例调用java.net.URL对象的setURLStreamHandlerFactory()方法。每个Java虚拟机只能调用一次这个方法,因此通常在静态方法中调用。如果该方法已被其他第三方组件调用,还可以通过FileSystem API读取数据
-
通过FileSystem API读取数据
-
如上所述,当不能在应用中调用setURLStreamHandlerFactory()方法时,我们需要用FileSystem API来打开一个文件的输入流。
-
获取FileSystem实例有下面这几个静态工厂方法
- public static FileSystem get(Configuration conf) throws IOException:该方法返回的是默认文件系统,即core-site.xml中指定的文件系统,如果没有指定则返回本地文件系统
- public static FileSystem get(URI uri, Configuration conf) throws IOException:第二个方法通过给定的URI方案和权限来确定要使用的文件系统,如果给定的URI中没有指定方案,则返回默认文件系统
- public static FileSystem get(URI uri, Configuration conf, String user) throws IOException:第三是作为指定用户来访问文件系统,对于安全来说至关重要
- public static LocalFileSystem getLocal(Configuration conf):如果想直接获取本地文件系统,可以通过getLocal()很方便地获取
-
有了FileSystem实例之后,我们可以调用open()函数来获取文件的输入流
- public FSDataInputStream open(Path f) throws IOException:该方法默认缓冲区大小4KB
- public FSDataInputStream open(Path f, int bufferSize) throws IOException:该方法可以自定义缓冲区大小
-
FSDataInputStream对象
-
实际上,FileSystem对象中的open()方法返回的是FSDatainputStream对象,而非标准的java.io类对象。这个类是继承了java.io.DataInputStream的一个特殊类,并支持随机访问,可以从流的任意位置读取数据。
-
Seekable接口支持在文件中找到指定位置,并提供两个定位方法getPos()和seek()
- getPos():查询当前位置相对于文件起始位置偏移量
- seek():java.io.InputStream的skip()方法只能相对于当前位置定位到另一个新位置,而seek()支持移动到文件中任意一个绝对位置,但调用seek()来定位大于文件长度的位置会引发IOException异常
-
FSDataInputStream类也实现了PositionedReadable接口,从一个指定的偏移量处读取文件的一部分,有三个方法
- int read(long position, byte[] buffer, int offset, int length) throws IOException:该方法从文件指定position处读取至多length字节的数据并存入缓冲区buffer的指定偏移量offset处。返回值是实际读到的字节数,可能小于指定的length
- void readFully(long position, byte[] buffer, int offset, int length) throws IOException:该方法将指定length长度的字节数数据读取到buffer中,除非已经读到文件末尾,这时会抛出EOFException异常
- int readFully(long position, byte[] buffer) throws IOException
- 所有这些方法都会保留文件当前偏移量,并且是线程安全的,因此它们提供了在读取文件主体时,访问文件其他部分(可能是元数据)的遍历方法。
-
-
牢记seek()方法是一个相对高开销的操作,需要慎重使用。建议用流数据来构建应用的访问模式(比如使用mapreduce),而非执行大量seek()方法。
-
-
写入数据
-
FileSystem有一系列新建文件的方法
- public FSDataOutputStream create(Path f) throws IOException:最简单的方法是给准备建的文件指定一个Path对象,然后返回一个用于写入数据的输出流。该方法有多个重载版本,允许我们指定是否需要强制覆盖现有文件、文件备份数量、写入时所用缓冲区大小、文件块大小以及文件权限。tips:create()方法能够为需要写入且当前不存在的文件创建父目录。如果希望父目录不存在就导致文件写入失败,可以调用exists()方法检查父目录是否存在。另一种方案是使用FileContext,可以控制是否创建父目录。
- public void progress():该方法可以把数据写入datanode的进度通知给应用。
- public FSDataOutputStream append() throws IOException:另一个新建文件的方法是使用append()方法在一个现有文件末尾追加数据。这样的追加操作允许一个writer打开文件后在访问该文件的最后偏移量处追加数据。有了这个api,某些应用可以创建无边界文件,例如,应用可以在关闭日志文件之后继续追加日志。该追加操作是可选的,并非所有Hadoop文件系统都支持追加操作,如HDFS支持追加,S3不支持。
-
-
目录:
- public boolean mkdirs(Path f) throws IOException:FileSystem实例提供了创建目录的方法,这个方法可以一次性新建所有必要但是还没有的父目录,就像java.io.mkdirs()方法一样。如果目录(以及所有的父目录)都已经创建成功,则返回true。通常,你不需要显式创建一个目录,因为调用create()方法写入文件时会自动创建父目录
-
查询文件系统
-
文件元数据:FileStatus类封装了文件系统中文件和目录的元数据,包括文件长度、块大小、复本、修改时间、所有者以及权限信息,FileSystem的getFileStatus()方法用于获取文件或目录的FileStatus对象。如果文件或目录不存在,会抛出一个FileNotFoundException异常。
-
列出文件
- public FileStatus[] listStatus(Path f)(Path f, PathFilter filter)(Path[] files)(Path[] files, PathFilter filter) throws IOException:当传入的参数是一个文件时,它会简单转变成以数组方式返回长度为1的FileStatus对象。当传入对象是一个目录时,则返回0或多个FileStatus对象,表示此目录中包含的文件和目录。它的重载方法允许使用PathFilter来限制匹配的文件和目录
-
文件模式:在单个操作中处理一批文件是一个很常见的需求。在一个表达式中使用通配符来匹配多个文件是比较方便的,Hadoop为执行统配提供了两个FileSystem方法,globStatus()方法返回路径格式与指定模式匹配的所有FileStatus对象组成的数组并按路径排序。PathFilter可以进一步对匹配结果进行限制:
- public FileStatus[] globStatus(Path pathPattern) throws IOException
- public FileStatus[] globStatus(Path pathPattern, PathFilter filter) throws IOException
-
PathFilter对象:可以用正则的方式更精确地排除匹配的路径
-
删除数据:FileSystem的delete()方法可以永久性删除文件或目录
-
-
-
数据流
-
剖析文件读取
- 客户端通过调用FileSystem的open()方法来打开希望读取的文件,对于HDFS来说,这个对象是DistributedFileSystem类的一个实例。
- DistributedFileSystem实例通过使用远程过程调用(RPC)来调用namenode,以确定文件起始块的位置。对于每一个块,namenode返回存有该块副本的datanode地址。此外,这些datanode根据它们与客户端的距离来排序。如果该客户端本身就是一个datanode,那么该客户端将会从保存有相应数据块复本的本地datanode读取数据。
- DistributedFileSystem实例返回一个FSDataInputStream对象给客户端以便读取数据。FSDataInputStream类转而封装DFSInputStream对象,该对象管理着datanode和namenode的IO。
- 接着,客户端对这个输入流调用read()方法,存储着文件几个起始块的datanode地址的DFSInputStream随机连接距离最近的文件中第一个块所在的datanode。
- 通过对数据反复调用read()方法,可以将数据从datanode传输到客户端。到达块的末端时,DFSInputStream关闭与该datanode连接,然后寻找下一个块的最佳datanode。
- 客户端从流中读取数据时,块是按照打开DFSInputStream与datanode新建连接的顺序读取的。它也会根据需要询问namenode来检索下一批数据块的datanode的位置。一旦客户端完成读取,就对FSDataInputStream调用close()方法。
- 在读取数据时,如果DFSInputStream与datanode通信出错,会尝试从这个块的另外一个最近邻datanode上读取数据。它也会记住这个故障的datanode,以保证以后不会反复读取该节点上后续的块。DFSInputStream也会通过校验和确认从datanode发来的数据是否完整。如果发现有损坏的块,DFSInputStream会试图从其他datanode读取其复本,也会将被损坏的块通知给namenode。这个设计的一个重点是,数据流分散在集群中的所有datanode,所以这种设计能使HDFS扩展到大量的并发客户端。因为客户端可以直接连接到datanode检索数据,namenode只需要响应块位置的请求(这些信息存储在内存,因此十分高效),无须响应数据请求,否则随着客户端数量的增长,namenode会很快成为瓶颈。
-
剖析文件写入
-
客户端通过对DistributedFileSystem对象调用create()方法来新建文件,DistributedFileSystem对namenode创建一个RPC调用,在文件系统的命名空间中新建一个文件,此时文件系统中还没有相应的数据块。
-
namenode执行各种不同的检查以确保这个文件不存在以及该客户端有新建该文件的权限。如果这些检查均通过,namenode就会为创建新文件增加一条记录;否则,文件创建失败并向客户端抛出一个IOException异常。
-
DistributedFileSystem向客户端返回一个FSDataOutputStream对象,由此客户端可以开始写入数据。就像读取数据一样,FSDataOutputStream封装了DFSOutputStream对象,该对象负责处理datanode和namenode之间的通信。
-
在客户端写入数据时,DFSOutputStream将它分成一个个的数据包,并且写入内部队列,称为数据队列。DataStreamer处理数据队列,它的责任是挑选处合适存储数据副本的一组datanode,并据此来要求namenode分配新的数据块。这一组datanode构成一个管线--假设复本数为3,所以管线中有3个节点。DataStreamer将数据包流式传输到管线中的第1个datanode,该datanode存储数据包并将它发送到第二个datanode。第2个datanode以同样的方式传输给第3个。
-
DFSOutputStream也维护着一个内部数据包队列来等待datanode的确认回执(ack)。收到管道中所有datanode的ack后,该数据包才会从确认队列中删除。
-
如果任何datanode在数据写入期间发生故障,则执行以下操作:首先关闭管线,确认把队列中的所有数据包都添加回数据队列的最前端,以确保故障节点下游的datanode不会漏掉任何一个数据包。为存储在另一正常datanode的当前数据块指定一个新的标识,并将该标识传送给namenode,以便故障datanode在恢复后可以删除存储的部分数据块。余下的数据块写入管线中正常的datanode。namenode注意到块复本量不足时,会在另一个节点上创建一个新的复本。后续的数据块继续正常接受处理。
- 一个块在被写入期间可能会有多个datanode同时发生故障,但非常少见。只要写入了dfs.namenode.reolication.min的复本数(默认为1),写操作就会成功,并且这个块可以在集群中异步复制,知道达到其目标复本数(dfs.replication的默认值为3)
-
客户端完成数据写入后,对数据流调用close()方法。该操作将剩余的所有数据包写入datanode管线,并等待namenode告诉客户端文件写入完成。namenode已经知道文件由哪些块组成(因为DataStreamer请求分配数据块),所以它在返回成功前只需要等待数据块进行最小量的复制。
-
-
一致性模型:文件系统的一致模型描述了文件读写的数据可见性。HDFS为性能牺牲了一些POSIX要求
- 新建一个文件之后,它能在文件系统的命名空间立即可见,但是写入文件的内容并不保证能立即可见,即使数据流已经刷新并存储。当写入的数据超过一个块后,第一个数据块对新的reader就是可见的。之后的块也不例外。总之,当前正在写入的块对其他reader不可见。
- HDFS提供了一种强行将所有缓存刷新到datanode的手段,即对FSDataOutputStream调用hflush()方法。当hflush()方法返回成功后,对所有新的reader而言,HDFS能保证文件中到目前为止写入的数据均到达所有datanode的写入管道并且对所有新的reader可见。
- 注意,hflush()不保证datanode已经将数据写到磁盘上,仅确保数据在datanode的内存中。为确保数据写到磁盘上,可以用hsync()替代。hsync()操作类似POSIX中的fsync()系统调用,该调用提交的是一个文件描述符的缓冲数据。HDFS中close()方法其实隐含了hflush()方法。
-