Hadoop源代码分析(完整图文版) part 2

来源:互联网 发布:网络安全设备图标 编辑:程序博客网 时间:2024/05/17 06:26

Hadoop源代码分析(一八)

DataNode的介绍基本告一段落。我们开始来分析NameNode。相比于DataNode,NameNode比较复杂。系统中只有一个NameNode,作为系统文件目录的管理者和“inode表”(熟悉UNIX的同学们应该了解inode)。为了高可用性,系统中还存在着从NameNode。

先前我们分析DataNode的时候,关注的是数据块。NameNode作为HDFS中文件目录和文件分配的管理者,它保存的最重要信息,就是下面两个映射:

文件名à数据块

数据块àDataNode列表

其中,文件名à数据块保存在磁盘上(持久化);但NameNode上不保存数据块àDataNode列表,该列表是通过DataNode上报建立起来的。

下图包含了NameNode和DataNode往外暴露的接口,其中,DataNode实现了InterDatanodeProtocol和ClientDatanodeProtocol,剩下的,由NameNode实现。

 

 

ClientProtocol提供给客户端,用于访问NameNode。它包含了文件角度上的HDFS功能。和GFS一样,HDFS不提供POSIX形式的接口,而是使用了一个私有接口。一般来说,程序员通过org.apache.hadoop.fs.FileSystem来和HDFS打交道,不需要直接使用该接口。

DatanodeProtocol:用于DataNode向NameNode通信,我们已经在DataNode的分析过程中,了解部分接口,包括:register,用于DataNode注册;sendHeartbeat/blockReport/blockReceived,用于DataNode的offerService方法中;errorReport我们没有讨论,它用于向NameNode报告一个错误的Block,用于BlockReceiver和DataBlockScanner;nextGenerationStamp和commitBlockSynchronization用于lease管理,我们在后面讨论到lease时,会统一说明。

NamenodeProtocol用于从NameNode到NameNode的通信。

下图补充了接口里使用的数据的关系。


Hadoop源代码分析(一九)

我们先分析INode*.java,类INode*抽象了文件层次结构。如果我们对文件系统进行面向对象的抽象,一定会得到和下面一样类似的结构图(类INode*):

 

INode是一个抽象类,它的两个字类,分别对应着目录(INodeDirectory)和文件(INodeFile)。INodeDirectoryWithQuota,如它的名字隐含的,是带了容量限制的目录。INodeFileUnderConstruction,抽象了正在构造的文件,当我们需要在HDFS中创建文件的时候,由于创建过程比较长,目录系统会维护对应的信息。

INode中的成员变量有:name,目录/文件名;modificationTime和accessTime是最后的修改时间和访问时间;parent指向了父目录;permission是访问权限。HDFS采用了和UNIX/Linux类似的访问控制机制。系统维护了一个类似于UNIX系统的组表(group)和用户表(user),并给每一个组和用户一个ID,permission在INode中是long型,它同时包含了组和用户信息。

INode中存在大量的get和set方法,当然是对上面提到的属性的操作。导出属性,比较重要的有:collectSubtreeBlocksAndClear,用于收集这个INode所有后继中的Block;computeContentSummary用于递归计算INode包含的一些相关信息,如文件数,目录数,占用磁盘空间。

INodeDirectory是HDFS管理的目录的抽象,它最重要的成员变量是:

private List<INode> children;

就是这个目录下的所有目录/文件集合。INodeDirectory也是有大量的get和set方法,都很简单。INodeDirectoryWithQuota进一步加强了INodeDirectory,限制了INodeDirectory可以使用的空间(包括NameSpace和磁盘空间)。

INodeFile是HDFS中的文件,最重要的成员变量是:

protected BlockInfo blocks[] = null;

这是这个文件对应的Block列表,BlockInfo增强了Block类。

INodeFileUnderConstruction保存了正在构造的文件的一些信息,包括clientName,这是目前拥有租约的节点名(创建文件时,只有一个节点拥有租约,其他节点配合这个节点工作)。clientMachine是构造该文件的客户端名称,如果构造请求由DataNode发起,clientNode会保持相应的信息,targets保存了配合构造文件的所有节点。

上面描述了INode*类的关系。下面我们顺便考察一下一些NameNode上的数据类。

BlocksMap保存了Block和它在NameNode上一些相关的信息。其核心是一个map:Map<Block, BlockInfo>。BlockInfo扩展了Block,保存了该Block归属的INodeFile和DatanodeDescriptor,同时还包括了它的前继和后继Block。有了BlocksMap,就可以通过Block找对应的文件和这个Block存放的DataNode的相关信息。

 

接下来我们来分析类Datanode*。DatanodeInfo和DatanodeID都定义在包org.apache.hadoop.hdfs.protocol。DatanodeDescriptor是DatanodeInfo的子类,包含了NameNode需要的附加信息。DatanodeID只包含了一些配置信息,DatanodeInfo增加了一些动态信息,DatanodeDescriptor更进一步,包含了DataNode上一些Block的动态信息。DatanodeDescriptor包含了内部类BlockTargetPair,它保存Block和对应DatanodeDescriptor的关联,BlockQueue是BlockTargetPair队列。

DatanodeDescriptor包含了两个BlockQueue,分别记录了该DataNode上正在复制(replicateBlocks)和Lease恢复(recoverBlocks)的Block。同时还有一个Block集合,保存的是该DataNode上已经失效的Block。DatanodeDescriptor提供一系列方法,用于操作上面保存的队列和集合。也提供get*Command方法,用于生成发送到DataNode的命令。

当NameNode收到DataNode对现在管理的Block状态的汇报是,会调用reportDiff,找出和现在NameNode上的信息差别,以供后续处理用。

readFieldsFromFSEditLog方法用于从日志中恢复DatanodeDescriptor。

Hadoop源代码分析(二零)

前面我们提过关系:文件名à数据块持久化在磁盘上,所有对目录树的更新和文件名à数据块关系的修改,都必须能够持久化。为了保证每次修改不需要从新保存整个结构,HDFS使用操作日志,保存更新。

现在我们可以得到NameNode需要存储在Disk上的信息了,包括:

[hadoop@localhost dfs]$ ls -R name
name:
current  image  in_use.lock

name/current:
edits  fsimage  fstime  VERSION

name/image:
fsimage

in_use.lock的功能和DataNode的一致。fsimage保存的是文件系统的目录树,edits则是文件树上的操作日志,fstime是上一次新打开一个操作日志的时间(long型)。

image/fsimage是一个保护文件,防止0.13以前的版本启动(0.13以前版本将fsimage存放在name/image目录下,如果用0.13版本启动,显然在读fsimage会出错J)。

我们可以开始讨论FSImage了,类FSImage如下图:

 

分析FSImage,不免要跟DataStorage去做比较(上图也保留了类DataStorage)。前面我们已经分析过DataStorage的状态变化,包括升级/回滚/提交,FSImage也有类似的升级/回滚/提交动作,而且这部分的行为和DataStorage是比较一致,如下状态转移图。图中update方法和DataStorage的差别比较大,是因为处理数据库和处理文件系统名字空间不一样,其他的地方都比较一致。FSImage也能够管理多个Storage,而且还能够区分Storage为IMAGE(目录结构)/EDITS(日志)/IMAGE_AND_EDITS(前面两种的组合)。

 

我们可以看到,FSImage和DataStorage都有recoverTransitionRead方法。FSImage的recoverTransitionRead方法主要步骤是检查系统一致性(analyzeStorage)并尝试恢复,初始化新的storage,然后根据启动NameNode的参数,做升级/回滚等操作。

FSImage需要支持参数-importCheckpoint,该参数用于在某一个checkpoint目录里加载HDFS的目录信息,并更新到当前系统,该参数的主要功能在方法doImportCheckpoint中。该方法很简单,通过读取配置的checkpoint目录来加载fsimage文件和日志文件,然后利用saveFSImage(下面讨论)保存到当前的工作目录,完成导入。

