Alluxio 跨集群同步机制的设计和实现确保了在运行多个 Alluxio 集群时,元数据是一致的。
Alluxio 位于存储和计算层之间,在不同的底层文件系统(UFS)上层提供高性能缓存和统一的命名空间。虽然通过 Alluxio 对 UFS 进行更新可使 Alluxio 与 UFS 保持一致,但在某些情况下, 例如在运行多个共享某一个或多个 UFS 命名空间的 Alluxio 集群时,结果可能并非如此。为了确保这种情况下的一致性,Alluxio 已经实现了跨集群同步机制,本文将对该机制进行详细介绍。
随着数据量的增长,这些数据的存储和访问方式也变得越来越复杂。例如,数据可能位于不同的存储系统中(S3、GCP、HDFS 等),也可能存储在云上或本地,或是位于不同的地理区域,还可能因为隐私或安全保护,被进一步隔离。此外,这些复杂性不仅体现在数据存储上,还包括如何将数据用于计算,例如,数据可能存储在云上,而计算则在本地进行。
Alluxio 是一个数据编排平台,通过在 UFS 上提供统一的访问接口来降低此类复杂性,并通过提供数据本地性和缓存来提高计算性能。
对于许多组织而言,运行一个 Alluxio 集群可能就足够了,但有些组织需要运行多个 Alluxio 集群。例如,如果计算是在多个区域运行,那么在每个区域运行一个 Alluxio 集群可能会带来更大的优势。此外,某些组织可能出于数据隐私保护的考虑,需要运行独立的集群,或是希望通过运行多个集群来提高可扩展性。虽然部分数据空间可能被隔离在某个集群中,但其他数据可以在多个集群之间共享。例如,一个集群可能负责提取和转换数据,而其他几个集群可能会查询这些数据并进行更新。
由于每个 Alluxio 集群可能会复制(即挂载)UFS 存储空间的某些部分,Alluxio 会负责保持其副本与 UFS 的一致性,以便用户查询到最新的文件副本。在本文中,我们将介绍在一个或多个集群中确保 Alluxio 数据与 UFS 一致所用到的组件。
在分布式系统中保持数据的一致性是很复杂的,其中有几十个不同的一致性级别,每个级别都允许不同的用户在特定时间查询和修改数据的不同状态。这些一致性级别形成了一个从弱到强的范围区间,一致性越强限制越多,通常越容易在上面搭建应用程序。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 同步即可。
元数据同步是用来检查和修复 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 集群的场景、背景以及如何进行元数据同步。本章将介绍如何在多集群场景下实现建立元数据同步,从而确保以提供元数据一致性。
在实践中,使用基于时间的同步不一定总是有效,因为只有特定的工作负载才会定期更新文件。事实上,对于许多工作负载来说,大部分文件仅被写入一次,而只有一小部分文件会经常更新。在这种情况下,基于时间的同步效率变低,这是因为大多数同步都是不必要的,增加时间间隔将导致经常修改的文件处于数据不一致状态的时间更长。
与基于时间的同步相比,跨集群同步具有两个主要优点。首先,只对已修改的文件执行同步,其次,修改可以快速地对其他集群可见,所需时间即大约等同于从一个集群发送消息到另一个集群的时间。
由此我们可以看到,当满足以下假设时,跨集群同步功能将是最有效用的。
● 多个 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 集群进行。当然这个条件不一定能满足,有几种方法来处理这个问题。
● 用户可以手动将一个路径标记为需要同步。
● 基于时间的同步可以和跨集群同步一起启用。
为什么不使用确保仅一次消息传递的 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 同步的过程。关于如何部署跨集群同步功能的更多内容,请点击阅读原文查看。