HDFS源码阅读–Namenode文件系统目录树及数据块管理

  • Post author:
  • Post category:其他




Namenode

Namenode作为HDFS的主节点:

  1. 文件系统目录树的管理:HDFS第一关系链;
  2. 数据块以及数据节点管理:HDFS第二关系链;
  3. 租约管理;
  4. 缓存管理



一、文件系统目录树

HDFS的命名空间是以“/”为根的整个目录树,是通过FSDirectory来管理的;

不管是目录还是文件,在文件系统目录树中都被看作是一个INode节点;

目录:InodeDirectory;

文件:INodeFile。

HDFS会将命名空间存放在本地文件系统上的一个叫fsimage的文件中,利用这个文件,namenode每次重启都能将命名空间重构。

fsimage由FSImage类负责,同时,对HDFS的各种操作,都会在editlog中记录,以便周期性的合并生成fsimage。



1. INode相关类



1.1 INode类

INode是INodeDirectory和INodeFile的父类。同时实现了INodeAttributes接口:

INodeArrtibutes定义的get方法
	userName:文件/目录所属用户名
	groupName:文件/目录所属组名
	fsPermission:文件/目录访问权限
	aclFeature:安全相关
	modificationTime:文件/目录上次修改时间
	accessTime:上次访问时间
	XAttrFeature:当前目录/文件的扩展属性

INode原信息的方法
	id:INodede id;
	name:文件/目录的名称
	fullPathName:文件/目录的完整路径
	parent:文件/目录的父节点