loadFSImage(File curFile)用于在fsimage中读入NameNode持久化的信息,是FSImage中最重要的方法之一,该文件的结构如下:

 

最开始是版本号(注意,各版本文件布局不一样,文中分析的样本是0.17的),然后是命名空间的ID号,文件个数和最高文件版本号(就是说,下一次产生文件版本号的初始值)。接下来就是文件的信息啦,首先是文件名,然后是该文件的副本数,接下来是修改时间/访问时间,数据块大小,数据块数目。数据块数目如果大于0,表明这是个文件,那么接下来就是numBlocks个数据块(浅蓝),如果数据块数目等于0,那该条目是目录,接下来是应用于该目录的quota。最后是访问控制的一些信息。文件信息一共有numFiles个,接下来是处于构造状态的文件的信息。(有些版本可能还会保留DataNode的信息,但0.17已经不保存这样的信息啦)。loadFSImage(File curFile)的对应方法是saveFSImage(File newFile),FSImage中还有一系列的方法(大概7,8个)用于配合这两个方法工作,我们就不再深入讨论了。

loadFSEdits(StorageDirectory sd)用于加载日志文件,并把日志文件记录的内容应用到NameNode,loadFSEdits只是简单地调用FSEditLog中对应的方法。

loadFSImage()和saveFSImage()是另外一对重要的方法。

loadFSImage()会在所有的Storage中,读取最新的NameNode持久化信息,并应用相应的日志,当loadFSImage()调用返回以后,内存中的目录树就是最新的。loadFSImage()会返回一个标记,如果Storage中有任何和内存中最终目录树中不一致的Image(最常见的情况是日志文件不为空,那么,内存中的Image应该是Storage的Image加上日志,当然还有其它情况),那么,该标记为true。

saveFSImage()的功能正好相反,它将内存中的目录树持久化,很自然,目录树持久化后就可以把日志清空。saveFSImage()会创建edits.new,并把当前内存中的目录树持久化到fsimage.ckpt(fsimage现在还存在),然后重新打开日志文件edits和edits.new,这会导致日志文件edits和edits.new被清空。最后,saveFSImage()调用rollFSImage()方法。

rollFSImage()上来就把所有的edits.new都改为edits(经过了方法saveFSImage,它们都已经为空),然后再把fsimage.ckpt改为fsimage。如下图:

 

为了防止误调用rollFSImage(),系统引入了状态CheckpointStates.UPLOAD_DONE。

有了上面的状态转移图,我们就很好理解方法recoverInterruptedCheckpoint了。

 

图中存在另一条路径,应用于GetImageServlet中。GetImageServlet是和从NameNode进行文件通信的接口,这个场景留到我们分析从NameNode时再进行分析。

 

最后我们分析一下和检查点相关的一个类,rollFSImage()会返回这个类的一个实例。CheckpointSignature用于标识一个日志的检查点,它是StorageInfo的子类,同时实现了WritableComparable接口,出了StorageInfo的信息,它还包括了两个属性:editsTime和checkpointTime。editsTime是日志的最后修改时间,checkpointTime是日志建立时间。在和从NameNode节点的通信中,需要用CheckpointSignature,来保证从NameNode获得的日志是最新的。

Hadoop源代码分析(二一)

不好意思,突然间需要忙项目的其他事情了,更新有点慢下来,争取月底搞定HDFS吧。

我们来分析FSEditLog.java,该类提供了NameNode操作日志和日志文件的相关方法,相关类图如下:

 

首先是FSEditLog依赖的输入/输出流。输入流基本上没有新添加功能;输出流在打开的时候,会写入日志的版本号(最前面的4字节),同时,每次将内存刷到硬盘时,会为日志尾部写入一个特殊的标识(OP_INVALID)。

FSEditLog有打开/关闭的方法,它们都是很简单的方法,就是关闭的时候,要等待所有正在写日志的操作都完成写以后,才能关闭。processIOError用于处理IO出错,一般这会导致对于的Storage的日志文件被关闭(还记得loadFSImage要找出最后写的日志文件吧,这也是提高系统可靠性的一个方法),如果系统再也找不到可用的日志文件,NameNode将会退出。

loadFSEdits是个大家伙,它读取日志文件,并把日志应用到内存中的目录结构中。这家伙大是因为它需要处理所有类型的日志记录,其实就一大case语句。logEdit的作用和loadFSEdits相反,它向日志文件中写入日志记录。我们来分析一下什么操作需要写log,还有就是需要log那些参数:

logOpenFile(OP_ADD):申请lease

path(路径)/replication(副本数,文本形式)/modificationTime(修改时间,文本形式)/accessTime(访问时间,文本形式)/preferredBlockSize(块大小,文本形式)/BlockInfo[](增强的数据块信息,数组)/permissionStatus(访问控制信息)/clientName(客户名)/clientMachine(客户机器名)

logCloseFile(OP_CLOSE):归还lease

path/replication/modificationTime/accessTime/preferredBlockSize/BlockInfo[]/permissionStatus

logMkDir(OP_MKDIR):创建目录

path/modificationTime/accessTime/permissionStatus

logRename(OP_RENAME):改文件名

src(原文件名)/dst(新文件名)/timestamp(时间戳)

logSetReplication(OP_SET_REPLICATION):更改副本数

src/replication

logSetQuota(OP_SET_QUOTA):设置空间额度

path/nsQuota(文件空间额度)/dsQuota(磁盘空间额度)

logSetPermissions(OP_SET_PERMISSIONS):设置文件权限位

src/permissionStatus

logSetOwner(OP_SET_OWNER):设置文件组和主

src/username(所有者)/groupname(所在组)

logDelete(OP_DELETE):删除文件

src/timestamp

logGenerationStamp(OP_SET_GENSTAMP):文件版本序列号

genstamp(序列号)

logTimes(OP_TIMES):更改文件更新/访问时间

src/modificationTime/accessTime

通过上面的分析,我们应该清楚日志文件里记录了那些信息。

rollEditLog()我们在前面已经提到过(配合saveFSImage和rollFSImage),它用于关闭edits,打开日志到edits.new。purgeEditLog()的作用正好相反,它删除老的edits文件,然后把edits.new改名为edits。这也是Hadoop在做更新修改时经常采用的策略。

Hadoop源代码分析(二二)

我们开始对租约Lease进行分析,下面是类图。Lease可以认为是一个文件写锁,当客户端需要写文件的时候,它需要申请一个Lease,NameNode负责记录那个文件上有Lease,Lease的客户是谁,超时时间(分布式处理的一种常用技术)等,所有这些工作由下面3个类完成。至于租约过期NameNode需要采取什么动作,并不是这部分code要完成的功能。


LeaseManager(左)管理着系统中的所有Lease(右),同时,LeaseManager有一个线程Monitor,用于检查是否有Lease到期。

一个租约由一个holder(客户端名),lastUpdate(上次更新时间)和paths(该客户端操作的文件集合)构成。了解了这些属性,相关的方法就很好理解了。LeaseManager的方法也就很好理解,就是对Lease进行操作。注意,LeaseManager的addLease并没有检查文件上是否已经有Lease,这个是由LeaseManager的调用者来保证的,这使LeaseManager跟简单。内部类Monitor通过对Lease的最后跟新时间来检测Lease是否过期,如果过期,简单调用FSNamesystem的internalReleaseLease方法。

这部分的代码比我想象的简单,主要是大部分的一致性逻辑都存在于LeaseManager的使用者。在开始分析FSNamesystem.java这个4.5k多行的庞然大物之前,我们继续来扫除外围的障碍。下面是关于访问控制的一些类:

 

Hadoop文件保护采用的UNIX的机制,文件用户分文件属主、文件组和其他用户,权限读,写和执行(FsAction中抽象了所有组合)。

