| 2.3 分布式文件系统的关键技术讨论 |

上述分布式文件系统的总体设计是对单机文件系统的扩展。在分布式环境下还需要满足其他的一些特性。本节将讨论分布式文件系统的一些关键技术,包括如何提高分布式文件系统的总体性能,如何保证数据的可靠性,以及由此引起的一致性问题的相关解决方案。

2.3.1 关于性能的讨论

首先来看一下单一名字空间的分布式文件系统的性能方面的可能会出现的问题。从总体设计来看,最可能出现性能问题的是元数据节点,因为元数据节点只有一个,而数据块节点有数千个。但是,需要进行全面的分析,以确保每一个部分都不会出现性能问题。下面的分析(包括对可靠性的分析)都采用了分而治之的办法,分成数据块节点和元数据节点的情况。

(1)数据块节点性能分析

由于数据块节点不太容易成为性能瓶颈,因此首先分析如何在实现的过程中保证这一点。这里,数据块节点保持高性能需要满足的条件有两个,一个是保存在每一个数据块节点上的数据块数目相近,即保持存储容量的负载均衡;另一个是数据块节点进行数据读写时提供类似的读写速率,即保持存储服务能力的负载均衡。

对于前一个条件来说,无非就是将总的数据块数目近乎平均地分配到每一个数据块节点上。保证这一点的策略有很多种,一种最简单直接的策略就是进行随机分配,即在每一次需要创建新的块时,就通过随机数算法随机挑选一个数据块节点保存数据。那么如何评价这个策略的好坏呢?实际上,从直观上理解,这样的分配策略大致是负载均衡的,因为服务器的数目足够多,数据块的数量足够大,大部分情况下选择好的随机数发生器可以保证数据块的数目在各个节点之间保持大致均衡。当然,也可以通过更加细致的做法保证数据块分配上的均衡。这里采用的方法也是非常直接的,即每一个数据块节点向元数据节点汇报自己本地维护的数据块的数目情况,而元数据节点依据这样的信息决定应该将下一个数据块分配在哪一个数据块服务器中。这种做法是可行的,并且不会影响元数据节点的性能,因为这里的汇报数据量很小,并且元数据节点本来也需要与每一个数据块节点进行通信,以保证每一个数据块节点的工作正常。这样,数据统计工作可以内嵌在系统监控的基础设施内部进行。另外,在进行负载均衡计算时,几千个数据选择数据容量最小的数据块节点也是非常快的,不会影响性能。这样,除了随机的算法,我们还有一个确定性更高的算法,能够保证数据块在各个数据块节点之间平均分配。

对于后一个条件,实际上不仅要求数据块分布是均衡的,而且要求热点的数据块不能分布在相同的物理节点上,否则也会影响性能。由此可以看到,这不仅需要统计每一个数据块节点的数据块数目,而且需要统计每一个数据块的访问频率以确定数据块的热度。数据块的频率是一个更加动态的信息,会随着系统运行情况的变化而发生变化。为了保证热点数据的负载均衡,采用的方法与数据块均衡的方法类似,也是每一个数据块节点向元数据节点汇报自己的数据访问情况。元数据节点收到信息后,可以衡量是否出现了热点数据访问,如果出现了热点数据访问,就将一些热点数据块迁移到其他负载较轻的节点上。由于元数据节点拥有这样的全局信息,因此其总是能够做出这样的判断,并进行负载均衡。这些数据的总数也不是很多,元数据节点进行负载均衡的计算过程不会负担过重,也就能够保证不会影响元数据节点本身的服务性能。

从上面的讨论中可以看到,一个是数据块节点的数目众多,另一个是这些负载的信息量不会很大,二者可以很好地保证元数据节点能够得到全局的信息,并做出负载均衡的策略。这样,通过元数据节点的帮助,能够比较完美地解决整个系统的数据块分布以及数据块访问的负载均衡问题,也就保证了数据块读写的高性能。

(2)元数据节点性能分析

下面需要解决的是比较难以解决的元数据节点的性能瓶颈问题。由于元数据节点只有一个,没有额外的节点能够分担元数据节点的工作,因此需要对其性能进行仔细的考虑,分析影响性能的因素及提高元数据节点性能的方法。

