本期介绍Hadoop的核心组成部分HDFS文件系统,包括其原理、安装与配置、管理及外部编程接口等。通过对本期内容的学习,使读者掌握分布式文件系统的主要结构、HDFS文件系统的内部运行原理和机制、HDFS的数据读写方式,同时,了解HDFS文件系统的数据传输和存储模式。

下期将详细介绍Hadoop的安装和基本配置。通过本期和下一期的学习,读者可以搭建自己的Hadoop集群。

本期要点

■HDFS文件系统的结构与组成

■HDFS系统的数据读写

■HDFS系统的数据存储及数据完整性

1.HDFS概述

Hadoop实现了一个分布式文件系统(Hadoop Distributed File System, HDFS), HDFS是Apache Hadoop Core项目的一部分,是Hadoop兼容性最好的标准级分布式文件系统。

1.1 分布式文件系统

当今的信息时代中,人们可以获取的数据成指数倍地增长。单纯通过增加硬盘个数来扩展计算机文件系统的存储容量的方式,在容量大小、容量增长速度、数据备份、数据安全等方面都不适用,对于数据量很大的应用系统来说尤其如此。分布式文件系统可以有效解决数据的存储和管理难题。

分布式文件系统(Distributed File System, DFS)指通过一套管理系统,能够将文件分散至不同的计算机进行存储,并通过规范的标准协议,方便客户机进行高效存取。

与单机的文件系统不同,分布式文件系统不是将数据放在一块磁盘上由上层操作系统来管理,而是存放在一个服务器集群上,由集群中的服务器通过各尽其责、通力合作的方式提供整个文件系统的服务。将固定于某个地点的某个文件系统,扩展到任意多个地点/多个文件系统,这些节点组成一个文件系统网络。每个节点可以分布在不同的地点,通过网络进行节点间的通信和数据传输。人们在使用分布式文件系统时,无须关心数据是存储在哪个节点上,或者是从哪个节点获取的,只需要像使用本地文件系统一样管理和存储文件系统中的数据即可。

分布式文件系统中,重要的服务器包括:主控服务器(Master/NameNode)、数据服务器(一般称为ChunkServer或DataNode)和客户服务器(Client)。分布式文件系统的典型架构如图1所示。

图1 典型分布式文件系统的结构

(1)分布式文件系统的特点

与传统文件系统相比,分布式文件系统具有以下主要特点。

①可扩展性强。扩展能力是一个分布式文件系统最重要的特点。基本上,所有的分布式文件系统都支持随时随地对数据服务器进行扩展,提升存储容量和访问带宽等。有的系统还支持多个目录/主控服务器。

②统一命名空间。采用统一命名空间,分布式文件系统对于客户端是完全透明的,客户端看到的是统一的全局命名空间,用户操作起来就像是管理本地文件系统。通过元数据管理,文件以块的方式采用多副本模式进行存放。

③高性能。由于一个文件被分成多份,保存在不同的数据服务器上,访问时,可以同时读取,性能会达到最优。

④高可用性。分布式文件系统必须具有高容错能力,即无论是客户端还是服务器出现故障,都不会影响整个系统的功能。为了做到这一点,单点失效是必须被避免的,例如使用资源冗余技术或者提供失效恢复服务。单个数据节点的故障并不会影响集群整体运转。

⑤弹性存储。可以根据业务需要灵活地增加或缩减数据存储以及增删存储池中的资源,而不需要中断系统运行。弹性存储的最大挑战,是减小或增加资源时的数据震荡问题。

(2)常见的分布式文件系统

分布式文件系统既有开源软件平台解决方案,如Hadoop HDFS、Fast DFS等;也有非开源平台解决方案,如最为著名的Google FS、也有像Windows Server 2003/2008平台上的DFS组件等。分布式文件系统在当前应用普遍,产品种类丰富。下面介绍几种典型的系统。

分布式文件系统在当前应用普遍,产品种类丰富。下面介绍几种典型的系统。

①Lustre。

Lustre最早是由HP、Cluster File System联合美国能源部共同开发的Linux平台下的分布式集群文件系统,后期由于Cluster File System公司被Sun收购,而Sun又被Oracle收购,因此,Lustre官方网站目前挂靠在Oracle公司。

Lustre主要面向超级计算机,拥有超强可扩展性与可靠性,能够支持上万个节点、PB级存储、100GB/s的高速访问能力。

Lustre采用GPL许可协议,属于开放源代码的分布式集群文件系统,开发语言采用C/C++,使用平台为Linux;当前,除了Oracle公司外,有新成立的名为Whamcloud的公司专注于Lustre平台的开源研发。

②Google FS。

Google FS(Google File System)是谷歌公司开发的一个分布式可扩展的文件系统,它主要用于大型、分布式、大数据量的互联网应用平台。

Google FS被设计运行在廉价普通的PC服务器上,提供多数据副本实现数据冗余,通过数据分块并行存取,满足互联网用户的海量数据存储需求。

Google FS最早是由Google工程师于2003年发表的一篇学术文章The Google File System而为世人所熟知的,Google FS提供了相似的访问接口,如read、write、create、delete、close等,使得开发者可以非常方便地使用。

Google FS运行于Linux平台上,开发语言是C/C++,本身并不开源,本期中所介绍的Hadoop平台,是在受到Google FS启发后,采用其理念重新用Java语言实现的一个开源平台。

③Fast DFS。

Fast DFS是一个类Google FS的开源分布式文件系统,它由C/C++语言开发,可运行于Linux、Unix、AIX平台。Fast DFS提供专用文件存取访问方式,不支持POSIX接口方式,在系统中也不能使用mount方式挂接。FastDFS在架构上充分考虑了冗余备份、负载均衡、可扩展等问题,平台本身具有高可用、高性能等优点。Fast DFS支持文件的高效存储、同步、上传、下载等,比较适合于互联网视频网站、文档分享网站、图片分享网站等应用。

