元数据系统是对象存储的核心。作者通过汇集在云存储系统的研发和运营实践中获得经验,探讨如何建立和保障一个元数据系统。本文主要讲述多副本模型,并从多方面比较主从模型与多副本模型的优缺点及解决问题的方案。
多副本模型
下面我们考查一个模型,暂且简单地称其为多副本模型。因为这种模型,就是利用众多元数据副本来保证可靠性和可用性的。与主从模型不同的是,多副本模型的各副本之间没有主次之分,所有的副本都处于相同的地位。因而,在向多副本模型写入元数据时,是同时向这些副本发起写入。而读取元数据时,也是向这些副本同时读(图1)。
图1 多副本模型
多副本模型基于这样一个简单而直观的思路:单点会造成数据丢失,并引发可用性问题,那么就将数据同时写入多个服务器,以防单点的出现。不同于主从模型中,读写都针对一台主服务器,从服务器只是间接地参与可靠性和可用性的保障,多副本模型的每台服务器任何时刻都在直接发挥着保障作用。
由于每次读写都施加在所有的副本服务器上,任何时刻都有不止一份数据被保存下来,所以可靠性自然就解决了。同样,任何时刻都有不止一台服务器在运行,它们同时下线的可能性极小(暂且不考虑机房级别的整体故障),所以可用性也无需额外措施,便可以得到保障。正是因为每台副本服务器都是平等的,所以任何一台服务器下线都不会对整个集群的可用性和可靠性产生影响,系统仍然依照正常情况下的方式运行,别无二致。
这是多副本模型相对于主从模型的最大优势。无论哪台服务器下线,都不会引起系统运行状态的变化,运维人员可以从容地进行善后处理,没有服务下线的顾虑。而软硬件升级之类的日常维护工作,也不需要大动干戈地进行主从切换之类的危险操作,直接将服务器下线,进行更新便可。多副本模型的系统甚至可以容忍多台服务器下线,而不会造成系统运行的波动。具体的容忍限度取决于配置设定,关于这一点,后文有详细论述。
任何事物都有两面,既然多副本模型有它的优点,自然也有缺点。多副本的问题主要在一致性方面。由于我们允许副本服务器写入失败,再加上各种原因造成的数据退化,所以副本服务器之间的数据会不一样。当我们读取的时候以哪个为准呢?
这是一个值得考虑的问题。我们无法确定哪台副本服务器包含了完整新鲜的数据。实际上不可能有这样的服务器存在。因而我们也就无法从任何一台服务器中准确无误地读出所需的数据。唯一的办法就是同时读所有的副本服务器,综合所得的副本数据,以获得所需的信息。
如何综合副本数据呢?首先要确定基准。基准就是判定有效数据的标准,有时间基准和空间基准之分。时间基准用来处理元数据的先后覆盖,而空间基准用于处理副本之间的对应关系。在描述如何确定基准之前,先定下这样一个准则:对于一个数据对象的元数据,只有时间最近的那条是有效的。有了这样的准则,建立基准就容易了。我们可以为每一次元数据写入加上一个时间戳,并且确保一次元数据写入的各个副本拥有相同的时间戳,同时还需保证历次写入的元数据拥有不同的时间戳。只要能确保这两个条件,数据的基准就容易确立了。
具体的方法并不复杂,将读取到的副本数据放在一起比对,时间戳上最近的那些副本,就是所需要的。但并非所有的最大时间戳都是有效的。假设这样一种情况:一个元数据系统有5个副本,一次写入时,由于种种原因,只有两个副本写成功了。在读取这条元数据时,恰好那两台写成功的副本服务器下了线。这种情况下,这条元数据就无法读到了。其结果就是元数据不见了,直到那两台服务器重新上线。
为了避免这种情况的出现,我们需要引入一个规则,以确保无论怎样都能读到正确的数据。
这样的方法有不少,最常用的是WRN算法(图2)。这种算法比较简单,容易实现,使用也较广。算法的具体操作如下。
图2 WRN算法
假设有N个副本,当需要访问数据时,同时写或者读所有副本。如果写入时,有超过W个副本写成功,那么就认为这次写入是成功的,否则就算失败,向客户端反馈写入失败。读取时也一样,如果有超过R个副本读取成功,就认为这次读取是成功的,否则就算失败,或者数据不存在(具体是失败,还是不存在,需根据副本读取的结果加以判别)。W和R必须满足一个条件:W+R>N。只要满足这个条件,成功写和成功读的副本之间必然存在重叠,因而肯定可以读到至少一个有效的数据。
在具体使用中,WRN算法还有不少需要注意的细节。首先,具体读取各副本时,最简单的策略是取得该副本服务器中最新的那条元数据,然后依靠WRN算法整合这些所得的数据。这种做法在一般情况下可以正常工作,但在一些异常情况中会存在一致性问题。一种情况是一次写入时,没有满足成功写入W份副本的条件,那么这次写入算作失败。但其中写成功的副本因为时间戳更近,读取该副本时,会覆盖先前成功写入的那些副本。于是,最新写入失败的那个元数据读取时没有达到足够数量,而先前成功写入的元数据因为某些副本被这次失败的残留副本遮盖,也无法达到R数。于是便会出现无法取得有效元数据的情况。
这种情况属于低概率事件,但云存储系统经年累月不间断地运行,任何低概率的事件都会发生,而且必然发生。一旦发生,便会引发数据错乱的情况,影响用户的使用和服务的声誉。
针对这样的问题,有一些解决手段。最基本的手段是对于失败的写入操作,将各副本进行回滚。也就是将那些已成功写入的元数据条目副本删除。这种做法可以在一定程度上有效地降低问题发生的概率。但基于“任何东西都会出错”这样一个事实,我们认为回滚也会失败。回滚失败意味着依然会发生元数据无效的问题。
另外,还有一个解决方法:读取副本时不是只读最近的那一条元数据,而是读出几条。把各副本读到的元数据根据时间戳相互对位,还原出最原始的写入状态。在此基础上,剔除那些失败的写入数据,得到正确的数据。
还原写入历史的操作要求元数据在数据库中采用只增的方式,以便保留历史的条目。每次元数据的写访问,无论是新增还是覆盖,都是增加一条记录。与原有的元数据记录不同的是,这条记录携带新的时间戳。对于删除操作,则同样增加一条数据,并且设置记录中的“删除”标志,系统将根据此标志判断记录的删除操作。(只增也有助于避免事务,减少数据库锁的使用,对提升性能有所帮助。)
但与任何一种问题解决手段一样,这种还原历史操作的方法也不能彻底消除一致性的问题,原因是数据退化。假设一次写入正好成功了W个副本,那么这次写入尽管成功了,但仍处于临界状态。如果有一个原先成功的副本发生了丢失,那么之后这次写入将会被认为是失败的,而被忽略。尽管这是更加鲜有的情况,但确实会发生,而且无法直接消除。一般情况下,我们也只能容忍这种情况的存在,毕竟发生的概率已经非常非常小了。
一些辅助手段可以进一步减少这类问题发生的可能性。如果一次写入的成功数超过了W,但依旧有少量失败,那么要实时地对这些失败写入进行修复,例如重试,或者使用异步的重试队列对失败进行修复。这种手段和失败回滚一起可以很有效地减少容易产生混淆的临界状态的存在。
失败修复和回滚之类的错误处理手段实际上并非很多人认为的那么有效,错误处理本身也会失败。如果试图以错误处理消除问题的隐患,那么必须也要对错误处理的失败情况加以处理。于是,就会有错误处理的错误处理,错误处理的错误处理的错误处理……如此便无穷无尽,这自然不是解决问题之道。错误处理的作用是减小问题发生的概率,最终将其减小到一个可以接受的范围。
但考虑到数据退化,仅有简单的修复是不够的。一则错误处理不可能永远成功,二则并非每一次数据丢失都会被感知。我们需要一个最终的一致性修复手段,这个手段可以弥补其他异常处理失败造成的问题。但这样的手段也会失败,为了能够最终确保错误得到修复,这种手段的失败处理方式就是它自己。一般情况下,最终手段不需要有很强的实时性要求,它的任务在于应对那些概率很低的情况。在对象存储系统中,这个手段就是定期合成元数据快照。
元数据快照的做法是这样的:定期导出所有副本的元数据,按照时间戳匹配各条元数据,然后依照WRN算法逐条将所有副本整合,抛弃那些失败的残留数据,剔除被覆盖的元数据。最终生成一个完整一致的快照。然后再重新载入元数据库,替换快照所涵盖的数据。
快照生成的主要问题是时间。元数据的量为数不少,通常都会在TB级别之上。对这样的数据规模进行匹配和计算,往往需要在一个集群中以Map-Reduce处理。考虑到成本,这样的集群规模不可能很大,因而处理的时间将会比较长,可能达数小时。而且,数据的导出和加载也需要较长的时间。同时,作为一个定期的运维过程,如何自动化,减少人力参与也是非常重要的。总体上,如何快速并且自动地合成快照是颇有挑战的任务。
快照还有一些额外的作用,包括:生成每个容器的数据量、对象数等统计信息;生成每块硬盘的对象清单,用于磁盘失效后的数据恢复和数据对象的校对。
快照的生成周期通常设置在一天。那么一条元数据的不一致情况最多只会存在一天,一天之后,该条元数据的所有副本又会恢复到一致的状态。而只要一天内没有发生多台服务器同时丢失数据的情况,元数据的可靠性便可以得到很好的保障。我们相信一天内2台以上服务器发生数据丢失的可能性极小。反倒是人为的操作失误更容易造成这类事故。而这就是另一个问题了,不在本文的讨论范围内。
除了上述这些奇妙问题之外,还有一个在WRN理论中没有被提及的问题。多数涉及WRN的论文都说这个算法可以保证强一致性,但实际上这是错的。WRN算法如果要保证强一致性需要有一个条件,就是对每一个副本的写入要么成功,要么失败。在实际的使用过程中,副本的写入却有第三种状态。
对于一个副本的写入必然是跨越网络的。于是,一次副本写入实际包含三个步骤:向副本服务器发送请求;副本服务器写入数据库;反馈写入的结果。如果前两个步骤发生错误,那么这次副本写入操作就是完完全全的失败。但如果前两个步骤都成功了,但第三个步骤出现了问题,如网络连接中断,或者数据报文丢失等,客户端会认为这次操作是失败的。但实际上这条数据已正确地写入了数据库。如果此时反馈信息恰巧达不到W数,那么就会回复用户这次元数据写入失败。而实际数据库中,成功写入的副本数却是满足W数的。在下一次读取这个元数据时,却读到了被认为失败的元数据。也就是说,外界认为一个不存在的元数据,在系统中却是真实存在的。这种一致性问题同样是无法消除的。失败回滚操作可以减少问题的发生概率,但终究无法彻底杜绝。
整体上,多副本模型轻而易举地解决了可靠性和可用性,却将问题集中到一致性上。一致性问题有很多既定的解决手段,每一种手段都有各自的问题,无法彻底将一致性问题解决。但综合这些手段,可以很好地将一致性提升到近似的强一致性级别,从而满足实际的需要。
WRN算法在具体使用中,N、W、R的选择颇有些讲究。首先,N数不能太少。我们来看几种不同的WRN设置:
N=3,W=2,R=2,W+R-N=1
N=6,W=4,R=4,W+R-N=2
N=9,W=6,R=6,W+R-N=3
这三种配置都满足W+R>N。但效果却完全不同。对于N=3的配置,只能容忍1台服务器下线。另外一个更麻烦的问题是,尽管写入时有两个副本成功了,一个副本失败,这算是成功的。但如果其中一个成功写入的副本发生了丢失,这时候残存的只有一个副本,那么我们在读取时,实际上无法判别这一个副本是写入失败残存的副本,还是副本退化造成的。N=3时,这种混淆很容易发生。
而随着N数的增大,所能够容忍的副本下线和副本退化的数量就多了。在既定时间段内,同时下线多个副本,或者同时丢失多个副本的概率就小很多了。因此,从一致性保障角度而言,N数越大越好。当然,考虑到资源,N数终究不可能无限多,一般6-9是比较容易接受的数字。尽管副本数如此多,但毕竟元数据相对数据存储本身有着多个数量级的差异,所占用的服务器资源毕竟还是少数。
在这里,我们可以看到那些去中心化方案在一致性保障方面的缺陷:因为数据本身的量较大,所以考虑到成本,副本数不会太多,通常会选择N=3。很明显,3这个数字对于一致性保障而言还是相当脆弱的。
至于W和R的具体数值,一般没有太多的教条,但它们受制于存储系统的部署。通常,对于云存储系统而言,为了保障尽可能高的可靠性和可用性,会设法将存储集群按照副本分散到不同的子集群中。每个子集群拥有独立的交换机、独立的配电线路,最好在物理位置上也相互独立。最极端的情况就是分布在不同的IDC中(当然不是所有的云计算服务商都有这样的资源)。这样的子集群通常称为zone。设置这样的zone的目的是为了消除在基础设施上的单点故障。
在这样的部署结构下,W和R的选择应当确保在一个zone里的副本服务器数量不大于W+R-N。这样的设置,是为了保证一个zone下线后,系统依然有足够的副本服务器满足WRN算法。
主从模型vs.多副本模型
从前面关于主从模型和多副本模型的介绍可以看到两者的差异。主从模型中,主服务器必然会由于各种原因而下线。一旦主服务器下线,那么就必须即刻采取行动,将从服务器切换成主服务器,保证系统依旧在线。但这种切换操作同样也会失败,而且失败的可能性并不小。
多副本模型更易于保障可靠性和可用性。在多副本模型下,一个成功写入的元数据任何时刻都会有多个副本存在,任何时刻都会有多个实例在提供服务。服务器单点故障不会对元数据系统的运行产生影响,整个系统还是按照正常的运作方式运行。这一点非常重要,这意味着可以将一个实时的在线运维事件变成了可以暂缓处理的离线运维事件。任何一台元数据服务器出现故障,也不需要立刻做出反应。系统继续正常运行,运维人员则可以从容地处理问题。这是确保高可用性的关键所在。
主从模型本可以简化程序的结构,因为客户端只需要向主服务器发出请求,访问元数据。但如同前面所描述的,可靠性、可用性和一致性的要求促使主从之间进行并发访问,以弥补这些特性方面的漏洞。这实际上就是将原本在元数据系统之外的多副本访问逻辑移到主服务器中,却凭空增加了延迟,以及主从切换的麻烦。
在一致性方面,主从模型在没有考虑软硬件异常和数据退化的情况下,基本上没有什么难点。但因为现实中没有软硬件异常是不可能的,数据退化也是时刻在发生的。于是,无论主、从服务器都可能缺少数据,更无法单纯通过主服务器来维持数据的一致性。为了获得尽可能高的一致性,主从模型也需要通过整合主从副本来实现一致性保障,而副本修复也同样是不可缺少的一部分。实际上,无论是主还是从,都应当看作是一个元数据的副本,既然是多个副本,那么也就存在一致性的问题。多副本遇到的问题,主从模型同样也会遇到,这就是主从模型不如多副本模型来得合用的原因。
主从模型的主要优势是可以执行复杂的数据库逻辑,如关联、聚合等。但这在多副本模型下非常难。主从模型下,一个复杂的查询得到执行后,将幂等的结果数据同步到从服务器。而对多副本模型来说,每个副本独自处理查询,会产生混乱的结果,因为复杂查询往往是非幂等的。
对象存储之所以可以使用多副本,是因为对象存储的业务模型已被简化到不需要复杂查询的地步。所有写入操作都只是增加一条元数据而已,天生的幂等操作。对象存储放弃目录结构,只实现Key/Value模型,也是为了避免复杂查询的存在。
总结
在对象存储中,元数据用于保存数据存储位置、数据属性等信息。元数据的好坏关系到整个对象存储系统的可靠性、可用性、一致性等方面。元数据有如此重要的功能,必须有强有力的保障加以支撑。
元数据的存在虽然表面上增加了系统的复杂性,但实际上是将一致性保障、数据存储基准、可靠性检测和修复、数据统计等重要的功能集中到更小的数据量上,更加易于实现和操作。这在去中心化的方案中是难以实现的。
元数据的两种主要模型包括主从模型和多副本模型。主从模型,作为常规的数据库高可用手段,并不能满足云存储系统的诸多特性。而多副本模型则能更好地平衡各方面需求,同时也很好地平衡了研发、部署和运维等方面的要求。
多副本模型的核心难点集中在一致性保障方面,针对这些问题都有相应的解决手段,但都无法完全消除问题的存在。综合运用各种保障方法,可以极大地减少一致性问题的发生概率,最终将其降低到服务可以接受的程度。
作者莫华枫,任职于七牛云存储。曾担任盛大云计算云存储部门技术负责人、部门主管。在大规模存储系统、在线服务系统、分布式集群方面拥有丰富经验。