HDFS导论(转)

1.流式数据访问

HDFS的构建思想是这样的:一次写入,多次读取是最高效的访问模式。数据集通常有数据源生成或从数据源复制而来,接着长时间在此数据集上进行各类分析。每次分析都将设计数据集的大部分数据甚至全部,因此读取整个数据集的时间延迟比读取第一条数据的时间延迟更重要。

2.关于时间延迟的数据访问

要求低时间延迟数据访问的应用,例如几十毫秒的范围,不适合在HDFS上运行,记住,HDFS是为高数据吞吐量应用优化的,这可能会以高时间延迟为代价。目前对于低时间延迟的数据访问应用,HBase是更好的选择。

3.大量的小文件

由于namenode将文件系统的元文件存储在内存中,因此该文件系统所能存储的文件总数受限于namenode的内存容量。根据经验,每个文件、目录和 数据快的存储信息大约占150字节。因此,举例来说,如果有一百万个文件,且每个文件占一个数据块,那么至少需要300MB的内存,尽管存储上百万的文件 是可行的,但是存储数十亿个文件就超出了当前硬件的能力。

4.HDFS中的块

HDFS中的块默认大小为64MB,与单一磁盘上的文件系统类似,HDFS中的文件也被划分为块大小的多个分块(chunk),作为独立的存储单 元。但与其他的文件系统不同的是,HDFS中小于一个块大小的文件不会占据整个块的空间。HDFS的块比磁盘块(一般为512KB)大,目的是为了最小化 寻址开销。如果块设置的足够大,从磁盘传输数据的时间明显大于定位这个块起始位置所需的时间。这样传输一个由多个块组成的文件的时间取决于磁盘传输速率。 但是该参数也不应过大,MapReduce中map任务通常一次处理一个块的数据,因此如果任务数太少(少于集群中的节点数),作业的运行速度就会比较慢。

对分布式文件系统中的块进行抽象会带来许多好处。第一个明显的好处是,一个文件的大小可以大于网络中任意一个磁盘的容量。文件的所有块并不需要存储 在同一个磁盘上,因此他们可以利用集群中的任意一个磁盘进行存储。事实上,尽管不常见,但对于HDFS集群而言,也可以仅存储一个文件,该文件的块占满集 群中的所有磁盘。

第二个好处是,使用块而非整个文件作为存储单元,大大简化了存储子系统的设计。

不仅如此,块非常适合用于数据备份进而提供数据容错能力和可用性。将每个块复制到少数几个独立的机器上(默认是3个),可以确保块、磁盘或机器故障时数据不丢失。

与磁盘文件系统类似,HDFS中fsck指令可以显示块信息。

hadoop fsck / -files -blocks

在HDFS里面,data node上的块大小默认是64MB(或者是128MB或256MB)

为什么不能远少于64MB(或128MB或256MB) (普通文件系统的数据块大小一般为4KB)?

1、减少硬盘寻道时间(disk seek time)

HDFS设计前提是支持大容量的流式数据操作,所以即使是一般的数据读写操作,涉及到的数据 量都是比较大的。假如数据块设置过少,那需要读取的数据块就比较多,由于数据块在硬盘上非连续存储,普通硬盘因为需要移动磁头,所以随机寻址较慢,读越多 的数据块就增大了总的硬盘寻道时间。当硬盘寻道时间比io时间还要长的多时,那么硬盘寻道时间就成了系统的一个瓶颈。合适的块大小有助于减少硬盘寻道时间,提高系统吞吐量。

2、减少Namenode内存消耗

对于HDFS,他只有一个Namenode节点,他的内存相对于Datanode来说,是极 其有限的。然而,namenode需要在其内存FSImage文件中中记录在Datanode中的数据块信息,假如数据块大小设置过少,而需要维护的数据 块信息就会过多,那Namenode的内存可能就会伤不起了。

