Hadoop源码分析(22)
1、 加载FSImage文件
在之前文档中分析了namenode的启动流程,其中namenode启动主要有两个任务:其一是加载元数据,其二是启动相关的服务。其中加载元数据在文档(10)中分析了其最终会调用FSImage类的loadFSImage方法来加载元数据。这个方法大致可以分为5个部分:是查找fsimage文件;初始化editlog;加载editlog流;加载fsimage文件;执行editlog。随后的文档依次分析了:查找FSImage文件、初始化editlog与加载editlog流。
接着继续分析加载FSImage文件,在loadFSImage方法中相关代码如下:
如上图所示其中重点在于调用的loadFSImageFile方法加载数据,而加载的FSImage,在之前文档解析了,其存储在配置文件设置的目录中,而FSImage文件的内容是二进制数据内容如下:
这种数据虽然利于程序读取,但不利于人读取,而hadoop提供了将FSImage文件转换成xml文件的命令,命令内容如下:
hdfs oiv -i fsimage_0000000000000076483 -p XML -o fsimage.xml
转换成xml文件后其内容如下:
这里以某一个段数据为例,解析FSImage文件存储的内容。xml数据如下:
<inode>
<id>16563</id>
<type>DIRECTORY</type>
<name>job_2bd7ecec0f9c1112be6d5accd220ea82</name>
<mtime>1611127906838</mtime>
<permission>root:supergroup:rwxr-xr-x</permission>
<nsquota>-1</nsquota>
<dsquota>-1</dsquota>
</inode>
<inode>
<id>16458</id>
<type>FILE</type>
<name>.tableinfo.0000000001</name>
<replication>2</replication>
<mtime>1585788692978</mtime>
<atime>1586310192376</atime>
<perferredBlockSize>134217728</perferredBlockSize>
<permission>hadoop:supergroup:rw-r--r--</permission>
<blocks>
<block>
<id>1073741834</id>
<genstamp>1010</genstamp>
<numBytes>312</numBytes>
</block>
</blocks>
</inode>
上述xml中的inode是fsimage转换成xml 中的基础节点。这里的inode实际是对应的namenode中的inode对象,而namenode维护的元数据在内存中其实就是这个inode结构。这里的inode分为两类,在上述xml中以type标签区分,DIRECTORY表示目录,FILE表示文件。对目录来说记录id、name、时间戳、权限等数据便可。对文件来说,除了目录有的几个数据外还有几个参数:perferredBlockSize,这个参数是记录文件的块大小;replication这个是记录块的副本数;还有一个blocks标签,其中有block标签,这个标签代表了文件的块信息。
简单分析了FSImage文件中的内容,之后再来细看加载该文件的方法,上文提到的loadFSImageFile方法内容如下:
void loadFSImageFile(FSNamesystem target, MetaRecoveryContext recovery,
FSImageFile imageFile, StartupOption startupOption) throws IOException {
LOG.info("Planning to load image: " + imageFile);
StorageDirectory sdForProperties = imageFile.sd;
storage.readProperties(sdForProperties, startupOption);
if (NameNodeLayoutVersion.supports(
LayoutVersion.Feature.TXID_BASED_LAYOUT, getLayoutVersion())) {
// For txid-based layout, we should have a .md5 file
// next to the image file
boolean isRollingRollback = RollingUpgradeStartupOption.ROLLBACK
.matches(startupOption);
loadFSImage(imageFile.getFile(), target, recovery, isRollingRollback);
} else if (NameNodeLayoutVersion.supports(
LayoutVersion.Feature.FSIMAGE_CHECKSUM, getLayoutVersion())) {
// In 0.22, we have the checksum stored in the VERSION file.
String md5 = storage.getDeprecatedProperty(
NNStorage.DEPRECATED_MESSAGE_DIGEST_PROPERTY);
if (md5 == null) {
throw new InconsistentFSStateException(sdForProperties.getRoot(),
"Message digest property " +
NNStorage.DEPRECATED_MESSAGE_DIGEST_PROPERTY +
" not set for storage directory " + sdForProperties.getRoot());
}
loadFSImage(imageFile.getFile(), new MD5Hash(md5), target, recovery,
false);
} else {
// We don't have any record of the md5sum
loadFSImage(imageFile.getFile(), null, target, recovery, false);
}
}
这段代码从第7行到第30行实际就是一个if else语句,这里根据NameNode的版本来确定使用不同来使用不同的参数来加载FSImage文件。但是其最终调用的都是loadFSImage方法,这个方法的内容如下:
private void loadFSImage(File curFile, MD5Hash expectedMd5,
FSNamesystem target, MetaRecoveryContext recovery,
boolean requireSameLayoutVersion) throws IOException {
// BlockPoolId is required when the FsImageLoader loads the rolling upgrade
// information. Make sure the ID is properly set.
target.setBlockPoolId(this.getBlockPoolID());
FSImageFormat.LoaderDelegator loader = FSImageFormat.newLoader(conf, target);
loader.load(curFile, requireSameLayoutVersion);
// Check that the image digest we loaded matches up with what
// we expected
MD5Hash readImageMd5 = loader.getLoadedImageMd5();
if (expectedMd5 != null &&
!expectedMd5.equals(readImageMd5)) {
throw new IOException("Image file " + curFile +
" is corrupt with MD5 checksum of " + readImageMd5 +
" but expecting " + expectedMd5);
}
long txId = loader.getLoadedImageTxId();
LOG.info("Loaded image for txid " + txId + " from " + curFile);
lastAppliedTxId = txId;
storage.setMostRecentCheckpointInfo(txId, curFile.lastModified());
}
这段代码的重点在第8行和第9行,这里会创建一个loader,然后调用其load方法来加载数据。其中load方法内容如下:
public void load(File file, boolean requireSameLayoutVersion)
throws IOException {
Preconditions.checkState(impl == null, "Image already loaded!");
FileInputStream is = null;
try {
is = new FileInputStream(file);
byte[] magic = new byte[FSImageUtil.MAGIC_HEADER.length];
IOUtils.readFully(is, magic, 0, magic.length);
if (Arrays.equals(magic, FSImageUtil.MAGIC_HEADER)) {
FSImageFormatProtobuf.Loader loader = new FSImageFormatProtobuf.Loader(
conf, fsn, requireSameLayoutVersion);
impl = loader;
loader.load(file);
} else {
Loader loader = new Loader(conf, fsn);
impl = loader;
loader.load(file);
}
} finally {
IOUtils.cleanup(LOG, is);
}
}
}
首先是第7行,这里利用传入的FSImage文件创建一个输入流,从文件中读取文件数据。然后是第8行,创建了一个固定长度的byte数组,然后是第9行从文件中读取固定长度的数据到第8行的数组中,以上文的解析的FSImage文件为例,这里读取的内容是HDFSIMG1,然后是第10行比较读取的magic(第8行的数组)是否为定义的MAGIC_HEADER。这个值的定义如下:
public static final byte[] MAGIC_HEADER =
"HDFSIMG1".getBytes(Charsets.UTF_8);
这里的两个值实际是相同的,所以它会执行if语句内的内容,再创建一个loader,再用这个loader加载数据。这个loader的load方法内容如下:
void load(File file) throws IOException {
long start = Time.monotonicNow();
imgDigest = MD5FileUtils.computeMd5ForFile(file);
RandomAccessFile raFile = new RandomAccessFile(file, "r");
FileInputStream fin = new FileInputStream(file);
try {
loadInternal(raFile, fin);
long end = Time.monotonicNow();
LOG.info("Loaded FSImage in {} seconds.", (end - start) / 1000);
} finally {
fin.close();
raFile.close();
}
}
这段代码的重点在第7行的loadInternal方法,这个方法内容如下:
private void loadInternal(RandomAccessFile raFile, FileInputStream fin)
throws IOException {
if (!FSImageUtil.checkFileFormat(raFile)) {
throw new IOException("Unrecognized file format");
}
FileSummary summary = FSImageUtil.loadSummary(raFile);
if (requireSameLayoutVersion && summary.getLayoutVersion() !=
HdfsConstants.NAMENODE_LAYOUT_VERSION) {
throw new IOException("Image version " + summary.getLayoutVersion() +
" is not equal to the software version " +
HdfsConstants.NAMENODE_LAYOUT_VERSION);
}
FileChannel channel = fin.getChannel();
FSImageFormatPBINode.Loader inodeLoader = new FSImageFormatPBINode.Loader(
fsn, this);
FSImageFormatPBSnapshot.Loader snapshotLoader = new FSImageFormatPBSnapshot.Loader(
fsn, this);
ArrayList<FileSummary.Section> sections = Lists.newArrayList(summary
.getSectionsList());
Collections.sort(sections, new Comparator<FileSummary.Section>() {
@Override
public int compare(FileSummary.Section s1, FileSummary.Section s2) {
SectionName n1 = SectionName.fromString(s1.getName());
SectionName n2 = SectionName.fromString(s2.getName());
if (n1 == null) {
return n2 == null ? 0 : -1;
} else if (n2 == null) {
return -1;
} else {
return n1.ordinal() - n2.ordinal();
}
}
});
StartupProgress prog = NameNode.getStartupProgress();
/**
* beginStep() and the endStep() calls do not match the boundary of the
* sections. This is because that the current implementation only allows
* a particular step to be started for once.
*/
Step currentStep = null;
for (FileSummary.Section s : sections) {
channel.position(s.getOffset());
InputStream in = new BufferedInputStream(new LimitInputStream(fin,
s.getLength()));
in = FSImageUtil.wrapInputStreamForCompression(conf,
summary.getCodec(), in);
String n = s.getName();
switch (SectionName.fromString(n)) {
case NS_INFO:
loadNameSystemSection(in);
break;
case STRING_TABLE:
loadStringTableSection(in);
break;
case INODE: {
currentStep = new Step(StepType.INODES);
prog.beginStep(Phase.LOADING_FSIMAGE, currentStep);
inodeLoader.loadINodeSection(in);
}
break;
case INODE_REFERENCE:
snapshotLoader.loadINodeReferenceSection(in);
break;
case INODE_DIR:
inodeLoader.loadINodeDirectorySection(in);
break;
case FILES_UNDERCONSTRUCTION:
inodeLoader.loadFilesUnderConstructionSection(in);
break;
case SNAPSHOT:
snapshotLoader.loadSnapshotSection(in);
break;
case SNAPSHOT_DIFF:
snapshotLoader.loadSnapshotDiffSection(in);
break;
case SECRET_MANAGER: {
prog.endStep(Phase.LOADING_FSIMAGE, currentStep);
Step step = new Step(StepType.DELEGATION_TOKENS);
prog.beginStep(Phase.LOADING_FSIMAGE, step);
loadSecretManagerSection(in);
prog.endStep(Phase.LOADING_FSIMAGE, step);
}
break;
case CACHE_MANAGER: {
Step step = new Step(StepType.CACHE_POOLS);
prog.beginStep(Phase.LOADING_FSIMAGE, step);
loadCacheManagerSection(in);
prog.endStep(Phase.LOADING_FSIMAGE, step);
}
break;
default:
LOG.warn("Unrecognized section {}", n);
break;
}
}
}
首先是第6行,从文件中读取summary的内容,然后是第21行从summary中读取sections信息,然后是第46行到末尾,遍历sections,根据section的类型来执行不同的方法。这里以INODE和INODE_DIR为例来解析,其如何加载数据。其中处理INODE的代码在第63行到第66行,其中重点是第66行loadINodeSection方法,该方法内容如下:
void loadINodeSection(InputStream in) throws IOException {
INodeSection s = INodeSection.parseDelimitedFrom(in);
fsn.dir.resetLastInodeId(s.getLastInodeId());
LOG.info("Loading " + s.getNumInodes() + " INodes.");
for (int i = 0; i < s.getNumInodes(); ++i) {
INodeSection.INode p = INodeSection.INode.parseDelimitedFrom(in);
if (p.getId() == INodeId.ROOT_INODE_ID) {
loadRootINode(p);
} else {
INode n = loadINode(p);
dir.addToInodeMap(n);
}
}
}
首先是第2行获取一个INodeSection,然后是第5行利用for循环遍历INodeSection中的节点。然后根据该节点是否是root节点来执行不同的方法,若是则执行loadRootINode节点,若不是则执行loadINode节点并将其添加到map中。
对于非root节点执行的loadINode节点内容如下:
private INode loadINode(INodeSection.INode n) {
switch (n.getType()) {
case FILE:
return loadINodeFile(n);
case DIRECTORY:
return loadINodeDirectory(n, parent.getLoaderContext());
case SYMLINK:
return loadINodeSymlink(n);
default:
break;
}
return null;
}
这里就是一个switch语句,根据节点的内容的不同执行不同的方法。其中上文提到的inode节点主要有两类:File和Directory。其中File类型执行的是loadINodeFile方法,Directory执行的是loadINodeDirectory方法。其中loadINodeFile方法内容如下:
private INodeFile loadINodeFile(INodeSection.INode n) {
assert n.getType() == INodeSection.INode.Type.FILE;
INodeSection.INodeFile f = n.getFile();
List<BlockProto> bp = f.getBlocksList();
short replication = (short) f.getReplication();
LoaderContext state = parent.getLoaderContext();
BlockInfoContiguous[] blocks = new BlockInfoContiguous[bp.size()];
for (int i = 0, e = bp.size(); i < e; ++i) {
blocks[i] = new BlockInfoContiguous(PBHelper.convert(bp.get(i)), replication);
}
final PermissionStatus permissions = loadPermission(f.getPermission(),
parent.getLoaderContext().getStringTable());
final INodeFile file = new INodeFile(n.getId(),
n.getName().toByteArray(), permissions, f.getModificationTime(),
f.getAccessTime(), blocks, replication, f.getPreferredBlockSize(),
(byte)f.getStoragePolicyID());
if (f.hasAcl()) {
int[] entries = AclEntryStatusFormat.toInt(loadAclEntries(
f.getAcl(), state.getStringTable()));
file.addAclFeature(new AclFeature(entries));
}
if (f.hasXAttrs()) {
file.addXAttrFeature(new XAttrFeature(
loadXAttrs(f.getXAttrs(), state.getStringTable())));
}
// under-construction information
if (f.hasFileUC()) {
ucFiles.add(file);
INodeSection.FileUnderConstructionFeature uc = f.getFileUC();
file.toUnderConstruction(uc.getClientName(), uc.getClientMachine());
if (blocks.length > 0) {
BlockInfoContiguous lastBlk = file.getLastBlock();
// replace the last block of file
file.setBlock(file.numBlocks() - 1, new BlockInfoContiguousUnderConstruction(
lastBlk, replication));
}
}
return file;
}
首先从第2行到第13行,这里主要是从文件中读取具体的文件信息到内存中,主要信息和上文解析的xml中的内容相同。然后是第15行利用之前读取的数据创建一个INodeFile类。然后是第20行到文件末尾,这里主要是三个if语句,这几个if语句主要是按需求第建立的节点设置一些参数。
然后再回到loadINode方法中,这个方法加载Directory使用的loadINodeDirectory方法内容如下:
public static INodeDirectory loadINodeDirectory(INodeSection.INode n,
LoaderContext state) {
assert n.getType() == INodeSection.INode.Type.DIRECTORY;
INodeSection.INodeDirectory d = n.getDirectory();
final PermissionStatus permissions = loadPermission(d.getPermission(),
state.getStringTable());
final INodeDirectory dir = new INodeDirectory(n.getId(), n.getName()
.toByteArray(), permissions, d.getModificationTime());
final long nsQuota = d.getNsQuota(), dsQuota = d.getDsQuota();
if (nsQuota >= 0 || dsQuota >= 0) {
dir.addDirectoryWithQuotaFeature(new DirectoryWithQuotaFeature.Builder().
nameSpaceQuota(nsQuota).storageSpaceQuota(dsQuota).build());
}
EnumCounters<StorageType> typeQuotas = null;
if (d.hasTypeQuotas()) {
ImmutableList<QuotaByStorageTypeEntry> qes =
loadQuotaByStorageTypeEntries(d.getTypeQuotas());
typeQuotas = new EnumCounters<StorageType>(StorageType.class,
HdfsConstants.QUOTA_RESET);
for (QuotaByStorageTypeEntry qe : qes) {
if (qe.getQuota() >= 0 && qe.getStorageType() != null &&
qe.getStorageType().supportTypeQuota()) {
typeQuotas.set(qe.getStorageType(), qe.getQuota());
}
}
if (typeQuotas.anyGreaterOrEqual(0)) {
DirectoryWithQuotaFeature q = dir.getDirectoryWithQuotaFeature();
if (q == null) {
dir.addDirectoryWithQuotaFeature(new DirectoryWithQuotaFeature.
Builder().typeQuotas(typeQuotas).build());
} else {
q.setQuota(typeQuotas);
}
}
}
if (d.hasAcl()) {
int[] entries = AclEntryStatusFormat.toInt(loadAclEntries(
d.getAcl(), state.getStringTable()));
dir.addAclFeature(new AclFeature(entries));
}
if (d.hasXAttrs()) {
dir.addXAttrFeature(new XAttrFeature(
loadXAttrs(d.getXAttrs(), state.getStringTable())));
}
return dir;
}
这个方法与上文加载文件的方法几乎完全一样,差别主要在第8行创建的是INodeDirectory类。
loadINode方法的主要内容便如同上文解析的,主要是根据文件中的内容创建对应的INodeFile或INodeDirector对象。在调用loadINode的loadINodeSection方法中,在创建好了INode对象后,会调用addToInodeMap方法将这个对象添加到inodemap中,这个方法的内容如下:
public final void addToInodeMap(INode inode) {
if (inode instanceof INodeWithAdditionalFields) {
inodeMap.put(inode);
if (!inode.isSymlink()) {
final XAttrFeature xaf = inode.getXAttrFeature();
addEncryptionZone((INodeWithAdditionalFields) inode, xaf);
}
}
}
这里重点在第3行,这里会将创建好的inode对象存储到inodeMap中。
至此其对inode的处理操作便完成了。然后回到上文的loadINodeFile方法中,这个方法的switch语句中,除了上述解析的inode,还有许多其他格式的数据,这里再解析一个INODE_DIR格式的处理方式,其处理方法在loadINodeFile方法的第73行,调用的是inodeLoader的loadINodeDirectorySection方法,该方法内容如下:
void loadINodeDirectorySection(InputStream in) throws IOException {
final List<INodeReference> refList = parent.getLoaderContext()
.getRefList();
while (true) {
INodeDirectorySection.DirEntry e = INodeDirectorySection.DirEntry
.parseDelimitedFrom(in);
// note that in is a LimitedInputStream
if (e == null) {
break;
}
INodeDirectory p = dir.getInode(e.getParent()).asDirectory();
for (long id : e.getChildrenList()) {
INode child = dir.getInode(id);
addToParent(p, child);
}
for (int refId : e.getRefChildrenList()) {
INodeReference ref = refList.get(refId);
addToParent(p, ref);
}
}
}
这里整体是使用了一个while循环来遍历数据,首先是第5行和第6行,这里从文件中读取目录的结构信息,然后是第11行拿到目录结构中的父节点,这里调用的dir的getInode方法,这个方法会从上文提到的inodemap读取inode节点。然后是第12行到第14行,使用for循环拿到父节点的所有子节点信息,然后同样利用getInode方法获取子节点,最后调用addToParent将子节点添加到父节点下。由此便能够创建一个文件目录树。
首先获取节点的getInode方法如下:
public INode getInode(long id) {
readLock();
try {
return inodeMap.get(id);
} finally {
readUnlock();
}
}
这个方法就是直接从inodeMap中获取数据。
然后是addToParent方法,这个方法内容如下:
private void addToParent(INodeDirectory parent, INode child) {
if (parent == dir.rootDir && FSDirectory.isReservedName(child)) {
throw new HadoopIllegalArgumentException("File name \""
+ child.getLocalName() + "\" is reserved. Please "
+ " change the name of the existing file or directory to another "
+ "name before upgrading to this release.");
}
// NOTE: This does not update space counts for parents
if (!parent.addChild(child)) {
return;
}
dir.cacheName(child);
if (child.isFile()) {
updateBlocksMap(child.asFile(), fsn.getBlockManager());
}
}
这里首先是的第9行调用用父节点的addChild方法将其添加到父节点下面,然后是第14行,如果子节点是文件则更新该节点在BlocksMap中的内容。
+ child.getLocalName() + "\" is reserved. Please "
+ " change the name of the existing file or directory to another "
+ "name before upgrading to this release.");
}
// NOTE: This does not update space counts for parents
if (!parent.addChild(child)) {
return;
}
dir.cacheName(child);
if (child.isFile()) {
updateBlocksMap(child.asFile(), fsn.getBlockManager());
}
}
<p> 这里首先是的第9行调用用父节点的addChild方法将其添加到父节点下面,然后是第14行,如果子节点是文件则更新该节点在BlocksMap中的内容。
</p>