背景介绍
Alluxio 是世界上第一个面向基于云的数据分析和人工智能的开源的数据编排技术。 它为数据驱动型应用和存储系统构建了桥梁, 将数据从存储层移动到距离数据驱动型应用更近的位置从而能够更容易被访问。同时Alluxio为应用程序提供了一个公共接口。应用程序通过连接到Alluxio就可以访问底层挂载的任意存储系统中的数据。
Alluixo作为一个分布式的缓存系统,采用了主从架构,一个Alluxio集群包含了一个或多个Master节点以及若干个Worker节点。其中Master节点主要负责响应用户请求以及维护整个集群的元数据信息。Worker节点主要负责管理集群中缓存的数据块。
本文主要介绍Alluxio集群中使用的元数据管理方案。
元数据(metadata)存储方式
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实现了上述接口。整体的依赖关系如下:
InodeStore在构造AlluxioMasterProcess类的时候会生成两个Factory来生成对应的store,在Factory中依据配置信息来生成不同的InodeStore和BlockStore。
ROCKS方案
RocksDB是一款嵌入式的Key-Value数据库。用户能够通过调用API接口来实现高效的KV数据的存储和访问。
在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[]类型。
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);
}
Heap方案
在Heap方案中Alluxio使用堆内存作为存储。在创建AlluxioMasterProcess时会构建HeapInodeStore和HeapBlockStore来实现InodeStore和BlockStore接口。
在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。
在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中的存储的位置。
总结
在Alluxio中元数据的存储提供了ROCKS和HEAP两种方案,默认配置采用的是ROCKS方案。在ROCKS方案中除了使用RocksDB外,Alluxio还提供了一个基于内存的缓存,用于提高元数据读写性能。因此在元数据存储量较小的时候能够获得较高的性能。同时借助RocksDB我们可以将元数据信息保存到硬盘中,获得更大存储空间。 所以在一般情况下我们可以使用RocksDB的方案。如果需要存储的元数据较少,同时又对元数据读写性能有非常高的要求,那么我们也可以考虑使用HEAP方案。