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