1. 背景
在Hadoop2.0之前,一个Hadoop集群只支持一对主备NameNode。如下所示,集群中的数据接近2.2亿block,会导致NameNode内存中的文件系统树过大,占用较多内存;同时NameNode crash后启动时,由于需要加载过多的block,导致启动时间过长。
集群 | 每日写入block | 每日净增block | 每日数据净增长 | 当前block数 | 空间使用率 |
---|---|---|---|---|---|
集群A | 500-600W | 60W-70W | 100TB+ | 2.2亿 | 71.93% |
最简单的解决方法就是通过distcp工具将该集群中的项目迁移到另一个空闲的集群,但是ditscp工具有自身的缺陷:迁移过程耗时较长,且迁移过程中,禁止用户写入数据,对用户不友好。
为了解决HDFS水平扩展问题,https://issues.apache.org/jira/browse/HDFS-1052提供了HDFS Federation联邦机制。一个HDFS集群中,可以部署多对Active/Standby NameNode,每对Active/Standby NameNode可以称为子集群,它们各自存储自己的元数据,所有子集群都共用所有datanode。
这样,当一个子集群的block数接近3亿时,可以水平扩展一个新的子集群,使用该block压力较小的子集群存储HDFS文件。
2. HDFS Federation介绍
每个联邦集群中有多个子集群,每个子集群有对应一个namespace。下图中,有n个子集群,它们的namespace分别为NS1、NSk、NSn。每个子集群的block都会存储到集群中所有的datanode中,因此不同的子集群,会设置一个Block Pool ID,datanode根据Block Pool ID存储对应的block,上报块时,也会根据Block Pool ID找到对应的子集群,向指定NameNode进行汇报。
例如,有一个HDFS联邦集群,它包含四个子集群,则集群中会有4个Block Pool,在datanode中,会保存4个Block Pool目录,分别保存4个子集群的block数据:
因此,在表示block块时,要通过<BlockPoolID><BlockID>指定是哪一个BlockPool中的块。
在源码中也可以看到Federation相关逻辑。如下,Federation中定义BPOfferService类,它负责向单个子集群汇报块信息。datanode接收客户端请求刷新NameNodes时,如果有新增的NameNode节点,就会新建一个该子集群对应的BPOfferService,用户向该子集群汇报块信息:
private void doRefreshNamenodes(
Map<String, Map<String, InetSocketAddress>> addrMap,
Map<String, Map<String, InetSocketAddress>> lifelineAddrMap)
throws IOException {
Set<String> toRefresh = Sets.newLinkedHashSet();
Set<String> toAdd = Sets.newLinkedHashSet();
Set<String> toRemove;
//省略
if (!toAdd.isEmpty()) {
LOG.info("Starting BPOfferServices for nameservices: " +
Joiner.on(",").useForNull("<default>").join(toAdd));
for (String nsToAdd : toAdd) {
//省略
BPOfferService bpos = createBPOS(nsToAdd, addrs, lifelineAddrs);
bpByNameserviceId.put(nsToAdd, bpos);
offerServices.add(bpos);
}
}
startAll();
}
3. ViewFS集成
虽然HDFS Federation支持子集群扩展,但是在用户视角,每个子集群都要都要指定其集群名,增加用户的记忆成本。为了降低使用成本,Hadoop集成ViewFS映射文件系统,在客户端中,可以设置一个路由表mountTable.xml文件,设置ViewFS中的路径与子文件系统中的路径对应关系。例如:
<configuration>
<property>
<name>fs.viewfs.mounttable.test1.homedir</name>
<value>/test1</value>
</property>
<property>
<name>fs.viewfs.mounttable.test1.link./user</name>
<value>hdfs://test1/user</value>
</property>
<property>
<name>fs.viewfs.mounttable.test1.link./data</name>
<value>hdfs://test1/data</value>
</property>
<property>
<name>fs.viewfs.mounttable.test1.link./src</name>
<value>hdfs://test1/src</value>
</property>
</configuration>
再在core-site.xml中设置viewfs默认URL:
<property>
<name>fs.defaultFS</name>
<value>viewfs://test1</value>
</property>
<property>
<name>fs.AbstractFileSystem.viewfs.impl</name>
<value>org.apache.hadoop.fs.viewfs.ViewFs</value>
</property>
这样,访问viewfs://test1/user时,底层就会访问hdfs://test1/user。其他挂载路径同理。当然,viewfs底层还可以挂载其他文件系统,这里不展开。
观察ViewFS源码,可以看到在InodeTree()方法中初始化文件系统:
InodeTree<AbstractFileSystem> fsState;
ViewFs(final URI theUri, final Configuration conf) throws IOException,
URISyntaxException {
super(theUri, FsConstants.VIEWFS_SCHEME, false, -1);
creationTime = Time.now();
ugi = UserGroupInformation.getCurrentUser();
config = conf;
// Now build client side view (i.e. client side mount table) from config.
String authority = theUri.getAuthority();
//调用InodeTree的构造函数创建文件系统
fsState = new InodeTree<AbstractFileSystem>(conf, authority) {
@Override
protected AbstractFileSystem getTargetFileSystem(final URI uri)
throws URISyntaxException, UnsupportedFileSystemException {
String pathString = uri.getPath();
if (pathString.isEmpty()) {
pathString = "/";
}
return new ChRootedFs(
AbstractFileSystem.createFileSystem(uri, config),
new Path(pathString));
}
//省略
}
在InodeTree中,读取mountTable.xml中的挂载点信息,创建对应的文件系统客户端对象:
protected InodeTree(final Configuration config, final String viewName)
throws UnsupportedFileSystemException, URISyntaxException,
FileAlreadyExistsException, IOException {
String mountTableName = viewName;
if (mountTableName == null) {
mountTableName = Constants.CONFIG_VIEWFS_DEFAULT_MOUNT_TABLE;
}
homedirPrefix = ConfigUtil.getHomeDirValue(config, mountTableName);
boolean isMergeSlashConfigured = false;
String mergeSlashTarget = null;
List<LinkEntry> linkEntries = new LinkedList<>();
final String mountTablePrefix =
Constants.CONFIG_VIEWFS_PREFIX + "." + mountTableName + ".";
final String linkPrefix = Constants.CONFIG_VIEWFS_LINK + ".";
final String linkFallbackPrefix = Constants.CONFIG_VIEWFS_LINK_FALLBACK;
final String linkMergePrefix = Constants.CONFIG_VIEWFS_LINK_MERGE + ".";
final String linkMergeSlashPrefix =
Constants.CONFIG_VIEWFS_LINK_MERGE_SLASH;
boolean gotMountTableEntry = false;
final UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
//读取配置中的每个挂载点
for (Entry<String, String> si : config) {
final String key = si.getKey();
if (key.startsWith(mountTablePrefix)) {
gotMountTableEntry = true;
LinkType linkType;
String src = key.substring(mountTablePrefix.length());
String settings = null;
if (src.startsWith(linkPrefix)) {
src = src.substring(linkPrefix.length());
//省略
//每个挂载点都进行记录
linkEntries.add(
new LinkEntry(src, target, linkType, settings, ugi, config));
//省略
//创建对应的文件系统客户端对象
for (LinkEntry le : linkEntries) {
//省略
fallbackLink = new INodeLink<T>(mountTableName, ugi,
getTargetFileSystem(new URI(le.getTarget())),
new URI(le.getTarget()));
} else {
createLink(le.getSrc(), le.getTarget(), le.getLinkType(),
le.getSettings(), le.getUgi(), le.getConfig());
}
}
rootFallbackLink = fallbackLink;
}
//省略
}
经过ViewFS测试,得到以下ViewFS使用要求:
- 表和分区的schem需要保持一致,统一设置为hdfs或者viewfs。
- 表和分区需要在同一个集群,配置viewfs的情况下,可以跨NS。
- Spark SQL测试和HIVE表现基本一致,HIVE检查表和分区的schem更严格。
4. ViewFS Based Federation弊端
由于挂载表记录在每个客户端的mountTable.xml中,有以下弊端:
- 用户误操作,修改了客户端挂载配置,可能导致集群数据误删等高危操作。
- 维护困难。如果数据在子集群间进行迁移,那么所有客户端的配置都需要变更。有些用户的客户端配置不在变更范围内,就会导致访问的数据不符预期。