写点什么

浅析 Alluxio 元数据管理的实现原理

  • 2022 年 4 月 13 日
  • 本文字数:3478 字

    阅读完需:约 11 分钟

 Alluxio 是世界上第一个用于云分析和人工智能的开源数据编排技术。本文主要介绍了 Alluxio 元数据管理的两种方案及其实现原理。

1Alluxio 简介

Alluxio 是世界上第一个面向基于云的数据分析和人工智能的开源的数据编排技术。它为数据驱动型应用和存储系统构建了桥梁,将数据从存储层移动到距离数据驱动型应用更近的位置从而能够更容易被访问。同时 Alluxio 为应用程序提供了一个公共接口。应用程序通过连接到 Alluxio 就可以访问底层挂载的任意存储系统中的数据。

Alluixo 作为一个分布式的缓存系统,采用了主从架构,一个 Alluxio 集群包含了一个或多个 Master 节点以及若干个 Worker 节点,其技术架构如下图所示。其中 Master 节点主要负责响应用户请求以及维护整个集群的元数据信息,Worker 节点主要负责管理集群中缓存的数据块。

2Alluxio 元数据存储方案

Alluxio 将大部分的元数据存储在 Master 节点,元数据包括文件系统树、文件权限信息以及数据块的位置信息。Alluxio 提供了两种元数据存储方案:

  • ROCKS:使用 RockDB 作为元数据存储;

  • HEAP:使用堆内存作为元数据存储;

其中默认的方案是 ROCKS 方案。

Alluxio 源码在/core/server/master/src/main/java/alluxio/master/metastore 目录下提供了两个接口,分别是 InodeStore 和 BlockStore。其中 InodeStore 管理的元数据信息包括单个 Inode 的信息以及不同 inodes 之间的父子关系;BlockStore 负责管理文件数据块的块大小和块位置信息。

在 ROCKS 方案中,通过 CachingInodeStore、RocksInodeStore 和 RocksBlockStore 实现了上述的接口。在 HEAP 方案中,通过 HeapInodeStore、HeapBlockStore 实现了上述接口。整体的依赖关系如下图:

在构造 AlluxioMasterProcess 类的时候会生成两个 Factory 来生成对应的 store,在 Factory 中依据配置信息来生成不同的 InodeStore 和 BlockStore。

接下来针对两种元数据管理方案的实现原理进行详细介绍。

3Alluxio 元数据存储方案-ROCKS

RocksDB 是一款嵌入式的 Key-Value 数据库。用户能够通过调用 API 接口来实现高效的 KV 数据的存储和访问。

(一)BlockStore 接口实现

在 ROCKS 方案中 BlockStore 接口由 RocksBlockStore 实现,RocksBlockStore 创建流程如下:

  1. 在构造 AlluxioMasterProcess 类时生成一个 BlockStore.Factory 添加到 mContext 中。

  2. MasterUtils.createMasters()方法会根据按顺序创建所有的 Master 线程,在创建 DefaultBlockMaster 时会调用 BlockStore.Factory,创建一个 RocksBlockStore 实例。

  3. 在 RocksBlockStore 的构造函数中会先调用 RocksDB.loadLibrary(),加载依赖的库,然后根据配置文件创建一个 RocksStore 类的实例。RocksStore 用于操作 RockDB 数据库,包括初始化数据库,备份和恢复数据库等操作。

  4. 在 RocksStore 的创建的过程中最终在 create()方法中调用了 RocksDB.open()方法创建了一个 RocksDB 类的实例。

  5. DefaultBlockMaster 通过操作这个 RocksDB 类来实现对 RocksDB 数据库的读写。

RocksBlockStore 主要实现了以下几个功能:

  • getBlock(long id)

  • putBlock(long id, BlockMeta meta)

  • removeBlock(long id)

  • getLocations(long id)

  • addLocation(long id, BlockLocation location)

  • removeLocation(long blockId, long workerId)

下面以 getBlock()方法为例介绍 RocksDB 的使用方法。getBlock()的作用是通过 blockId 来获取对应的 block 的元数据信息,获取的过程如下。

@Override  public Optional<BlockMeta> getBlock(long id) {    byte[] meta;    try {      meta = db().get(mBlockMetaColumn.get(), Longs.toByteArray(id));    } catch (RocksDBException e) {      throw new RuntimeException(e);    }    if (meta == null) {      return Optional.empty();    }    try {      return Optional.of(BlockMeta.parseFrom(meta));    } catch (Exception e) {      throw new RuntimeException(e);    }  }
复制代码

在该方法中和 RocksDB 交互的主要过程为:

meta = db().get(mBlockMetaColumn.get(), Longs.toByteArray(id));
复制代码

上述代码中 db()方法会返回之前构建的 RocksDB 类,然后调用 RocksDB.get()方法。get()方法需要传入两个参数,第一个是 ColumnFamilyHandle 类,第二个是 blockId。

在 RocksDB 中每一个 KV 对都对应了一个 ColumnFamily,ColumnFamily 相当于 RocksDB 中的逻辑分区。当我们需要查询一个 ColumnFamily 中的数据时,就需要通过 ColumnFamilyHandle 来操作底层的数据库。ColumnFamilyHandle 是在创建 RocksDB 实例的时候一起创建的。

