数据节点简介
我们继续来了解Hadoop分布式文件系统各个实体内部的工作原理。首先是数据节点,它以数据块的形式在本地Linux文件系统上保存HDFS文件的内容,并对外提供文件数据访问功能。客户端读写HDFS文件时,必须根据名字节点提供的信息,进一步和数据节点交互;同时,数据节点还必须接受名字节点的管理,执行名字节点指令,并上报名字节点感兴趣的事件,以保证文件系统稳定、可靠、高效地运行。
数据块存储
HDFS采用了一种非常典型的文件系统实现方法。对一个不定长度的文件进行分块,并将块保存在存储设备上。文件数据分块,保存在HDFS的存储设备–数据节点上。数据节点将数据块以Linux文件的形式保存在节点的存储系统上。数据节点的第一个基本功能,就是管理这些保存在Linux文件系统中的数据块。
在HDFS中,名字节点、数据节点和第二名字节点都需要在磁盘上组织、存储持久化数据,它们会在磁盘上维护一定的文件结构。在第一次启动HDFS集群前,需要通过hadoop namenode -format对名字节点进行格式化,让名字节点建立对应的文件结构。
由于需要在HDFS集群运行时动态添加或删除数据节点。所以,数据节点和名字节点不一样,它们不需要进行格式化,而是在第一次启动的时候,创建存储目录。另外,数据节点可以管理多个数据目录,被管理的目录通过配置项${dfs.data.dir}指定,如果该配置项的值为“/data/datanode,/data2/datanode”,则数据节点会管理这两个目录,并把它们作为数据块保存目录。
${dfs.data.dir}目录介绍
${dfs.data.dir}目录下一般有4个目录和2个文件。(目录:blocksBeginWriten、current、detach、tmp,文件:in_use.lock、storage)
其中各个目录的作用如下:
blocksBeginWritten:由名字可以知道,该文件夹保存着当前正在“写”的数据块。和位于“tmp”目录下保存的正在“写”的数据块相比,差别在于“blocksBeginWritten”中的数据块写操作由客户端发起。
current:数据节点管理的最重要目录,它保存着已经写入HDFS文件系统的数据块,也就是写操作已经结束的已“提交”数据块。该目录还包含一些系统工作时需要的文件。
detach:用户配合数据节点升级,供数据块分离操作保存临时工作文件。
tmp:该文件夹也保存着当前正在“写”的数据块,这里的写操作时由数据块复制引发的,另一个数据节点正在发送数据到数据块。
上述目录其实也隐含了数据块在数据节点上可能存在的状态,以上述目录名作为状态。
数据节点上的数据块最开始会处于“blocksBeingWriteen”(由客户端写入创建)或“tmp”(由数据块复制写入创建)状态,当数据写入顺利结束后,提交数据块,它们的状态会迁移到“current”。已经提交的数据块可以重新被打开,追加数据,这时,它的状态会放回到“blocksBeginWritten”。处于上述的三个状态的数据块都可以被删除,并转移到“最终状态”。
状态“detach”比较特殊,它用于配合数据节点升级,是数据块可能存在的一个临时、短暂的状态。
${dfs.data.dir}目录下还有两个文件,其中,“storage”保存着如下一段提示信息:
protected void writeCorruptedData(RandomAccessFile file) throws IOException {
final String messageForPreUpgradeVersion =
"\nThis file is INTENTIONALLY CORRUPTED so that versions\n"
+ "of Hadoop prior to 0.13 (which are incompatible\n"
+ "with this directory layout) will fail to start.\n";
file.seek(0);
file.writeInt(FSConstants.LAYOUT_VERSION);
org.apache.hadoop.io.UTF8.writeString(file, "");
file.writeBytes(messageForPreUpgradeVersion);
file.getFD().sync();
}
该文件的最开始保存着一个二进制整数,它是一个二进制文件,不是文本文件。由这段信息可知,0.13版本以前的Hadoop,使用“storage”作为数据块的保存目录,和现在的目录结构不兼容。这是一种值得学习的技巧,可以防止过旧的Hadoop版本在新的目录结构上成功启动,损坏系统。
${dfs.data.dir}目录下的另一个文件是”in_use.lock“,它表明目录已经被使用,实现了一种”锁“机制。如果停止数据节点,该文件会消失,通过文件”in_use.lock“,数据节点可以保证独自占用该目录,防止两个数据节点(当然,可以在一个节点上启动归属不同HDFS集群的多个数据节点)实例共享一个目录,造成混乱。
${dfs.data.dir}/current目录
current目录是${dfs.data.dir}下唯一带目录结构的子目录,其他三个(blocksBeingWritten、detach和tmp)都没有子目录。
”current“中既包含目录,也包含文件。其中,大部分文件都以blk_作为前缀,这些文件有两种类型:
1、 HDFS数据块,用来保存HDFS文件的内容,如”blk_1221212122“。
2、使用meta后缀标识的校验信息文件,用来保存数据块的校验信息。
和ChecksumFileSystem文件系统不同,数据节点中,数据块的校验信息文件不是隐藏文件,但它们的文件内容格式是一样的,都由包含校验配置信息的文件头与一系列校验信息组成。图中,文件”blk_1222“对应的校验信息文件时”blk_1222“。
当目录中存储的数据块增加到一定规模时,数据节点会创建一个新的目录,用于保存新的块及元数据。目录块中的块数据达到64时,(由配置项${dfs.datanode.numblocks}指定),便会创建子目录(如:subdir56),并形成一个更宽的目录树结构。同时,同一个父目录下最多会创建64个子目录,也就是说,默认配置下,一个目录下最多只有64个数据块(128个文件)和64个目录。通过这两个手段,数据节点既保证了目录的深度不会太深,影响了检索文件的性能,同时避免了某个目录保存了大量数据块,确保每个目录中的文件块数是可控的。
${dfs.data.dir}/current目录中还有三个特殊的文件,”VERSION“文件是一个Java属性文件,包含了运行的HDFS版本信息。另外两个”dncp_block_verification.log.curr“和”dncp_block_verification.log.prev“,则是数据块扫描器工作时需要的文件。
数据节点存储的实现
根据软件开发的一般原则,数据节点的业务逻辑不会在数据节点的文件结构上直接操作,当需要对磁盘上的数据进行操作时,业务逻辑只需要调用管理这些文件结构的对象提供的服务即可。数据节点的文件结构管理包括两部分内容,数据(节点)存储DataStorage和文件系统数据集FSDataset。
HDFS的服务器实体,名字节点、数据节点和第二名字节点都需要使用磁盘保存数据。所以,在org.apache.hadoop.hdfs.server.commom包中定义了一些基础类,他们抽象了磁盘上的数据组织。
数据节点存储DataStorage是抽象类Storage的子类,而抽象类Storage又继承自StorageInfo。在这个继承体系中,和DataStorage同级的FSImage类用于组织名字节点的磁盘数据。FSImage的子类CheckpointStorage和FSImage的继承关系,体现了名字节点和第二名字节点的密切联系。
这个继承结构中的另外两个类:NamespaceInfo和CheckpointSignature,出现在HDFS节点间的通信的远程接口中,它们分别应用于数据节点和名字节点的通信的DatanodeProtocol接口和第二名字节点和名字节点通信的NamenodeProtocol接口。NamespaceInfo包含HDFS集群存储的一些信息,CheckpointSignature对象,则用于标识名字节点元数据的检查点。另外,StorageInfo包含的三个字段的含义,分别是HDFS存储系统信息结构的版本号layoutVersion、存储系统标识namespaceID和存储系统创建时间cTime。在${dfs.data.dir}/current目录中,文件”VERSION“保存着这些信息。
“VERSION”是一个典型的Java属性文件,数据节点的“VERSION”文件除了上面提到的三个属性外,还有storageType和strorangeID属性。
storageType属性的意义是一目了然,它可以表明这是一个数据节点数据存储目录(如:storageType=DATA_NODE);storageID用于名字节点标识数据节点,一个集群的不同数据节点拥有不同的storageID。
storageID属性保存在DataStorage类中,如下所示:
/**
* Data storage information file.
* <p>
* @see Storage
*/
public class DataStorage extends Storage {
// Constants
final static String BLOCK_SUBDIR_PREFIX = "subdir";
final static String BLOCK_FILE_PREFIX = "blk_";
final static String COPY_FILE_PREFIX = "dncp_";
private String storageID;
......
}
StorageInfo是一个非常简单的类,包含三个属性和相应的get/set方法。它的抽象子类Storage也定义在org.apache.hadoop.hdfs.server,common包中,为数据节点、名字节点等提供通用的存储服务。Storage可以管理多个目录,存储目录StorageDirectory是Storage的内部类。提供了存储目录上的一些通用操作,它们的实现都很简单,值得分析的是StorageDirectory的成员变量和getVersionFile()、tryLock()等方法。
成员变量StorageDirectory.root保存着存储目录的根,dirType保存着该目录对应的类型。需要注意的是类型为java.nio.channels.FileLock的成员变量lock。FileLock,就是文件锁。Java的文件锁,要么独占,那么共享。在这里,StorageDirectory使用的是独占文件锁,对lock的加锁代码在tryLock()中。
数据节点的文件结构中,当数据节点运行时,${dfs.data.dir}下会有一个名为“in_use.lock”的文件,就是由tryLock()方法创建并上锁的。注意,“创建并上锁”两个操作缺一不可,如果只是创建文件但不加锁,不能防止用户对文件的误操作,如删除文件或移动文件造成“in_use.lock”文件丢失,导致StorageDirectory.tryLock()判断逻辑失效。通过java.nio.channels.FileChannel.tryLock()方法,StorageDirectory的tryLock()方法获得了文件的独占锁定,可以避免上述问题,并通过数据节点的实现逻辑,保证对StorageDirectory对象指向目录的独占使用。同时,“in_use.lock”文件会在数据节点退出时删除,对应的实现代码是lockF.deleteOnExit();
deleteOnExit()方法时由java.io.File类提供,当虚拟机退出时,调用了该方法的文件会被虚拟机自动删除。当然,如果tryLock()加锁失败,deleteOnExit()方法自然也不会其作用。相关代码如下:
/**
* One of the storage directories.
*/
public class StorageDirectory {
File root; // root directory
FileLock lock; // storage lock
StorageDirType dirType; // storage dir type
.......
/**
* Attempts to acquire an exclusive lock on the storage.
*
* @return A lock object representing the newly-acquired lock or
* <code>null</code> if storage is already locked.
* @throws IOException if locking fails.
*/
FileLock tryLock() throws IOException {
File lockF = new File(root, STORAGE_FILE_LOCK);
lockF.deleteOnExit();
RandomAccessFile file = new RandomAccessFile(lockF, "rws");
FileLock res = null;
try {
res = file.getChannel().tryLock();
} catch(OverlappingFileLockException oe) {
file.close();
return null;
} catch(IOException e) {
LOG.error("Cannot create lock on " + lockF, e);
file.close();
throw e;
}
return res;
}
......
}
StorageDirectory还提供了一系列的get/set方法,如获取存储目录中的“VERSION”文件的File对象,可以调用getVersionFile()方法。StorageDirectory.analyzeStorage()和StorageDirectory.doRecover()涉及系统升级。
Storage管理着一个或多个StorageDirectory对象,所有Storage类中的方法都很简单,或者是在StorageDirectory基础提供的整体操作,或者是对保存的StorageDirectory对象进行管理。Storage是个抽象类,有两个抽象方法,它们和系统升级相关。
和Storage一样,DataStorage的代码可以分为两部分,升级相关代码和升级无关代码。与升级无关的代码很少,需要关注的是DataStorage定义的三个常量,对照数据节点的文件结构。代码如下:
public class DataStorage extends Storage {
// Constants
final static String BLOCK_SUBDIR_PREFIX = "subdir";
final static String BLOCK_FILE_PREFIX = "blk_";
final static String COPY_FILE_PREFIX = "dncp_";
private String storageID;
DataStorage() {
super(NodeType.DATA_NODE);
storageID = "";
}
......
void format(StorageDirectory sd, NamespaceInfo nsInfo) throws IOException {
sd.clearDirectory(); // create directory
this.layoutVersion = FSConstants.LAYOUT_VERSION;
this.namespaceID = nsInfo.getNamespaceID();
this.cTime = 0;
// store storageID as it currently is
sd.write();
}
}
数据节点在第一次启动的时候,会调用DataStorage.format()创建存储目录结果。
format()方法很简单,通过StorageDirectory.clearDirectory()删除原有目录及数据并重新创建目录,然后“VERSION”文件中的属性赋值并将其持久化到磁盘。format()的第一个参数类型是StorageDirectory,如果数据节点管理这多个目录,这个方法会被调用多次,在不同的目录下创建节点文件结构;第二个参数nsInfo是从名字节点返回的NamespaceInfo对象,携带了存储系统标识namespaceID等信息,该标识最终存放在“VERSION”文件中。注意,由于“VERSION”文件保存在current目录中,保存文件的同时会创建对应的目录。
数据节点升级
上面介绍将DataStorage的功能分为升级相关和升级无关两个部分有点勉强,其实,DataStorage的主要功能是对存储空间的生存期进行管理,通过DataStorage.format()创建存储目录,或者利用DataStoarage.doUpgrade()进行升级,都是存储空间生存期管理的一部分。对于数据节点,存储空间生存期管理的关注点还是系统升级。
Hadoop实现了严格的版本兼容性要求,只有来自相同版本的组件才能保证相互的兼容性。大型分布式系统的升级需要一个周密的计划,必须考虑到诸如:持久化的信息是否改变?如果数据布局改变,如何自动地将原有数据和元数据迁移到新版本格式?升级过程中如果出现错误,如何保证数据不丢失?升级出现问题时,怎么才能够回滚升级前的状态?
如果文件系统的布局不需要改变,集群的升级变得非常简单,关闭旧进程,升级配置文件,然后启动新版本的守护进程,客户端使用新的库,就可以完成升级。同时,升级回滚也很简单,使用旧版本和旧配置文件,重启系统即可。如果持久化的信息在上述过程中需要改变到新的格式,特别是考虑到可能的回滚,升级的过程就变得复杂。
HDFS升级时,需要复制以前版本的元数据(对名字节点)和数据(对数据节点)。在数据节点上,升级并不需要两倍的集群存储空间,数据节点使用了Linux文件系统的硬链接,保留了对同一个数据块的两个引用,即当前版本和以前版本。通过这样的技术,就可以在需要的时候,轻松回滚到以前版本的文件系统。注意,在已升级系统上对数据的修改,升级回滚结束后将会消失。
1、升级
为了简化实现,HDFS最多保留前一版本的文件系统,没有多版本的升级、回滚机制。同时,引入升级提交机制(通过hadoop dfsadmin-finalizeUpgrade可提交一次升级),该机制用于删除以前的版本,所以在升级提交后,就不能回滚到以前的版本了。
数据节点升级的实现在DataStorage.doUpgrade()方法中。其中,升级过程涉及如下几个目录:
curDir:当前版本目录,通过StorageDirectory.getCurrentDir()获得,目录名为“current”。
prevDir:上以版本目录,目录名为”previous”,可通过StorageDirectory的getPreviousDir()方法得到;这里的“上一版本”指的是升级前的版本。
tmpDir:上一个版本临时目录,即目录${dfs.data.dir}/previous.tmp
doUpgrade()方法的主要流程是:首先确保上述涉及的工作目录处于正常状态。如检查目录“current”目录是否存在,如果“previous”目录存在,删除该目录。注意,该删除操作相当于提交了上一次升级,同时保证HDFS最多保留前一段版本的要求;保证“tmpDir”目录不存在。应该说,在(通过硬链接)移动数据前,“previous”目录和“previous.tmp”都是不存在的。
支持升级回滚,就必须保留升级前的数据,在数据节点,就是保存数据块以及数据块的校验信息文件。在doUpgrade()中,保留升级前数据是通过建立文件的硬链接实现的。
DataStorage.doUpgrade()保留升级前数据的动作一共有两步:首先将“current”目录名改为“previous.tmp”,然后调用linkBlocks(),在新创建的“current”目录下,建立到“pervious.tmp”目录中数据块和数据块校验信息文件的硬链接。DataStorage.lineBlocks()执行结束后,doUpgrade()方法还需要在“current”目录下创建这版本的“VERSION”文件,最后,将“previous.tmp”目录改为”previous”,完成升级。这个时候,数据节点的存储空间会有”previous”和“current”两个目录,而且,“previous”和“current”包含了同样的数据块和数据块校验信息文件,但它们有各自“VERSION”文件。另外,升级过程中需要的“previous.tmp”目录已经消失。代码如下:
/**
* Move current storage into a backup directory,
* and hardlink all its blocks into the new current directory.
*
* @param sd storage directory
* @throws IOException
*/
void doUpgrade(StorageDirectory sd,
NamespaceInfo nsInfo
) throws IOException {
LOG.info("Upgrading storage directory " + sd.getRoot()
+ ".\n old LV = " + this.getLayoutVersion()
+ "; old CTime = " + this.getCTime()
+ ".\n new LV = " + nsInfo.getLayoutVersion()
+ "; new CTime = " + nsInfo.getCTime());
// enable hardlink stats via hardLink object instance
HardLink hardLink = new HardLink();
File curDir = sd.getCurrentDir();
File prevDir = sd.getPreviousDir();
assert curDir.exists() : "Current directory must exist.";
// delete previous dir before upgrading
if (prevDir.exists())
deleteDir(prevDir);
File tmpDir = sd.getPreviousTmp();
assert !tmpDir.exists() : "previous.tmp directory must not exist.";
// rename current to tmp
rename(curDir, tmpDir);
// hardlink blocks
linkBlocks(tmpDir, curDir, this.getLayoutVersion(), hardLink);
// write version file
this.layoutVersion = FSConstants.LAYOUT_VERSION;
assert this.namespaceID == nsInfo.getNamespaceID() :
"Data-node and name-node layout versions must be the same.";
this.cTime = nsInfo.getCTime();
sd.write();
// rename tmp to previous
rename(tmpDir, prevDir);
LOG.info( hardLink.linkStats.report());
LOG.info("Upgrade of " + sd.getRoot()+ " is complete.");
}
2、升级回滚
升级回滚doRollBack()的实现有如下要点:
1)在完成对各个工作目录的状态的检查后,需要保证升级能够回滚到正确的版本上去。这个步骤是通过比较保存着在“previous”目录下“VERSION”文件中的HDFS存储系统信息结构的版本号layoutVersion和存储系统创建时间cTime和回滚后相应的layoutVersion和cTime来做判断。
2)由于“pervious”目录保存了升级前的所有数据,所以,doRollback()其实只需要简单的将“previous”目录改名称“current”,就可以完成回滚。但由于现在版本的工作目录“current”目录存在,所有采用了三步操作完成改名动作:先将“current”目录改为“removed.tmp”,然后将“previous”目录名修改为“current”,最后删除“removed.tmp”目录。代码如下:
void doRollback( StorageDirectory sd,
NamespaceInfo nsInfo
) throws IOException {
File prevDir = sd.getPreviousDir();
// regular startup if previous dir does not exist
if (!prevDir.exists())
return;
DataStorage prevInfo = new DataStorage();
StorageDirectory prevSD = prevInfo.new StorageDirectory(sd.getRoot());
prevSD.read(prevSD.getPreviousVersionFile());
// We allow rollback to a state, which is either consistent with
// the namespace state or can be further upgraded to it.
if (!(prevInfo.getLayoutVersion() >= FSConstants.LAYOUT_VERSION
&& prevInfo.getCTime() <= nsInfo.getCTime())) // cannot rollback
throw new InconsistentFSStateException(prevSD.getRoot(),
"Cannot rollback to a newer state.\nDatanode previous state: LV = "
+ prevInfo.getLayoutVersion() + " CTime = " + prevInfo.getCTime()
+ " is newer than the namespace state: LV = "
+ nsInfo.getLayoutVersion() + " CTime = " + nsInfo.getCTime());
LOG.info("Rolling back storage directory " + sd.getRoot()
+ ".\n target LV = " + nsInfo.getLayoutVersion()
+ "; target CTime = " + nsInfo.getCTime());
File tmpDir = sd.getRemovedTmp();
assert !tmpDir.exists() : "removed.tmp directory must not exist.";
// rename current to tmp
File curDir = sd.getCurrentDir();
assert curDir.exists() : "Current directory must exist.";
rename(curDir, tmpDir);
// rename previous to current
rename(prevDir, curDir);
// delete tmp dir
deleteDir(tmpDir);
LOG.info("Rollback of " + sd.getRoot() + " is complete.");
}
3、升级提交
升级提交doFinalize()简单更简单,它只需要将“previous”目录删除即可。但实际上,DataStorage.doFinalize()需要将“previous”目录名改为“finalized.tmp”,然后删除“finalized.tmp”来删除目录,而不是直接删除“previous”来提交升级。代码如下:
void doFinalize(StorageDirectory sd) throws IOException {
File prevDir = sd.getPreviousDir();
if (!prevDir.exists())
return; // already discarded
final String dataDirPath = sd.getRoot().getCanonicalPath();
LOG.info("Finalizing upgrade for storage directory "
+ dataDirPath
+ ".\n cur LV = " + this.getLayoutVersion()
+ "; cur CTime = " + this.getCTime());
assert sd.getCurrentDir().exists() : "Current directory must exist.";
final File tmpDir = sd.getFinalizedTmp();
// rename previous to tmp
rename(prevDir, tmpDir);
// delete tmp dir in a separate thread
new Daemon(new Runnable() {
public void run() {
try {
deleteDir(tmpDir);
} catch(IOException ex) {
LOG.error("Finalize upgrade for " + dataDirPath + " failed.", ex);
}
LOG.info("Finalize upgrade for " + dataDirPath + " is complete.");
}
public String toString() { return "Finalize " + dataDirPath; }
}).start();
}
也许读者会有疑问,为什么在doFinalize()中不能直接删除“previous”目录?同样,在doRollback()和doUpgrade()中,都存在“removed.tmp”和“previous.tmp”这样的临时目录。
在升级、升级提交或升级回滚都需要进行一定的操作。在执行这些任务的中间,如果设备出现故障(如:停电),那么,存储空间就可能处于某一个中间状态。引入上述这些临时目录,系统就能够判断目前doUpgrade()处于什么阶段,并采取相应的应对措施。
Storage和DataStorage一起提供了一个完美的HDFS数据节点升级机制,简化了大型分布式系统的运维。它们不但解决了升级过程中数据格式转换、升级回滚等常见问题、并保证了这个过程中不会丢失数据;同时,它们的设计,特别是Storage状态机,以及配合状态机工作的临时文件,提供了一个完备的升级方法,在升级过程或者回滚过程中的任意一个步骤出现错误,都可以通过状态机,恢复到正常状态。HDFS数据节点升级的Storage和DataStorage实现,非常具有借鉴意义。
文件系统数据集的工作机制
DataStorage专注于数据节点的存储空间的生存期管理,在存储空间上,与数据节点逻辑密切相关的存储服务,如创建数据块文件、维护数据块文件和数据块校验信息文件的关系等,则实现在文件系统数据集FSDataset中。 FSDataset继承了FSDatasetInterface接口。
FSDatasetInterface接口的方法可以分为以下三类:
数据块相关的方法:FSDataset管理了数据节点上的数据块,大量的FSDataset方法和数据块相关,如创建数据块、打开数据块上的输入、输出流、提交数据块等。
数据块校验信息文件相关:包括维护数据块和校验信息文件关系,获取校验信息文件输入流等。
其他:包括FSDataset健康检查、关闭FSDataset的shutdown()方法等。
FSDataset借鉴了Linux逻辑卷管理的一些思想。LVM使系统管理员可以更方便地分配存储空间,它的出现,改变了磁盘空间的管理理念。传统的文件系统是基于分区的,一个文件系统对应一个分期,这种方式直观,但不灵活,在实际应用中,会出现分去不平衡,存储空间和硬件能力得不到充分利用等问题。
FSDataset没有使用Linx LVM的方式管理它的存储空间,但是FSDataset借鉴了LVM的一些概念,可以管理多个数据目录。一般情况下,这些不同的数据目录配置在不同的物理设备上,以提高磁盘的数据吞吐量。
文件数据集将它管理的存储空间分为三个级别,分别是FSDir、FSVolume和FSVolumeSet进行抽象。
FSDir对象表示了“current”目录下的子目录,其成员变量children包括了目录下的所有子目录,形成了目录树。FSVolume是数据目录配置项${dfs.data.dir}中的一项,数据节点可以管理一个或者多个数据目录,系统中也就存在一个或者多个FSVolum对象,这些FSVolume对象由FSVolumSet管理。
由于上述内部类存在,FSDataset的成员变量不多,比较重要的有volumes,它管理数据节点的所有存储空间。成员ongoingCreates和VolomeMap是两个HaseMap,其中,ongoingCreates保存着当前正在进行写操作的数据块和对应文件的影射,volumeMap则保存着已经提交的数据块和对应的文件影射。代码如下:
/**************************************************
* FSDataset manages a set of data blocks. Each block
* has a unique name and an extent on disk.
*
***************************************************/
public class FSDataset implements FSConstants, FSDatasetInterface {
/** Find the metadata file for the specified block file.
/**
* Start writing to a block file
* If isRecovery is true and the block pre-exists, then we kill all
volumeMap.put(b, v);
volumeMap.put(b, v);
* other threads that might be writing to this block, and then reopen the file.
* If replicationRequest is true, then this operation is part of a block
* replication request.
*/
public BlockWriteStreams writeToBlock(Block b, boolean isRecovery,
boolean replicationRequest) throws IOException {
//
// Make sure the block isn't a valid one - we're still creating it!
//
if (isValidBlock(b)) {
if (!isRecovery) {
throw new BlockAlreadyExistsException("Block " + b + " is valid, and cannot be written to.");
}
// If the block was successfully finalized because all packets
// were successfully processed at the Datanode but the ack for
// some of the packets were not received by the client. The client
// re-opens the connection and retries sending those packets.
// The other reason is that an "append" is occurring to this block.
detachBlock(b, 1);
}
long blockSize = b.getNumBytes();
//
// Serialize access to /tmp, and check if file already there.
//
File f = null;
List<Thread> threads = null;
synchronized (this) {
//
// Is it already in the create process?
//
ActiveFile activeFile = ongoingCreates.get(b);
if (activeFile != null) {
f = activeFile.file;
threads = activeFile.threads;
if (!isRecovery) {
throw new BlockAlreadyExistsException("Block " + b +
" has already been started (though not completed), and thus cannot be created.");
} else {
for (Thread thread:threads) {
thread.interrupt();
}
}
ongoingCreates.remove(b);
}
FSVolume v = null;
if (!isRecovery) {
v = volumes.getNextVolume(blockSize);
// create temporary file to hold block in the designated volume
f = createTmpFile(v, b, replicationRequest);
} else if (f != null) {
DataNode.LOG.info("Reopen already-open Block for append " + b);
// create or reuse temporary file to hold block in the designated volume
v = volumeMap.get(b).getVolume();
volumeMap.put(b, new DatanodeBlockInfo(v, f));
} else {
// reopening block for appending to it.
DataNode.LOG.info("Reopen Block for append " + b);
v = volumeMap.get(b).getVolume();
f = createTmpFile(v, b, replicationRequest);
File blkfile = getBlockFile(b);
File oldmeta = getMetaFile(b);
File newmeta = getMetaFile(f, b);
// rename meta file to tmp directory
DataNode.LOG.debug("Renaming " + oldmeta + " to " + newmeta);
if (!oldmeta.renameTo(newmeta)) {
throw new IOException("Block " + b + " reopen failed. " +
" Unable to move meta file " + oldmeta +
" to tmp dir " + newmeta);
}
// rename block file to tmp directory
DataNode.LOG.debug("Renaming " + blkfile + " to " + f);
if (!blkfile.renameTo(f)) {
if (!f.delete()) {
throw new IOException("Block " + b + " reopen failed. " +
" Unable to remove file " + f);
}
if (!blkfile.renameTo(f)) {
throw new IOException("Block " + b + " reopen failed. " +
" Unable to move block file " + blkfile +
" to tmp dir " + f);
}
}
}
if (f == null) {
DataNode.LOG.warn("Block " + b + " reopen failed " +
" Unable to locate tmp file.");
throw new IOException("Block " + b + " reopen failed " +
" Unable to locate tmp file.");
}
// If this is a replication request, then this is not a permanent
// block yet, it could get removed if the datanode restarts. If this
// is a write or append request, then it is a valid block.
if (replicationRequest) {
volumeMap.put(b, new DatanodeBlockInfo(v));
} else {
volumeMap.put(b, new DatanodeBlockInfo(v, f));
}
ongoingCreates.put(b, new ActiveFile(f, threads));
}
try {
if (threads != null) {
for (Thread thread:threads) {
thread.join();
}
}
} catch (InterruptedException e) {
throw new IOException("Recovery waiting for thread interrupted.");
}
//
// Finally, allow a writer to the block file
// REMIND - mjc - make this a filter stream that enforces a max
// block size, so clients can't go crazy
//
File metafile = getMetaFile(f, b);
DataNode.LOG.debug("writeTo blockfile is " + f + " of size " + f.length());
DataNode.LOG.debug("writeTo metafile is " + metafile + " of size " + metafile.length());
return createBlockWriteStreams( f , metafile);
}
synchronized File createTmpFile( FSVolume vol, Block blk,
boolean replicationRequest) throws IOException {
if ( vol == null ) {
vol = volumeMap.get( blk ).getVolume();
if ( vol == null ) {
throw new IOException("Could not find volume for block " + blk);
}
}
return vol.createTmpFile(blk, replicationRequest);
}
private synchronized void finalizeBlockInternal(Block b, boolean reFinalizeOk)
throws IOException {
ActiveFile activeFile = ongoingCreates.get(b);
if (activeFile == null) {
if (reFinalizeOk) {
return;
} else {
throw new IOException("Block " + b + " is already finalized.");
}
}
File f = activeFile.file;
if (f == null || !f.exists()) {
throw new IOException("No temporary file " + f + " for block " + b);
}
FSVolume v = volumeMap.get(b).getVolume();
if (v == null) {
throw new IOException("No volume for temporary file " + f +
" for block " + b);
}
File dest = null;
dest = v.addBlock(b, f);
volumeMap.put(b, new DatanodeBlockInfo(v, dest));
ongoingCreates.remove(b);
}
}
打开数据块和提交数据块的方法如上代码所示。
版权申明:本文部分摘自【蔡斌、陈湘萍】所著【Hadoop技术内幕 深入解析Hadoop Common和HDFS架构设计与实现原理】一书,仅作为学习笔记,用于技术交流,其商业版权由原作者保留,推荐大家购买图书研究,转载请保留原作者,谢谢!