1.2 HDFS介绍

HDFS是Hadoop的核心子项目,是整个Hadoop平台数据存储与访问的基础,在此之上,承载其他如MapReduce、HBase等子项目的运转。

HDFS是类似于Google FS的开源分布式文件系统,被设计成适合运行在通用硬件上的分布式文件系统。它与现有的分布式文件系统有很多共同点。但同时,它与其他的分布式文件系统的区别也是很明显的。

HDFS是一个高度容错性的系统,适合部署在廉价的机器上。HDFS能提供高吞吐量的数据访问,非常适合大规模数据集上的应用。HDFS放宽了一部分POSIX约束,来实现流式读取文件系统数据的目的。

HDFS是易于使用与管理的分布式文件系统,主要特点和设计目标如下。

(1)硬件故障是常态

整个HDFS系统可以由数百或数千个存储着文件数据片段的服务器组成。实际上,它里面有非常巨大的组成部分,每一个组成部分都很可能出现故障,这就意味着HDFS里总是有一些部件是失效的,因此故障的检测和自动快速恢复是HDFS一个很核心的设计目标。

(2)流式数据访问

HDFS被设计成适合批量处理的,而不是用户交互式的。POSIX的很多硬性需求对于HDFS应用都是非必需的,HDFS放宽了POSIX的要求,这样,可以实现以流的形式访问(Streaming Access)文件系统中的数据。同时去掉POSIX一小部分关键语义,可以获得更好的数据吞吐率。

(3)简单的一致性模型

大部分HDFS程序对文件操作需要的是一次写、多次读取的操作模式。HDFS假定一个文件一旦创建、写入、关闭之后就不需要修改了。这简单化了数据一致的问题,并使高吞吐量的数据访问变得可能。

(4)名字节点(NameNode)和数据节点(DataNode)

HDFS是一个主从结构,一个HDFS集群包括一个名字节点(也叫名称节点),它是一个管理文件命名空间和调节客户端访问文件的主服务器,当然,还有一些数据节点,通常是一个节点一个机器,它来管理对应节点的存储。HDFS对外开放文件命名空间,并允许用户数据以文件形式存储。内部机制是将一个文件分割成一个或多个块,这些块被存储在一组数据节点中。名字节点用来操作文件命名空间的文件或目录操作,如打开、关闭、重命名等。它同时确定块与数据节点的映射。数据节点负责来自文件系统客户的读写请求。数据节点同时还要执行块的创建、删除,以及来自名字节点的块复制指令。

(5)大规模数据集

HDFS被设计为PB级以上存储能力,单个的存储文件可以是GB或者TB级。因此,HDFS的一个设计原则是支持成千上万大数据文件的存储,即将单个文件分成若干标准数据块,分布存储于多个节点上,当用户访问整个文件时,由这些节点集群向用户传输所拥有的数据块,由此可以获得极高的并行数据传输速率。

(6)可移植性

HDFS在设计之初,就考虑到了异构软硬件平台间的可移植性,能够适应于主流硬件平台。它基于跨操作系统平台的Java语言进行编写,这有助于HDFS平台的大规模应用推广。

名字节点是整个HDFS的核心。一个标准的HDFS集群应由名字节点、备用名字节点、数据节点组成,HDFS的基本结构如图2所示。

图2 HDFS系统的基本结构

集群中,一台机器上只运行一个NameNode实例,而集群中其他机器分别运行一个DataNode实例。NameNode是一个中心服务器,负责管理文件系统的名字空间以及客户端对文件的访问,用户能够以文件的形式在上面进行名字空间操作,比如打开、关闭、重命名文件或目录,同时,NameNode还决定了数据块到数据节点的映射关系。NameNode也可以称为管理文件系统的元数据。集群中,每一个节点配置一个DataNode,每个DataNode负责管理它所在节点上的数据存储。从内部看,一个文件被分成一个或多个数据块,这些块存储在一组DataNode上。同时,DataNode负责处理文件系统客户端的读写请求,在NameNode的统一调度下进行数据块的创建、删除和复制。

HDFS的数据块:磁盘存储文件时,是按照数据块(block)来存储的,也就是说,数据块是磁盘读/写的最小单位。数据块也称磁盘块。在HDFS中也有块的概念,默认为64MB,每个块作为独立的存储单元。

基于数据块的存储方式非常适合用于备份,可提供数据容错能力和可用性(如图3所示)。HDFS提供给应用程序例如MapReduce数据服务。一般来说,MapReduce的Map任务通常一次处理一个块中的数据,如果任务数太少(少于集群中节点的数量),就没有发挥多节点的优势,甚至作业的运行速度就会与单节点一样。

图3 HDFS块副本

2.HDFS的运行机制

本期将详细介绍HDFS的结构与运行原理。

2.1 HDFS的结构与组成

HDFS采用主/从(Master/Slave)结构,整个集群由一个名字节点和多个数据节点组成。

NameNode主要负责管理文件命名空间和客户端访问的主服务器,而DataNode则负责对存储进行管理。

HDFS的体系结构如图4所示。

图4 HDFS的体系结构

由图4可知,名字节点NameNode上保存着控制数据节点DataNode信息的元数据(Metadata)。客户端Client可以通过NameNode对元数据进行操作,也可以直接对DataNode进行读和写操作。

2.1.1 NameNode的主要功能

(1)管理元数据信息。元数据信息包括名字空间、文件到文件块的映射、文件块到数据节点的映射三部分。管理文件块包括创建新文件块、文件复制、移除无效文件块以及回收孤立文件块等内容。