为什么不能远大于64MB(或128MB或256MB)?(这里主要从上层的MapReduce框架来讨论)

1、Map崩溃问题

系统需要重新启动,启动过程需要重新加载数据,数据块越大,数据加载时间越长,系统恢复过程越长。

2、监管时间问题

主节点监管其他节点的情况,每个节点会周期性的把完成的工作和状态的更新报告回来。如果一个节点保持沉默超过一个预设的时间间隔,主节点记录下这个节点状 态为死亡,并把分配给这个节点的数据发到别的节点。对于这个“预设的时间间隔”,这是从数据块的角度大概估算的。假如是对于64MB的数据块,我可以假设 你10分钟之内无论如何也能解决了吧,超过10分钟也没反应,那就是死了。可对于640MB或是1G以上的数据,我应该要估算个多长的时间内?估算的时间 短了,那就误判死亡了,分分钟更坏的情况是所有节点都会被判死亡。估算的时间长了,那等待的时间就过长了。所以对于过大的数据块,这个“预设的时间间隔” 不好估算。

3、问题分解问题

数据量大小是问题解决的复杂度是成线性关系的。对于同个算法,处理的数据量越大,它的时间复杂度也就越大。

4、约束Map输出

在Map Reduce框架里,Map之后的数据是要经过排序才执行Reduce操作的。想想归并排序算法的思想,对小文件进行排序,然后将小文件归并成大文件的思想,然后就会懂这点了....

5.namenode和datanode

HDFS集群中有两类节点,并以管理者-工作者模式运行,即一个namenode(管理者)和多个datanode(工作者)。

namenode管理文件系统的命名空间。它维护文件系统树和文件系统数中所有文件和目录。这些信息以两种方式永久保存在本地磁盘上:命名空间镜像文件和编辑日志文件。

datanode是文件系统的工作者。它们存储并提供定位块的服务(被用户或名称节点调用时),并且定时的向名称节点发送它们存储的块的列表。

没有namenode,文件系统将无法使用。如果namenode机器损坏,那么文件系统上的文件将会丢失,因此对实现namenode的容错非常重要,Hadoop为此提供了两种机制:

第一种机制是备份那些组成文件系统元数据持久状态的文件。一般配置是,将持久态写入本地磁盘的同时,写入一个远程挂在的网络文件系统(NFS)。

另一种方式是运行一个辅助的namenode。虽然它不能作为名称节点使用。这个二级名称节点的重要作用就是定期的通过编辑日志合并命名空间镜像, 以防止编辑日志过大。这个二级名称节点一般在其他单独的物理计算机上运行,因为它也需要占用大量 CPU 和内存来执行合并操作。它会保存合并后的命名空间镜像的副本,在名称节点失效后就可以使用。

但是,二级名称节点的状态是比主节点滞后的,所以主节点的数据若全部丢失,损失仍在所难免。在这种情况下,一般把存在 NFS 上的主名称节点元数据复制到二级名称节点上并将其作为新的主名称节点运行。

6.客户端读取HDFS中的数据

 

(1)客户端通过调用FileSystem对象的open()方法来打开希望读取的文件,对于HDFS来说,这个对象是分布式文件系统的一个实例。

(2)DistributedFileSystem通过RPC(远程过程调用)来调用namenode,以确定文件起始块的位置。对于每一个块,namenode返回存有该块副本的datanode地址。此外这些datanode根据他们与namenode的距离来排序。

          DistributedFileSystem类返回一个FSDataInputStream对象(一个支持文件定位的输入流)给客户端并读取数据。 FSDataInputStream类转而封装DFSInputStream对象,该对象管理着namenode和datanode的I/O。

(3)接着客户端对这个输入流调用read()方法。存储着文件起始块的datanode地址的DFSInputStream随即连接距离最近的datanode。

(4)通过对数据流反复调用read()方法,可以将数据从datanode传输到客户端。

