一、Alluxio应用场景和背景
Alluxio跨集群同步机制的设计和实现确保了在运行多个Alluxio集群时,元数据是一致的。
Alluxio位于存储和计算层之间,在不同的底层文件系统(UFS)上层提供高性能缓存和统一的命名空间。虽然通过Alluxio对UFS进行更新可使Alluxio与UFS保持一致,但在某些情况下,例如在运行多个共享某一个或多个UFS命名空间的Alluxio集群时,结果可能并非如此。为了确保这种情况下的一致性,Alluxio已经实现了跨集群同步机制,本文将对该机制进行详细介绍。
1. 背景介绍
随着数据量的增长,这些数据的存储和访问方式也变得越来越复杂。例如,数据可能位于不同的存储系统中(S3、GCP、HDFS等),也可能存储在云上或本地,或是位于不同的地理区域,还可能因为隐私或安全保护,被进一步隔离。此外,这些复杂性不仅体现在数据存储上,还包括如何将数据用于计算,例如,数据可能存储在云上,而计算则在本地进行。
Alluxio是一个数据编排平台,通过在UFS上提供统一的访问接口来降低此类复杂性,并通过提供数据本地性和缓存来提高计算性能。
对于许多组织而言,运行一个Alluxio集群可能就足够了,但有些组织需要运行多个Alluxio集群。例如,如果计算是在多个区域运行,那么在每个区域运行一个Alluxio集群可能会带来更大的优势。此外,某些组织可能出于数据隐私保护的考虑,需要运行独立的集群,或是希望通过运行多个集群来提高可扩展性。虽然部分数据空间可能被隔离在某个集群中,但其他数据可以在多个集群之间共享。例如,一个集群可能负责提取和转换数据,而其他几个集群可能会查询这些数据并进行更新。
由于每个Alluxio集群可能会复制(即挂载)UFS存储空间的某些部分,Alluxio会负责保持其副本与UFS的一致性,以便用户查询到最新的文件副本。在本文中,我们将介绍在一个或多个集群中确保Alluxio数据与UFS一致所用到的组件。
2. Alluxio 数据一致性
在分布式系统中保持数据的一致性是很复杂的,其中有几十个不同的一致性级别,每个级别都允许不同的用户在特定时间查询和修改数据的不同状态。这些一致性级别形成了一个从弱到强的范围区间,一致性越强限制越多,通常越容易在上面搭建应用程序。Alluxio也不例外,它会根据配置和使用的UFS提供不同的一致性保障(详细信息见Alluxio的数据一致性模型)。
为了简化关于一致性的讨论,我们将做如下假设:
- 对于任何文件,UFS都是文件的 "唯一数据源"。
这意味着Alluxio中的每个文件都对应于UFS上的一个文件,并且UFS中总是有该文件的最新版本。如果Alluxio存储的文件副本与UFS中的文件不同,那么Alluxio中的文件版本是不一致的。(这里我们假设UFS本身确保了强一致性,即某种程度的线性一致性(linearizability)或外部一致性(external consistency)。从高层次来看,这允许用户把UFS(即便系统是由许多分布式部分所组成)当作类似实时按顺序执行操作的单一的文件系统来访问。
在讨论Alluxio和UFS的一致性之前,让我们先来看一下Alluxio的基本架构。Alluxio是由master节点和worker节点组成的。master节点负责跟踪文件的元数据,例如它的路径、大小等,而worker节点负责存储数据本身。如果client要读一个文件,必须先从某一个master节点上读取元数据,然后用它来定位存储该数据副本的worker(必要时可以从UFS上加载数据)。如果client要写一个文件,必须首先在master中为该文件创建元数据,然后通过worker将该文件写到UFS,最后在master上将该文件标记为完成。当文件正在被写入时,它的元数据会被标记为未完成,从而阻止其他client访问该文件。
从这个基本设计中,我们可以看到,只要所有对文件的更新都通过Alluxio写入UFS,那么Alluxio中的数据将与UFS中的数据保持一致,client将会始终查询到最新的数据版本。
然而现实情况,并没有这么简单,例如,某些用户可能在更新UFS时不通过Alluxio,或者client可能出现故障,只将部分文件写入UFS,而没有在Alluxio master上标记完成,这些都可能导致Alluxio和UFS中的数据不一致。
那么,这些问题是如何处理的呢?由于我们重点假设了UFS是唯一的数据源,要解决这些不一致的问题只需让Alluxio与UFS同步即可。
3. 元数据同步
元数据同步是用来检查和修复Alluxio和UFS之间不一致的主要组件。当client访问Alluxio中的某个路径时,该功能在一定条件下(后面会讨论)可能会被触发。基本程序如下:
- 从UFS加载该路径的元数据。
- 将UFS中的元数据与Alluxio中的元数据进行比较。元数据中包含文件数据的指纹(例如最后修改时间和抗碰撞的哈希值),可用于检查数据不一致情况。
- 如果发现任何不一致,则更新Alluxio中的元数据,并标记过时的数据,以便将其从worker中驱逐。最新数据会根据需要从UFS加载到worker。
图:client读取时的元数据同步过程。1. client读取文件系统中的一个路径。2. master上的元数据同步模块根据用户配置检查是否需要同步。3. 通过从UFS加载元数据进行同步,并创建一个指纹来比较Alluxio和UFS中的元数据。如果指纹不同,则Alluxio中的元数据会被更新。4. client根据更新后的元数据从worker中读取文件数据,必要时从UFS中加载数据。
唯一的问题就是决定何时执行这个元数据同步程序,需要我们在更强的一致性和更好的性能之间进行权衡。
每次访问数据时进行元数据同步
如果Alluxio中的client每次访问一个路径时都进行元数据同步,那么client将始终能查看到UFS上最新的数据状态。这将为我们提供最高的一致性级别,通常可以达到UFS所能确保的最强的一致性。但是,由于每次访问数据(即使数据没有被修改)都会与UFS进行同步,这也会将导致性能下降。
基于时间进行元数据同步
另外,元数据同步可以基于一个物理时间间隔来执行。在这种情况下,Alluxio master上的元数据包含路径最后一次与UFS成功同步的时间。现在,只有当用户定义的时间间隔过后,才会进行新的同步(详细信息见UFS元数据同步)。
虽然这种方式可能极大地提高了性能,但也导致了相对较弱级别的一致性保障,即最终一致性。这意味着,任何特定的读取结果可能与UFS一致,也可能不一致。此外,数据更新被查询到的顺序可能是任意顺序。例如,在UFS中,文件A的更新实际早于另一个文件B,但是,Alluxio集群查询到的可能是文件B的更新早于文件A。因此,系统的用户必须了解这些不同级别的一致性保障,并根据需要调整应用程序。
二、跨集群同步机制
在上一章节,我们讨论了单个Alluxio集群的场景、背景以及如何进行元数据同步。本章将介绍如何在多集群场景下实现建立元数据同步,从而确保以提供元数据一致性。
1. 基于时间同步的多集群一致性
其中一个基于时间的元数据同步用例是使用多个Alluxio集群且集群共享部分UFS数据空间的场景。通常,我们可以认为这些集群正在运行单独的工作负载,这些工作负载可能需要在某些时间点共享数据。例如,一个集群可能会提取和转换来自某一天的数据,然后另一个集群会在第二天对该数据进行查询。运行查询任务的集群可能不需要总是看到最新的数据,例如可以接受最多一个小时的延迟。
在实践中,使用基于时间的同步不一定总是有效,因为只有特定的工作负载才会定期更新文件。事实上,对于许多工作负载来说,大部分文件仅被写入一次,而只有一小部分文件会经常更新。在这种情况下,基于时间的同步效率变低,这是因为大多数同步都是不必要的,增加时间间隔将导致经常修改的文件处于数据不一致状态的时间更长。
2. 使用跨集群同步(Cross Cluster Sync)实现多集群一致性
为了避免基于时间同步的低效性,跨集群同步功能允许直接跟踪不一致性,因此只在必要时才会同步文件。这意味着每当在Alluxio集群上一条路径发生更改时,该集群将发布一个失效消息,通知其他Alluxio集群该路径已被修改。下次当有client在订阅(跨集群同步功能的)集群上访问此路径时,将触发与UFS的同步操作。
与基于时间的同步相比,跨集群同步具有两个主要优点。首先,只对已修改的文件执行同步,其次,修改可以快速地对其他集群可见,所需时间大约等同于从一个集群发送消息到另一个集群的时间。
由此我们可以看到,当满足以下假设时,跨集群同步功能将是最有效的。
- 多个Alluxio集群挂载的一个或多个UFS中有交叉部分。(我们认为系统中部署的Alluxio集群数量的合理范围是2-20个)。
- 至少有一个集群会对UFS上的文件进行更新。
- 所有对UFS的更新都要经过Alluxio集群(关于处理其他情况的方法,请参见下文 "其他用例"内容)。
现在我们要确保来自一个Alluxio集群的更新将最终在其他所有Alluxio集群中被监测到(即集群与UFS满足最终一致性保障),这样应用程序就可以在集群间共享数据。
路径失效发布/订阅
跨集群同步功能是基于发布/订阅(pub/sub)机制实现的。当Alluxio集群挂载某个UFS路径时,就会订阅该路径,每当集群修改UFS上的文件时,它都会向所有订阅者发布修改的路径。
表1:三个Alluxio集群挂载不同的UFS路径示例。
参考表1中的例子,有三个Alluxio集群,每个集群挂载一个不同的S3路径。这里,集群C1将S3桶(bucket)s3://bucket/挂载到其本地路径/mnt/,集群C2将同一个bucket的子集s3://bucket/folder挂载到其本地路径/mnt/folder,最后C3将s3://bucket/other挂载到其根路径/。
由此,集群C1将订阅路径(pub/sub语义中的“主题”)s3://bucket,集群C2将订阅路径s3://bucket/folder,而集群C3将订阅路径s3://bucket/other。订阅者将收到所有发布的以订阅“主题”开头的消息。
例如,如果集群C1创建了一个文件/mnt/folder/new-file.dat,它将发布一个包含s3://bucket/folder/new-file.dat的无效消息,集群C2将会收到该消息。另外,如果集群C1创建了一个文件/mnt/other-file.dat,则不会发送任何消息,这是因为没有订阅者的主题与s3://bucket/other-file.dat相匹配。
如前所述,Alluxio的元数据包括该路径最近一次同步发生的时间。在跨集群同步的情况下,它还包含最近一次通过pub/sub接口收到的路径失效信息的时间。利用这一点,当client访问一个路径时,在以下两种情况下将会与UFS进行同步。
a) 该路径第一次被访问。
b) 路径的失效时间晚于最近一次同步时间。
假设系统中没有故障,显然最终一致性将得到保证。对文件的每一次修改都会导致每个订阅集群收到一个失效消息,从而在下一次访问该文件时进行同步。
图1:文件创建过程中的跨集群同步机制。A. client在集群1上创建一个文件。B. client将文件写入worker。C. worker把文件写入UFS。D. client在master上完成了该文件。E. 集群1向集群2的订阅者发布文件的失效消息。F. 集群2在其元数据同步组件中将该文件标记为需要同步。以后当client访问该文件时,将同样使用图1所示的步骤1-5进行同步。
实现Pub/sub机制
Pub/sub机制是通过发现机制(discovery mechanism)和网络组件来实现的,前者允许集群知道其他集群挂载了什么路径,后者用来发送消息。
发现机制是一个名为CrossClusterMaster的单一java进程,须能让所有Alluxio集群通过可配置的地址/端口组合进行访问。每当一个Alluxio集群启动时,都会通知CrossClusterMaster该集群的所有master节点的地址。此外,每当集群挂载或卸载UFS时,挂载的路径都将被发送到CrossClusterMaster。每次这些值被更新时,CrossClusterMaster节点都会把新值发送给所有Alluxio集群。
利用这些信息,每个Alluxio集群将计算其本地UFS挂载路径与外部集群的所有UFS挂载路径的交集。对于每个相交的路径,集群的master将使用GRPC连接创建一个以该路径为主题的订阅给外部集群的master。在表1的例子中,C1将向C2创建一个主题为s3://bucket/folder的订阅,以及向C3创建一个主题为s3://bucket/other的订阅。此外,C2将向C1创建一个主题为s3://bucket/folder的订阅,而C3将向C1创建一个主题为s3://bucket/other的订阅。这样一来,每当集群要修改某个路径时,例如创建一个文件,它都会把修改的路径发布给任何主题是该路径前缀的订阅者。例如,如果C1创建一个文件/mnt/other/file,它将发布s3://bucket/other/file到C3。
为了主动维护对其他集群的订阅,每个Alluxio master上都会运行一个线程,以应对路径的挂载或卸载、集群的加入或者脱离,以及出现连接故障等情况的发生
每当订阅者收到路径时,它就会将失效时间元数据更新为当前时间,这样一来,下一次client访问该路径时,就会与UFS进行一次同步。按照我们上面的例子,下一次client在集群C3上读取路径/file时,将在s3://bucket/other/file上执行与UFS的同步。
确保最终一致性
如果能保证每条发布的消息都向所有订阅者(包括未来的订阅者)仅传递一次(exactly once) ,那么显然最终一致性将得到保证,因为每一次修改都会让订阅者在访问路径时进行同步。但是,连接可能中断、集群可能脱离和接入系统、节点也可能出现故障,我们该如何保证消息的准确传递呢?简单的答案是,我们不能。相反,只有在订阅(使用底层TCP连接)处于运行状态时,才能确保仅一次消息传递。此外,当订阅首次建立时,订阅者将标记根路径(主题)的元数据为需要同步。这意味着,在订阅建立后,对于任何作为主题的超集路径,在第一次访问该路径时将进行同步。
例如,当C1用主题s3://bucket/folder建立对C2的订阅时,C1将标记s3://bucket/folder为需要同步。然后,例如在第一次访问s3://bucket/folder/file时,将进行同步。
这大大简化了处理系统中的故障或配置变化的任务。如果某个订阅因为任何原因而失败,如网络问题、master故障切换、配置变化,那么恢复过程是一样的——重新建立订阅,并将相应的路径标记为不同步。为了减轻网络问题的影响,可以设置一个用户定义的参数,以确定有多少消息可以缓存在发布者的发送队列中,以及在队列已满的情况下超时等待多久会发生操作阻塞的可能性。
当然,按照预期,虽然我们的系统会发生故障,但不会经常发生,否则性能会受到影响。所幸即使在频繁发生故障的情况下,性能下降也会与使用基于时间的同步的情况相似。例如,如果每5分钟发生一次故障,预计性能与启用基于时间(5分钟间隔)同步下的性能类似。
请注意,如果CrossClusterMaster进程发生故障,那么新的集群和路径挂载发现将不起作用,但集群将保持其现有的订阅而不会中断。此外,CrossClusterMaster是无状态的(可以把它看作是集群交换地址和挂载路径的一个点),因此,可以在必要时停止和重新启动。
其他用例
前面提到,为了使这个功能发挥作用,所有对UFS的更新都应该通过Alluxio集群进行。当然这个条件不一定能满足,有几种方法来处理这个问题。
- 用户可以手动将一个路径标记为需要同步。
- 基于时间的同步可以和跨集群同步一起启用。
三、探讨与结论
1. 探讨与未来工作
为什么不使用确保仅一次消息传递的pub/sub机制?
我们知道,如果使用确保仅一次消息传递的pub/sub机制会大大简化我们的设计,而且也确实存在许多强大的系统,如Kafka和RabbitMQ,正是为了解决这个问题而创建的。使用这些系统的好处是,故障对性能的影响可能较小。例如,如果某个订阅者处连接断开,在重新连接时,系统可以从它之前断开的地方继续运行。
尽管如此维护这些系统本身就是一项非常复杂的任务。首先,你需要弄清楚一些问题,比如,要部署多少个节点的物理机,要复制多少次消息,保留多长时间,当由于连接问题而不能发布消息时要不要阻塞操作等。而且,最终很可能还是需要故障恢复机制,从而导致更复杂的设计。
(注意,为了保证最终一致性,我们实际上只需要至少一次(at least once)消息传递,因为多次传递消息只会对性能产生负面影响,而不会影响数据一致性,但即便在这种情况下,大部分困难仍然存在)。
扩展至20个Alluxio集群以上或处理频发故障
未来,我们希望能支持扩展到数百个Alluxio集群,但从20个集群扩展至数百个集群可能有不同的设计考量。首先,我们预期故障的发生会更加频繁;其次,设计可能会导致master产生大量开销。
如前所述,故障频繁发生会使性能降低到与采用基于时间同步时类似。在有数百个集群的情况下,我们预期网络或master节点故障会相当频繁地发生。(请注意,这也取决于配置,因为故障只会影响挂载了与故障UFS路径有交集的集群。因此,如果集群大多挂载了不相交的UFS路径,那么可能问题不大)。此外,如果所有集群挂载的路径都有交集,那么它们将必须维护对所有其他集群的订阅,且一个发布就需要发送数百条消息。
在这种情况下,我们可能需要纳入一个可靠的pub/sub机制,如Kafka或RabbitMQ,但这里只是替代点对点的订阅,而不是改变整个系统的设计。故障仍然会发生,集群将以同样的方式恢复——将相交的UFS路径标记为需要同步。只有可靠的pub/sub机制才会隐藏Alluxio的许多故障。例如,如果该机制想要可靠地存储最后5分钟的消息,那么只有持续时间超过5分钟的故障才需要用原来的方法进行恢复。此外,这些系统能够不考虑Alluxio集群的数量进行扩展,在必要时添加更多节点。不过,使用和维护这些系统会产生大量的开销,可能只有在某些配置中才值得尝试。
关于一致性的一些看法
虽然本文介绍了确保最终一致性的基本思路,但还有几个重要的内容没有详细说明。
首先,失效消息必须在对UFS的修改完成后才能发布,其次,UFS必须在线性一致性或外部一致性(S3中的一致性)层面上确保强一致性。如果这两个条件中的任何一个没有得到满足,那么当订阅者收到失效信息并执行同步时,集群可能无法观测到文件的最新版本。第三,如果一个集群与CrossClusterMaster的连接断开,后来又重新建立了连接,那么该集群也必须经历故障恢复过程,这是因为在连接中断期间可能有某个外部集群挂载并修改了路径。
发布完整的元数据
如前所述,发布的失效消息只包含被修改的路径。但是,这些消息也可以包括路径的更新元数据,从而避免在订阅集群上进行同步。之所以不这样做是因为无法通过常规方法知道哪个版本的元数据是最新的版本。
例如,两个Alluxio集群C1和C2在UFS上更新同一个文件。在UFS上,集群C1的更新发生在集群C2的更新之前。然后,两个集群都将他们更新的元数据发布到第三个集群C3。由于网络条件的原因,C2的消息比C1先到达。此时,C3需要知道,它应该放弃来自C1的更新,因为已经有了最新的元数据版本。当然,如果元数据包含版本信息,就可以做到这一点,但可惜对于Alluxio支持的所有UFS,常规方法都做不到。因此,C3仍然需要与UFS进行元数据同步,以便直接从唯一的数据源获得最新的版本。
订阅通知服务
某些底层存储系统(UFS)(例如 Amazon SNS 和 HDFS iNotify)提供通知服务,让用户知道文件何时被修改了。对于这类UFS,相较于订阅Alluxio集群,订阅这些服务可能是更好的选择。这样做的好处是支持不通过Alluxio对UFS进行写入。同样,系统设计将保持不变,只是不订阅其他Alluxio集群,而是订阅此类通知服务。
请注意,Alluxio还为HDFS提供了ActiveSync功能,允许元数据与底层UFS保持同步。这与跨集群的同步机制有所不同,因为ActiveSync在文件更新时执行同步,而跨集群同步只在文件被访问时执行同步。
四、结论
本文主要介绍了运行多个Alluxio集群能带来优势的场景,以及Alluxio使用基于时间同步和跨集群同步功能,用来保持集群与所挂载UFS同步的过程。关于如何部署跨集群同步功能的更多内容,请点击链接查看。