首先可以计算元数据节点中可能的元数据总量,看看元数据节点真正需要负担的数据量如何。可以借用谷歌文件系统中关于元数据的统计,即每一个64 MB的数据块的元数据大小约为64 B。这样的数据量是可以接受的,因为这些元数据的主要目的是指出存放对应数据块的物理节点的位置,这些位置信息包括定位到某一个具体的节点以及在该节点下如何寻找到数据块。由于我们假设系统中有数千个节点,所以单个数据块的元数据信息不会占用太多的空间。对于一个具有10 PB数据的大规模的单一名字空间的分布式文件系统来说,总体的元数据大小为10 GB左右。这样的数据量对于现在任何一台服务器的内存来说,不存在任何存储问题。因此,一个方法是将所有的元数据保存在内存中,而不是放在磁盘上。我们知道,内存和磁盘的性能有3个数量级的差别,特别是内存的时延远远小于磁盘的时延。而文件系统元数据的操作是小数据的操作,这样小数据的操作会受限于系统中的关键模块的时延。因此,使用内存的方式进行元数据的服务能够大大提高系统的服务能力。然而,这种方法会带来一个可靠性问题,因为数据被保存在内存中,一旦系统掉电,将会丢失所有元数据。而元数据在文件系统中处于特别重要的位置,元数据的丢失会导致严重的后果。总之,通过在内存中进行元数据的服务,可以提高元数据服务器(主服务器)的服务能力。由于内存与磁盘之间的性能差距,使用内存的方法进行元数据的服务,使得单台元数据服务器可以服务数千台数据服务器。

除了上述使用器件进行性能加速的方法,还有其他的系统性方法可以解决性能问题。与其他文件系统一样,分布式文件系统也可以使用缓存的方法提高系统的性能。这里的元数据也是可以进行缓存的。由于每一个数据块的数据所对应的元数据量是非常少的,因此客户端在请求时,可以请求多个数据块的元数据,例如请求100个数据块的元数据,其数据总量也不过是6.4 KB。对于这样的数据量,仅仅需要一个网络请求就可以获得。由于只需要一次网络请求,获得数据的时间等同于请求一个数据块的元数据所需要的时间。在获得一批数据块的元数据之后,可以将其缓存在客户端,客户端在请求下一个数据块或附近的数据块时,就不需要再通过访问元数据服务器来获取对应的元数据了。通过缓存的方式可以大大降低对元数据服务器的访问频率。访问频率降低后,一个元数据服务器就可以服务更多的数据服务器。在实际的系统中,这也是经常使用的提高性能的手段。但是,通过缓存的方法提高性能也有一定的缺陷,即不一致性,这个缺陷是所有采用数据缓存进行加速的方法都有的。现在,一个逻辑上的数据(即文件系统的元数据)会有不同的物理位置,一个是在元数据服务器中,另一个是缓存在一个或多个客户端中。一致性的要求是所有数据都应该是一样的,否则不同客户端看到的分布式文件系统的状态是不一样的,这是不允许的。现在,不同位置的元数据可能产生了不一致的情况,必须要进行解决。这里不一致的原因是在客户端中缓存了旧版本的数据,而在服务器端(即元数据服务器端)保存的是所有元数据的最新的状态。在正常的工作情况下,客户端可以依据自己获得的元数据寻找相应的数据服务器。但是,客户端会出现元数据过期的情况,这会导致其缓存的元数据出错。此时,系统中的主服务器会帮助完成对信息的更新,客户端只需要在探测到不一致(如数据块服务器没有保存相应块的信息)时,与主服务器通信并更新即可。

总之,在性能方面,通过上面的分析可以看到,通过将元数据保存在内存中,以及将元数据缓存在客户端,可以大大提高元数据服务器的性能。这是两种通用的方法,前者通过物理硬件的方式提高性能,而后者可以通过系统优化提高性能,这两个方面在实际的系统中都是需要考虑的。虽然在引入两者时会引入一定的新的问题,但是仍然可以通过合适的技术手段进行弥补。

2.3.2 关于可靠性方面的讨论

下面就着手解决分布式文件系统的可靠性问题。在实际的分布式系统中,可靠性问题与可用性问题是紧密相关的,但是有不同的着重点。可靠性问题一般是指数据不丢失,即出现问题时可以通过适当的技术手段恢复数据。但是可用性的要求更高,可用性不仅要求保证数据不丢失,还要求保证在需要时可以对数据进行访问。也就是说,即使是在整个系统的内部出现了节点以及网络的错误,数据对外是可以继续提供服务的。针对这里的分布式文件系统,我们笼统地用可靠性来指代可靠性以及可用性,不再进行区分。我们需要尽力实现可用性,但是在某一些特别困难的情况下,或者基于实际系统实现的考虑,只保证数据的可靠性也是可以接受的。

在澄清上述概念之后,可以看看实现可靠性需要处理的问题。实际上系统的各个组成模块都会出现问题,如磁盘、网络、节点、机柜,甚至数据中心等。在这些情况下,我们需要尽力保证数据的可靠性。这里我们先不考虑整个数据中心的损坏,先看看其他模块的失效如何处理。若磁盘出现损坏,数据不可能从当前的磁盘中获得,因此不能仅仅依赖一块磁盘来保证数据的可靠性。同样,数据的可靠性也不能仅仅依赖一个节点。这就告诉我们必须要做数据的副本,将同一个数据分散保存在多个节点的多个磁盘中。通过副本的方式可以实现在某一部分节点失效时,整个系统还有其他数据副本可用,保证了数据的可靠性。可以说,数据副本的方式是保证系统可靠性的基本手段,在本书后面的讨论中,将具体分析如何使用副本的方式解决不同的可靠性问题。

