元数据同步(sync)是Alluxio中的核心功能,它使文件和目录与所在存储系统下真实的来源保持一致,进而使用户能够轻松地从Alluxio中检索出最新版的数据。同时了解内部流程对调整性能也非常重要。本文介绍了Alluxio中保持元数据同步的设计和实现。
元数据同步为什么在Alluxio中很重要
在Alluxio中,元数据指的是Alluxio文件系统中文件和目录的信息,包括它们的所有者、组、权限、创建以及修改时间等信息。元数据独立于其内容——即使文件或目录是空的,但它仍然具有关联的元数据。
Alluxio维护文件系统或底层存储系统的对象存储命名空间的副本。在Alluxio中,元数据一致性很重要,尤其是不同集群在数据管道中写入或读取数据后,并在Alluxio之外进行更改时。
在上图中是一个典型的场景,结合了Spark ETL和Presto SQL的数据管道。ETL集群(不带Alluxio)写入数据,然后是分析集群,Alluxio读取转换后的数据。因为Alluxio维护了底层存储的元数据副本并管理元数据,因此当底层存储中的数据通过ETL步骤发生变化时,必须使分析群集上的Alluxio实例感知到并与底层存储系统中的元数据保持一致以便正确操作。
在Alluxio中元数据同步是如何工作的
Alluxio在一个或多个底层存储系统上的统一命名空间中提供了文件系统抽象。通过Alluxio访问文件或目录,会得到和直接访问under storage一样的结果。比如如果挂载到Alluxio根目录的底层存储是s3://bucket/data,那么在Alluxio中列出“/”目录与在s3://bucket/data中列出对象并在其中打印“/file”产生相同的结果应该返回与s3://bucket/data/file一样的结果。
在Alluxio中元数据只从Alluxio master中存储和提供,但单个文件的内容则由Alluxio worker提供。
默认情况下,Alluxio根据需要从底层存储加载元数据。在上面的例子中,一个从空开始的Alluxio master在启动后没有任何关于s3://bucket/data/file的信息。仅当某些用户在Alluxio中列出“/”目录或尝试访问“/file”时才会识别此文件。这种“惰性”行为可以防止不必要的工作并能显著提高性能,因为底层存储中的元数据操作可能很慢。
注意,更新元数据可以是双向的。如果对文件系统的所有修改都是通过Alluxio发生,那么Alluxio只需要扫描一次under storage即可检索初始状态,然后作为文件系统RPC调用的一部分同步应用Alluxio和under storage中的更改。这将为用户提供一致的存储不足视图。然而实际上Alluxio之外的存储不足经常发生变化,因此Alluxio master必须监控对under storage中文件和方向的添加、删除和更新,并将更改应用到Alluxio文件系统中。这个同步两个命名空间的过程称为元数据同步。
如何触发元数据同步
当应用程序更改了 Alluxio 文件的元数据并且该文件被持久化时,更改将始终同步传播到底层存储无需触发元数据同步。当应用程序在存储文件下更新而不让 Alluxio 知道时,有两种方法可以控制元数据同步的时间。
1. 基于时间的自动同步
将同步间隔设置到Alluxio属性键“alluxio.user.file.metadata.sync.interval”上。
- 当该值为-1(默认值)时,Alluxio将永远不会在初始加载后与under storage 重新同步;
- 当它的值设置为0时,每当访问元数据Alluxio将始终与 under storage 重新同步;
- 当该值为正数时(默认单位为毫秒),Alluxio将(尽力而为)不会在该时间间隔内重新同步路径。
注意,使用这种方式如果从未访问过Alluxio中的路径,则它将永远不会触发同步。一旦在同步间隔到期后访问路径,Alluxio将再次与under storage同步。例如在Presto作业中,查询计划阶段列出了该作业所需的所有文件,如果这些路径最近未被访问则会触发同步。但是除非作业持续时间超过同步间隔,否则作业的后续阶段将不会同步。
因此,在这种情况下,从技术上来讲我们可以比同步间隔更频繁地重新同步。
可以使用全新的全局默认值(在 alluxio-site.properties 中设置时)进行自定义,也可以在目录基础上递归地应用其所有子项来自定义此属性键。
2. 使用 LoadMetadata 标志手动同步
如果同步元数据时由于同步间隔而未发生,则大多数Alluxio操作将继续使用Alluxio文件系统中当前的元数据执行,但也有一些例外:
- 对于大多数用户来说,Alluxio CLI“loadMetadata”是手动触发同步的最简单方法。例如,可以运行“bin/alluxio fs loadMetadata /path/to/sync”来强制更新Alluxio路径“/path/to/sync”的元数据;
- 对于基于Alluxio文件系统SDK(Java)构建的应用程序,有两种API方法getStatus和listStatus可以检索路径或目录的元数据。在调用这些方法时,每次调用的option中都会多出一个LoadMetadataPType字段,这可能会在被查询的Alluxio路径上触发master的“loadMetadata“进程。这个过程可以说是同步的简化版,只从底层存储加载文件元数据。但如果文件已经在Alluxio中了,就不会修改文件的元数据。如果LoadMetadataPType设置为NEVER,则不会加载任何内容,如果文件不存在则会抛出FileNotFound异常。当LoadMetadataPType为ONCE时,只会为每个目录加载一次元数据。这仅影响这两个文件系统的调用,并且仅在未发生同步时才考虑此选项。
如何实现元数据同步
当Alluxio master收到RPC请求检索此路径的元数据时,Alluxio master可能会在Alluxio路径上触发元数据同步。而不是有一个专用的服务来遍历整个文件系统inode树并保持同步,这项工作由master上的每个单独的Alluxio文件系统操作来分摊。在 RPC 请求中同步的高级过程是:
- 给定Alluxio路径,确定它是否与相应的存储路径一致。 这意味着存储不足的路径不存在或具有与Alluxio不同的元数据,这部分是使用RPC线程完成的;
- 步骤1填充到同步队列中,我们循环访问同步队列,并从单独的线程池处理工作线程中的每个路径。遍历顺序是 BFS 顺序,因为在队列末尾添加了其他路径。并行性和执行器将在并行性部分中更详细地讨论。此部分由同步线程执行,并使用存储不足的预取线程读取存储不足的信息。这样做的原因是与计算的通信重叠。同步线程需要操作 inode 树,一旦我们确定在将来的某个时候需要该信息,存储不足的预取就可以启动。预取线程将存储不足状态信息加载到存储不足状态缓存中,缓存部分对此进行了讨论。
注意如果元数据同步过程涉及inode树的同一部分,则元数据同步过程可能会相对昂贵,并且会阻止其他操作。这是因为同步进程可能会写锁定它正在更新的文件系统的元数据部分。特别是当同步树中的特定路径时,RPC处理线程将首先获取文件整个路径上的读锁。因为同步线程也需要创建路径的能力,所以它需要同步根路径的写锁。
当同步线程处理根路径下的每个路径时会获得额外的锁,同步线程获取文件路径的写锁并在处理路径后立即释放。
Performance Optimization
调整并行度
可以通过控制三个配置参数来调整并行度来同步元数据:
- alluxio.master.metadata.sync.concurrency.level 表示在单个元数据同步请求中(比如在目录上)要同步的单个文件的数量。
- alluxio.master.metadata.sync.executor.pool.size 表示所有同步操作的并发线程数。
- alluxio.master.metadata.sync.ufs.prefetch.pool.size 表示所有同步操作在存储预取操作下可以执行的并发线程数。
缓存结果
有三种类型的不同缓存,在元数据同步过程中具有不同的目标和用途。以下是所有这些内容的快速总结。
- AbsentCache 是负缓存,用于避免检查那些已知不存在的路径的存储不足。它使用前缀匹配来确定路径是否在底层存储中。例如如果路径/a/b在不存在的缓存中,我们知道/a/b/c 也不能存在于底层存储中。此外AbsentCache条目附有时间戳,以便我们知道上次在under storage中检查的时间。这在同步间隔是某个时间段时很有用,我们使用时间戳来确定是否需要重新检查文件或目录的存在。
- UfsStatusCache 是用于在同步过程中从存储状态下预取的缓存。我们通常可以在处理当前目录时预取一些文件状态,而不是在需要时获取路径信息。
- UfsSyncPathCache 是一个正缓存,包含最近与底层存储同步的路径。当我们收到元数据操作时,我们将检查此缓存以确定我们是否需要同步特定路径。
总结
元数据同步是Alluxio中最重要的功能之一。有多种不同的方法可以触发同步,但需要权衡不同的性能。在Alluxio master内部有一个优化列表,用于加速同步。