元数据系统是对象存储的核心,也是云存储系统架构和保障的重中之重。本文试图通过汇集在云存储系统的研发和运营实践中获得的知识和经验,探讨如何建立和保障一个元数据系统,以便为对象存储系统建立坚实的基础。
本文主旨并非阐述一个可用的架构设计,而是展示出对象存储的元数据保障中将会遇到的问题。问题往往比答案更重要。多数问题只要认识清楚,解决并非难事。但需要注意的是,没有哪个问题存在十全十美的彻底解决方案。我们所采取的策略更多的是将一个关键性问题转化成另一个次要和易于处理的问题,或者将一个问题发生的可能性尽可能降低。很多问题是无法彻底解决的,我们的现实目标是将其发生的概率下降到业务可接受的范围。因此,希望大家能够更多地关注和发掘所存在的问题,而不是紧盯方案和设计。我们相信,认清了问题之后,找到解决办法并非难事。
在开始讨论元数据保障的问题之前,我们先要牢牢记住一句话:“任何事物都会出错。”这个真理时时刻刻在发挥着惊人的作用。
元数据
元数据系统的好坏关系到整个对象存储系统的可靠性、可用性和一致性,并且会影响到性能。因此,元数据部分是对象存储系统的核心,也是架构和保障的重中之重。
元数据最基本的作用在于数据对象的定位。对象存储的任务是保存用户提交的数据对象,并以用户指定的名称(Key)对其标识。用户如需获取一个数据对象,要向对象存储系统提交Key,存储系统便会根据Key找到相应的数据对象,然后反馈给用户。存储系统根据Key找到数据对象存放位置的过程,便是依托元数据完成的。图1是元数据方案架构图。
元数据的核心内容是Key=>Pos(存储位置)的映射,本质上是一个Map。此外,元数据还承载了数据对象归属(容器和用户)、大小、校验值等元信息,用于记账、校对、修复和分析等辅助操作。
说到这里,大家或许会有疑问:现今的存储系统都流行去中心化,设法去掉元数据,为何还要谈元数据的保障?原因其实很简单,当今各种去中心化的存储设计,在云存储中并不适用。其中原因颇为繁复,已超出本文范畴,这里只做简单说明。
去中心化的方案利用算法确定数据对象的位置。最常见的算法是一致性哈希。这些方案的问题在于,如果仅考虑数据对象的存取,逻辑很简单,实现上也简单。但如果考虑到可靠性、可用性、一致性,以及运维,就会变得复杂。
数据对象的存储副本总会损坏和丢失。一旦丢失,需要尽快修复。最极端的情况是某一磁盘失效,进而造成大量的数据副本需要修复。去中心化方案将数据对象的副本同磁盘一一对应起来,结果就是修复的数据只能灌入一块磁盘。单块磁盘的吞吐能力有限,从而造成一块磁盘的数据需要近一天的时间方能修复。而在大型存储系统中,这段时间内可能会有更多的磁盘发生损坏,因而可能造成数据的丢失。
同样因为数据对象与服务器和磁盘绑定,一旦一台服务器下线,便会降低相应对象的存取可用性。有些方案临时将数据对象映射到其他服务器,从而提高可用性,但临时状态的管理、其间的异常处理等问题的复杂性远远超出了去中心化所带来的便利。
当存储系统扩容时,去中心化方案需要在服务器之间迁移大量的数据,以求各磁盘的可用容量达到平衡。迁移规模随扩容规模增大而增加。迁移过程消耗系统资源,并且存在中间状态,使得数据存取逻辑复杂化。而且漫长的迁移过程中一旦发生异常,其处理过程相当繁复。
在一致性方面,去中心化方案碍于副本数较少,在某些特定情况下会造成一致性问题,而且难以修复。关于这一点,后面会具体论述。
对于list操作、数据校验、记账等辅助操作,各种去中心化的方案都很不友好。因为数据量庞大,无法通过遍历所有数据计算用量、分析统计。因此,一些去中心化方案会设置数据对象和用户的关联数据库,以方便使用量计算。这些数据库实际上就是一种元数据库,同样面临着元数据保障的问题。
去中心化方案的诸多问题并非无法解决,总能找到合适的方法应对。但其实这只是将压力转移到运维阶段,使得运维成为瓶颈。我们说存储系统是“三分研发,七分运维”,运维是重中之重,无论从设计还是研发角度,都应当以方便运维,减少运维压力为先。表1中对比了去中心化方案和元数据方案。
元数据的特性
让我们回到元数据上来。元数据相对于数据对象本身,总量较少。根据统计,元数据和数据对象的大小之比大体在1:100到1:10000之间,具体的比率,取决于数据对象的平均大小。因此,元数据相比数据对象本身更加容易操作和处理。
当用户索取数据对象时,存储系统需要检索元数据库,找到相应的元数据条目,获得存储信息后,进行读取。因此,元数据的操作是查询操作,也就是数据库操作。正因为元数据的量较少,并且都是结构化数据,使得这种检索得以方便地实现。
即便如此,当存储量达到几十PB级别时,元数据的数据量也将达到几十上百TB的级别。这已远超出单台服务器的存储能力。因此,元数据库天生便是一个分布式集群。在分布式数据库集群中,很难维持复杂的数据结构,特别是在可靠性、可用性、一致性,以及性能的约束下,同时满足这些要求是极难做到的。在动辄数以PB计的云存储系统中,只得维持最简单的对象存储形态,这也就是大型的云存储系统不支持文件系统的原因。
元数据的重要性在于它是整个数据存储的基准:整个系统拥有哪些数据对象,归属哪些容器、哪些用户。一个数据对象,元数据说有,就有了;元数据说没有,就没有了。所以,元数据的可靠性代表着存储系统的可靠性。数据校验、空间回收等操作都依赖于元数据。元数据一旦有所差错,必然造成整个数据存储的错失和混乱。
同时,几乎所有操作都需要操作元数据,或读取,或写入,是存储系统的关键路径。如果元数据系统下线,那么整个存储系统就会宕机。因此,元数据部分的可用性决定了存储系统的可用性。
性能方面,几乎所有的操作都涉及到元数据,整个元数据系统的访问压力远远超过存储系统。因而,元数据的性能决定了存储系统的整体响应。
元数据的一致性也决定了存储系统的一致性。一致性决定了用户在不同时间对同一个对象访问是否得到同样的结果。当一致性发生问题时,用户在不同的时间可能获得某个对象的不同版本,甚至无法获得有效的对象。这些违背用户预期的情况往往造成用户业务逻辑的混乱,引发不可预期的结果。而在某些特定的情况下,一致性的错乱会增加数据丢失的可能性。
作为存储系统的枢纽,元数据占到整个系统大部分的维护工作量。元数据系统可维护性上的提升对于整体的运维有莫大的帮助。
元数据保障的真正困难在于上述这些特性的平衡和协调。这些特性之间有时会相互补充,但更多的时候会相互矛盾和冲突。试图解决其中一个方面的问题,可能导致其他方面受到损害。解决问题的过程充满了大量的折中手段。
接下来具体说一说云存储元数据保障所面临的问题和一般应对手段。
主从模型
首先我们必须解决可靠性和可用性问题,尽可能保证保存下来的元数据不丢失,也尽可能让服务始终在线。这里有一个关键点:怎样才算保存下来?这个问题看似简单,实则至关重要。一般我们会认为所谓保存下来就是存储到磁盘或其他持久存储设备上。但在一个在线系统中无法以此界定。在一个在线服务中,数据完成保存是指明确向客户端反馈数据已经正确保存。一旦向客户端发出这样的反馈,那么便是承诺数据已经保存下来。在这个界定之下,元数据的可靠性保障则颇为复杂。
前面说过,元数据系统是一个数据库系统,保障数据库系统可靠性最常用手段就是主从模型(图2),即读写主数据库,而主数据库将数据复制到从数据库,从而确保数据库没有单点。主从模型同时也保障了可用性,当主服务器下线时,从服务器可以切换成主服务器,继续提供服务。这样的保障手段对于一般的在线应用大体上够用了,但对于云存储的元数据而言,则存在诸多问题。其中缘由颇为复杂,让我们从最基本的可靠性开始。
主从模型是依靠主服务器向从服务器复制每次写入的数据,以确保数据存留不止一份。但这里有个同步和异步的问题。所谓“异步”是指主服务器完成数据写入之后,即刻向客户端反馈写入成功信息,然后在适当的时候(通常都是尽快)向从服务器复制数据。客户端得到保存完成的信息后便欢欢喜喜做后面的事情去了。而此刻相应的数据只有主服务器上保存着,如果发生磁盘损坏之类的问题,便会丢失那些尚未来得及复制到从服务器的数据。之后如果客户端来提取这条数据,服务端却无法给出,对存储系统而言,是很丢人的事。
即便没有发生硬件损坏的情况,依然可能丢失数据。由于硬盘之类的存储介质速度缓慢,无法跟上数据访问的节奏,所以数据库都会使用内存进行缓存。当服务器发生掉电或者崩溃,数据库发生非正常的退出时,都会造成缓存中没有写入磁盘的数据丢失,进而造成数据的丢失。
在存储系统长期的运行过程中,硬件肯定会坏,掉电必然会发生,系统也难保不会崩溃。所以这种异步的数据复制过程必然会有数据的丢失。这对于在线存储服务而言是无法容忍的。
既然异步存在这样的问题,那么同步地复制数据是否会好些呢?所谓“同步”是指主服务器完成数据写入后,并不立即反馈用户,而是先将数据复制到从服务器。等到从服务器正确写入,再向用户反馈数据写入完成。这样,任何时刻都会至少有两台服务器拥有某一条数据,即便一台服务器出了问题,也可以确保数据还在。如果有多台从服务器自然就万无一失了。
同步模式解决了可靠性问题,却引入了新问题。最基本的问题就是同步的数据复制造成延迟的增加:主服务器需要等待从服务器完成写入才能反馈。
让我们先放下性能问题,毕竟在线对象存储对于主从同步的延迟还没有那么敏感。剩下的问题则是根本性的。同步模式解决了可靠性问题,下一步需要考虑可用性问题。对于在线存储服务而言,必须要确保系统不受单点故障的影响。同步模式则恰恰受制于此。为了确保可靠性,必须主从服务器都写入成功,才能向用户反馈成功。如果从服务器下线,那么这个条件就被破坏,不得不向用户反馈失败,直到从服务器重新恢复。而当主服务器下线后,从服务器则设法升级为主服务器继续服务。但此时只有一台服务器,也不能满足数据不少于两份的要求。如果允许主从之间容忍从服务器的下线,那么可靠性又受到了削弱。这样产生了令人啼笑皆非的结果,主从模型是为了提高可靠性和可用性,现在反到受制于单点故障。
解决这一问题的方法也不是很难:增加从服务器的数量,并且允许主从复制失败。如此,当一台从服务器下线后,其他从服务器依然可以接收数据,确保任何时刻一份数据都至少拥有两个以上的副本,以维持可靠性。不过,具体操作上并没有那么简单,主从之间复制成功的数量必须满足一个阈值。这个阈值也必须满足一个条件才能保证数据一致性,关于这个要点后面会具体阐述。
又有新的问题出现。当主服务器下线时,一台从服务器便会被升级成为主服务器。但前面说了,为了可用性,并不要求主从服务器间的复制每次都成功,那么新的主服务器的数据可能是不完整的。这样对于用户而言,便会出现数据一致性的问题:原先成功写入的数据却读不到了,或者读到的都是很旧的版本,已被覆盖掉的东西。给用户造成的印象便是数据丢失了,这是严重的问题。
最直观的解决途径是设法补上那些数据。但考虑到元数据的量,这样的修补是无法在短期内完成的。另一个更复杂却真正有效的方法是读取数据时,同时读所有的服务器,不管主从。然后将所得的结果整合在一起,找出正确的版本。
事情还没有完结,存放在数据库中的数据经年累月地运行之后,会逐步退化。磁盘的失效,磁道的损坏,乃至人为的操作失误,都可能造成数据副本的丢失。换句话说,即便是主服务器,我们也无法完全保证数据的完整性和一致性。无论如何都无法回避主从服务器之间的一致性问题。
无论是主服务器,还是从服务器,终究需要对缺失的数据副本进行修补,否则数据的退化终究会造成数据的遗失。这种修补的过程也是困难重重,后面会进一步分析。
最后,最重要的就是运维。架构、设计、开发之类的都是短期过程,虽说系统需要不断演进,但毕竟都是阶段性的工作。而运维,则是持久的、不间断的任务。一个在线服务的正常运行最终还要依赖运维,运维决定了服务的品质。因此,一个设计方案的可维护性具有很高的优先级。
在可维护性方面,主从模型除了常规的系统维护事务外,还有很多额外的运维过程。主服务器下线,需要将某一台从服务器切换为主服务器,这个过程是一个运维过程。理论上,一个主从集群可以实现自动切换。但实际使用中,这种切换并不能确保成功,往往需要人为干预,切换失败也是常有的事。对于计划中的下线,如升级软硬件等,运维强度尚不算大,但对于软硬件故障造成的突发下线,主从切换往往是紧急运维事件。一旦失败,便是服务下线的大事故。其中充满了风险和不可控因素,并不利于存储服务的高可用。
一个在线系统的架构,最好的情况就是无论是正常运行,还是发生意外故障,都能够以同一种方式运行,无须额外的应急处理。用通俗的话说,在线系统必须能够不动声色地扛住局部异常,为运维人员赢得回旋处理的余地。对于云存储的元数据系统尤是如此。元数据位居关键路径,又存在持久化的状态,还受到各种特性要求的约束,它的架构着实是一项富于挑战的任务。