(2)管理文件系统的命名空间。任何对文件系统元数据产生修改的操作,NameNode都会使用事务日志记录(下称EditLog)来表示;同样地,修改文件的副本系数也将往EditLog中插入一条记录,NameNode将EditLog存储在本地操作系统的文件系统中。同时,文件系统的命名空间被存储在一个称为映像文件(FsImage)的文件中,包括文件的属性、文件块到文件的映射以及文件块到数据节点的映射等内容,FsImage文件也是存放在NameNode所在的本地文件系统中。

(3)监听请求。指监听客户端事件和DataNode事件。客户端事件包含名字空间的创建和删除,文件的创建、读写、重命名和删除,文件列表信息获取等信息。DataNode事件主要包括文件块信息、心跳响应、出错信息等。处理请求指处理上面的监听请求事件并返回结果。

(4)心跳检测。DataNode会定期将自己的负载情况通过心跳信息向NameNode汇报。NameNode全权管理数据块的复制,它周期性地从集群中的每个DataNode接收心跳信号和块状态报告(Block Report)。接收到心跳信号意味着该DataNode节点工作正常。块状态报告包含了一个该DataNode上所有数据块的列表。

NameNode决定是否将文件映射到DataNode的复制块上。

对于最常见的三个复制块,第一个复制块存储在同一机架的不同节点上,最后一个复制块存储在不同机架的某个节点上。

实际的I/O事务并没有经过NameNode,只有表示DataNode和块的文件映射的元数据经过NameNode。当外部客户机发送请求,要求创建文件时,NameNode会以块标识和该块的第一个副本的DataNode IP地址作为响应。这个NameNode还会通知其他将要接收该块的副本的DataNode。

NameNode在FsImage文件中存储所有关于文件系统名称空间的信息,包含所有事务的记录文件EditLog存储在NameNode的本地文件系统上。FsImage和EditLog文件也需要复制副本,以防文件损坏或NameNode系统丢失。

2.1.2DataNode的主要功能

(1)数据块的读写。一般是文件系统客户端需要请求对指定的DataNode进行读写操作,DataNode通过DataNode的服务进程与文件系统客户端打交道。同时,DataNode进程与NameNode统一结合,对是否需要对文件块的创建、删除、复制等操作进行指挥与调度,当与NameNode交互过程中收到了可以执行文件块的创建、删除或复制操作的命令后,才开始让文件系统客户端执行指定的操作。具体文件的操作并不是DataNode来实际完成的,而是经过DataNode许可后,由文件系统客户端进程来执行实际操作。

(2)向NameNode报告状态。每个DataNode节点会周期性地向NameNode发送心跳信号和文件块状态报告,以便NameNode获取到工作集群中DataNode节点状态的全局视图,从而掌握它们的状态。如果存在DataNode节点失效的情况,NameNode会调度其他DataNode执行失效节点上文件块的复制处理,保证文件块的副本数达到规定数量。

(3)执行数据的流水线复制。当文件系统客户端从NameNode服务器进程中获取到要进行复制的数据块列表(列表中包含指定副本的存放位置,亦即某个DataNode节点)后,会首先将客户端缓存的文件块复制到第一个DataNode节点上,此时,并非整个块都复制到第一个DataNode完成以后才复制到第二个DataNode节点上,而是由第一个DataNode向第二个DataNode节点复制,如此反复进行下去,直到完成文件块及其块副本的流水线复制。

2.2 HDFS的数据操作

HDFS被设计成在一个大集群中可以跨机器地可靠地存储海量的文件。它将每个文件存储成block(即数据块)序列,除了最后一个block,其他所有的block都是同样的大小。

2.2.1数据写入

在HDFS文件系统上创建并写一个文件的流程如图5所示。

图5 HDFS写入流程

具体流程描述如下。

(1)Client调用DistributedFileSystem对象的create方法,创建一个文件输出流(FSDataOutputStream)对象。

(2)通过DistributedFileSystem对象与Hadoop集群的NameNode进行一次远程调用(RPC),在HDFS的Namespace中创建一个文件条目(Entry),该条目没有任何的数据块。

(3)通过FSDataOutputStream对象,向DataNode写入数据,数据首先被写入FSDataOutputStream对象内部的Buffer中,然后数据被分割成一个个Packet数据包。

(4)以Packet为最小单位,基于Socket连接发送到按特定算法选择的HDFS集群中的一组DataNode(正常是3个,可能大于等于1)中的一个节点上,在这组DataNode组成的Pipeline上依次传输Packet。

(5)这组DataNode组成的Pipeline反方向上发送ack确认,最终由Pipeline中第一个DataNode节点将Pipeline ack发送给Client。

(6)完成向文件写入数据,Client在文件输出流(FSDataOutputStream)对象上调用close方法,关闭流。

(7)调用DistributedFileSystem对象的complete方法,通知NameNode文件写入成功。

小提示:

写文件过程中,Client/DataNode与NameNode进行的RPC调用。

①写文件开始时创建文件:Client调用create,在NameNode节点的命名空间中创建一个标识该文件的条目。

②在Client连接Pipeline中第一个DataNode节点之前,Client调用addBlock分配一个数据块。

③如果与Pipeline中第一个DataNode节点连接失败,Client调用abandonBlock放弃一个已经分配的数据块。

④一个Block已经写入到DataNode节点磁盘,Client调用fsync让NameNode持久化数据块的位置信息数据。

⑤文件写完以后,Client调用complete方法通知NameNode写入文件成功。

⑥ DataNode节点接收到并成功持久化一个数据块的数据后,调用blockReceived方法通知NameNode已经接收到数据块。

2.2.2数据读取

相比于写入流程,HDFS文件的读取过程比较简单,如图6所示。

图6 HDFS读取数据

文件的读取操作流程如下。

(1)客户端调用FileSystem的open()函数打开文件,DistributedFileSystem用RPC调用元数据节点,得到文件的数据块信息。