落实到这里的分布式文件系统中,可靠性问题同样可以被分为两个部分进行讨论,一个是数据块服务器的可靠性,这个看起来比较容易解决;另一个是元数据服务器的可靠性,因为只有一个元数据服务器,可靠性原则上是不能解决的,我们需要看一下如何通过扩展来解决这个问题。

(1)数据块服务器的可靠性提高方法

数据块服务器的可靠性原则是在保存一个数据块时一定要将其保存在多个数据块服务器中,不能只保存在一个数据块服务器中。现在的数据块服务器的数目非常庞大,可以有足够多的选择来保存某一个特定的数据块。因此,为了保证数据块的可靠性,需要稍微修改一下在元数据服务器中的关于数据块定位的元数据,不是将一个数据块定位到多个节点,而是将一个数据块定位到3个或更多个数据块服务器。下面的讨论暂且假设使用的副本数目为3,实际情况中,特定的3个服务器同时失效的概率是非常低的,因此可以认为保存了3个副本,就保证了对应数据块的可靠性。

另外,为了保证数据块的可靠性,需要在系统中加入一些额外的操作,主要的工作包括探测数据块服务器的失效情况以及失效之后恢复数据块副本的数目。对于探测失效情况的工作,可以通过多种途径获知失效情况,例如在数据块服务器与元数据服务器之间通信,或者在数据块服务器与客户端之间通信时都可以探知某一个数据块服务器是否失效。这里,由于网络的丢包等问题,这样的探知不会十分准确。虽然不准确但也不会存在太大的问题,因为系统的元数据服务器可以基于自己的信息来定义数据块服务器是否可以正常工作,如果出现不正常情况,可以启动副本恢复的流程。不准确性导致的最终结果只是多几个数据副本,不会造成整个系统的不可用。

与解决性能问题一样,元数据服务器在解决可靠性问题中也扮演了一个关键的系统节点裁判的角色,判断另一些服务器的工作状态。这一点非常重要,因为只有明确指出其他服务器的状态,才能够明确开始相应的处理步骤。显而易见,如果不是由单一节点决定系统中的其他节点是否处于正常工作的状态,而是由两个节点决定,那么针对某一个具体的数据块服务器,这两个节点分别依据自己的本地信息决定这个数据块服务器否正常工作。在这种情况下,很容易出现一个节点认为这个数据块服务器是正常工作的,另一个节点认为这个数据块服务器工作不正常,这就会出现问题。两个节点对整个系统的状态没有一个统一的认识,若这个状态是一个关键的状态,就很有可能使得整个系统变得不可用。这里的元数据服务器只有一个,正好可以解决这个问题,不会导致节点对系统的状态有不同的认识。但是,这个假设的前提条件是元数据服务器不会出现问题,是永远能够正常工作的。这个假设不合理,后面会讨论利用其他技术提高元数据服务器的可用性。

完成数据块服务器工作状态的探测之后,下一步的工作就是在探测到某一个数据块服务器失效时进行数据块副本的恢复。由于现在的探测工作是元数据服务器负责的,那么数据块恢复的工作也可以交给元数据服务器。这个方法也相对简单直接,在探测到数据块服务器失效之后,元数据服务器可以获知副本数目下降的那些数据块。这些数据块在其他数据块服务器中实际上还保留了其他副本,因此可以从数目众多的数据块服务器中选择新的数据块服务器,指导还拥有这些数据块的服务器将数据传输到新选择的数据块服务器中。由于每一个数据块都是相互独立的,并且数据块服务器的数目众多,因此实际上所有的数据块可以实现并行恢复,基本上与每一个数据块服务器的容量无关。数据副本的并行恢复过程是相当快的。

这里我们基本上产完成了关于数据块服务器的可靠性保证的讨论,包括对数据块服务器失效情况的探测以及恢复数据副本数目的流程。但是,实际上还留下了几个更为棘手的问题,包括数据块之间的数据一致性问题,以及数据块可靠性将依赖元数据服务器的可靠性的问题。下面先对数据块一致性问题做一个简要的讨论。这里,由于将数据块保存在多个数据块服务器中,那么一个最基本的要求就是所有的数据都应当是一样的。如果出现不同的数据块服务器保存相同的数据块,但是保存的内容却不一样,那么客户端从不同的数据块服务器获得的数据是不一样的。客户端在这种情况下显然无法继续工作,因为其无法决定哪一个数据块服务器返回的数据是正确的。这是一个由可靠性问题引入的一致性问题,往往至少与可靠性问题具有一样的处理难度。我们将在第2.3.3节具体讨论一致性问题,下面先讨论元数据服务器的可靠性问题。