存储在 RocksDB 中的 KV 数据都是以字节串的形式进行存储的,因此我们需要将传入的 blockId 转换成 byte[],最终返回的 meta 也是 byte[]类型。


(二)InodeStore 接口实现

ROCKS 方案中的 InodeStore 接口由 CachingInodeStore 和 RocksInodeStore 实现。其中 CachingInodeStore 利用内存来实现元数据的缓存,RocksInodeStore 是通过 RocksDB 实现的元数据存储,作为 CachingInodeStore 的 backing store。

当集群的元数据信息能够完全的存储在 CachingInodeStore 的时候,Alluxio 不会和 RocksInodeStore 交互,而是通过操作 CachingInodeStore 来获得更好的性能。当 CachingInodeStore 的存储容量达到某一个设置的阈值时,Alluxio 会自动将原本保存在 CachingInodeStore 中的元数据信息迁移到 RocksInodeStore 中。此时元数据访问的性能就取决于 CachingInodeStore 的缓存命中率和 RocksDB 的性能。

RocksInodeStore 的创建流程和使用方法和 RocksBlockStore 类似。在构造 InodeStore 的时候会根据 MASTER_METASTORE_INODE_CACHE_MAX_SIZE 配置来确定是否需要使用 CachingInodeStore。如果 MASTER_METASTORE_INODE_CACHE_MAX_SIZE 设为 0,则直接构建 RocksInodeStore,如果不为 0,则构建需要同时 CachingInodeStore 和 RocksInodeStore。

case ROCKS:InstancedConfiguration conf = ServerConfiguration.global();  if (conf.getInt(PropertyKey.MASTER_METASTORE_INODE_CACHE_MAX_SIZE) == 0) {    return lockManager -> new RocksInodeStore(baseDir);  } else {    return lockManager -> new CachingInodeStore(new RocksInodeStore(baseDir), lockManager);  }
复制代码

4Alluxio 元数据存储方案-HEAP

在 Heap 方案中 Alluxio 使用堆内存作为存储。在创建 AlluxioMasterProcess 时会构建 HeapInodeStore 和 HeapBlockStore 来实现 InodeStore 和 BlockStore 接口。

(一)BlockStore 接口实现

在 HeapBlockStore 中会创建一个叫 mBlocks 的 ConcurrentHashMap,用于存储每个 block 的 metadata。同时会创建一个叫 mBlockLocations 的 TwoKeyConcurrentMap,用于存储 block 在 worker 中的位置。

 // Map from block id to block metadata.  public final Map<Long, BlockMeta> mBlocks = new ConcurrentHashMap<>();  // Map from block id to block locations.  public final TwoKeyConcurrentMap<Long, Long, BlockLocation, Map<Long, BlockLocation>>      mBlockLocations = new TwoKeyConcurrentMap<>(() -> new HashMap<>(4));
复制代码

mBlockLocations 中的两个 key 分别是 blockId 和 workerId,value 是 block 存储的具体位置。在使用时能够通过 blockId 获取到该 block 在 worker 中的存储的位置。

(二)InodeStore 接口实现

在 HeapInodeStore 中会创建一个叫 mInodes 的 ConcurrentHashMap,用于存储文件和文件夹的 Inode 信息。同时会创建一个叫 mEdges 的 TwoKeyConcurrentMap,用于存储不同节点的父子关系。

private final Map<Long, MutableInode<?>> mInodes = new ConcurrentHashMap<>();  // Map from inode id to ids of children of that inode. The inner maps are ordered by child name.  private final TwoKeyConcurrentMap<Long, String, Long, Map<String, Long>> mEdges =      new TwoKeyConcurrentMap<>(() -> new ConcurrentHashMap<>(4));
复制代码

TwoKeyConcurrentMap 是 Alluxio 中定义的一个类,通过这个类实现了一个支持两个 key 的 ConcurrentMap,它的逻辑结构为:<k1, <k2, value>>。

mEdges 中 k1 为父 Inode 的 ID,k2 是子 Inode 的 name,value 是子 Inode 的 ID。使用时可以通过父 Inode 的 ID 获取一个包含所有子 Inode 的一个 InnerMap,在这个 InnerMap 中可以通过子 Inode 的 name 来获取对应的子 Inode 的 ID。

5 总结

Alluxio 的元数据存储提供了 ROCKS 和 HEAP 两种方案,默认配置采用的是 ROCKS 方案。在 ROCKS 方案中除了使用 RocksDB 外,Alluxio 还提供了一个基于内存的缓存,用于提高元数据读写性能,在元数据存储量较小的时候能够获得较高的性能;同时借助 RocksDB 可以将元数据信息保存到硬盘中,获得更大存储空间。

因此,Alluxio 的元数据存储在一般情况下建议使用 RocksDB 的方案。如果需要存储的元数据较少,同时又对元数据读写性能有非常高的要求时,也可以考虑使用 HEAP 方案。

用户头像

还未添加个人签名 2019.02.13 加入

还未添加个人简介

评论

发布
暂无评论
浅析Alluxio元数据管理的实现原理_元数据_移动云大数据_InfoQ写作平台