我们先分析包org.apache.hadoop.fs.permission的几个类吧。FsAction抽象了操作权限,FsPermission记录了某文件/路径的允许情况,分文件属主、文件组和其他用户,同时提供了一系列的转换方法,applyUMask用于去掉某些权限,如某些操作需要去掉文件的写权限,那么可以通过该方法,生成对应的去掉写权限的FsPermission对象。PermissionStatus用于描述一个文件的文件属主、文件组和它的FsPermission。

INode在保存PermissionStatus时,用了不同的方法,它用一个long变量,和SerialNumberManager配合,保存了PermissionStatus的所有信息。

SerialNumberManager保存了文件主和文件主号,用户组和用户组号的对应关系。注意,在持久化信息FSImage中,不保存文件主号和用户组号,它们只是SerialNumberManager分配的,只保存在内存的信息。通过SerialNumberManager得到某文件主的文件主号时,如果找不到文件主号,会往对应关系中添加一条记录。

INode的long变量作为一个位串,分组保存了FsPermission(MODE),文件主号(USER)和用户组号(GROUP)。

PermissionChecker用于权限检查。

Hadoop源代码分析(二三)

下面我们来分析FSDirectory。其实分析FSDirectory最好的地方,应该是介绍完INode*以后,FSDirectory在INode*的基础上,保存了HDFS的文件目录状态。系统加载FSImage时,FSImage会在FSDirectory对象上重建文件目录状态,HDFS文件目录状态的变化,也由FSDirectory写日志,同时,它保存了文件名à数据块的映射关系。

FSDirectory只有很少的成员变量,如下:

  final FSNamesystem namesystem;
  final INodeDirectoryWithQuota rootDir;
  FSImage fsImage;
  boolean ready = false;

其中,namesystem,fsImage是指向FSNamesystem对象和FSImage对象的引用,rootDir是文件系统的根,ready初值为false,当系统成功加载FSImage以后,ready会变成true,FSDirectory的使用者就可以调用其它FSDirectory功能了。

FSDirectory中剩下的,就是一堆的方法(我们不讨论和MBean相关的类,方法和过程)。

 

loadFSImage用于加载目录树结构,它会去调用FSImage的方法,完成持久化信息的导入以后,它会把成员变量ready置为true。系统调用loadFSImage是在FSNamesystem.java的initialize方法,那是系统初始化重要的一步。

addFile用于创建文件或追加数据时创建INodeFileUnderConstruction,下图是它的Call Hierachy图:

 

addFile首先会试图在系统中创建到文件的路径,如果文件为/home/hadoop/Hadoop.tar,addFile会调用mkdirs(创建路径为/home/hadoop,这也会涉及到一系列方法),保证文件路径存在,然后创建INodeFileUnderConstruction节点,并把该节点加到目录树中(通过addNode,也是需要调用一系列方法),如果成功,就写操作日志(logOpenFile)。

unprotectedAddFile也用于在系统中创建一个目录或文件(非UnderConstruction),如果是文件,还会建立对应的block。FSDirectory中还有好几个unprotected*方法,它们不检查成员变量ready,不写日志,它们大量用于loadFSEdits中(这个时候ready当然是false,而且因为正在恢复日志,也不需要写日志)。

addToParent添加一个INode到目录树中,并返回它的上一级目录,它的实现和unprotectedAddFile是类似的。

persistBlocks比较有意思,用于往日志里记录某inode的block信息,其实并没有一个对应于persistBlocks的写日志方法,它用的是logOpenFile。这个大家可以去检查一下logOpenFile记录的信息。closeFile对应了logCloseFile。

addBlock和removeBlock对应,用于添加/删除数据块信息,同时它们还需要更新FSNamesystem.java中对应的信息。

unprotectedRenameTo和renameTo实现了UNIX的mv命令,主要的功能都在unprotectedRenameTo中完成,复杂的地方在于对各种各样情况的讨论。

setReplication和unprotectedSetReplication用于更新数据块的副本数,很简单的方法,注意,改变产生的对数据块的删除/复制是在FSNamesystem.java中实现。

setPermission,unprotectedSetPermission,setOwner和unprotectedSetOwner都是简单的方法。

Delete和unprotectedDelete又是一对方法,删除如果需要删除数据块,将通过FSNamesystem的removePathAndBlocks进行。

……(后续的方法和前面介绍的,都比较类似,都是一些过程性的东西,就不再讨论了)

Hadoop源代码分析(二四)

下面轮到FSNamesystem出场了。FSNamesystem.java一共有4573行,而整个namenode目录下所有的Java程序总共也只有16876行,把FSNamesystem搞定了,NameNode也就基本搞定。

FSNamesystem是NameNode实际记录信息的地方,保存在FSNamesystem中的数据有:

l           文件名à数据块列表(存放在FSImage和日志中)

l           合法的数据块列表(上面关系的逆关系)

l           数据块àDataNode(只保存在内存中,根据DataNode发过来的信息动态建立)

l           DataNode上保存的数据块(上面关系的逆关系)

l           最近发送过心跳信息的DataNode(LRU)

我们先来分析FSNamesystem的成员变量。

  private boolean isPermissionEnabled;
是否打开权限检查,可以通过配置项dfs.permissions来设置。

 

  private UserGroupInformation fsOwner;
本地文件的用户文件属主和文件组,可以通过hadoop.job.ugi设置,如果没有设置,那么将使用启动HDFS的用户(通过whoami获得)和该用户所在的组(通过groups获得)作为值。

 

  private String supergroup;
对应配置项dfs.permissions.supergroup,应用在defaultPermission中,是系统的超级组。

 

  private PermissionStatus defaultPermission;
缺省权限,缺省用户为fsOwner,缺省用户组为supergroup,缺省权限为0777,可以通过dfs.upgrade.permission修改。

 

  private long capacityTotal, capacityUsed, capacityRemaining;
系统总容量/已使用容量/剩余容量

 

  private int totalLoad = 0;
系统总连接数,根据DataNode心跳信息跟新。

 

  private long pendingReplicationBlocksCount, underReplicatedBlocksCount, scheduledReplicationBlocksCount;
分别是成员变量pendingReplications(正在复制的数据块),neededReplications(需要复制的数据块)的大小,scheduledReplicationBlocksCount是当前正在处理的复制工作数目。

 

  public FSDirectory dir;
指向系统使用的FSDirectory对象。

 

  BlocksMap blocksMap = new BlocksMap();
保存数据块到INode和DataNode的映射关系

public CorruptReplicasMap corruptReplicas = new CorruptReplicasMap();
保存损坏(如:校验没通过)的数据块到对应DataNode的关系,CorruptReplicasMap类图如下,类只有一个成员变量,保存Block到一个DatanodeDescriptor的集合的映射和这个映射上的一系列操作:


 

  Map<String, DatanodeDescriptor> datanodeMap = new TreeMap<String, DatanodeDescriptor>();
保存了StorageID à DatanodeDescriptor的映射,用于保证DataNode使用的Storage的一致性。

 

  private Map<String, Collection<Block>> recentInvalidateSets
保存了每个DataNode上无效但还存在的数据块(StorageID à ArrayList<Block>)。

  Map<String, Collection<Block>> recentInvalidateSets
保存了每个DataNode上有效,但需要删除的数据块(StorageID à TreeSet<Block>),这种情况可能发生在一个DataNode故障后恢复后,上面的数据块在系统中副本数太多,需要删除一些数据块。

 

  HttpServer infoServer;

  int infoPort;

  Date startTime;