(5)到达块的末端时,DFSInputStream会关闭与该datanode的连接,然后寻找下一个块的最佳datanode。客户端只需连续的读取连续的流,并且对于客户端都是透明的。

          客户端从流中读取数据时,块是按照打开DFSInputStream与datanode新建连接的顺序读取的。它也需要询问namenode来检索下一批所需块的datanode的位置。

(6)一旦客户端完成读取,就对DFSInputStream调用close()方法。

          在读取数据的时候,如果DFSInputStream与datanode的通信出现错误,它便会尝试从这个块的另外一个最邻近的datanode读取数据。它也会记住出现故障的datanode,以保证以后不会反复读取该节点上后续的块。DFSInputStream也会通过“校验和”确认从 datanode发来的数据是否完整。如果发现一个损坏的块,它就会在DFSInputStream试图从其他datanode读取一个块副本之前通知 namenode。

7.写入HDFS

我们要考虑的情况是如何创建一个新文件,并把数据写入该文件,最后关闭该文件。

(1)客户端对DistributedFileSystem对象调用create()方法来创建文件。

(2)DistributedFileSystem对namenode创建一个RPC调用,在文件系统的命名空间中创建一个新文件,此时该文件中还 没有相应的数据块。namenode执行各种检查以确保这个文件不存在,并且客户端有创建该文件的权限。如果这些检查均通过,namenode就会为创建 新文件记录一条记录;否则,创建失败,并向客户端抛出一个IOException异常。DistributedFileSystem向客户端返回一个 FSDataOutputStream对象,由此客户端可以开始写数据。就像读取数据一样,FSDataOutputStream封装一个 DFSOutputStream对象,该对象负责处理datanode和namenode之间的通信。

(3)在客户端写入数据时,DFSOutputStream将他们分成一个个的数据包,并写入内部队列,成为数据队列(data queue)。

(4)DataStreamer处理数据队列,它的责任是根据datanode队列来要求namenode分配合适的新块来存储数据备份。这一组 datanode组成一个管线——我们假设副本数量为3,所以管线中有3个节点。DataStreamer将数据包流式的传输到管线的第一个 datanode,该datanode存储数据并将数据发送到管线的第二个datanode。同样的,第二个datanode存储该数据包并发送给管线中 的第三个(也就是最后一个)datanode。

(5)FSOutputStream也维护着一个内部数据包队列来等待datanode的收到确认回执,成为“确认队列”(ack queue)。当管线中所有datanode确认信息后,该数据包才会从确认队列中删除。

          如果在写入期间,datanode遇到故障,则执行一下操作,这对于写入客户端是透明的。首先关闭管线,确认把队列中的任何数据包都放回到数据队列的最前 端,以保证故障点下游的datanode不会漏掉任何一个数据包。为存储在另一个正常datanode的当前数据块指定一个新的标识,并将标识传递给 namenode,以便故障datanode在恢复后可以删除存储的部分数据块。从管线中删除故障节点并把余下的数据块写入管线中的两个正常 datanode。namenode注意到块副本量不足,会在另一个节点上创建一个新的副本。后续的数据块继续正常接受处理。

(6)客户端完成写入后,会对数据流调用close()方法。该操作将剩余的所有数据包写入datanode管线中,并在联系namenode且发送文件写入完成信号之前,等待确认。

(7)namenode已经知道文件由那些数据块组成(通过DataStreamer询问数据块的分配),所以它在返回成功之前只需等待数据块进行最小量的复制。

8.复本的布局

namenode如何选择在哪个datanode存储复本(replica)?这里需要在可靠性,写入带宽和读取带宽之间进行权衡。

Hadoop的默认布局策略是在运行客户端的节点上放第一个复本。第二个复本与第一个不同且随机另外选择的机架中节点上(离架)。第三个复本与第二 个复本放在相同的机架上,且随机选择另外一个节点。其他的复本放在集群中随机选择的节点上,不过系统会避免在相同的机架上放太多复本。