(2)对于每一个数据块,元数据节点返回保存数据块的数据节点的地址。DistributedFileSystem返回FSDataInputStream给客户端,用来读取数据。

(3)客户端调用stream的read()函数开始读取数据。

(4)DFSInputStream连接保存此文件第一个数据块的最近的数据节点。

(5)Data从数据节点读到客户端,当此数据块读取完毕时,DFSInputStream关闭与此数据节点的连接,然后连接此文件下一个数据块的最近的数据节点。

(6)当客户端读取数据完毕的时候,调用FSDataInputStream的close函数。

在读取数据的过程中,如果客户端在与数据节点通信时出现错误,则尝试连接包含此数据块的下一个数据节点。失败的数据节点将被记录下来,以后不再连接。

2.3 访问权限

HDFS实现了一个与POSIX类似的文件和目录的权限模型。有三类权限模式:只读权限(r)、写入权限(w)和可执行权限(x)。每个文件和目录都有所属用户(owner)、所属组(group)和模式(mode)。文件或目录对其所有者、同组的其他用户以及所有其他用户分别有着不同的权限。对文件而言,当读取这个文件时,需要有r权限,当写入或者追加到文件时,需要有w权限。对目录而言,当列出目录内容时,需要具有r权限,当新建或删除子文件或子目录时,需要有w权限,当访问目录的子节点时,需要有x权限。不同于POSIX模型,HDFS权限模型中的文件没有sticky、setuid或setgid位,因为这里没有可执行文件的概念。

每个访问HDFS的用户进程的标识分为两个部分,分别是用户名和组名列表。每次用户进程访问一个文件或home目录,HDFS都要对其进行权限检查:

●如果用户是home的所有者,则检查所有者的访问权限。

●如果home关联的组在组名列表中出现,则检查组用户的访问权限;否则检查home其他用户的访问权限。

●如果权限检查失败,则客户的操作会失败。

在HDFS中,客户端用户身份是通过宿主操作系统给出的。

对类Unix系统来说:

●用户名等于‘whoami'。

●组列表等于‘bash -c groups'。

每次文件或目录操作都传递完整的路径名给NameNode,每一个操作都会对此路径做权限检查。客户框架会隐式地将用户身份和与NameNode的连接关联起来,从而减少改变现有客户端API的需求。经常会有这种情况:当对一个文件的某一操作成功后,之后同样的操作却会失败,这是因为文件或路径上的某些目录可能已经不复存在了。比如,客户端首先开始读一个文件,它向NameNode发出一个请求以获取文件第一个数据块的位置。但接下去获取其他数据块的第二个请求可能会失败。另一方面,删除一个文件并不会撤销客户端已经获得的对文件数据块的访问权限。而权限管理能使得客户端对一个文件的访问许可在两次请求之间被收回。重复一下,权限的改变并不会撤销当前客户端对文件数据块的访问许可。

如果权限检查失败,所有使用一个路径参数的方法都可能抛出AccessControlException异常。

2.4 通信协议簇

HDFS所有的通信协议都是构建在TCP/IP协议上的。客户端通过一个可配置的端口连接到NameNode,通过ClientProtocol与NameNode交互。而DataNode是使用DataNodeProtocol与NameNode交互的。从ClientProtocol和DataNodeProtocol抽象出一个远程调用,在设计上,NameNode不会主动发起RPC,而是响应来自客户端和DataNode的RPC请求。

HDFS中的主要通信协议见表1。

表1 HDFS的主要通信协议

(1)ClientProtocol。

ClientProtocol协议是用户进程(包括客户端进程与DataNode进程)与NameNode进程之间进行通信所使用的协议。当客户端进程想要与NameNode进程进行通信的时候,需要通过

org.apache.hadoop.hdfs.DistributedFileSystem类,基于ClientProtocol协议来实现交互过程。用户代码通过

ClientProtocol协议,可以操纵HDFS的目录命名空间、打开与关闭文件流等。

该接口协议中定义的与文件内容相关的操作主要有:①文件管理,文件的增、删、改,权限控制、文件块管理等;②文件系统管理,查看文件系统状态和设置元数据信息,例如容量、块大小、副本因子数等;③持久会话类,如放弃对指定块的操作、客户端同步等。

协议位置如图7所示。

图7 HDFS协议示意

(2)DataNodeProtocol。

该协议是用于DataNode与NameNode进行通信的协议,例如发送心跳报告和块状态报告。一般来说,NameNode不直接对DataNode进行RPC调用,如果一个NameNode需要与DataNode通信,唯一的方式,就是通过调用该协议接口定义的方法。

(3)ClientDatanodeProtocol。

当客户端进程需要与DataNode进程进行通信的时候,需要基于该协议。该协议接口定义数据块恢复的方法。

(4)NameNodeProtocol。

该协议接口定义了备用NameNode(Secondary NameNode)与NameNode进行通信所需进行的操作。其中,Secondary NameNode是一个用来辅助NameNode的服务器端进程,主要是对映像文件执行特定的操作,另外,还包括获取指定DataNode上块的操作。

(5)DataTransferProtocol。

该协议用于客户端与DataNode之间通信,主要实现文件块的读写及验证等操作。

(6)InterDatanodeProtocol。

该协议是DataNode进程之间进行通信的协议,例如客户端进程启动复制数据块,此时可能需要在DataNode节点之间进行块副本的流水线复制操作。

2.5 HDFS的高可用性

在Hadoop 2.0之前的版本中,NameNode在HDFS集群中存在单点故障,每一个集群中存在一个NameNode,如果NameNode所在的机器出现了故障,那么,将导致整个集群无法利用,直到NameNode重启或者在另一台主机上启动NameNode守护线程。在可预知的情况下(比如NameNode所在的机器硬件或者软件需要升级)以及在不可预测的情况下,如果NameNode所在的服务器崩溃了,都将导致整个集群无法使用。