用于内部信息传输的HTTP请求服务器(Servlet的容器)。现在有/fsck,/getimage,/listPaths/*,/data/*和/fileChecksum/*,我们后面还会继续讨论。

 

  ArrayList<DatanodeDescriptor> heartbeats;
所有目前活着的DataNode,线程HeartbeatMonitor会定期检查。

private UnderReplicatedBlocks neededReplications
需要进行复制的数据块。UnderReplicatedBlocks的类图如下,它其实是一个数组,数组的下标是优先级(0的优先级最高,如果数据块只有一个副本,它的优先级是0),数组的内容是一个Block集合。UnderReplicatedBlocks提供一些方法,对Block进行增加,修改,查找和删除。


  private PendingReplicationBlocks pendingReplications;
保存正在复制的数据块的相关信息。PendingReplicationBlocks的类图如下:


其中,pendingReplications保存了所有正在进行复制的数据块,使用Map是需要一些附加的信息PendingBlockInfo。这些信息包括时间戳,用于检测是否已经超时,和现在进行复制的数目numReplicasInProgress。timedOutItems是超时的复制项,超时的复制项在FSNamesystem的processPendingReplications方法中被删除,并从新复制。timerThread是用于检测复制超时的线程的句柄,对应的线程是PendingReplicationMonitor的一个实例,它的run方法每隔一段会检查是否有超时的复制项,如果有,将该数据块加到timedOutItems中。Timeout是run方法的检查间隔,defaultRecheckInterval是缺省值。PendingReplicationBlocks和PendingBlockInfo的方法都很简单。

 

  public LeaseManager leaseManager = new LeaseManager(this);
租约管理器。

Hadoop源代码分析(二五)

继续对FSNamesystem进行分析。

 

  Daemon hbthread = null;   // HeartbeatMonitor thread

  public Daemon lmthread = null;   // LeaseMonitor thread

  Daemon smmthread = null;  // SafeModeMonitor thread

public Daemon replthread = null;  // Replication thread
NameNode上的线程,分别对应DataNode心跳检查,租约检查,安全模式检查和数据块复制,我们会在后面介绍这些线程对应的功能。

 

  volatile boolean fsRunning = true;

  long systemStart = 0;

系统运行标志和系统启动时间。

 

接下来是一堆系统的参数,比方说系统每个DataNode节点允许的最大数据块数,心跳检查间隔时间等… …

  //  The maximum number of replicates we should allow for a single block

  private int maxReplication;

  //  How many outgoing replication streams a given node should have at one time

  private int maxReplicationStreams;

  // MIN_REPLICATION is how many copies we need in place or else we disallow the write

  private int minReplication;

  // Default replication

  private int defaultReplication;

  // heartbeatRecheckInterval is how often namenode checks for expired datanodes

  private long heartbeatRecheckInterval;

  // heartbeatExpireInterval is how long namenode waits for datanode to report

  // heartbeat

  private long heartbeatExpireInterval;

  //replicationRecheckInterval is how often namenode checks for new replication work

  private long replicationRecheckInterval;

  //decommissionRecheckInterval is how often namenode checks if a node has finished decommission

  private long decommissionRecheckInterval;

  // default block size of a file

  private long defaultBlockSize = 0;

 

  private int replIndex = 0;
和neededReplications配合,记录下一个进行复制的数据块位置。

public static FSNamesystem fsNamesystemObject;
哈哈,不用介绍了,还是static的。

  private String localMachine;
  private int port;
本机名字和RPC端口。

private SafeModeInfo safeMode;  // safe mode information
记录安全模式的相关信息。
安全模式是这样一种状态,系统处于这个状态时,不接受任何对名字空间的修改,同时也不会对数据块进行复制或删除数据块。NameNode启动的时候会自动进入安全模式,同时也可以手工进入(不会自动离开)。系统启动以后,DataNode会报告目前它拥有的数据块的信息,当系统接收到的Block信息到达一定门槛,同时每个Block都有dfs.replication.min个副本后,系统等待一段时间后就离开安全模式。这个门槛定义的参数包括:

l           dfs.safemode.threshold.pct:接受到的Block的比例,缺省为95%,就是说,必须DataNode报告的数据块数目占总数的95%,才到达门槛;

l           dfs.replication.min:缺省为1,即每个副本都存在系统中;

l           dfs.replication.min:等待时间,缺省为0,单位秒。

SafeModeInfo的类图如下:



 

threshold,extension和safeReplication保存的是上面说的3个参数。Reached等于-1表明安全模式是关闭的,0表示安全模式打开但是系统还没达到threshold。blockTotal是计算threshold时的分母,blockSafe是分子,lastStatusReport用于控制写日志的间隔。

SafeModeInfo(Configuration conf)使用配置文件的参数,是NameNode正常启动时使用的构造函数,SafeModeInfo()中,this.threshold = 1.5f使得系统用于处于安全模式。

enter()使系统进入安全模式,leave()会使系统离开安全模式,canLeave()用于检查是否能离开安全模式而needEnter(),则判断是否应该进入安全模式。checkMode()检查系统状态,如果必要,则进入安全模式。其他的方法都比价简单,大多为对成员变量的访问。

 

讨论完类SafeModeInfo,我们来分析一下SafeModeMonitor,它用于定期检查系统是否能够离开安全模式(smmthread就是它的一个实例)。系统离开安全模式后,smmthread会被重新赋值为null。


Hadoop源代码分析(二六)

(没想到需要分页啦)

private Host2NodesMap host2DataNodeMap = new Host2NodesMap();
保存了主机名(String)到DatanodeDescriptor数组的映射(Host2NodesMap唯一的成员变量为HashMap<String,DatanodeDescriptor[]> map,它的方法都是对这个map进行操作)。

 

  NetworkTopology clusterMap = new NetworkTopology();
  private DNSToSwitchMapping dnsToSwitchMapping;

定义了HDFS的网络拓扑,网络拓扑对应选择数据块副本的位置很重要。如在一个层次型的网络中,接到同一个交换机的两个节点间的网络速度,会比跨越多个交换机的两个节点间的速度快,但是,如果某交换机故障,那么它对接到它上面的两个节点会同时有影响,但跨越多个交换机的两个节点,这种影响会小得多。下面是NetworkTopology相关的类图:

 

Hadoop实现了一个树状的拓扑结构抽象,其中,Node接口,定义了网络节点的一些方法,NodeBase是Node的一个实现,提供了叶子节点的一些方法(明显它没有子节点),而InnerNode则实现了树的内部节点,如果我们考虑一个网络部署的话,那么叶子节点是服务器,而InnerNode则是服务器所在的机架或交换机或路由器。Node提供了对网络位置信息(采用类似文件树的方式),节点名称和Node所在的树的深度的方法。NodeBase提供了一个简单的实现。InnerNode是NetworkTopology的内部类,对比NodeBase,它的clildren保存了所有的子节点,这样的话,就可以构造一个拓扑树。这棵树的叶子可能是服务器,也可能是机架,内部则是机架或者是路由器等设备,InnerNode提供了一系列的方法区分处理这些信息。

NetworkTopology的add方法和remove用于在拓扑结构中加入节点和删除节点,同时也给出一些get*方法,用于获取一些对象内部的信息,如getDistance,可以获取两个节点的距离,而isOnSameRack可以判断两个节点是否处于同一个机架。chooseRandom有两个实现,用于在一定范围内(另一个还有一个排除选项)随机选取一个节点。chooseRandom在选择数据块副本位置的时候调用。

DNSToSwitchMapping配合上面NetworkTopology,用于确定某一个节点的网络位置信息,它的唯一方法,可以通过一系列机器的名字找出它们对应的网络位置信息。目前有支持两种方法,一是通过命令行方式,将节点名作为输入,输出为网络位置信息(RawScriptBasedMapping执行命令CachedDNSToSwitchMapping缓存结果),还有一种就是利用配置参数hadoop.configured.node.mapping静态配置(StaticMapping)。

 

  ReplicationTargetChooser replicator;

用于为数据块备份选择目标,例如,用户写文件时,需要选择一些DataNode,作为数据块的存放位置,这时候就利用它来选择目标地址。chooseTarget是ReplicationTargetChooser中最重要的方法,它通过内部的一个NetworkTopology对象,计算出一个DatanodeDescriptor数组,该数组就是选定的DataNode,同时,顺序就是最佳的数据流顺序(还记得我们讨论DataXceiver些数据的那个图吗?)。

 

  private HostsFileReader hostsReader; 

保存了系统中允许/不允许连接到NameNode的机器列表。

 

  private Daemon dnthread = null;

线程句柄,该线程用于检测DataNode上的Decommission进程。例如,某节点被列入到不允许连接到NameNode的机器列表中(HostsFileReader),那么,该节点会进入Decommission状态,它上面的数据块会被复制到其它节点,复制结束后机器进入DatanodeInfo.AdminStates.DECOMMISSIONED,这台机器就可以从HDFS中撤掉。

 

  private long maxFsObjects = 0;          // maximum number of fs objects

系统能拥有的INode最大数(配置项dfs.max.objects,0为无限制)。

 

  private final GenerationStamp generationStamp = new GenerationStamp();

系统的时间戳生产器。

 

  private int blockInvalidateLimit = FSConstants.BLOCK_INVALIDATE_CHUNK;

发送给DataNode删除数据块消息中,能包含的最大数据块数。比方说,如果某DataNode上有250个Block需要被删除,而这个参数是100,那么一共会有3条删除数据块消息消息,前面两条包含了100个数据块,最后一条是50个。

 

private long accessTimePrecision = 0;

用于控制文件的access时间的精度,也就是说,小于这个精度的两次对文件访问,后面的那次就不做记录了。

Hadoop源代码分析(二七)

我们接下来分析NameNode.java的成员变量,然后两个类综合起来,分析它提供的接口,并配合说明接口上请求对应的处理流程。

前面已经介绍过了,NameNode实现了接口ClientProtocol,DatanodeProtocol和NamenodeProtocol,分别提供给客户端/DataNode/从NameNode访问。由于NameNode的大部分功能在类FSNamesystem中实现,那么NameNode.java的成员变量就很少了。

  public FSNamesystem namesystem;
指向FSNamesystem对象。

 

  private Server server;
NameNode的RPC服务器实例。

 

  private Thread emptier;
处理回收站的线程句柄。

 

  private int handlerCount = 2;
还记得我们分析RPC的服务器时提到的服务器请求处理线程(Server.Handle)吗?这个参数给出了server中服务器请求处理线程的数目,对应配置参数为dfs.namenode.handler.count。

 

  private boolean supportAppends = true;
是否支持append操作,对应配置参数为dfs.support.append。

  private InetSocketAddress nameNodeAddress = null;
NameNode的地址,包括IP地址和监听端口。

下面我们来看NameNode的启动过程。main方法是系统的入口,它会调用createNameNode创建NameNode实例。createNameNode分析命令行参数,如果是FORMAT或FINALIZE,调用对应的方法后退出,如果是其他的参数,将创建NameNode对象。NameNode的构造函数会调用initialize,初始化NameNode的成员变量,包括创建RPC服务器,初始化FSNamesystem,启动RPC服务器和回收站线程。

FSNamesystem的构造函数会调用initialize方法,去初始化上面我们分析过的一堆成员变量。几个重要的步骤包括加载FSImage,设置系统为安全模式,启动各个工作线程和HTTP服务器。系统的一些参数是在setConfigurationParameters中初始化的,其中一些值的计算比较麻烦,而且也可能被其它部分的code引用的,就独立出来了,如getNamespaceDirs和getNamespaceEditsDirs。initialize对应的是close方法,很简单,主要是停止initialize中启动的线程。

对应于initialize方法,NameNode也提供了对应的stop方法,用于初始化时出错系统能正确地退出。

NameNode的format和finalize操作,都是先构造FSNamesystem,然后利用FSNamesystem的FSImage提供的对应方法完成的。我们在分析FSImage.java时,已经了解了这部分的功能。

Hadoop源代码分析(二八)

万事俱备,我们可以来分析NameNode上的流程啦。

 

首先我们来看NameNode上实现的ClientProtocol,客户端通过这个接口,可以对目录树进行操作,打开/关闭文件等。

getBlockLocations用于确定文件内容的位置,它的输入参数为:文件名,偏移量,长度,返回值是一个LocatedBlocks对象(如下图),它携带的信息很多,大部分字段我们以前都讨论过。


 

getBlockLocations直接调用NameSystem的同名方法。NameSystem中这样的方法首先会检查权限和对参数进行检查(如偏移量和长度要大于0),然后再调用实际的方法。找LocatedBlocks先找src对应的INode,然后通过INode的getBlocks方法,可以拿到该节点的Block列表,如果返回为空,表明该INode不是文件,返回null;如果Block列表长度为0,以空的Block数组构造返回的LocatedBlocks。

如果Block数组不为空,则通过请求的偏移量和长度,就可以把这个区间涉及的Block找出来,对于每一个block,执行:

l           通过BlocksMap我们可以找到它存在于几个DataNode上(BlocksMap.numNodes方法);

l           计算包含该数据块但数据块是坏的DataNode的数目(通过NameSystem.countNodes方法,间接访问CorruptReplicasMap中的信息);

l           计算坏数据块的数目(CorruptReplicasMap.numCorruptReplicas方法,应该和上面的数相等);

l           通过上面的计算,我们得到现在还OK的数据块数目;

l           从BlocksMap中找出所有OK的数据块对应的DatanodeDescriptor(DatanodeInfo的父类);

l           创建对应的LocatedBlock。

收集到每个数据块的LocatedBlock信息后,很自然就能构造LocatedBlocks对象。getBlockLocations其实只是一个读的方法,请求到了NameNode以后只需要查表就行了。

create方法,该方法用于在目录树上创建文件(创建目录使用mkdir),需要的参数比较多,包括文件名,权限,客户端名,是否覆盖已存在文件,副本数和块大小。NameNode的create调用NameSystem的startFile方法(startFile需要的参数clientMachine从线程局部变量获取)。

startFile方法先调用startFileInternal完成操作,然后调用logSync,等待日志写完后才返回。

startFileInternal不但服务于startFile,也被appendFile调用(通过参数append区分)。方法的最开始是一堆检查,包括:安全模式,文件名src是否正确,权限,租约,replication参数,overwrite参数(对append操作是判断src指向是否存在并且是文件)。租约检查很简单,如果通过FSDirectory.getFileINode(src)得到的文件是出于构造状态,表明有客户正在操作该文件,这时会抛出异常AlreadyBeingCreatedException。

如果对于创建操作,会通过FSDirectory的addFile往目录树上添加一个文件并在租约管理器里添加一条记录。

对于append操作,执行的是构造一个新的INodeFileUnderConstruction并替换原有的节点,然后在租约管理器里添加一条记录。

总的来说,最简单的create流程就是在目录树上创建一个INodeFileUnderConstruction对象并往租约管理器里添加一条记录。

 

我们顺便分析一下append吧,它的返回值是LocatedBlock,比起getBlockLocations,它只需要返回数组的一项。appendFile是NameSystem的实现方法,它首先调用上面讨论的startFileInternal方法(已经在租约管理器里添加了一条记录)然后写日志。然后寻找对应文件INodeFile中记录的最后一个block,并通过BlocksMap.getStoredBlock()方法得到BlockInfo,然后再从BlocksMap中获得所有的DatanodeDescriptor,就可以构造LocatedBlock了。需要注意的,如果该Block在需要被复制的集合(UnderReplicatedBlocks)中,移除它。

如果文件刚被创建或者是最后一个数据块已经写满,那么append会返回null,这是客户端需要使用addBlock,为文件添加数据块。

Hadoop源代码分析(二九)

  public boolean setReplication(String src,
                                      short replication
                                      ) throws IOException;

setReplication,设置文件src的副本数为replication,返回值为boolean,在FSNameSystem中,调用方法setReplicationInternal,然后写日志。

setReplicationInternal上来自然是检查参数了,然后通过FSDirectory的setReplication,设置新的副本数,并获取老的副本数。根据新旧数,决定删除/复制数据块。

增加副本数通过调用updateNeededReplications,为了获取UnderReplicatedBlocks. update需要的参数,FSNameSystem提供了内部方法countNodes和getReplication,获得对应的数值(这两个函数都很简单)。

proccessOverReplicatedBlock用于减少副本数,它被多个方法调用:

 

主要参数有block,副本数,目标DataNode,源DataNode(用于删除)。proccessOverReplicatedBlock首先找出block所在的,处于非Decommission状态的DataNode的信息,然后调用chooseExcessReplicates。chooseExcessReplicates执行:

l           按机架位置,对DatanodeDescriptor进行分组;

l           将DataNode分为两个集合,分别是一个机架包含一个以上的数据块的和剩余的;

l           选择可以删除的数据块(顺序是:源DataNode,同一个机架上的,剩余的),把它加到recentInvalidateSets中。

  public void setPermission(String src, FsPermission permission
                                 ) throws IOException;

setPermission,用于设置文件的访问权限。非常简单,首先检查是否有权限,然后调用FSDirectory.setPermission修改文件访问权限。

  public void setOwner(String src, String username, String groupname
      ) throws IOException;

  public void setTimes(String src, long mtime, long atime) throws IOException;

  public void setQuota(String path, long namespaceQuota, long diskspaceQuota)
                      throws IOException;

setOwner,设置文件的文件主和文件组,setTimes,设置文件的访问时间和修改时间,setQuota,设置某路径的空间限额和空间额度,和setPermission类似,调用FSDirectory的对应方法,简单。

  public boolean setSafeMode(FSConstants.SafeModeAction action) throws IOException;

前面我们已经介绍了NameNode的安全模式,客户端通过上面的方法,可以让NameNode进入(SAFEMODE_ENTER)/退出(SAFEMODE_LEAVE)安全模式或查询(SAFEMODE_GET)状态。FSNamesystem的setSafeMode处理这个命令,对于进入安全模式的请求,如果系统现在不处于安全模式,那么创建一个SafeModeInfo对象(创建的这个对象有别于启动时创建的那个SafeModeInfo,它不会自动退出,因为threshold=1.5f),这标志着系统进入安全模式。退出安全模式很简单,将safeMode赋空就可以啦。

  public FileStatus[] getListing(String src) throws IOException;

分析完set*以后,我们来看get*。getListing对应于UNIX系统的ls命令,返回值是FileStatus数组,FileStatus的类图如下,它其实给出了文件的详细信息,如大小,文件主等等。其实,这些信息都存在INode*中,我们只需要把这些信息搬到FileStatus中就OK啦。FSNamesystem和FSDirectory中都有同名方法,真正干活的地方在FSDirectory中。getListing不需要写日志。


  public long[] getStats() throws IOException; 

getStatus得到的是文件系统的信息,UNIX对应命令为du,它的实现更简单,所有的信息都存放在FSNamesystem对象里。

  public DatanodeInfo[] getDatanodeReport(FSConstants.DatanodeReportType type)

  throws IOException;

getDatanodeReport,获取当前DataNode的状态,可能的选项有DatanodeReportType.ALL, IVE和DEAD。FSNamesystem的同名方法调用getDatanodeListForReport,通过HostsFileReader读取对应信息。

  public long getPreferredBlockSize(String filename) throws IOException;

getPreferredBlockSize,返回INodeFile.preferredBlockSize,数据块大小。

  public FileStatus getFileInfo(String src) throws IOException;

和getListing类似,不再分析。

  public ContentSummary getContentSummary(String path) throws IOException;

得到文件树的一些信息,如下图:

 

  public void metaSave(String filename) throws IOException;

这个也很简单,它把系统的metadata输出/添加到指定文件上(NameNode所在的文件系统)。

Hadoop源代码分析(三零)

软柿子都捏完了,我们开始啃硬骨头。前面已经分析过getBlockLocations,create,append,setReplication,setPermission和setOwner,接下来我们继续回来讨论和文件内容相关的操作。

  public void abandonBlock(Block b, String src, String holder
      ) throws IOException;

abandonBlock用于放弃一个数据块。普通的文件系统中并没有“放弃”操作,HDFS出现放弃数据块的原因,如下图所示。当客户端通过其他操作(如下面要介绍的addBlock方法)获取LocatedBlock后,可以打开到一个block的输出流,由于从DataNode出错到NameNode发现这个信息,需要有一段时间(NameNode长时间收到DataNode心跳),打开输出流可能出错,这时客户端可以向NameNode请求放弃这个数据块。

 

abandonBlock的处理不是很复杂,首先检查租约(调用checkLease方法。block对应的文件存在,文件处于构造状态,租约拥有者匹配),如果通过检查,调用FSDirectory的removeBlock,从INodeFileUnderConstruction/BlocksMap/CorruptReplicasMap中删除block,然后通过logOpenFile()记录变化(logOpenFile真是万能啊)。

 

  public LocatedBlock addBlock(String src, String clientName) throws IOException;

写HDFS的文件时,如果数据块被写满,客户端可以通过addBlock创建新的数据块。具体的创建工作由FSNamesystem的getAdditionalBlock方法完成,当然上来就是一通检查(是否安全模式,命名/存储空间限额,租约,数据块副本数,保证DataNode已经上报数据块状态),然后通过ReplicationTargetChooser,选择复制的目标(如果目标数不够副本数,又是一个异常),然后,就可以分配数据块了。allocateBlock创建一个新的Block对象,然后调用addBlock,检查参数后把数据块加到BlocksMap对象和对应的INodeFile对象中。allocateBlock返回后,getAdditionalBlock还会继续更新一些需要记录的信息,最后返回一个新构造的LocatedBlock。

 

  public boolean complete(String src, String clientName) throws IOException;

当客户端完成对数据块的写操作后,调用complete完成写操作。方法complete如果返回是false,那么,客户端需要继续调用complete方法。

FSNamesystem的同名方法调用completeFileInternal,它会:

l           检查环境;

l           获取src对应的INode;

l           如果INode存在,并且处于构造状态,获取数据块;

l           如果获取数据块返回空,返回结果CompleteFileStatus.OPERATION_FAILED,FSNamesystem的complete会抛异常返回;

l           如果上报文件完成的DataNode数不够系统最小的副本数,返回STILL_WAITING;

l           调用finalizeINodeFileUnderConstruction;

l           返回成功COMPLETE_SUCCESS

其中,对finalizeINodeFileUnderConstruction的处理包括:

l           释放租约;

l           将对应的INodeFileUnderConstruction对象转换为INodeFile对象,并在FSDirectory进行替换;

l           调用FSDirectory.closeFile关闭文件,其中会写日志logCloseFile(path, file)。

l           检查副本数,如果副本数小于INodeFile中的目标数,那么添加数据块复制任务。

我们可以看到,complete一个文件还是比较复杂的,需要释放很多的资源。

  public void reportBadBlocks(LocatedBlock[] blocks) throws IOException;

调用reportBadBlocks的地方比较多,客户端可能调用,DataNode上也可能调用。 

 

由于上报的是个数组,reportBadBlocks会循环处理,调用FSNamesystem的markBlockAsCorrupt方法。markBlockAsCorrupt方法需要两个参数,blk(数据块)和dn(所在的DataNode信息)。如果系统目前副本数大于要求,那么直接调用invalidateBlock方法。

方法invalidateBlock很简单,在检查完系统环境以后,先调用addToInvalidates方法往FSNamesystem.recentInvalidateSets添加一项,然后调用removeStoredBlock方法。

removeStoredBlock被多个方法调用,它会执行:

l           从BlocksMap中删除记录removeNode(block, node);

l           如果目前系统中还有其他副本,调用decrementSafeBlockCount(可能的调整安全模式参数)和updateNeededReplications(跟新可能存在的block复制信息,例如,现在系统中需要复制1个数据块,那么更新后,需要复制2个数据块);

l           如果目前系统中有多余数据块等待删除(在excessReplicateMap中),那么移除对应记录;

l           删除在CorruptReplicasMap中的记录(可能有)。

 

removeStoredBlock其实也是涉及了多处表操作,包括BlocksMap,excessReplicateMap和CorruptReplicasMap。

我们回到markBlockAsCorrupt,如果系统目前副本数小于要求,那么很显然,我们需要对数据块进行复制。首先将现在的数据块加入到CorruptReplicasMap中,然后调用updateNeededReplications,跟新复制信息。

markBlockAsCorrupt这个流程太复杂了,我们还是画个图吧:


Hadoop源代码分析(三一)

下面是和目录树相关的方法。

  public boolean rename(String src, String dst) throws IOException;

更改文件名。调用FSNamesystem的renameTo,干活的是renameToInternal,最终调用FSDirectory的renameTo方法,如果成功,更新租约的文件名,如下:

      changeLease(src, dst, dinfo);

  public boolean delete(String src) throws IOException;

  public boolean delete(String src, boolean recursive) throws IOException;

第一个已经废弃不用,使用第二个方法。

最终使用deleteInternal,该方法调用FSDirectory.delete()。

  public boolean mkdirs(String src, FsPermission masked) throws IOException;

在做完一系列检查以后,调用FSDirectory.mkdirs()。

  public FileStatus[] getListing(String src) throws IOException;

前面我们已经讨论了。

 

下面是其它和系统维护管理的方法。

  public void renewLease(String clientName) throws IOException;

就是调用了一下leaseManager.renewLease(holder),没有其他的事情需要做,简单。

  public void refreshNodes() throws IOException;

还记得我们前面分析过NameNode上有个DataNode在线列表和DataNode离线列表吗,这个命令可以让NameNode从新读这两个文件。当然,根据前后DataNode的状态,一共有4种情况,其中有3种需要修改。

对于从工作状态变为离线的,需要将上面的DataNode复制到其他的DataNode,需要调用updateNeededReplications方法(前面我们已经讨论过这个方法了)。

对于从离线变为工作的DataNode,只需要改变一下状态。

  public void finalizeUpgrade() throws IOException;

finalize一个升级,确认客户端有超级用户权限以后,调用FSImage.finalizeUpgrade()。

  public void fsync(String src, String client) throws IOException;

将文件信息持久化。在检查租约信息后,调用FSDirectory的persistBlocks,将文件的原信息通过logOpenFile(path, file)写日志。

Hadoop源代码分析(三二)

搞定ClientProtocol,接下来是DatanodeProtocol部分。接口如下:


 

public DatanodeRegistration register(DatanodeRegistration nodeReg

                                       ) throws IOException

用于DataNode向NameNode登记。输入和输出参数都是DatanodeRegistration,类图如下:


 

前面讨论DataNode的时候,我们已经讲过了DataNode的注册过程,我们来看NameNode的过程。下面是主要步骤:

l           检查该DataNode是否能接入到NameNode;

l           准备应答,更新请求的DatanodeID;

l           从datanodeMap(保存了StorageID à DatanodeDescriptor的映射,用于保证DataNode使用的Storage的一致性)得到对应的DatanodeDescriptor,为nodeS;

l           从Host2NodesMap(主机名到DatanodeDescriptor数组的映射)中获取DatanodeDescriptor,为nodeN;

l           如果nodeN!=null同时nodeS!=nodeN(后面的条件表明表明DataNode上使用的Storage发生变化),那么我们需要先在系统中删除nodeN(removeDatanode,下面再讨论),并在Host2NodesMap中删除nodeN;

l           如果nodeS存在,表明前面已经注册过,则:

1.      更新网络拓扑(保存在NetworkTopology),首先在NetworkTopology中删除nodeS,然后跟新nodeS的相关信息,调用resolveNetworkLocation,获得nodeS的位置,并从新加到NetworkTopology里;

2.      更新心跳信息(register也是心跳);

l           如果nodeS不存在,表明这是一个新注册的DataNode,执行

1.      如果注册信息的storageID为空,表明这是一个全新的DataNode,分配storageID;

2.      创建DatanodeDescriptor,调用resolveNetworkLocation,获得位置信息;

3.      调用unprotectedAddDatanode(后面分析)添加节点;

4.      添加节点到NetworkTopology中;

5.      添加到心跳数组中。

上面的过程,我们遗留了两个方法没分析,removeDatanode的流程如下:

l           更新系统的状态,包括capacityTotal,capacityUsed,capacityRemaining和totalLoad;

l           从心跳数组中删除节点,并标记节点isAlive属性为false;

l           从BlocksMap中删除这个节点上的所有block,用了(三零)分析到的removeStoredBlock方法;

l           调用unprotectedAddDatanode;

l           从NetworkTopology中删除节点信息。

unprotectedAddDatanode很简单,它只是更新了Host2NodesMap的信息。

Hadoop源代码分析(三三)

下面来看一个大家伙:

public DatanodeCommand sendHeartbeat(DatanodeRegistration nodeReg,

                                       long capacity,

                                       long dfsUsed,

                                       long remaining,

                                       int xmitsInProgress,

                                       int xceiverCount) throws IOException

DataNode发送到NameNode的心跳信息。细心的人会发现,请求的内容还是DatanodeRegistration,应答换成DatanodeCommand了。DatanodeCommand类图如下:

前面介绍DataNode时,已经分析过了DatanodeCommand支持的命令:

  DNA_TRANSFER:拷贝数据块到其他DataNode

  DNA_INVALIDATE:删除数据块

  DNA_SHUTDOWN:关闭DataNode

  DNA_REGISTER:DataNode重新注册

  DNA_FINALIZE:提交升级

  DNA_RECOVERBLOCK:恢复数据块



 

有了上面这些基础,我们来看FSNamesystem.handleHeartbeat的处理过程:

l           调用getDatanode方法找对应的DatanodeDescriptor,保存于变量nodeinfo(可能为null)中,如果现有NameNode上记录的StorageID和请求的不一样,返回DatanodeCommand.REGISTER,让DataNode从新注册。

l           如果发现当前节点需要关闭(已经isDecommissioned),抛异常DisallowedDatanodeException。

l           nodeinfo是空或者现在状态不是活的,返回DatanodeCommand.REGISTER,让DataNode从新注册。

l           更新系统的状态,包括capacityTotal,capacityUsed,capacityRemaining和totalLoad;

l           接下来按顺序看有没有可能的恢复数据块/拷贝数据块到其他DataNode/删除数据块/升级命令(不讨论)。一次返回只能有一条命令,按上面优先顺序。

 

下面分析应答的命令是如何构造的。

首先是DNA_RECOVERBLOCK(恢复数据块),那是个非常长的流程,同时需要回去讨论DataNode上的一些功能,我们在后面介绍它。

 

对于DNA_TRANSFER(拷贝数据块到其他DataNode),从DatanodeDescriptor.replicateBlocks中取出尽可能多的项目,放到BlockCommand中。在DataNode中,命令由transferBlocks执行,前面我们已经分析过啦。

 

删除数据块DNA_INVALIDATE也很简单,从DatanodeDescriptor.invalidateBlocks中获取尽可能多的项目,放到BlockCommand中,DataNode中的动作,我们也分析过。

 

我们来讨论DNA_RECOVERBLOCK(恢复数据块),在讨论DataNode的过程中,我们没有讲这个命令是用来干什么的,还有它在DataNode上的处理流程,是好好分析分析这个流程的时候了。DNA_RECOVERBLOCK命令通过DatanodeDescriptor.getLeaseRecoveryCommand获取,获取过程很简单,将DatanodeDescriptor对象中队列recoverBlocks的所有内容取出,放入BlockCommand的Block中,设置BlockCommand为DNA_RECOVERBLOCK,就OK了。

关键是,这个队列里的信息是用来干什么的。我们先来看那些操作会向这个队列加东西,调用关系图如下:


租约有两个超时时间,一个被称为软超时(1分钟),另一个是硬超时(1小时)。如果租约软超时,那么就会触发internalReleaseLease方法,如下:

  void internalReleaseLease(Lease lease, String src) throws IOException

该方法执行:

l           检查src对应的INodeFile,如果不存在,不处于构造状态,返回;

l           文件处于构造状态,而文件目标DataNode为空,而且没有数据块,则finalize该文件(该过程在completeFileInternal中已经讨论过,租约在过程中被释放),并返回;

l           文件处于构造状态,而文件目标DataNode为空,数据块非空,则将最后一个数据块存放的DataNode目标取出(在BlocksMap中),然后设置为文件现在的目标DataNode;

l           调用INodeFileUnderConstruction.assignPrimaryDatanode,该过程会挑选一个目前还活着的DataNode,作为租约的主节点,并把<block,block目标DataNode数组>加到该DataNode的recoverBlocks队列中;

l           更新租约。

上面分析了租约软超时的情况下NameNode发生租约恢复的过程。DataNode上收到这个命令后,将会启动一个新的线程,该线程为每个Block调用recoverBlock方法:recoverBlock(blocks[i], false, targets[i], true)。

  private LocatedBlock recoverBlock(Block block, boolean keepLength,

      DatanodeID[] datanodeids, boolean closeFile) throws IOException

它的流程并不复杂,但是分支很多,如下图(蓝线是上面输入,没有异常走的流程):


 

 首先是判断进来的Block是否在ongoingRecovery中,如果存在,返回,不存在,加到ongoingRecovery中。

接下来是个循环(框内部分是循环体,奇怪,没找到表示循环的符号),对每一个DataNode,获取Block的BlockMetaDataInfo(下面还会分析),这需要调用到DataNode间通信的接口上的方法getBlockMetaDataInfo。然后分情况看要不要把信息保存下来(图中间的几个判断),其中包括要进行同步的节点。

根据参数,更新数据块信息,然后调用syncBlock并返回syncBlock生产的LocatedBlock。

上面的这一圈,对于我们这个输入常数来说,就是把Block的长度,更新成为拥有最新时间戳的最小长度值,并得到要更新的节点列表,然后调用syncBlock更新各节点。

getBlockMetaDataInfo用于获取Block的BlockMetaDataInfo,包括Block的generationStamp,最后校验时间,同时它还会检查数据块文件的元信息,如果出错,会抛出异常。

syncBlock定义如下:

private LocatedBlock syncBlock(Block block, List<BlockRecord> syncList,

      boolean closeFile)

它的流程是:

l           如果syncList为空,通过commitBlockSynchronization向NameNode提交这次恢复;

l           syncList不为空,那么先NameNode申请一个新的Stamp,并根据上面得到的长度,构造一个新的数据块信息newblock;

l           对于没一个syncList中的DataNode,调用它们上面的updateBlock,更新信息;更新信息如果返回OK,记录下来;

l           如果更新了信息的DataNode不为空,调用commitBlockSynchronization提交这次恢复;并生成LocatedBlock;

l           如果更新的DataNode为空,抛异常。

通过syncBlock,所有需要恢复的DataNode上的Block信息都被更新。

DataNode上的updateBlock方法我们前面已经介绍了,就不再分析。

下面我们来看NameNode的commitBlockSynchronization方法,它在上面的过程中用于提交数据块恢复:

public void commitBlockSynchronization(Block block,

      long newgenerationstamp, long newlength,

      boolean closeFile, boolean deleteblock, DatanodeID[] newtargets

      )

参数分别是block,数据块;newgenerationstamp,新的时间戳;newlength,新长度;closeFile,是否关闭文件,deleteblock,是否删除文件;newtargets,新的目标列表。

上面的两次调用,输入参数分别是:

commitBlockSynchronization(block, 0, 0, closeFile, true, DatanodeID.EMPTY_ARRAY);

commitBlockSynchronization(block, newblock.getGenerationStamp(), newblock.getNumBytes(), closeFile, false, nlist);

处理流程是:

l           参数检查;

l           获取对应的文件,记为pendingFile;

l           BlocksMap中删除老的信息;

l           如果deleteblock为true,从pendingFile删除Block记录;

l           否则,更新Block的信息;

l           如果不关闭文件,那么写日志保存更新,返回;

l           关闭文件的话,调用finalizeINodeFileUnderConstruction。

这块比较复杂,不仅涉及了NameNode和DataNode间的通信,而且还存在对于DataNode和DataNode间的通信(DataNode间的通信就只支持这两个方法,如下图)。后面介绍DFSClient的时候,我们还会再回来分析它的功能,以获取全面的理解。


Hadoop源代码分析(三四)

继续对NameNode实现的接口做分析。

public DatanodeCommand blockReport(DatanodeRegistration nodeReg,
                                     long[] blocks) throws IOException

DataNode向NameNode报告它拥有的所有数据块,其中,参数blocks包含了数组化以后数据块的信息。FSNamesystem.processReport处理这个请求。一番检查以后,调用DatanodeDescriptor的reportDiff,将上报的数据块分成三组,分别是:

l           删除:其它情况;

l           加入:BlocksMap中有数据块,但目前的DatanodeDescriptor上没有对应信息;

l           使无效:BlocksMap中没有找到数据块。

对于删除的数据块,调用removeStoredBlock,这个方法我们前面已经分析过啦。

对应需要加入的数据块,调用addStoredBlock方法,处理流程如下:

l           从BlocksMap获取现在的信息,记为storedBlock;如果为空,返回;

l           记录block和DatanodeDescriptor的关系;

l           新旧数据块记录不是同一个(我们这个流程是肯定不是啦):

1.      如果现有数据块长度为0,更新为上报的block的值;

2.      如果现有数据块长度比新上报的长,invalidateBlock(前面分析过,很简单的一个方法)当前数据块;

3.      如果现有数据块长度比新上报的小,那么会删除所有老的数据块(还是通过invalidateBlock),并更新BlocksMap中数据块的大小信息;

4.      跟新可用存储空间等信息;

l           根据情况确定数据块需要复制的数目和目前副本数;

l           如果文件处于构建状态或系统现在是安全模式,返回;

l           处理当前副本数和文件的目标副本数不一致的情况;

l           如果当前副本数大于系统设定门限,开始删除标记为无效的数据块。

还是给个流程图吧:



对于标记为使无效的数据块,调用addToInvalidates方法,很简单的方法,直接加到FSNamesystem的成员变量recentInvalidateSets中。

public void blockReceived(DatanodeRegistration registration,
                            Block blocks[],

                            String[] delHints)

DataNode可以通过blockReceived,向NameNode报告它最近接受到的数据块,同时给出如果数据块副本数太多时,可以删除数据块的节点(参数delHints)。在DataNode中,这个信息是通过方法notifyNamenodeReceivedBlock,记录到对应的列表中。

 

 

NameNode上的处理不算复杂,对输入参数进行检查以后,调用上面分析的addStoredBlock方法。然后在PendingReplicationBlocks对象中删除相应的block。

 

  public void errorReport(DatanodeRegistration registration,

                          int errorCode,
                          String msg)

向NameNode报告DataNode上的一个错误,如果错误是硬盘错,会删除该DataNode,其它情况只是简单地记录收到一条出错信息。

  public NamespaceInfo versionRequest() throws IOException;

从NameNode上获取NamespaceInfo,该信息用于构造DataNode上的DataStorage。

 

 

  UpgradeCommand processUpgradeCommand(UpgradeCommand comm) throws IOException;

我们不讨论。

  public void reportBadBlocks(LocatedBlock[] blocks) throws IOException

报告错误的数据块。NameNode会循环调用FSNamesystem的markBlockAsCorrupt方法。处理流程不是很复杂,找对应的INodeFile,如果副本数够,那么调用invalidateBlock,使该DataNode上的Block无效;如果副本数不够,加Block到CorruptReplicasMap中,然后准备对好数据块进行复制。

目前为止,我们已经完成了NameNode上的ClientProtocol和DatanodeProtocol的分析了,NamenodeProtocol我们在理解从NameNode的时候,才会进行分析。

0 0