INode提供的基本判断方法:
	isFile():判断是否为文件
	isDirectory():判断是否为目录
	isSymlink():判断是否为符号链接
	isRoot():判断是否为文件系统目录树的根节点```



1.2 INodeWithAdditionalFields类

INode抽象类只定义了一个字段parent,其余字段的值都靠抽象的get()方法获得,并留给子类定义;

INodeWithAdditionalFields定义字段有:

id, name, permission, modificationTime, accessTime



1.3 INodeDirectory类

抽象了HDFS文件系统中的目录,里面保存了一组文件和其他一些目录。添加了成员变量

Children

//使用一个Children字段保存该目录中所有孩子节点的INode对象
private List<INode> children = null

INodeDirectory的方法分为三类:

1.子目录相关的方法:children字段的增、删、改、查。

public boolean addChild(INode node){
		//首先找到INode节点在children列表中的位置
		final int low = searchChildren(node.getLocalNameBytes());
		if(low >= 0){
			return false;
		}
		//调用addChild()方法将INode节点插入到children列表的low位置
		addChild(node, low);
		return true;
	}

	public boolean removeChild(final INode child){
		//找到INode节点在children列表中的位置
		final int i = searchChildren(child.getLocalNameBytes());
		if( i < 0){
			return false;
		}
		//从children列表中删除
		final INode removed = children.remove(i);
		Preconditions.checkState(removed == child);
		return true;
	}

特性(Feature)相关方法:可以对目标添加磁盘配额以及快照等特性

//向当前目录添加磁盘配额特性
	DirectoryWithQuotaFeature addDirectoryWithQuotaFeature(
		long nsQuota, long dsQuota){
		//构造DirectoryWithQuotaFeature对象
		//调用addFeature方法添加到fetures集合中
		final DirectoryWithQuotaFeature quota = new DirectoryWithQuotaFeature(
			nsQuota, dsQuota);
		addFeature(quota);
		return quota;
	}

	//获取当前目录磁盘配额特性对应的DirectoryWithQuotaFeature对象
	public final DirectoryWithQuotaFeature getDirectoryWithQuotaFeature(){
		return getFeature(DirectoryWithQuotaFeature.class);
	}
  1. 快照相关



1.4 INodeFile类

INodeFile类抽象了一个HDFS文件,继承自INodeWithAdditionalFeilds类。

INodeFile保存了文件最重要的两个信息:

文件头header字段

和文件对应的

数据块信息blocks字段

其中:1.Header保存了有多少个副本,以及数据块的大小;2. Blocks字段是一个数组,保存了所有数据块信息。

	//文件头信息
	private long header = 0L;
	//文件数据库信息
	private BlockInfo[] blocks;

INodeFile有一个内部类HeaderFormat,用于处理INodeFile.header字段;

从header中提取出

前4位的存储策略信息,中间12位的文件备份系统信息,后48位的数据块大小信息



1.5 INodeReference类

当HDFS的目录/文件处于某个快照中,这个文件/目录被重命名或者移动到其他路径时,该文件/目录会存在多条访问路径。INodeReference及其子类就是为了解决这个问题。



2. FSEditLog类

在namenode中,命名空间(namespace,指文件系统中的目录树、文件元数据等)是被

全部缓存在内存中的

,一旦namenode重启或者宕机,内存中的数据会全部丢失。



必须有个机制能够将整个命名空间持久化保存,并在namenode重启时重建命名空间–fsimage

目前namenode是将命名空间信息记录在一个叫fsimage二进制 文件中。


fsimage将目录树中的每个文件或者目录保存为一条记录

:名称、大小、用户、用户组、修改时间、创建时间等

HDFS将更新操作记录在editlog日志中,HDFS所有的写操作被记录在editlog,HDFS会定期将editlog文件与fsimage文件合并,以保持fsimage根namenode内存中记录的命名空间完全同步。

FSEditLog类管理editlog文件,随着namenode运行实时更新,所以FSEditLog类的实现依赖于底层的输入流和输出流。



2.1 transactionId机制

HDFS采用transactionId的日志管理方法来管理editlog。

TransactionId与客户端每次发起的RPC操作相关, 当客户端RPC请求对namenode的命名空间修改后,namenode就会在editlog中发起一个新的transaction用于记录这次操作,每个transaction用唯一的transactionId标识。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xXFhUeJy-1575622457791)(C:\Users\Nice\AppData\Roaming\Typora\typora-user-images\1575618089441.png)]

  • edit_start transaction id-end transaction id: edit文件就是我们描述的editlog文件,每个edits文件包含start-end之间的所有事务;
  • edits_inprogress_start transaction id:正在处理的editlog。所有的新修改操作都会记录在这个文件中,直到HDFS重置这个文件;
  • fsimage_end transaction id: fsimage文件是hadoop元数据的一个永久性的检查点,包含end_transaction id前完整的HDFS命名空间元数据镜像。.md5文件是保证fsimage文件的正确性;
  • seen_txid: 保存了上一个检查点checkpoint(合并edits和fsimage文件)以及编辑日志重置时最新的事务id。只在检查点操作以及编辑日志重置操作时更新。Namenode启动时会检查seen_txid并确保内存中加载的事务id至少超过seen_txid。



2.2 FSEditLog状态机

FSEditLog类被设计成一个状态机,用内部类FSEditLog.State描述:

  • UNINITIALIZED:editlog的初始状态;
  • BETWEEN_LOG_SEGMENTS: editlog的前一个segment已经关闭,新的还没开始;
  • IN_SEGMENT: editlog处于可写状态;
  • OPEN_FOR_READING: editlog处于可读状态;
  • CLOSED: editlog处于关闭状态。


对于非HA机制的情况:

FSEditLog应该开始于UNINITIALIZED或者CLOSED状态,初始化完成之后进入BETWEEN_LOG_SEGMENT状态,表明前一个segment已经结束,新的还没开始。当打开日志服务时,改变FSEditLog状态为IN_SEGMENT,表示可以写editlog文件。


对于HA机制的情况:

FSEditLog同样应该处于UNINITIALIZED或者CLOSED状态。



2.3 EditLogOutputStream类

FSEditLog类会调用FSEditLog.editLogStream字段的write方法在editlog文件中记录一个操作,数据会被先写入到editlog文件输出流的缓存中,然后调用flush()方法写到磁盘。FSEditLog的editLogStream字段是EditLogOutputStream类型的。

其是一个抽象类,

定义向持久化存储写editlog文件的相关接口

双缓存的数据结构:EditDoubleBuffer,

数据会被先写入到一块缓存中

,而

另一块缓存可能正在进行磁盘的同步操作

,这样输出流进行同步操作时,不会影响数据写入。

	//正在写入的缓冲区
	private TxnBuffer bufCurrent;
	//准备好同步的缓冲区
	private TxnBuffer bufReady;
	//缓冲区的大小
	private final int initBufferSize;



输出流要进行同步操作时,首先调用EditDoubleBuffer.setReadyToFlush()方法交换两个缓冲区,将正在写入的缓存改变为同步缓存,然后再进行同步操作

public void setReadyToFlush(){
    	assert isFlushed():	"Previous data not flushed yet";
    	//交换两个缓冲区
    	TxnBuffer tmp = bufReady;	
    	bufReady = bufCurrent;
    	bufCurrent = tmp;
    }

完成setReadyToFlush()调用之后输出流就可以调用flushTo()方法将同步缓存中的数据写入到文件中:

public void flushTo(OutputStream out) throws IOException{
    	bufReady.writeTo(out);	//将同步缓存中的数据写入文件
    	bufReady.reset();		//将同步缓存中保存的数据清空
    }



2.4 EditLogInputStream类

EditLogInputStream类抽象了从持久化存储上 读editlog文件的相关接口。

  • logEdit()和logSync()同步机制:

    	void logEdit(FSEditLogOp op) {
            synchronized(this) {
                assert this.isOpenForWrite() : "bad state: " + this.state;
    
                this.waitIfAutoSyncScheduled();
                long start = this.beginTransaction();
                op.setTransactionId(this.txid);
    
                try {
                    this.editLogStream.write(op);
                } catch (IOException var11) {
                } finally {
                    op.reset();
                }
    
                this.endTransaction(start);
                if (!this.shouldForceSync()) {
                    return;
                }
    
                this.isAutoSyncScheduled = true;
            }
    		//刷新操作语句并不在同步块中。
            this.logSync();
        }
    
    

logEdit()方法调用了

beginTransaction()、editLogStream()以及endTransaction()

三个方法使用了synchronized关键字进行同步操作,保证了多线程向editlog文件中写数据的时候,editlog文件记录的内容不会互相影响。

保证对应的transactionId是唯一并递增的 。




logEdit()方法中调用logSync()方法执行刷新操作的语句并不在synchronized代码段中,因为sync会触发写editlog文件的磁盘操作,这非常耗时并容易造成其他FSEditLog.log

()线程等待时间过长。所以HDFS设计者将输出日志记录和刷新数据缓冲区数据到磁盘这两个操作分离。同时利用EditLogOutputStream的两个缓冲区,使得这两个操作可以并发进行,提高了namenode的吞吐量。

*



3. FSImage类

namenode会定期将文件系统的命名空间保存到fsimage文件中,以防止namenode掉电或者进程崩溃。

namenode会将命名空间的修改操作保存在editlog文件中,然后定期合并fsimage和editlog文件

  • 保存命名空间:将当前时刻的namenode内存中的命名空间保存到fsimage文件中;
  • 加载fsimage文件:将磁盘上fsimage文件中保存的命名空间加载到内存中;
  • 加载editlog文件:namenode对命名空间的后序修改操作进行加载。



3.1 保存命名空间

FSNameSyatem—>FSImage.saveNamespace()触发保存操作—>saveFSImageAllDirs()执行具体逻辑。

Namenode可以定义多个存储路径来保存fsimage文件,每个存储路径saveFSImageAllDirs()都会启动一个线程负责在这个路径上保存fsimage文件,同时,

为了防止保存过程出错,命名空间信息会先保存在fsimage.ckpt,当操作全部完成,才会重命名为fsimage

。之后清理过期的editlog和fsimage文件。

fsimage包括四个部分:

  • MAGIC:fsimage文件头,是“HDFSIMAGE1”这个字符串的二进制形式,表示当前fsimage使用的是protobuf格式序列化的;
  • SECTIONS:fsimage文件将统一类型的namenode元信息保存在一个section中;
  • FileSummary:记录fsimage文件的元信息,以及保存的所有的section信息。



3.2 加载fsimage文件

FSImage.loadFSImageFile()执行加载fsimage文件,对于不同类型的section,会调用不同的方法加载section。



3.3 加载editlog文件

Namenode将fsimage中记录的特定时刻的命名空间加载到内存后,还需要加载后续对命名空间的修改,也就是editlog。

FSImage.loadEdits()方法将editlog文件的更新操作与当前的namenode的命名空间合并。



3.4 检查点机制

一个正常大小的editlog文件往往在几十到几百字节,但是某些某些情况下,editlog会变得非常大,在启动的时候,namenode会逐条读取editlog与fsimage合并,

editlog太大会导致namenode启动过程过长,因此,HDFS引入了检查点机制(checkpointing)。

检查点操作触发条件:

  1. 超过配置的检查点操作时长;
  2. 从上次检查点操作之后,发生的事务数(transaction)数超过了配置。

HDFS的检查点机制会定期的将editlog文件与fsimage文件合并并生成新的fsimage。这样namenode直接从fsimage加载数据。

合并fsimage和editlog是非常消耗I/O和CPU资源的,因此HDFS一般将检查点操作放在Secondary Namenode或者Standby namenode上


  • Secondary Namenode执行检查点操作(非HA):
  1. Secondary Namenode与NN之间没有共享的editlog文件目录,所以最新的事务id(transactionId)是2NN通过调用RPC方法NamenodeProtocol.getTransactionId()获取的;
  2. Secondary Namenode调用RPC方法触发editlog重置操作,将正在写的editlog段落结束,并创建新的edit.new。
  3. Secondary Namenode有了新的txid和seen_id,会通过HTTP GET请求获取fsimage和editlog文件;
  4. Secondary Namenode下载新的fsimage文件以重建2NN命名空间;
  5. Secondary Namenode读取edits中的记录,并与当前命名空间合并,这样就完成了与namenode的同步;
  6. Secondary Namenode将最新的同步的命名空间写入新的fsimage;
  7. namenode发起HTTP GET请求下载fsimage文件,该文件需要MD5校验。

  • Standby Namenode执行检查点操作(HA):

HA配置下没有Secondary Namenode这个节点,而是配置奇数个JournalNode来实现Namenode热备HA策略。

Standby Namenode负责观察editlog的变化,从JourNalNodes读取editlog并合并更新到命名空间

  1. Standby Namenode检查是否满足触发检查点操作的两个条件;
  2. Standby会将当前命名空间保存到fsimage.ckpt_txid文件中,之后Standby写入fsimage的MD5校验和,重命名为fsimage_txid;
  3. Standby向Active的ImageServlet发送HTTP GET请求,这个请求的URL包含了新的fsimage文件的事务id;
  4. Active根据SN提供的信息下载fsimage镜像。



4. FSDirectory

HDFS引入FSDirectory是想要目录树的所有操作抽象成一个统一的接口;

有了FSDirectory这个门面类,会使得所有对目录树的操作变得简单。

//向某个目录节点添加一个child子节点
	private boolean addChild();
//在指定的INode节点上添加一个数据块
	BlockInfo addBlock()



二. 数据块管理

INodeFile.blocks字段记录了HDFS文件拥有的所有的数据块,也是通过这个字段,HDFS的第一关系和第二关系发生了关联。

Blocks字段是一个BlockInfo类型的数组,BlockInfo是Block的子类。

HDFS使用Block类抽象Namenode中的数据块



1.Block类

Block类是HDFS数据块最基本的抽象接口。实现了Writable接口,可序列化;还实现了Comparable接口,按照Blockid大小排序。

    //blockId唯一标识这个Block对象
	private long blockId;
	//numBytes是这个数据块的大小
	private long numBytes;
	//generationStamp是这个数据块的时间戳
	private long generationStamp;



2.BlockInfo类

BlockInfo是Block类的扩展说明

	//数据块属于哪一个HDFS文件
	private BlockCollection bc;
	//有3*replication个元素的数量
	private Object[] triplets;



3.BlockMap类

BlockMap是Namenode上与数据块相关的最重要的类,它管理着namenode上数据块的元数据;当前数据块属于哪个HDFS文件,当前数据块保存在哪些Datanode上。

	private final int capacity;
	//GSet对象维护了Block->BlockInfo的映射关系
	private volatile GSet<Block, BlockInfo> blocks;

为什么BlocksMap要维护Block->BlockInfo的对应关系?

因为BlockInfo保存的数据节点的信息都在Datanode启动上报的,动态构建的。

而Namenode启动时内存中保存的关于数据块的信息只有Block类中维护的那些

,所以Namenode维护Block->BlockInfo,

随着DataNode不断上报数据块信息,BlockInfo信息会不断更新。



4.BlockManager类

BlockManager类管理维护与数据块相关的操作。存放不同状态数据块副本的数据结构

  //损坏的数据块副本集合
final CorruptReplicasMap corruptReplicas = new CorruptReplicasMap();
  //等待删除的数据块副本集合
private final InvalidateBlocks invalidateBlocks;
  //推迟操作的数据块副本集合
private final LightWeightHashSet<Block> postponedMisreplicatedBlocks =
      new LightWeightHashSet<>();
  //多于的数据块副本集合
public final Map<String, LightWeightLinkedSet<Block>> excessReplicateMap =
    new HashMap<>();
  //等待复制的数据块副本集合
public final UnderReplicatedBlocks neededReplications = new UnderReplicatedBlocks();
  //已经生成复制请求的数据块副本
final PendingReplicationBlocks pendingReplications;



版权声明:本文为weixin_42946624原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。