“ Alluxio 是世界上第一个用于云分析和人工智能的开源数据编排技术。本文主要介绍了Alluxio元数据管理的两种方案及其实现原理。”
01
—
Alluxio简介
Alluxio 是世界上第一个面向基于云的数据分析和人工智能的开源的数据编排技术。它为数据驱动型应用和存储系统构建了桥梁,将数据从存储层移动到距离数据驱动型应用更近的位置从而能够更容易被访问。同时Alluxio为应用程序提供了一个公共接口。应用程序通过连接到Alluxio就可以访问底层挂载的任意存储系统中的数据。
Alluixo作为一个分布式的缓存系统,采用了主从架构,一个Alluxio集群包含了一个或多个Master节点以及若干个Worker节点,其技术架构如下图所示。其中Master节点主要负责响应用户请求以及维护整个集群的元数据信息,Worker节点主要负责管理集群中缓存的数据块。
02
—
Alluxio元数据存储方案
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。
接下来针对两种元数据管理方案的实现原理进行详细介绍。
03
—
Alluxio元数据存储方案-ROCKS
RocksDB是一款嵌入式的Key-Value数据库。用户能够通过调用API接口来实现高效的KV数据的存储和访问。
(一)BlockStore接口实现
在ROCKS方案中BlockStore接口由RocksBlockStore实现,RocksBlockStore创建流程如下:
在构造AlluxioMasterProcess类时生成一个BlockStore.Factory添加到mContext中。
MasterUtils.createMasters()方法会根据按顺序创建所有的Master线程,在创建DefaultBlockMaster时会调用BlockStore.Factory,创建一个RocksBlockStore实例。
在RocksBlockStore的构造函数中会先调用RocksDB.loadLibrary(),加载依赖的库,然后根据配置文件创建一个RocksStore类的实例。RocksStore用于操作RockDB数据库,包括初始化数据库,备份和恢复数据库等操作。
在RocksStore的创建的过程中最终在create()方法中调用了RocksDB.open()方法创建了一个RocksDB类的实例。
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); }
04
—
Alluxio元数据存储方案-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。
05
—
总结
Alluxio的元数据存储提供了ROCKS和HEAP两种方案,默认配置采用的是ROCKS方案。在ROCKS方案中除了使用RocksDB外,Alluxio还提供了一个基于内存的缓存,用于提高元数据读写性能,在元数据存储量较小的时候能够获得较高的性能;同时借助RocksDB可以将元数据信息保存到硬盘中,获得更大存储空间。因此,Alluxio的元数据存储在一般情况下建议使用RocksDB的方案。如果需要存储的元数据较少,同时又对元数据读写性能有非常高的要求时,也可以考虑使用HEAP方案。
- End -
关于本周文章如果大家有疑问,或者有其他感兴趣的大数据内容,欢迎在评论区留言~后续会持续分享大数据领域的干货知识~