(2)元数据服务器的可靠性提高方法

下面来看看提高元数据服务器的可靠性的方法。这里需要面对的至少有两个问题,一个前面已经说过了,将元数据保存在内存中是不合适的,一旦系统掉电,将丢失元数据,并且整个系统也无法使用;另一个是元数据本身包括其磁盘都会损坏,元数据同样不能只放在一个节点中。

先看第一个问题的处理手段。既然放在内存中的元数据可能会丢失,一个显而易见的处理手段就是将其保存到磁盘中,这样系统掉电之后还可以从磁盘中重新读取元数据。因此,这里的关键问题是如何保存元数据才可以保证系统的高性能,即如何将元数据快速写入磁盘以及快速从磁盘中恢复。对于磁盘的读写特征来说,从带宽上来讲,顺序的读写要远远大于随机的读写,要高出2~3个数量级,这是磁盘的机械特征所决定的,因此在将元数据写入磁盘时一定要考虑这个特征。显然,在内存中完成操作之后,立刻在磁盘中继续完成操作以达到持久性的目的是不可取的。这会引起大量的数据随机写入,急剧降低性能。另一种方法就是等待一个周期,然后将内存中的数据镜像一次性刷入磁盘中,这充分利用了磁盘的顺序写入性能。这种方式的困难在于周期不好确定,并且一次写入全部元数据所花费的时间是比较长的。例如10 GB的元数据,磁盘顺序写入的带宽为200 MB/s,读者可以自行算一下写入所需的时间。采用周期写入的方式可能会丢失比较多的元数据(考虑到未写入时发生掉电情况)。另外,在写入时,若元数据服务器不可用,或者需要采用写时复制的方式做复杂的处理,那么这段时间内的客户端写入操作可能丢失。因此需要一种元数据的写入方法,使得写入时尽量使用磁盘的顺序写入,并且在出现突然错误时也不会丢失太多客户端写入的元数据。

这里有一种极为有效的方法,就是在将元数据写入内存时,同时在磁盘中记录该操作的日志。这种方法可以保证操作日志写入时使用的是磁盘的顺序写入,不会引起任何的磁盘随机操作。一旦发生了故障或错误,只需要从磁盘中把操作日志读出,重新在内存中操作一遍,重构内存数据结构即可。重构过程的磁盘读写也是顺序的。日志记录的方法需要每次记录所需要进行的操作,而不是直接修改对应的数据,这是常用的保证元数据持久性的方法,在许多系统中被广泛使用,包括数据库以及文件系统。这样的话,在发生数据丢失时,最多丢失一条操作日志,即使是这样,也只是在磁盘中留下一条不完整的日志记录。这对于客户端来说,对应的操作是没有完成的。因此,容易判断恢复过程中哪些操作已经完成,哪些操作尚未完成。

但是单纯使用日志的方法还是会存在问题。想象一下,整个系统在长期运行的过程中会积累大量的日志。这种情况下,一旦发生故障,恢复需要从头开始,所需要的时间是非常长的。为了解决这个问题,就需要结合使用将整个内存数据刷出到磁盘镜像(内存数据快照)以及日志记录两种方法。系统在运行的过程中定时刷出内存中的所有元数据镜像,并且同时记录系统所进行的操作。那么系统的最新状态就是最近一次完整的内存镜像加上最近的操作日志。系统恢复时可以先读入最近的镜像,并在此基础上完成最近的日志操作,获得最新的元数据。这种方法不仅可以保证元数据正常操作时的高性能,所有操作基本不丢失,也可以保证恢复过程快速完成。

图2-6给出了文件系统元数据的快速记录与恢复的方法。元数据会定期进行快照,并且在两个快照之间进行日志记录的操作。如果出现错误,则从最近的快照开始,重放对应的日志就可以得到最新的元数据。

图2-6 文件系统元数据的快速记录与恢复

解决了元数据服务器保存元数据到内存的问题之后,就需要解决元数据服务器整体失效的问题。这个时候没有太多的办法,只能加入一台新的服务器作为后备,可以称这样的服务器为元数据服务器的影子服务器(这是典型的主备副本容错方案)。影子服务器在正常的操作过程中只备份元数据服务器的操作。前面已经讨论了元数据服务器的操作流程,即元数据的整体镜像与日志结合的快速元数据保存方法,这种方法也可以用于影子服务器对元数据的保存。这样的话,元数据的操作流程依次经过了元数据服务器以及影子服务器两个节点的保存。完成这样的保存工作,就可以保证在元数据服务器出现错误时,影子服务器中还有一份数据可用。另外,由于影子服务器与元数据服务器是同时运行的,如果出现元数据服务器失效,可以立即切换到影子服务器,将影子服务器升级为元数据服务器继续工作,这不仅保证了可靠性,还提供了可用性。