在Hadoop 2.0及以后的版本中,HDFS的高可用性(High Availability)通过在同一个集群中运行两个NameNode实现:活动节点(Active NameNode)和备用节点(Standby NameNode),允许在服务器崩溃或者机器维护期间快速地启用一个新的NameNode来恢复故障。在典型的HA集群中,通常有两台不同的机器充当NameNode。在任何时间都只有一台机器处于活动(Active)状态;另一台处于待命(Standby)状态。Active NameNode负责集群中所有客户端的操作;而Standby NameNode主要用于备用,它维持足够的状态,在必要时提供快速的故障恢复。

图8展示了HDFS的高可用性实现原理,其中,NameNode简写为NN, DataNode简写为DN。由图中可以看出,两个NameNode都与一组称为JNs(JournalNodes)的互相独立的守护进程保持通信,实现Standby NN的状态和Active NN的状态同步,使元数据保持一致。当Active NN执行任何有关命名空间的修改时,需要发送到一半以上的JNs上(通过Edits log进行持久化存储)。当Standby NN观察到Edits log的变化时,它会从JNs中读取edits信息,并更新其内部的命名空间。一旦Active NN出现故障,Standby NN首先确保自己在发生故障之前从JNs中读出了全部的修改内容,然后切换到Active状态。

图8 HDFS高可用性的实现

为了提供快速的故障恢复,Standby NN也需要保存集群中各个文件块的存储位置。为了达到这一目的,DataNodes上需要同时配置这两个NameNode的地址,同时,与它们都建立心跳连接,并把block位置等信息发送给它们。对于JNs而言,任何时候,只允许一个NameNode作为数据写入者。对于DataNodes,只执行Active NN发送过来的命令。

2.6 集中缓存管理

HDFS采用集中式的缓存管理(HDFS centralized cache management)技术。

HDFS集中式缓存管理是一个明确的缓存机制,它允许用户指定缓存的HDFS路径。NameNode会与保存着所需块数据的所有DataNode通信,并指导它们把块数据放在堆外缓存(off-heap)中。HDFS集中式缓存管理的架构如图9所示。

图9 HDFS集中式缓存的架构

由图9可以看到,NameNode负责协调集群中所有DataNode的off-heap缓存。NameNode周期性地接收来自每个DataNode的缓存报告,缓存报告中描述了缓存在给定DataNode中的所有块的信息。

NameNode通过借助DataNode心跳上的缓存和非缓存命令,来管理DataNode缓存。缓存指令存储在fsimage和

editlog中,可以通过Java和命令行API添加、移除或修改,NameNode查询自身的缓存指令集,来确定应该缓存哪个路径。NameNode还存储了一组缓存池(缓存池是一个管理实体,用于管理缓存指令组)。

NameNode周期性地重复扫描命名空间和活跃的缓存,以确定需要缓存或不缓存哪个块,并向DataNode分配缓存任务。重复扫描也可以由用户动作来触发,比如添加或删除一条缓存指令,或者删除一个缓存池。

HDFS集中化缓存管理具有许多优势。

(1)用户可以根据自己的逻辑指定一些经常被使用的数据或者高优先级任务对应的数据,让它们常驻内存而不被淘汰到磁盘。当工作集的大小超过了主内存大小(这种情况对于许多HDFS负载都是常见的)时,这一点尤为重要。

(2)由于DataNode缓存是由NameNode管理的,所以在分配任务时,应用程序可以通过查询一组缓存块的位置,把任务和缓存块副本放在同一位置上,提高读操作的性能。

(3)当数据块已经被DataNode缓存时,客户端就可以使用一个新的更高效的零拷贝读操作API。因为缓存数据的校验和只需由DataNode执行一次,所以,使用零拷贝API时,客户端基本上不会有开销。

(4)集中式的缓存可以提高整个集群的内存使用率。当依赖于单独的DataNode上操作系统的内存进行缓存时,重复读取一个块数据会导致该块的一个或多个副本全部被送入内存中缓存。使用集中化缓存管理,用户就能明确地只锁定这N个副本中的M个了,从而节省了(N-M)个内存的使用量。

(5)即使出现缓存数据的DataNode节点宕机、数据块移动或集群重启等问题,缓存都不会受到影响。因为缓存被NameNode统一管理并被持久化到fsimage和editlog中,出现问题后,NameNode会调度其他存储了这个数据副本的DataNode,把它读取到内存。

零拷贝

零拷贝(zero-copy)是实现主机或路由器等设备高速网络接口的主要技术。零拷贝技术通过减少或消除关键通信路径影响速率的操作,降低数据传输的操作系统开销和协议处理开销,从而有效提高通信性能,实现高速数据传输。

零拷贝技术可以减少数据拷贝和共享总线操作的次数,消除通信数据在存储器之间不必要的中间拷贝过程,有效地提高通信效率,是设计高速接口通道、实现高速服务器和路由器的关键技术之一。数据拷贝受制于传统的操作系统或通信协议,限制了通信性能。采用零拷贝技术,通过减少数据拷贝次数,简化协议处理的层次,在应用和网络间提供更快的数据通路,可以有效地降低通信延迟,增加网络的吞吐率。

2.7 日志和检查点

Hadoop中有两个非常重要的文件fsimage和edits,前面已经粗略地介绍了一些,这里做一个详细的讲解。它们位于NameNode的$dfs.namenode.name.dir/current/文件夹中。在current目录中,我们可以看到存在大量的以edits开头的文件和少量的以fsimage开头的文件,如图10所示。

图10 current目录

对edits和fsimage文件的概念说明如下。

(1)edits文件存放的是Hadoop文件系统的所有更新操作的日志,HDFS文件系统客户端执行的所有写操作首先会被记录到edits文件中。

