前言
HDFS作为一套成熟的分布式文件系统,其上可以存储上千万个文件,这些大大小小的文件存储着海量的数据.随着数据的变多,其中的安全性就显得非常的重要.但是在其中,有一类数据同样非常重要,那就是元数据,也就是所谓的描述数据的数据.为什么说他重要呢,举个例子,元数据就像一本书的目录信息,书中的具体内容就是真实的数据,如果有一天这本书的目录信息没了,那么你怎么迅速查阅里面的信息呢?可能你会想这本书的目录上什么信息也没有,肯定没有我想要的信息.元数据绝对不能被忽视,在HDFS中有专门的文件用来存储这样的信息,叫做fsImage,还配有专门的机制来定期生成和更新此文件(就是HDFS中的QJM机制,感兴趣的同学可自行查阅相关资料).但是本文的重点不在于分析此机制,因为这样的资料太多了,随处可以找到.最近社区做了一个关于fsImage的new feature HDFS-9825,挺有意思的,本文结合这个issue和目前现有的fsImage的解析来聊一聊HDFS镜像文件的解析与反解析.
HDFS的FsImage镜像文件
在详细了解镜像文件的解析与反解析之前需要对FsImage有一个全面的了解,否则你会不知道文章在讲什么.
FsImage的存储位置
没有专门运维过Hadoop集群的同学可能只是或多或少听到过这个名词,但是真正见到过,打开看过此文件的人应该不多.即便你打开了,你看到的应该是这样的乱码:
HDFSIMG1^V^H¸<8c>ßË^D^Pè^G^Xô^G ^@(<8c><80><80><80>^D0P^F^H<95><80>^A^P^U1^H^B^P<81>
<80>^A^Z^@*'^H<94>¾ <99>¹*^Pÿÿÿÿÿÿÿÿ^?^Xÿÿÿÿÿÿÿÿÿ^A!í^A^A^@^@^@^@^@:^@6^H^B^P<82><80>^A^Z^Ddir0*(^Hϼ
<99>¹*^Pÿÿÿÿÿÿÿÿÿ^A^Xÿÿÿÿÿÿÿÿÿ^A!í^A^A^@^@^@^@^@:^@<^H^A^P<83><80>^A^Z^Efile0"-^H^A^P§¼
可能会有人疑问为什么是这样的呢,这里本人认为有2点原因:
- 因为考虑到元数据信息可能随着数据的变多而不断变大,为了缩小文件的空间大小,需要进行压缩编码处理.
- 进行编码处理,避免直接明文保存的不安全性.
第二点原因是我的个人看法,主要还是第一点.当然想要看里面的信息前提是找到此文件的位置信息,由此配置项所控制:
<property>
<name>dfs.namenode.name.dir</name>
<value></value>
</property>
FsImage的存储信息
下面列举几个常见的可能存储的信息:
- 文件目录信息
- 位置信息
- 副本数
- 权限信息
可能以上几点就是传统意义上的元信息组成结构了,但是很显然,HDFS有他自己的独特性,fsImage保存的类型信息远远多于此,下文还将会提到.
FsImage的解析
fsImage镜像文件的解析指的是将HDFS镜像文件解析成使用者能直接阅读的形式,而不是像上文一样的乱码.这里就不得不提到一个重要的解析类PBImageXmlWriter.当然解析成XML文件格式只是其中一种常见的解析方式,同样可以直接解析展示到终端上或者以行的形式直接保存在普通文件中.下面进入解析操作的入口:
Configuration conf = new Configuration();
try (PrintStream out = outputFile.equals("-") ?
System.out : new PrintStream(outputFile, "UTF-8")) {
switch (processor) {
case "FileDistribution":
long maxSize = Long.parseLong(cmd.getOptionValue("maxSize", "0"));
int step = Integer.parseInt(cmd.getOptionValue("step", "0"));
new FileDistributionCalculator(conf, maxSize, step, out).visit(
new RandomAccessFile(inputFile, "r"));
break;
case "XML":
new PBImageXmlWriter(conf, out).visit(
new RandomAccessFile(inputFile, "r"));
break;
...
进入其中的visit方法,首先会进行FileSummary的解析
FileSummary summary = FSImageUtil.loadSummary(file);
解析summary的目的是要获取其中section.
ArrayList<FileSummary.Section> sections = Lists.newArrayList(summary
.getSectionsList());
Section的概念非常的重要,每个section会对应一类的数据.Xml格式文件也会依赖section进行每段XML数据的输出.从FsImage中解析出的section有下面几类(代码部分省略)
switch (SectionName.fromString(s.getName())) {
case NS_INFO:
break;
case STRING_TABLE:
break;
case INODE:
break;
case INODE_REFERENCE:
break;
case INODE_DIR:
break;
case FILES_UNDERCONSTRUCTION:
break;
case SNAPSHOT:
break;
case SNAPSHOT_DIFF:
break;
case SECRET_MANAGER:
break;
case CACHE_MANAGER:
break;
default:
break;
}
以上10个分支可大致分为以下7大类:
- 命名空间类Section, 包括namespaceId, rollingUpgradeStartTime等类型的变量.
- INode相关Section,包含了文件,目录相关inode的信息
- fileUnderConstructionSection正在构建中的文件信息.
- SnapShot快照相关信息.
- SecretManager安全管理相关信息
- CacheManager缓存管理相关信息
- StringTable权限相关的信息(辅助其他section输出xml信息)
具体对应section里面保存有哪些信息可以在文章末尾的fsImage解析的XML文件样本中进行查看.但其实section本质上并不保存信息,只是提供了偏移量和长度,最终获取内部的数据还是从fsImage的输入流中而来,从下面的代码中可以得出这样的结论.
for (FileSummary.Section s : sections) {
fin.getChannel().position(s.getOffset());
InputStream is = FSImageUtil.wrapInputStreamForCompression(conf,
summary.getCodec(), new BufferedInputStream(new LimitInputStream(
fin, s.getLength())));
switch (SectionName.fromString(s.getName())) {
case NS_INFO:
dumpNameSection(is);
...
我们以一个简单的解析来看一下XML是怎么被输出的,以NameSection为例:
private void dumpNameSection(InputStream in) throws IOException {
NameSystemSection s = NameSystemSection.parseDelimitedFrom(in);
out.print("<" + NAME_SECTION_NAME + ">");
o(NAME_SECTION_NAMESPACE_ID, s.getNamespaceId());
o(NAME_SECTION_GENSTAMPV1, s.getGenstampV1())
.o(NAME_SECTION_GENSTAMPV2, s.getGenstampV2())
.o(NAME_SECTION_GENSTAMPV1_LIMIT, s.getGenstampV1Limit())
.o(NAME_SECTION_LAST_ALLOCATED_BLOCK_ID,
s.getLastAllocatedBlockId())
.o(NAME_SECTION_TXID, s.getTransactionId());
out.print("</" + NAME_SECTION_NAME + ">\n");
}
原理很简单,就是逐行的out的print输出.OK,到此fsImage文件的解析过程就是如此.
FsImage的反解析
前文中已经提过,fsImage的反解析是最近社区完成的一个new feature.就是针对xml文件的反解析,重新生成fsImage文件.与以往只能保存纯粹的无法直接查阅的fsImage文件相比,保存解析好后的xml文件显然非常放便于用户的使用.反解析核心类OfflineImageReconstructor,这个类可能要获取hadoop-trunk分支代码才能看到,目前发布版本的源码中并不存在.同样进入入口处理方法:
try (PrintStream out = outputFile.equals("-") ?
System.out : new PrintStream(outputFile, "UTF-8")) {
switch (processor) {
...
case "ReverseXML":
try {
OfflineImageReconstructor.run(inputFile, outputFile);
} catch (Exception e) {
System.err.println("OfflineImageReconstructor failed: " +
e.getMessage());
e.printStackTrace(System.err);
System.exit(1);
}
break;
接着进入其run方法:
public static void run(String inputPath, String outputPath)
throws Exception {
MessageDigest digester = MD5Hash.getDigester();
FileOutputStream fout = null;
File foutHash = new File(outputPath + ".md5");
Files.deleteIfExists(foutHash.toPath()); // delete any .md5 file that exists
CountingOutputStream out = null;
FileInputStream fis = null;
InputStreamReader reader = null;
try {
Files.deleteIfExists(Paths.get(outputPath));
fout = new FileOutputStream(outputPath);
fis = new FileInputStream(inputPath);
reader = new InputStreamReader(fis, Charset.forName("UTF-8"));
out = new CountingOutputStream(
new DigestOutputStream(
new BufferedOutputStream(fout), digester));
OfflineImageReconstructor oir =
new OfflineImageReconstructor(out, reader);
oir.processXml();
} finally {
IOUtils.cleanup(LOG, reader, fis, out, fout);
}
// Write the md5 file
MD5FileUtils.saveMD5File(new File(outputPath),
new MD5Hash(digester.digest()));
}
主要做了这么几件事情:
- 先判断之前是否已经存在目标文件,如果存在则进行删除
- 在内部进行了reconstructor对象的构建
- 然后进行xml文件的处理.
然后进入OfflineImageReconstructor的构造函数,里面会做哪些初始化操作呢
private OfflineImageReconstructor(CountingOutputStream out,
InputStreamReader reader) throws XMLStreamException {
this.out = out;
XMLInputFactory factory = XMLInputFactory.newInstance();
this.events = factory.createXMLEventReader(reader);
this.sections = new HashMap<>();
this.sections.put(NameSectionProcessor.NAME, new NameSectionProcessor());
this.sections.put(INodeSectionProcessor.NAME, new INodeSectionProcessor());
this.sections.put(SecretManagerSectionProcessor.NAME,
new SecretManagerSectionProcessor());
this.sections.put(CacheManagerSectionProcessor.NAME,
new CacheManagerSectionProcessor());
this.sections.put(SnapshotDiffSectionProce