讨论到这里,一部分读者可能会意识到即使使用影子服务器也不能彻底解决元数据失效的问题。原因很简单,即我们无论如何也不能彻底解决影子服务器本身的可靠性问题。系统中显然会出现两个服务器都失效的情况,这个时候就不能保证元数据的可靠性。使用影子服务器的影子服务器显然不是一个好办法,因为这也不能彻底解决问题,还引入了额外的复杂性。好在两个特定的服务器同时失效的概率非常小,可以合理假设它们的失效是相互独立的。因此,基于这个前提,不需要再次引入新一级的影子服务器,在系统正常工作以及出现仅一个元数据服务器或影子服务器失效时,就依赖这两个服务器中的一个来提供元数据的服务。但是还是需要提供后备的措施,即对可能出现的两个服务器同时失效的情况进行预备处理。方法也很简单,即影子服务器可以定时将数据备份到系统中的其他多个服务器,其他服务器不需要进行元数据的服务工作,只需要保存数据即可。这种情况可以降低系统的复杂性。如果两个元数据服务器同时失效,可以从磁盘中恢复元数据到新的元数据服务器。这可能需要几十分钟的时间,好在这种情况发生的概率相当小,虽然可用性较低,但极端情况下这样处理是可以接受的。

以上讨论详细完善了关于系统可靠性的处理方法,但是还引入了一些新的问题。其中一个问题之前已经提到了,就是如何保证数据块副本之间的一致性。另一个问题没有提及,那就是元数据服务器之间的一致性。因为现在有一个元数据服务器,以及一个元数据的影子服务器,所以必须要保证它们之间的一致性。对于前面的一致性,将在第2.3.3节讨论,而对于后面的服务器之间的一致性的保证,还需要一些额外的机制。在实际的系统中,往往使用像ZooKeeper这样的外部软件来帮助。这是一个完全独立的内容,就不在这里进行讨论了。

2.3.3 关于一致性方面的讨论

这里重点需要解决的是如何维持数据块副本的一致性。下面看一下数据块副本之间的一致性如何进行维护。这实际上也需要一个系统化的方案,因为需要考虑的情况非常多:任意一个节点、任意一个网络都会出现问题,并且它们的组合数目更加庞大,无法也不可能穷尽所有的情况,需要一个系统化的模型方案来统一解决这个问题。这里就需要引入一项极为重要的技术,即副本状态机的技术。

副本状态机的工作流程如图2-7所示。它基于某一个对象的状态s,为了保证可靠性,这个对象有多个副本。在这个对象上可以进行一系列的操作,这些操作都作用到每一个副本上。在满足下面3个条件时,所有副本的最终状态都是一样的。

• 所有副本的初始状态都是一样的。

• 所有副本都按照相同的顺序进行相同数目的操作,每一个对应序号的操作都是相同的。

• 所有的操作都是确定性的。

图2-7 副本状态机模型

这样,我们可以在一组状态机上进行一系列的操作,并且保证经过相同的步骤之后,最终的状态也是一样的。这里的结论是显而易见的,只需要用数学归纳法观察这个状态机就可以理解这个过程以及结论。这里需要说一下关于操作的确定性。操作的确定性的含义是相同的操作作用在相同的状态上所获得的结果是一样的。确定性操作能够保证副本状态机的最终状态一致。一个简单的例子就是这里所有的操作都不引入随机数,比如随机往文件里写入一个值,这样的操作就不是确定性的。这种操作导致的结果就是即使进行了相同的操作,最后的结果也会不同。当然,在实际系统中这种操作也非常普遍,这个时候的处理手段就是在某一个副本(往往是主副本)上执行这个操作,然后将这个操作的结果传给其他副本,而不是把操作传给其他副本。

使用副本状态机来保证数据块的一致性,其出发点就是保证上面的3个条件。第一个条件的保证是非常容易的,因为所有数据块的初始状态都为空,没有任何数据。第三个条件,即所有的操作都是确定性的,这一点也很容易保证,因为所有的关于数据块的操作无非就是对数据块的读写,读操作不会改变数据块的内容,写操作是一些关于偏移以及写入的数据,这些操作无疑都是确定性的。那么关键的地方就在于如何保证第二个条件,即所有的操作按照相同的顺序作用在所有的状态机副本上。