(2)fsimage文件是Hadoop文件系统元数据的一个永久性检查点,其中包含Hadoop文件系统中的所有目录和文件的索引节点序列化信息。对于文件来说,包含的信息有修改时间、访问时间、块大小信息等;对于目录来说,包含的信息主要有修改时间、访问控制权限等信息。fsimage并不包含DataNode的信息,而是包含DataNode上块的映射信息,并存放到内存中,当一个新的DataNode加入到集群中时,DataNode都会向NameNode提供块的信息,而NameNode会定期地索取块的信息,以使得NameNode拥有最新的块映射。

其中,edits负责保存自最新检查点后命名空间的变化,起到日志的作用;而fsimage则保存了最新的元数据检查点信息。

fsimage和edits文件都是经过序列化的。在NameNode启动的时候,会将fsimage文件中的内容加载到内存中,然后再执行edits文件中的各项操作,使得内存中的元数据与实际的同步。存在于内存中的元数据支持客户端的读操作。

NameNode启动后,HDFS中的更新操作会重新写到edits文件中。对于一个文件来说,当所有的写操作完成以后,在向客户端发送成功代码之前,将同步更新edits文件。在NameNode运行期间,由于HDFS的所有更新操作都是直接写到edits中的,时间长了会导致edits文件变得很大。

在Hadoop 1.x中,通过SecondaryName合并fsimage和edits,以此来减小edits文件的大小,从而减少了NameNode重启的时间。在Hadoop 2.x中,已经不用SecondaryName,通过配置HA机制实现,即在standby NameNode节点上运行一个叫作CheckpointerThread的线程,这个线程调用StandbyCheckpointer类的doWork()函数,每隔一定的时间(可配置)做一次合并操作。

edits和fsimage文件中的内容使用普通文本编辑器是无法直接查看的,为此,Hadoop准备了专门的工具,用于查看文件的内容,分别为oev和oiv。

oev是offline edits viewer(离线edits查看器)的缩写,该工具只操作文件,并不需要Hadoop集群处于运行状态。oev提供了几个输出处理器,用于将输入文件转换为相关格式的输出文件,可以使用参数-p指定。目前支持的输出格式有binary(Hadoop使用的二进制格式)、xml(在不使用参数p时的默认输出格式)和stats(输出edits文件的统计信息)。由于没有与stats格式对应的输入文件,所以,一旦输出为stats格式,将不能再转换为原有格式。比如输入格式为binary,输出格式为xml,可以通过将输入文件指定为原来的输出文件,将输出文件指定为原来的输入文件,实现binary和xml的转换,而stats则不可以。

oev的具体语法可以通过在命令行输入“hdfs oev”来查看,如图11所示。

图11 oev的具体语法

oiv是offline image viewer的缩写,用于将fsimage文件的内容转储到指定文件中,以便于阅读,该工具还提供了只读的WebHDFS API,以允许离线分析和检查Hadoop集群的命名空间。oiv在处理非常大的fsimage文件时是相当快的,如果不能够处理fsimage,它会直接退出。oiv不具备向后兼容性,比如使用Hadoop 2.4版本的oiv不能处理hadoop 2.3版本的fsimage,只能使用Hadoop 2.3版本的oiv。与oev一样,oiv也不需要Hadoop集群处于运行状态。oiv的具体语法可以通过在命令行输入“hdfs oiv”来查看。

如果fsimage丢失或者损坏了,我们将失去文件到块的映射关系,也就无法使用DataNode上的所有数据了。因此,定期及时地备份fsimage和edits文件非常重要。

fsimage和edit log是HDFS的核心数据结构。这些文件的损坏会导致整个集群的失效。因此,NameNode可以配置成支持多个fsimage和edit log的副本。任何fsimage和edit log的更新都会同步到每一份副本中。同步更新多个edit log副本会降低NameNode的命名空间事务处理速率。但是这种降低是可以接受的,因为HDFS程序中大量产生的是数据请求,而不是元数据请求。NameNode重新启动时,会选择最新一致的fsimage和edit log。

2.8 HDFS快照

在Hadoop 2.x版本中,HDFS提供了支持元数据快照的解决方案。

快照(Snapshot)支持存储在某个时间的数据复制,当HDFS数据损坏时,可以回滚到过去一个已知正确的时间点。

快照分为两种:一种是建立文件系统的索引,每次更新文件不会真正改变文件,而是新开辟一个空间用来保存更改的文件;一种是复制所有的文件系统。HDFS把元数据和数据分离,元数据被存储在单独的NameNode上,实际的数据被复制并扩散到整个集群。使用单个节点来管理元数据,使我们能够使用一个单一的逻辑时钟,建立元数据快照。

HDFS的快照是在某一时间点对指定文件系统复制,可以是整个文件系统的,也可以是文件系统的一部分。快照采用只读模式,对重要数据进行恢复、防止用户错误性的操作。HDFS的快照有以下特征。

(1)快照的创建是瞬间的,代价为O(1),取决于子节点扫描文件目录的时间。

(2)当且仅当快照的文件目录下有文件更新时,才会占用小部分内存,占用内存的大小为O(M),其中,M为更改文件或者目录的数量。

(3)新建快照时,DataNode中的block不会被复制,快照中只是记录了文件块的列表和大小等信息。

(4)快照不会影响正常文件系统的读写等操作。对做快照之后的数据进行的更改将会按照时间顺序逆序记录下来,用户访问的还是当前最新的数据,快照里的内容为快照创建的时间点时文件的内容减去当前文件的内容。

3.HDFS的数据存储

前面主要介绍了HDFS系统的运行机制和原理,本节将介绍HDFS系统中的文件数据是如何存储和管理的。

3.1 数据完整性