9.一致模型

HDFS提供一个方法来强制所有缓存与数据节点同步,及对DataOutputStream调用sync()方法。当sync()方法放回成功后,对所有新的reader而言,HDFS能保证到目前为止写入的数据均一致且可见。

10.HDFS的数据完整性

HDFS会对写入的所有数据计算校验和(checksum),并在读取数据时验证校验和。它针对每个有io.bytes.per.checksum 指定字节的数据计算校验和。默认情况下为512字节,由于CRC -32校验和是4个字节,所以存储校验和的额外开销小于1%。

datanode负责在验证收到的数据后存储数据及其校验和。它在收到客户端数据或复制其它datanode数据期间执行这个操作。正在写数据的客户端将数据及其校验和发送到由一系列datanode组成的管线,管线的最后一个datanode负责验证校验和。

客户端从datanode中读取数据时也会验证校验和,将它们与datanode中的校验和进行比较。每个datanode都会持久化存储一个用户 验证的校验和日志,所以它知道每个块最后一次验证时间。客户端成功验证一个数据块后,会告诉这个datanode,datanode由此更新日志。

不只是客户端读取数据时会验证校验和,每个datanode也会在一个后台线程中运行一个DataBlockScanner,从而定期检查存储在这个datanode上的所有数据块。

可以使用RawLocalFileSystem类来禁用校验和。

11.压缩

压缩格式总结
压缩格式 工具 算法 文件扩展名 是否包含多个文件 是否可切分
DEFLATE N/A DEFLATE .deflate
Gzip gzip DEFLATE .gz
bzip2 bzip2 bzip2 .bz2
LZO Lzop LZO .lzo

 

 

 

 

 

所有压缩算法都要权衡时间/空间:压缩和解压缩速度更快,其代价通常只是节省少量空间。表中列出的压缩工具都提供9个不同的选项来控制压缩时必须考虑的权衡:选项-1为优化速度,-9为优化压缩空间。

gzip是一个通用的压缩工具,在空间/时间权衡中,居于其他两种压缩方法之间。bzip2更高效,但是更慢。LZO优化压缩速度,但是压缩效率稍逊一筹。

在hadoop中可以使用CompressionCodec对数据流进行压缩和解压缩。如果要对写入输出流的数据进行压缩,可用 createOutputStream(OutputStream out)方法在在底层的数据流中对需要以压缩格式写入在此之前尚未压缩的数据建立一个CompressionOutputStream对象,相反,对输入 数据流读取数据进行解压缩时,调用createInputStream(InputStream in)获取CompressionInputStream。

12.序列化

所谓序列化(serialization),是将结构化对象转化成字节流,以便在网络上传输或写入磁盘永久保存。反序列化,是将字节流转化回结构化对象的过程。

序列化在分布式数据处理的两大领域中广泛出现:进程间通信(RPC)和永久储存。

hadoop只用自己的序列化格式Writable,它格式紧凑,速度快。

13.Writable

Writable类的层次结构:

Java类型的Writable封装
Java基本类型 Writable实现 序列化大小(字节)
boolean BooleanWritable 1
byte ByteWritable 1
int IntWritable 4
  VintWritable 1~5
float FloatWritable 4
long LongWritable 8
  VlongWritable 1~9
double DoubleWritable 8
String(UTF-8) Text  

 

 

 

 

 

 

 

 

 

 

14.MapReduce作业运行机制

可以只用一行代码来运行一个MapReduce作业:JobClient.runJob(conf)(如果是较新的版本,其实质也是调用这个方法)。分析其过程细节:

整个过程如图所示,包含如下4个独立的实体:

  • 客户端:提交MapReduce作业。
  • jobtracker:协调作业运行。
  • tasktracker:运行作业划分后的任务。
  • 分布式文件系统:用来在其他实体间共享作业文件。

(1)作业的提交:

JobClient的runjob()方法是创建JobClient实例并调用它的submitJob()方法的快捷方式(步骤1)。作业提交 后,runJob()每秒轮询作业的进度,如果发现自上次报告后有变化,便把进度报告到进度台。作业完成后,如果成功,就显示作业计数器。如果失败,导致 作业失败的错误被记录到控制台。

JobClient实现的submitJob()方法实现的作业提交过程如下:

  • 向jobtracker请求一个新的作业ID。
  • 检查作业的输出说明。例如,如果没有指定作业的输出目录或者输出目录已存在,就不能提交作业,错误抛回MapReduce程序。
  • 计算作业的输入分片。
  • 将作业所需的资源(包括作业JAR文件,配置文件和计算所得的输入分片)复制到一个以作业ID命名的目录下jobtracker的文件系统中。
  • 告知jobtracker作业准备执行。

(2)作业的初始化

当JobTracker接收到jobclient的submitJob()方法调用后,会把此调用放入一个内部队列中,交由作业调度器(job scheduler)进行调度,并对其进行初始化。初始化包括建立一个正在运行作业的对象——封装任务和记录信息,以便跟踪任务的状态和进程。

为了创建任务运行列表,作业调度器首先从共享文件系统获取JobClient已经计算好的输入分片信息。然后为每个分片创建一个map任务。创建的 reduce任务数有JobConf的mapred.reduce.task属性决定。然后调度器创建相应数量的reduce任务。任务ID在此时被指 定。

(3)任务的分配

tasktracker运行一个简单的循环来定期发送“心跳”(heartbeat)给jobtracker。心跳告诉 jobtracker,tasktracker是否还存活,同时也充当两者之间的消息通道。作为“心跳”的一部分,tasktracker会指明它是否已 经准备好运行新的任务,如果是,jobtracker会为它分配一个任务,并使用“心跳”的返回值与tasktracker进行通信(步骤7)。

在jobtracker为tasktracker选定任务之前,jobtracker必须先选定任务所在的作业。默认的方法是维护一个简单的作业优先级列表。当然还有各种调度算法。

对于map任务和reduce任务,tasktracker有固定数量的任务槽。默认调度器会在处理reduce任务槽之前,先填满map任务槽。

为了选择一个reduce任务,jobtracker简单的从待运行的reduce任务列表中选取下一个来执行,用不着考虑数据的本地化。然后,对 于一个map任务,jobtracker会考虑tasktracker的网络位置,并选取一个距离其输入分片最近的tasktracker。

(4)任务的执行

现在,tasktracker已经被分配了一个任务,下一步是运行任务。第一步,通过共享文件系统将作业的JAR复制到tasktracker所在 的文件系统,从而实现JAR文件本地化。同时,tasktracker将程序所需的全部文件从分布式缓存复制到本地磁盘(步骤8)。第二 步,tasktracker为任务新建一个本地工作目录,并把JAR文件解压到这个文件夹下。第三步,tasktracker新建一个 TaskRunner实例来运行该任务。

TaskRunner启动一个新的JVM(步骤9)来运行每个任务(步骤10),以便用户定义的map和reduce函数的任何软件问题都不会影响 到tasktracker(例如导致崩溃或挂起等)。但是在不同的任务间共享JVM是可能的。子进程通过umbilical接口与父进程进行通信。任务的 子进程每隔几秒便告诉父进程它的进度,直到任务完成。

(5)作业的完成

当jobtracker收到作业最后一个任务完成的通知后,便把作业的状态设为“成功”。然后,JobClient查看作业状态时,便知道任务已完成,于是JobClient打印一条消息告知用户,然后从runJob()方法返回。

最后jobtracker清空作业的工作状态,指示tasktracker也清空工作状态(如删除中间输出等)。

posted @ 2013-10-24 18:22  Kyle_Java  阅读(895)  评论(0编辑  收藏  举报