因此,这里关键的一点就是将所有的操作规定一个顺序,然后所有的状态机都按照这个顺序执行操作。等价地说,需要将所有的在一个数据块上的操作都进行定序,标记一个序号(标记为第0、1、2、3…号操作)。如果能够有一台机器负责定序,那么定序问题也是非常容易解决的,即首先将所有的数据都发送给一个节点,然后这个节点定出一个序号,这个序号就是全局定序的序号。如果定序由两个节点完成,就需要在这两个节点之间进行协调,而这种协调是很难的,这在维护元数据服务器与其影子服务器之间一致性时就能看到,不如让单台服务器去定序简单。下面就需要看一下这样个方案是否可行。首先,对于定序的节点,比较好的选择是保存这个数据块的3个数据块服务器中的一个。如果选择元数据服务器节点,会大大加重元数据服务器节点的负担,因为元数据服务器节点将需要为所有的写入操作定序,这在性能上是不可接受的。第二个问题是这个节点是固定的还是浮动的。显然,固定一个节点是不可能的,这个节点一旦失效,定序的问题就没办法解决了,因此这个定序的节点需要在这3个数据块服务器之间进行轮换。同样地,必须要获得一个轮换的顺序,使得这3个服务器都确定知道当前的定序服务器。不能在时间上出现重叠,重叠的话序号就不能确定了。这实际上还是一个比较难以解决的问题。好在现在是定一个服务器,而不是直接定操作的顺序,定一个服务器的负载要比定操作的负载轻很多。这样的话,确定一个定序服务器的工作可以交给元数据服务器,并且这种方法也能够保证即使某一个定序服务器失效也不会出现问题。这样就可以由元数据服务器来确定一个新的定序服务器。当然,定序服务器的选择也要具有可靠性,前提条件是元数据服务器是可靠的,对于这一点,通过前面的一系列技术手段,元数据服务器的可靠性已经提高到可接受的程度。通过前面的讨论,所有的副本状态机中的操作顺序按照两个因素来确定,一个是由元数据服务器指定的定序服务器能够进行定序的时间区间,另一个是定序服务器自己确定的操作顺序。一个操作的顺序的标号为一个二元组:[定序服务器的定序时间区间,定序服务器给出的操作顺序]。

这里的时间区间相当于定序服务器从元数据服务器中获得一个定序租期,在租期过期之前需要续订。如果续订不成功,就说明定序服务器已经发生了更改,原来的定序服务器就不能继续进行定序的工作了。

可以看出,通过这个二元组,能够对某一个数据块的所有操作定义出一个全局的顺序。之后,保存这个数据块的所有节点按照这个全局的顺序操作对应的数据块。至此,前面副本状态机的3个条件都可以满足了,可以通过副本状态机的方式操作3个副本,并且可以保证经过相同的有限步骤之后,3个副本都是一致的。

通过上述的讨论可以看到,通过副本状态机的方式可以实现数据块的一致性。随后就可以讨论真正的数据写入流程。依据前面的副本状态机的基本思路,可以勾勒出一个细致的写入流程,如图2-8所示。之前的讨论中,对于写入流程的描述十分粗略,为了能够尽可能地了解写入流程,对每一个步骤进行细化是十分必要的。当然,这里的每一步都是必需的步骤,读者自己也可以通过分析获得。

图2-8 分布式文件系统的数据写入流程

步骤1:客户端向元数据服务器申请需要具体写入的3个数据块服务器列表。

步骤2:元数据服务器返回数据块服务器列表包括这3个服务器的具体位置以及哪一个服务器为当前的定序服务器。

步骤3:客户端可以将所有的数据传输给所选择的数据块服务器,可以做一个小优化,即通过流水线的方式传输数据,客户端只需要将数据传输给一个数据块服务器,而数据块服务器形成一个流水线来接收数据。需要注意的是,这里的数据接收完成不代表写入完成。

步骤4:客户端现在可以发出写入操作,因为已经知道了定序服务器,将这个写入操作发给定序服务器即可。此时,定序服务器已经被授权可以进行定序的工作。

步骤5:定序服务器完成定序工作,如果只有一个客户端写入,直接按照接收的写入操作顺序定序即可;如果存在多个客户端的并发写入,那么定序服务器自行定义出一个顺序即可。定序服务器按照自己定序的结果写入数据,并且将这个顺序告诉其他数据块服务器,让其他数据块服务器也按照同样的顺序写入数据。

步骤6:定序服务器等待其他数据块服务器的返回,在其他数据块服务器返回之前是不会响应客户端的。

步骤7:在其他两个数据块服务器返回正确写入之后,定序服务器返回成功操作给客户端,指示本次写入操作成功完成。

值得注意的是,以上各个操作步骤并没有说明任意一步失败需要如何处理。在实际工作流程中,其中的任意一个步骤都可能出现错误。任意一个步骤出现了错误,就会导致后面的一系列步骤不能进行下去,最后,要么是客户端超时探测到错误,要么是定序服务器返回错误。定序服务器也可以通过超时的办法探测其他错误。探测到错误之后,客户端可以重复上述过程,直到写入成功为止,或者尝试几次之后报告给应用程序无法写入。由于整个系统的数据块服务器数目众多,一般来说,经过少数的几次重复就会成功,否则就是整个系统出现严重错误。这里,若出现错误,就不能保证3个数据块副本的一致性了,但是这是可以接受的,因为客户端已经明确获知了写入的错误情况,对于写入错误的数据,读出的内容是没有定义的,应用程序不能使用,需要特殊处理,这也是标准的文件系统的语义所允许的。当然,也有可能所有的工作都完成了,只是最后一步没有返回给客户端,那么客户端仍然可以认为写入失败而进行重写。在分布式系统中,这种悲观的做法往往是正确的做法,但是对性能会有所影响。