I/O操作过程中,难免会出现数据丢失或脏数据,数据传输的量越大,出错的概率越高。校验错误最常用的办法,就是传输前计算一个校验和,传输后计算一个校验和,两个校验和如果不相同,就说明数据存在错误。为了保证数据的完整性,一般采用下列数据校验技术:①奇偶校验技术;②MD5、SHA1等校验技术;③CRC-32循环冗余校验技术;④ECC内存纠错校验技术。其中,比较常用的错误校验码是CRC-32。

HDFS将一个文件分割成一个或多个数据块,这些数据块被编号后,由名字节点保存,通常需要记录的信息包括文件的名称、文件被分成多少块、每块有多少个副本、每个数据块存放在哪个数据节点上、其副本存放于哪些节点上,这些信息被称为元数据。

HDFS为了保证数据的完整性,采用校验和(checksum)检测数据是否损坏。当数据第一次引入系统时计算校验和,并且在一个不可靠的通道中传输的时候,再次检验校验和。但是,这种技术并不能修复数据(注意:校验和也可能损坏,但是,由于校验和小得多,所以可能性非常小)。数据校验和采用的是CRC-32,任何大小的数据输入都可以通过计算,得出一个32位的整数校验和。

DataNode在接收到数据后存储该数据及其校验和,或者将数据和校验和复制到其他的DataNode上。当客户端写数据时,会将数据及其DataNode发送到DataNode组成的管线,最后一个DataNode负责验证校验和,如果有损坏,则抛出ChecksumException,这个异常属于IOException的子类。客户端读取数据的时候,也会检验校验和,会与DataNode上的校验和进行比较。每个DataNode上面都会有一个用于记录校验和的日志。客户端验证完之后,会告诉DataNode,然后更新这个日志。

不仅客户端在读写数据的时候验证校验和,每个DataNode也会在后台运行一个DataBlockScanner,从而定期检查存储在该DataNode上面的数据块。

如果客户端发现有block坏掉,按照以下步骤进行恢复。

(1)客户端在抛出ChecksumException之前,会把坏的block和block所在的DataNode报告给NameNode。

(2)NameNode把这个block标记为已损坏,这样,NameNode就不会把客户端指向这个block,也不会复制这个block到其他的DataNode。

(3)NameNode会把一个好的block复制到另外一个DataNode。

(4)NameNode把坏的block删除。

HDFS会存储每个数据块的副本,可以通过数据副本来修复损坏的数据块。客户端在读取数据块时,如果检测到错误,首先向NameNode报告已损坏的数据块及其正在尝试读取操作的这个DataNode。NameNode会将这个数据块标记为已损坏,对这个数据块的请求会被NameNode安排到另一个副本上。之后,它安排这个数据块的另一个副本复制到另一个DataNode上,如此,数据块的副本因子又回到期望水平。此后,已损坏的数据块副本会被删除。

Hadoop的LocalFileSystem执行客户端的校验和验证。当写入一个名为filename的文件时,文件系统客户端会明确地在包含每个文件块校验和的同一个目录内建立一个名为filename.crc的隐藏文件。

3.2 数据压缩

Hadoop作为一个较通用的海量数据处理平台,每次运算都会需要处理大量的数据。使用文件和数据压缩技术有明显的优点:①节省数据占用的磁盘空间;②加快数据在磁盘和网络中的传输速度,从而提高系统的处理速度。我们来了解一下Hadoop中的文件压缩。

Hadoop支持多种压缩格式。我们可以把数据文件压缩后再存入HDFS,以节省存储空间。在表2中,列出了几种压缩格式。

表2 Hadoop中的压缩格式

所有的压缩算法都存在空间与时间的权衡:更快的压缩速率和解压速率是以牺牲压缩率为代价的。Deflate算法是同时使用了LZ77与哈夫曼编码的一个无损数据压缩算法,源代码可以在zlib库中找到。Gzip算法是以Deflate算法为基础扩展出来的一种算法。Gzip在时间和空间上比较适中,Bzip2算法压缩比Gzip更有效,但速度更慢。Bzip2的解压速度比它的压缩速度要快,但与其他压缩格式相比,又是最慢的,但压缩效果明显是最好的。

使用压缩,有两个比较麻烦的地方:第一,有些压缩格式不能被分块、并行地处理,比如Gzip;第二,另外的一些压缩格式虽然支持分块处理,但解压的过程非常缓慢,使作业瓶颈转移到了CPU上,例如Bzip2。LZO是一种既能够被分块并且并行处理速度也非常快的压缩算法。在Hadoop中,使用LZO压缩算法可以减小数据的大小并缩短数据的磁盘读写时间,在HDFS中存储压缩数据,可以使集群能保存更多的数据,延长集群的使用寿命。不仅如此,由于MapReduce作业通常瓶颈都在I/O上,存储压缩数据就意味着更少的I/O操作,作业运行更加高效。例如,将压缩文件直接作为入口参数交给MapReduce处理,MapReduce会自动根据压缩文件的扩展名来自动选择合适的解压器处理数据。处理流程如图12所示。

图12 MapReduce的压缩框架

LZO的压缩文件是由许多小的blocks组成(约256KB),使得Hadoop的作业可以根据block的划分来分块工作(split job)。

不仅如此,LZO在设计时就考虑到了效率问题,它的解压速度是Gzip的两倍,这就让它能够节省很多的磁盘读写,它的压缩比不如Gzip,大约压缩出来的文件比Gzip压缩的大一半,但是,这仍然比没有经过压缩的文件要节省20%~50%的存储空间,这样,就可以在效率上大大地提高作业执行的速度。

在考虑如何压缩由MapReduce程序将要处理的数据时,压缩格式是否支持分割是很重要的。比如,存储在HDFS中的未压缩的文件大小为1GB, HDFS的块大小为64MB,所以该文件将被存储为16块,将此文件用作输入的MapReduce作业,会创建1个输入分片(split,也称为“分块”。对应block,我们统一称为“块”),每个分片都被作为一个独立map任务的输入,单独进行处理。