到现在为止,基本上已经将一个分布式文件系统的基本架构,对性能的分析,对可靠性的分析以及对一致性的分析讨论完整了。虽然还留了一些不太容易解决的问题,如两个活动的元数据服务器的问题,但是,总体来看,整个文件系统在上述的设计以及技术完善下可以正常地工作。只不过现在应用程序需要去适应这个分布式文件系统的特殊的一致性语义。我们也知道,在分布式情况下,为了性能有时这也是不得已的选择。

2.3.4 其他特性讨论

(1)分布式文件系统的数据删除操作

前面的讨论基本上已经覆盖了常见的文件系统的所有操作,包括元数据的操作以及数据的读写操作。读者可以体会这些操作在分布式文件系统中如何进行,如创建文件、创建目录、写入数据或读出数据。但是有一个最基本的操作还没有涉及,这个基本操作就是文件的删除操作。这个删除操作并不好实现。这也是分布式文件系统与单机的文件系统的一个比较重要的不同之处。

在传统的文件系统中,删除时并不需要真正地把数据删除,因为实际操作的是磁盘的数据块。文件删除的操作首先将文件从目录树中删除,之后再将文件所占用的数据块放置到空闲数据块列表中,完成删除操作。因为在单机文件系统中只有一个节点,所以这个流程在节点内部是可以顺利完成的。即使节点在执行的过程中出现错误,在重启之后,这个过程也可以继续完成。如果读者对日志文件系统有所了解,就很容易理解这个过程在单机文件系统中是如何处理的。

在分布式文件系统中,依据前面的设计,元数据与数据块是分别存放的,分别放置在元数据服务器以及数据块服务器中。现在的问题就是在删除文件的流程中,需要知道数据块所在的数据块服务器,因此不能直接删除文件的元数据。但是,如果数据块服务器不在线,那么对应的数据块不能从数据块服务器中删除,进而元数据服务器中的元数据也不能被删除。这就会对删除操作带来困难,进而无法完成。这种情况本质上来说是多个服务器维持一个逻辑上完整的对象,因此删除工作在这些服务器同时在线、正常工作的条件下才能完成。但是,保存数据块及元数据的所有服务器不可能总是持续在线的,肯定存在某些服务器不能在线的情况,这就造成删除不成功。这个时候,元数据必须等到所有数据块删除完成之后才能被删除,但是实际上这也会成为一个问题,因为某一些服务器如果彻底损坏,这些服务器是不会再上线的,对应的元数据就无法通过自动的方式删除。此时,元数据又会被永远保存在元数据服务器中,这也是不可接受的。

这个矛盾看似没法解决,但是实际上只需要删除元数据服务器中的元数据即可。在支持这个简单的删除操作时,分布式文件系统需要一整套垃圾搜集的办法。这里的垃圾搜集与Java语言中的垃圾搜集类似,垃圾也被定义为系统中没有元数据描述的数据块。有了垃圾搜集之后,文件的删除操作支持起来就十分简单了,只需要将数据从文件系统的元数据空间中删除即可,这样关于这个文件的所有数据就会变成垃圾,将会依靠垃圾搜集的方法进行回收工作。

垃圾搜集的工作不仅是对文件删除进行支持,实际上垃圾搜集的工作是分布式文件系统的一项必需的工作。在数据块服务器暂时下线,恢复之后继续上线的过程中,元数据服务器可能已经发起数据块副本恢复的工作,那么这些新上线的数据块很有可能成为垃圾。另外,数据写入失败,并且客户端重新发起写操作时,原来写操作失败的数据块也会成为垃圾。这些情况在大规模系统中是不可避免的。

下面来看一下垃圾搜集的简要流程。首先是垃圾搜集工作的频率一般不会太高,因为假设的条件是一个集中管理的集群,节点条件相对较好。因此垃圾搜集的工作可以由元数据服务器完成。元数据服务器发起垃圾搜集时,会询问每一个数据块服务器持有的数据块的情况。由于每一个数据块都有一个唯一的标识,依据这个标识,元数据服务器可以查看在其保存的元数据中是否有对应的记录。如果没有对应的记录,对应的数据块就可以被认为是一个垃圾(有这个数据块,但是在元数据中没有引用),通知对应的数据块服务器将其删除即可。这个工作并不需要全局的信息,元数据服务器可以分别与每一个数据块服务器进行交互,清除其垃圾数据块即可。