现在假设该文件是一个Gzip格式的压缩文件,压缩后的大小为1GB。与前面一样,HDFS将此文件存储为16块。然而,针对每一块创建一个分块是没有用的,因为不可能从Gzip数据流中的任意点开始读取,map任务也不可能独立于其他分块只读取一个分块中的数据。Gzip格式使用Deflate算法来存储压缩过的数据,Deflate将数据作为一系列压缩过的块进行存储。但是,每块的开始没有指定用户在数据流中任意点定位到下一个块的起始位置,而是其自身与数据流同步。因此,Gzip不支持分割(块)机制。

在这种情况下,MapReduce不分割Gzip格式的文件,因为它知道输入是Gzip压缩格式的(通过文件扩展名得知),而Gzip压缩机制不支持分割机制。这样是以牺牲本地化为代价的:一个map任务将处理16个HDFS块。大都不是map的本地数据。与此同时,因为map任务少,所以作业分割的粒度不够细,从而导致运行时间变长。

在我们假设的例子中,如果是一个LZO格式的文件,我们会遇到同样的问题,因为基本压缩格式不为reader提供方法使其与流同步。但是,Bzip2格式的压缩文件确实提供了块与块之间的同步标记(一个48位的PI近似值),因此它支持分割机制。

对于文件的收集,这些问题会稍有不同。Zip是存档格式,因此,它可以将多个文件合并为一个Zip文件。每个文件单独压缩,所有文档的存储位置存储在Zip文件的尾部。这个属性表明Zip文件支持文件边界处分割,每个分片中包括Zip压缩文件中的一个或多个文件。

3.3 序列化

序列化是指将结构化对象转换成字节流,以便于进行网络传输,或写入持久存储的过程。与之相对的反序列化,就是将字节流转化为一系列结构化对象的过程。

(1)序列化有以下特征。

●紧凑:可以充分利用稀缺的带宽资源。

●快速:通信时大量使用序列化机制,因此,需要减少序列化和反序列化的开销。

●可扩展:随着通信协议的升级而可升级。

●互操作:支持不同开发语言的通信。

(2)序列化的主要作用如下:

●作为一种持久化格式。

●作为一种通信的数据格式,支持不同开发语言的通信。●作为一种数据拷贝机制。

Hadoop的序列化机制与Java的序列化机制不同,它实现了自己的序列化机制,将对象序列化到流中,值得一提的是,Java的序列化机制是不断地创建对象,但在Hadoop的序列化机制中,用户可以复用对象,减少了Java对象的分配和回收,提高了应用效率。

在分布式系统中,进程将对象序列化为字节流,通过网络传输到另一进程,另一进程接收到字节流,通过反序列化,转回到结构化对象,以实现进程间通信。在Hadoop中,Mapper、Combiner、Reducer等阶段之间的通信都需要使用序列化与反序列化技术。举例来说,Mapper产生的中间结果<key: value1, value2...>需要写入到本地硬盘,这是序列化过程(将结构化对象转化为字节流,并写入硬盘),而Reducer阶段,读取Mapper的中间结果的过程则是一个反序列化过程(读取硬盘上存储的字节流文件,并转回为结构化对象)。需要注意的是,能够在网络上传输的只能是字节流,Mapper的中间结果在不同主机间洗牌时,对象将经历序列化和反序列化两个过程。

序列化是Hadoop核心的一部分,在Hadoop中,位于org.apache.hadoop.io包中的Writable接口是Hadoop序列化格式的实现,Writable接口提供两个方法:

不过,没有提供比较功能,需要进行比较的话,要实现WritableComparable接口:

Hadoop的Writable接口是基于DataInput和DataOutput实现的序列化协议,紧凑(高效使用存储空间)、快速(读写数据、序列化与反序列化的开销小)。

Hadoop中的键(key)和值(value)必须是实现了Writable接口的对象(键还必须实现WritableComparable,以便进行排序)。

adoop自身提供了多种具体的Writable类,包含了常见的Java基本类型(boolean、byte、short、int、float、long和double等)和集合类型(BytesWritable、ArrayWritable和MapWritable等),如图13所示。

图13 Writable接口

Text:Text是UTF-8的Writable,可以理解为与java.lang.String相类似的Writable。

Text类替代了UTF-8类。Text是可变的,其值可以通过调用set()方法来改变。最大可以存储2GB的大小。

NullWritable:NullWritable是一种特殊的Writable类型,它的序列化长度为零,可以用作占位符。

BytesWritable:BytesWritable是一个二进制数据数组封装,序列化格式是一个int字段。BytesWritable是可变的,其值可以通过调用set()方法来改变。

ObjectWritable:ObjectWritable适用于字段使用多种类型时。

ArrayWritable和TwoDArrayWritable是针对数组和二维数组的。

MapWritable和SortedMapWritable是针对Map和SortMap的。

虽然Hadoop内建了多种Writable类供用户选择,Hadoop对Java基本类型的包装Writable类实现的RawComparable接口,使得这些对象不需要反序列化过程,便可以在字节流层面进行排序,从而大大缩短了比较的时间开销。但是,当我们需要更加复杂的对象时,Hadoop的内建Writable类就不能满足我们的需求了(需要注意的是Hadoop提供的Writable集合类型并没有实现RawComparable接口,因此也不满足我们的需要),这时,我们就需要定制自己的Writable类,特别在将其作为键(key)的时候更应该如此,以求实现更高效的存储和快速的比较。

举报/反馈

司波达也加图索

552获赞 177粉丝
分享学习笔记,共享专业课程知识
关注
0
0
收藏
分享