(2)分布式文件系统的数据原子追加操作

分布式文件系统除了上述基本的操作,由于其本身的特征以及可能的应用程序的需求,会提供一些额外的功能。例如,在我们设计的分布式文件系统中还可以提供一个数据原子追加的操作,并且还具有不同于传统的文件系统一致性语义。这里的追加操作是这个文件系统特有的,原因是在谷歌(Google)的一些应用中,例如针对搜索引擎爬虫的应用,只需要下载数据,将数据追加到大文件的最后即可,并不需要一个明确的文件写入地址。这样可以引入一个原子追加的操作,使用原子追加不仅可以保证按照应用程序的意愿将数据追加到文件末尾,也可以提高文件系统的总体性能。

在图2-8所示的数据写入流程中也可以加入原子追加的工作,只需要在定序服务器中进行一些额外的处理即可。此时,定序服务器不需要判断文件的写入位置,而是要判断写入的数据是否会超过对应数据块的界限,如果超过就不能追加了,否则会引起追加错误。如果能够对当前这个数据块的内容进行追加,定序服务器将数据追加到当前的数据块,并将写入的偏移告诉其他两个数据块服务器。其他两个数据块服务器将数据写入同样的位置。当然,如果有任意一个副本出现错误,客户端将再次执行这个操作,那么就可能存在多次追加操作的数据。但是,在追加成功的条件下,3个数据块服务器能够保证数据是作为整体原子性写入的。这个时候的一致性模型(文件系统的一致性语义)又产生了变化。在原子性写入成功的那一个区域,所有的数据是客户端追加的数据。但是,由于数据超过块边界时,数据块服务器主动放弃追加,因此这部分数据就是不一致的,并且追加不成功的区域也是不一致的。若数据小于一个数据块大小,追加时会在该数据后面补充一部分数据,使得其与数据块大小相等。然而在3个数据块服务器中,补充的那部分数据不需要保持一致。这里造成的后果是在追加成功的那些区域中,部分数据确实是一致的,并且与用户提供的追加数据一样,但是也存在一些不一致的数据(即补充的那部分数据)。这种情况对性能是有好处的,但是会带来新的数据区域不一致问题。应用程序必须要自行进行处理,例如自行对数据进行校验,以获得有意义的数据。

(3)分布式文件系统的快照

随着现代操作系统以及上层应用的丰富,现在的文件系统变得越来越复杂,增加的功能也越来越多。这一点也体现在了分布式文件系统之上。为了能够解决文件系统由于误操作引起的数据丢失问题,现在的文件系统开始加入快照的功能。快照功能简单地说就是记录文件系统的某一个历史状态,等将来有需要时可以将文件系统的状态回滚到历史上的某一个时间点。

下面分析如何在一个分布式文件系统的范围内支持一个文件系统的快照,或某一个目录及文件的快照。同样地,这个问题可以分为两个部分考虑,一个是如何对元数据进行快照的操作,另一个是如何对数据进行快照的操作。对于元数据来说,快照比较简单,只需要复制一份元数据即可,而对于数据来说,需要进行写时复制(Copy-on-Write)的操作。但是,由于现在有多个并发写入的客户端,因此需要对写入的客户端进行控制。好在这个分布式文件系统有一个定序服务器对写入进行控制。如果元数据服务器取消了定序服务器的定序权利,那么客户端进行写入操作前,需要从元数据服务器中获取下一个定序服务器,此时元数据服务器可以阻止客户端的写入操作,进而完成写时复制的操作。

具体来说,可以通过以下流程完成对数据的快照操作。首先是客户端发出一个快照的操作,这个快照的操作实际上就是一条存放在元数据操作日志序列中的一条日志。元数据服务器收到这个请求时,取消所有定序服务器的定序权利,或者等待定序服务器定序权利周期结束后不再发出定序权利。等到所有定序服务器都不再有效时,文件系统可以保证涉及的所有数据写入操作不能够成功,此时元数据服务器会将这条操作日志写入磁盘中。对内存中的数据结构进行复制,将元数据复制为两份,但是两份元数据都指向原来的数据块。这种复制完成之后,内存中就会生成新的快照,可以支持不同快照的读取操作,并且互不影响。对于整个文件系统来说,快照在这个时候就算是完成了。但是,之后的数据写入操作需要进行特殊的处理。如果有一个新的写入操作,那么这个写入操作必须先跟元数据服务器打交道,因为客户端需要知道是哪3台数据块服务器。这个时候,元数据服务器可以在客户端直接操作数据块服务器之前让这3台数据块服务器完成数据复制的工作,并且更新数据块与元数据的对应关系。之后,就可以让客户端进行写入操作,完成数据的快照工作。上述流程的关键在于各项工作的操作顺序,如果顺序不对,就很容易造成错误。在其他分布式系统中的操作顺序也非常重要,值得注意。