NoSQL数据库引见(6)

NoSQL数据库介绍(6)

6 面向列的数据库



6.1 Google Bigtable
     Bigtable被描述为“用于管理结构化数据的分布式存储系统,其被设计为可扩展到非常大的尺度:几千台商用服务器上的PB级数据”([ CDG+06,页1 ])。当2006年它在Google被超过六十个项目使用,包括web索引,Google Earth,Google Analytics,Orkut,以及Google Docs(原名Writely)。这些项目有非常不同的数据规模、基础设施和时延的要求:“从面向吞吐量的批处理作业到为终端用户提供的时延敏感的数据服务。这些产品使用的Bigtable集群跨越了一个广泛的配置,从少数到数千台服务器,存储多达几百TB数据”([ CDG+06,页1 ])。根据Chang等,在Google的经验表明“Bigtable达到了几个目标:广泛的适用性,可扩展性,高性能,高可用性”([ CDG+06,页1 ])。它的用户“喜欢由Bigtable实现提供的性能和高可用性,且可以通过简单地将更多机器添加到系统中来扩展集群的容量,因为他们的资源需求随时间变化。Google作为一家公司,Bigtable的设计和实现已经显示是有利的,因为它“从为Bigtable设计自己的数据模型获得了大量的灵活性。此外,我们对Bigtable实施的控制,和Bigtable依赖的其他Google基础设施,意味着我们可以消除瓶颈和效率低下的出现”([ CDG+06,页13 ])。
     Bigtable被Google描述为数据库因为“它共享了许多数据库的实施策略”,如并行和主内存数据库。然而,它和关系数据库有区别因为“不支持全面的关系数据模型”,但一个更简单的模型可以动态地被客户端控制。Bigtable还允许“客户端推断数据的本地性”,其在“底层存储中”体现([ CDG+06,页1 ])。与RDBMS相比,数据可以在不止一个维度上被Bigtable索引——不仅是行上也有列上的。进一步区分的命题是,Bigtable允许数据被从内存或磁盘送出——这可以通过配置指定。

6.1.1 数据模型
     Chang等人说他们“相信由分布式B树或分布式哈希表提供的键-值对模型太有限。键-值对是一个有用的构建块,但它们不应该是唯一的提供给开发者的构建块。”因此他们为Bigtable设计的数据模型应该“比简单的键-值对强大,和[支持]稀疏的半结构化数据”。另一方面,它应该保持“足够简单,使其适合于一个非常有效的扁平文件表示,且...足够透明…以允许用户调整系统的重要行为”([ CDG+06,页12 ])。
     Google的Bigtable提供和处理的数据结构被描述为“一个稀疏,分布式,持久化的多维排序图”。值被存储为字节数组,其不被数据存储所解释。它们对应三元组(row-key, column-key, timestamp)(注:行键,列键,时间戳)([ CDG+06,页1 ])。

图6.1:Google Bigtable——网络爬虫结果的示例(来自[ CDG+06,页2 ])

     行键在Bigtable是最大64KB的字符串。行保持字典序,并被数据存储动态分区到所谓的片(tablets)(注:有些书翻译成“子表”,我认为这个词sql味太重,还是译成“片”了,大家明白这回事就行),Bigtable中的“分布式和负载均衡单元”。客户端应用程序通过明智地选择行键可以利用这些属性:因行键的排序直接影响到行划分为片,字典距离较小的行范围可能只划分成几片,使得读取操作时将只有少量的服务器提供这些片([ CDG+06,页2 ])。在上述图6.1的例子中,作为行键的域名被分层降序存储(从DNS视点),使得子域具有较小的字典距离,与域名被逆序存储的情况相比(如com.cnn.blogs,com.cnn.www对比blogs.cnn.com,www.cnn.com)。
     每个表的列数不受限制。通过键前缀组合进集合的列称为列族。列族是Bigtable中一个非常重要的概念,具有特定的性质和含义([ CDG+06,页2 ]):
  •      他们“构成访问控制的基本单位”,识别列出、读取、修改和添加列族的权限。
  •      他们预计将存储类型相同或类似的数据。
  •      他们的数据被Bigtable一起压缩。
  •      他们必须在数据被存储进列族中的列前被指定。
  •      他们的名字必须是可打印的。相比之下,列名“可以是任意的字符串”。
  •      Chang等人建议“在表中的不同的列族的数目较小(最多几百),且在操作过程中列族很少改变”。
     图6.1的例子显示了两个列族:content和anchor。content列族只包含一个列,其名称不需要进一步的描述。相比之下,anchor列族包含两个列,使用网站的域名来描述。
     时间戳,表示为64位的整数,在Bigtable中用于区分单元格值的不同修订。一个时间戳的值或者由数据存储分配(即实际存储单元格值的时间戳)或者由客户端应用程序选择(需要是唯一的)。Bigtable以时间戳值的降序排列单元格值“使得的最新修订可以最先被读取”。为使客户端应用程序与删除单元格值的旧的或不相关的修订解耦,自动垃圾收集被提供,并且可以通过为每个列族指定保留的修订数量或最大年龄来参数化([ CDG+06,页2 ])。

6.1.2 API
     Bigtable客户端应用程序暴露以下几类操作([ CDG+06,页3 ]):
     读取操作  包括通过键、列族的限制和时间戳查找和选择行(对比关系型数据库中的投影),以及列上的迭代。
     行的写操作  包括创建、更新和删除特定行的一个列的值。Bigtable还支持“跨行键的批量写”。
     为表和列族的写操作  包括他们的创建和删除。
     管理操作  允许更改“集群、表和列族的元数据,如访问控制权。
     服务器端代码执行  为用Google的数据处理语言Sawzall编写脚本提供([ PDG+05 ],[ Gri08 ])。截至2006,这样的脚本是不允许写或修改存储在bigtables中的数据,但允许“基于任意表达式的多种形式的数据转换、过滤,和通过多种操作的汇总”。
     MapReduce操作  可以使用Bigtable map的内容作为输入源,以及输出目标。

     事务在单个行的基础上被提供:“每一行键下的数据读或写是原子的(不管行中被读写的不同列的数目),这一设计决策使得客户端更容易推断系统的行为,在同一行被并发更新的情况下”([ CDG+06,页2 ])。

6.1.3 设施依赖
     Bigtable依赖于Google的基础设施的一些技术和服务([ CDG+06,页3 ]):
  •      分布式Google文件系统(GFS)([ GL03 ])被Bigtable使用以持久化其数据和日志文件。
  •      因为Bigtable通常与“各种各样的其他分布式应用程序”共享机器,这依赖于一个集群管理系统“用于调度作业,管理共享机器上的资源,处理机器故障,监控机器状态”。
  •      Bigtable数据以Google的SSTable文件格式存储(参见3.3节)。SSTable是一个“持久的、有序的不可改变的map”,其键和值“是任意字节字符串”。SSTable允许应用程序通过其键查找值,和“遍历一个指定键范围内的所有键/值对”。在内部,SSTable被表示为一个块的序列,具有一个可配置的、固定的大小。当一个SSTable打开,一个位于SSTable尾部的块索引被加载到内存。如果一个SSTable的块被请求,只有一次磁盘寻道是必要的以读取数据块,在一个内存中块索引的二分查找发生后。为进一步提高读性能,SSTable可以完全加载到内存中。
  •      “高可用性和持久性分布式锁服务”Chubby([ Bur06 ])被Bigtable采用于几个任务:
–“[保证]在任何时刻最多只有一个活跃的主”在一个Bigtable集群中
–存储引导Bigtable实例所需的数据位置信息
–发现片服务器
–片服务器挂掉后的终止
–存储模式信息,Bigtable实例每个表的列族
–存储访问控制列表
     每个Chubby服务的实例包含一个“五个活动副本,其中之一被选为主且主动响应请求”的集群。为保持潜在易失性副本的一致,使用了一个Paxos一致性算法的实现([ Lam98 ])。Chubby为目录和(小)文件提供名称空间,并对客户端暴露了一个简化的文件系统接口。每个文件和目录“可以作为一个读写锁”([ Bur06,338页 ])。客户端应用以一个Chubby实例开始会话,请求租赁并不时刷新。如果一个Chubby集群变得不可用,其客户端——包括Bigtable——的实例在预定时间内无法刷新他们的会话租赁,使得他们的会话过期;在Bigtable的情况下,这会导致Bigtable本身“在其后较长的一段时间”的不可用(即Chubby不可用;参见[ CDG+06,页4 ])。

6.1.4 实现
组件

     Bigtable的实现包括每个实例中三个主要组件([ CDG+06,页4 ]):
     多个片服务器,每一个都负责一些片。这意味着处理片的读写请求以及“已经增长到太大”的片的分裂。可以在运行时添加和删除片服务器。
     一个客户端库,提供给应用程序与BigTable的实例交互。该库负责查找应被读或写的数据所在的片服务器,将请求定向到它们,并将它们的返回值提供给客户端应用程序。
     一个主服务器,具有一系列的责任。首先,它管理片和片服务器:它将片分配给片服务器,检测添加和删除的片服务器,并在它们之间分发工作负载。其次,它负责对处理Bigtable模式的变化,如创建表和列族。最后,它对已删除或过期文件进行垃圾收集并储存在特定Bigtable实例的GFS中。尽管有这些责任,主服务器上的负载预计是较低的,因为客户端库自己查找片的位置信息,因此“绝大多数客户端从不与主通信”。鉴于主服务器是一个Bigtable实例的故障单点,其被另一台机器备份,根据Ippolito([ Ipp09 ])。

片位置信息

     正如前一节所述,表动态地拆分成片,这些片分布在多个片服务器上,其可以在运行时动态地进入和离开一个Bigtable实例。因此,Bigtable必须提供管理和查找片的位置的手段,使得主服务器可以重新分发片,且客户端库可以发现负责表的特定行的片服务器。图6.2描述了Bigtable中是怎样存储片位置信息的([ CDG+06,页4 ])。

图6.2:Google Bigtable——片位置层次结构(来自[ CDG+06,页4 ])

     片的位置被存储在一个名为METADATA的表中,其完全位于内存中。这个表被划分为一种特殊的一级片(根片)和任意数量的进一步的片(其他METADATA片)。其他METADATA片包含用户表(即由客户端应用程序创建的表)的所有片的位置信息,而根片包含其他METADATA片的位置信息且从不分裂自己。根片的位置信息被存储在Chubby命名空间的一个文件中。一个片的位置信息按行存储,行通过“片的表标识符和其结束行”被标识。每行大约1 KB,每个片大约128 MB,Bigtable可寻址2的34次个片,通过图6.2中展示的三层结构。
     对每次与数据存储的交互,客户端库不会读取所有层次的片位置信息,而是缓存片位置,并“递归地在片位置层次中上移”如果发现一个位置不正确。在Google一个进一步的优化是客户端库预取片的位置,即每当他们读METADATA表时读超过一个的片位置信息。

主服务器、片服务器和片的生命周期

     片生命周期  一个片通过主服务器被创建、删除和分配给一个片服务器。Bigtable中每个片被分配到最多一个片服务器上;“最多一个”由于片也可能未被分配,直到主服务器找到一个片服务器可提供足够的容量来服务该片。片也可能被主服务器合并,或通过一个片服务器被分裂,该分裂必须通知主服务器。关于片如何在运行时表示以及读写操作如何应用于它们的详细信息,将在下面的片的表示的小节中讨论。

     片服务器生命周期  当一个片服务器启动时,它会在Chubby命名空间的一个预定义的目录下创建一个唯一命名的文件,并获取其独占锁。一个Bigtable实例的主服务器不断地监视片服务器,通过询问它们是否仍然锁定它们在Chubby上的文件;如果某个片服务器不响应,主服务器检查Chubby目录以得知是否特定的片服务器仍持有其锁。如果不是,主服务器将删除Chubby中的文件,并将这个片服务器服务的片放入未分配的片的集合中。当它失去其Chubby锁时,片服务器自己停止对任何片的服务。如果片服务器仍在运行但无法保持其锁,例如由于网络分区,它会尝试再次获得该锁如果其Chubby内的文件尚未被删除。如果这个文件已不存在,则片服务器停止自身。如果片服务器被管理员以受控的方式关闭,它会尝试释放其Chubby锁(注:原文有误),使得主服务器可以尽快重新分配片([ CDG+06,页5 ])。

     主服务器生命周期  当主服务器启动时,它也放置一个特殊的文件到Chubby命名空间并获取其独占锁(防止“并发的主服务器实例”)。如果主服务器不能够持有该锁,其Chubby会话过期,它会自我关闭,因为它没有到Chubby的可靠连接时无法正确地监控片服务器。因此,一个Bigtable实例的可用性依赖于主服务器和其使用的Chubby服务之间的连接的可靠性。
     除了通过文件及其锁在Chubby中注册,主服务器在启动时处理如下步骤:
     1、通过扫描Chubby目录和其中的片服务器文件并检查其锁,发现活着的片服务器。
     2、连接到每一个活着的片服务器,并请求其正在服务的片信息。
     3、扫描METADATA表以构建一个所有表和片的列表。通过减去正在运行的片服务器已在服务的片,推导出未分配的片的集合。

片的表示

     图6.3展示片在运行时如何被表示。

图6.3:Google Bigtable——运行时片的表示(来自[ CDG+06,页6 ])

     片上的所有写操作“被提交到一个存储重做记录的提交日志”,并在谷歌文件系统(GFS)中持久化。最近提交的更新被放入一个排序的内存缓冲称为memtable中。当memtable达到一定规模,它被冻结,一个新memtable被创建,冻结的memtable转化为SSTable格式并写入GFS;这个过程称为一个次紧缩。因此,老的更新被持久化为磁盘上的一系列的SSTables,而最近的更新在内存中。构成一个片的SSTables的位置信息存储在METADATA表中,伴随着一组指针指向一个或多个提交日志,通过该指针当片被分配到一个片服务器时memtable可被重建。
     写操作被检查合法性以及授权,在它们被写到提交日志和memtable前(当他们最终提交)。授权信息以列族为基础提供,并存储在Chubby命名空间中。
     读操作也检查合法性以及请求客户端是否是被授权的。如果允许一个读操作,它“被执行在一个SSTables序列和memtable的合并视图上”。读操作所需的合并视图可以有效地建立,因为SSTables和memtable是以字典序排序的。
     除了次紧缩——memtables以SSTables格式的冻结、转换和持久化——SSTables也不时被压缩。这样的合并紧缩被一个后台服务以修改时复制的方式异步执行。合并紧缩的目标是限制SSTables的数量,其在读操作时必须被考量。
     一种特殊的合并转换的情况被称为主紧缩,其对一批SSTables只产生一个SSTable。他们被Bigtable周期性地执行以“回收被删除数据使用的资源,也允许它确保以定时方式从系统中删除的数据彻底消失,这对存储敏感数据的服务非常重要。
     在所有的紧缩过程中,读和写操作仍然可以被服务。这是因为SSTables以及冻结的memtable不可变的事实,只会在紧缩成功完成时被丢弃。此外,为已提交写操作服务的memtable是永远存在的。

6.1.5 改进
     为了提高Bigtable的性能、可用性和可靠性,几个改进已在Google实现([ CDG+06,页6 ]):
     局部性组  是一个由客户端应用程序定义并且预期通常被一起访问的列族的组。局部性组使得Bigtable为片中的每一个局部性组创建一个单独的SSTables。局部性组的概念是一种通过减少磁盘寻道以及处理的数据量以增加读取性能的方法,为应用程序中典型的读取操作。读取性能的进一步提高可以通过要求一个局部性组从内存获得实现,使得Bigtable在相关的SSTables第一次被读取时懒加载到内存。Bigtable内部为METADATA表使用内存局部性组的概念。
     压缩  对每一个局部性组,SSTables的压缩可以由客户端应用程序请求。客户端还可以指定应用于压缩的算法和格式。SSTable内容被Bigtable按块压缩,这导致与SSTable整体压缩相比较低的压缩率,但“我们受益于SSTable的一小部分可以被读取而不需要整个文件解压缩”。Google的经验表明,许多应用程序“使用一种双路自定义的压缩方案。第一路使用Bentley和McIlroy的方案,其压缩一个大窗口中常见的长字符串。第二路使用快速压缩算法寻找一个小的16 KB的数据窗口中的重复。两路压缩都非常快——他们在现代机器上以100 - 200 MB /秒进行编码,并以400 - 1000 MB /秒解码”。虽然优化了速度而不是空间,这种两路方法具有良好的压缩率,如10:1对图6.1中Webtable的示例那样数据相似的Bigtables。这是由于,相似的数据(如来自同一域名的文档)通常是以行键按字典序排序和选择的方式聚在一起,使得代表类似数据的行键的字典距离很小。
     片服务器的缓存  为优化读取性能,片服务器上实现了两级缓存:SSTable API返回的键/值对的缓存(称为扫描缓存)和SSTable块的缓存(称为块缓存)。扫描缓存优化读取性能如果重复读取相同的数据,块缓存则“对于倾向于读取最近读过数据附近的数据的应用程序是有用的”。
     布隆过滤器  为进一步提高读取性能,应用程序可以要求Bigtable创建和利用一个局部性组的布隆过滤器。这些布隆过滤器是用来检测是否SSTables可能包含某些键/值对的数据,从而当创建处理读操作所需的片的合并视图时,减少要从磁盘读取的SSTables数。
     组提交  提交日志和memtable中“大量小改动的吞吐量”被组提交策略提高([ CDG+06,页6 ])。
     减少提交日志  每个片服务器为它提供的所有片只保留两个只追加的提交日志文件。这是由于这样的事实,如果为每个片创建并维护一个这样的文件,提交日志的数量会增长迅速,导致许多到GFS的并发写入,潜在的大量磁盘寻道,以及组提交优化变得低效。这种方法的一个缺点在片被重新分配时出现(例如片服务器崩溃或由于负载均衡使片重新分布导致的):作为一个片服务器通常只提供另一个服务器之前提供的片的一部分,它必须通读之前的服务器产生的提交日志,以建立如图6.3所示的片的表示;因此,如果一个片服务器曾提供的片被重新分派到n个片服务器上,提交日志必须被读n次。为了解决这个问题,提交根据键——三元组(表,行,日志序列号)被存储和排序。这导致对每一个需评估提交日志的片服务器,只需要一个磁盘寻道与随后的连续读取,即可加载以前被另一台片服务器提供的片。对提交日志的排序做了两种优化。首先,提交日志被懒排序:当一个片服务器必须提供额外的片,它示意主服务器它必须对提交日志进行评估,以便主服务器可以启动此提交日志的排序。第二个优化是如何将提交日志排序:主服务器将它们拆分为64MB的块,并在多个片服务器上启动对这些块的并行排序。
     减少GFS延迟的影响  分布式谷歌文件系统对延迟高峰不是鲁棒的,如由于服务器崩溃或网络拥塞。为了减少这种延迟的影响,每个片服务器使用2个写线程以写入日志,分别写入到它自己的文件。在任一时刻只有一个线程主动写到GFS。如果这个活动线程遭受GFS“性能间断”,写提交日志被切换到第二个线程。因为提交日志中的任何操作有一个唯一的序列号,当片服务器加载片时,在2个提交日志中的重复条目可以被消除。
     改进片恢复  片恢复是指由一个特定的片被分配到的片服务器所完成的片加载操作。如本节和第6.1.4节讨论,片服务器需要为加载片评估提交日志。除了前述布隆过滤器的优化,Bigtable试图在恢复片时避免片服务器读取全部的提交日志。这是通过当一个片服务器停止提供一个片时引入两个次紧缩实现的。第一个紧缩是用来减少“片服务器的提交日志中未压缩的状态的量”。第二个紧缩处理第一个紧缩开始后已提交的更新操作;在第二个紧缩执行前,片服务器停止服务任何请求。这些减少片恢复时间的优化,只能在片服务器以可控的方式被停止(即不是由于崩溃)时才能发生和生效。
     利用不变性  Bigtable通过多种方式利用了SSTables是不可变的事实。首先,对文件系统上SSTables的读操作不需要同步。其次,数据的移除被委托给后台进程,以压缩SSTables和垃圾收集过时的数据;因此,数据的移除可以异步地发生,当为请求提供服务时不必消耗这些时间。最后,当一个片被分割,产生的子片继承了其父片的SSTables。
     为对可变的memtable提供有效的读写访问,一个局部的和暂时的不可变性被引入,通过使memtable的行“写时复制且允许并行读写”。

6.1.6 教训
     在设计、实现和使用Google的Bigtable时,取得了不少经验。Chang等人特别提到了以下教训:
     分布式系统中的故障类型  Chang等人批评了许多分布式协议中作出的假设是,大型分布式系统只对极少数的失败是脆弱的,如“标准网络分区和失败停机故障”。相反,他们面临着更多的问题:“内存和网络损坏,大时钟偏移,挂起的机器,扩展的和非对称的网络分区,我们使用的其他系统中的bug(如Chubby),GFS配额溢出,计划和计划外的硬件维护。因此,他们认为,设计和实现分布式系统协议时,这种失败的来源也必须被解决。在Google实现的例子是RPC调用的校验,以及去除一部分系统关于另一部分系统的假设(例如,只有一些固定的错误,可以被如Chubby返回)。
     功能实现  Google开发Bigtable时的一个教训是,只有当实际使用模式被清楚地知道时,才在这样的系统中实现新功能。Chang等人提到的一个反例是,通用的分布式事务为Bigtable规划,但从未实施,因为没有迫切需要。原来使用Bigtable的大多数应用程序只需单行的事务。分布式事务的唯一用例是二级索引的维护,但其可以处理通过“专门机制...这将不是通用的分布式事务,但将效率更高”。因此,没有实际需求和使用模式时提出的通用实现应避免,根据Chang等人。
     系统级监控  一个切实可行的建议是在系统及其客户端上监控,以检测和分析问题。在Bigtable中,如被使用的RPC产生“重要行为的详细的痕迹”,这有助于“检测和修复许多问题,如片数据结构上的锁争用,提交Bigtable修改时写入GFS缓慢,当METADATA表不可用时被卡住的METADATA表访问。
     简单设计的价值  在Chang等人的眼中,从Bigtable开发学到的最重要的教训是,设计和代码的简单性和清晰性尤其重要——尤其对Bigtable这样大且不可预料地进化的系统。作为一个例子,他们提到片服务器成员协议设计时过于简单,重构迭代后变得太复杂,太多依赖于很少使用的Chubby特性,最后被重新设计成“一个新的简单的协议,只依赖广泛使用的Chubby特性”(见第6.1.4节)。


6.2 Bigtable衍生品
     由于Bigtable代码以及运行所需的部件不采用开源或自由软件许可证,开源项目已经出现,采用了Chang等人的Bigtable论文中描述的概念。这个领域中值得注意的是Hypertable和HBase。

Hypertable

     Hypertable仿照Google的Bigtable并受到“我们自己在解决大规模数据密集型任务的经验”,根据其开发者所说。该项目的目标是“为高度可用、PB级规模的数据库系统建立开源标准”。Hypertable几乎完全是用C++写的,并依赖于一个分布式文件系统如Apache Hadoop的HDFS(Hadoop分布式文件系统)以及分布式锁管理器。关于它的数据模型,它支持Bigtable中所有可用的抽象;与HBase不同,Hypertable中列族可包含任意数量的不同的列。表是由行键范围分区(如Bigtable),且得到的分区在服务器之间复制。运行时的数据表示和处理也借鉴了Bigtable:“[更新]是在内存中完成后刷新到磁盘”。Hypertable有自己的查询语言称为HQL(Hypertable查询语言),且暴露一个原生C++的以及一个Thrift的API。最初是由Zvents公司开发的,2007年在GPL下开源,并由百度赞助,其自2009以来是中国领先的搜索引擎([ Hyp09a ]、[ Hyp09b ]、[ Hyp09c ]、[ Cat10 ]、[ Jon09 ]、[ Nor09 ],[ Int10 ]、[ Wik11b ])。

HBase

     HBase数据存储是一个Bigtable的克隆,用Java开发作为Apache的MapReduce框架Hadoop的一部分,提供一个“容错的方式储存大量稀疏数据”。如Hypertable,HBase依赖于一个分布式文件系统(HDFS),与Bigtable上下文中的GFS扮演相同的角色。从Bigtable借鉴的概念还有内存和磁盘的使用模式,需要对不可变或仅追加文件的紧缩、数据压缩以及为减少磁盘访问的布隆过滤器。HBase数据库可以是通过Hadoop执行的MapReduce作业的来源或目的地。HBase暴露了一个Java原生API,也可以通过Thrift或REST方式访问。一个值得注意的HBase使用是2010以来Facebook建立在HBase上的实时消息系统([ Apa11 ]、[ Nor09 ]、[ Jon09 ]、[ Int10 ]、[ Hof10a ]、[ Wik11a ])。


6.3 Cassandra

6.3.1 起源和主要需求
     导致初始设计与开发Cassandra的用例,是Facebook的题为“收件箱搜索问题”。全球最大的社交网络Facebook允许用户交换个人信息,通过在收件人的收件箱中出现的邮件。收件箱搜索问题可以描述为寻找一种有效的方法来存储、索引和搜索这些消息。
     收件箱搜索问题的主要要求以及同一性质的问题是([ Lak08 ],[ LM10,页1 ]):
  •      处理大量且高速增长的数据(假定2008年6月用户1亿,2009年8月250万,截至2011年1月超过6亿,参见[ LM10,页1 ],[ Car11 ])
  •      高和增量的可扩展性
  •      成本效益
  •      “在大规模下的可靠性”,因为“[中断]在服务中可能有显著的负面影响”
  •      “在数百个节点[即商用服务器]的基础设施上运行”的能力(可能在不同的数据中心扩展)
  •      “高写吞吐量,不牺牲读效率”
  •      无单点故障
  •      对待故障“作为一种常态而非一个例外”
     经过Facebook一年的生产环境使用,Lakshman总结出“Cassandra实现了几个目标——可扩展性、高性能、高可用性和适用性”([ Lak08 ])。在2009年8月Lakshman和Malik认定“Cassandra目前为止一直保持承诺”,并称其也“为Facebook内的多个服务部署作为后端存储系统”([ LM10,页1 ])。

6.3.2 数据模型
     Cassandra实例通常由只有一个表,代表“用一个键索引的分布式多维map”。表被结构化为以下维度([ LM10,页2 ],[ Lak08 ]):
     行  用一个任意长度的字符串键标识。行上的操作是“每个副本上原子的,不管有多少列正在读或写”。
     列族  每行可以存在任意数量个。如同Bigtable,列族必须被预先定义,即在一个组成Cassandra实例的服务器集群启动前。每个表的列族数不限;但是,预计只有少数列族被指定。列族由列和超列组成,超列可以被动态添加(即在运行时)到列族,不限制数量([ Lak08 ])。
     列  有一个名称且每行存储一些值,通过时间戳标识(如同Bigtable)。表中的每一行可以有不同的列数,所以表不能被认为是一个矩形。客户端应用程序可以指定列族或超列中列的顺序,通过按名称或按时间戳。
     超列  有名字和任意数量的与他们相关的列。再次,每个超列种列的数目可能每行都不同。

     因此,Cassandra中值以三元组(行键,列键,时间戳)被寻址,列键为column-family:column(对列族中包含的简单列)或column-family:supercolumn:column(对归入超列下的列)。

6.3.3 API
     Cassandra暴露给客户端应用程序的API仅有三种操作([ LM10,页2 ]):
  •      get(table, key, columnName)
  •      insert(table, key, rowMutation)
  •      delete(table, key, columnName)
     get和delete操作的columnName参数标识一个列族中的列或超列,或作为一个整体的列族。
     所有客户端应用程序发出的请求被路由到Cassandra集群的任意一个服务器,其确定为请求的键提供数据的副本。对写操作insert和update,一个法定数量的副本节点必须“要确认写的完成”。对读取操作,客户端可以指定他们希望的一致性保证,并且基于此定义,或者最接近客户端的节点响应请求,或者在请求返回前一个法定个数的来自不同节点的返回被等待;因此,通过定义一个仲裁,客户端应用程序可以决定他们需要的“最终一致性的程度”([ LM10,页2 ])。
     Cassandra的API通过Thrift暴露。此外,编程语言库如Java、Ruby、Pyhton、C#和其他可用于与Cassandra交互的语言([ Cat10 ],[ Int10 ])。

6.3.4 系统架构
分区

     由于Cassandra需要逐步扩展,机器可以加入或离开集群(或崩溃),因此数据必须被分区和分布在集群中的节点之间,以一种允许再分区和再分配的方式。一个Cassandra表的数据因此被分区和分布到节点间,通过一种保留了行键顺序的一致性哈希函数。哈希函数的顺序保持属性是重要的,以支持表中数据的范围扫描。基础一致性哈希的常见问题被Cassandra和Amazon Dynamo不同地处理:Dynamo多次哈希物理节点到环上(作为虚拟节点),而Cassandra衡量并分析服务器的负载信息,并在一致性哈希环上移动节点,以得到数据和处理负载的均衡。根据Lakshman和Malik,这种方法被选择因为“它使设计和实现非常易于处理,有助于对负载均衡做出非常确定的选择”([ LM10,页2 ],[ Lak08 ])。

复制

     为实现Cassandra集群的高可扩展性和持久性,数据被复制到多个节点,其可以对每个Cassandra实例定义为一个复制因子。对特定被修改的键,复制是由一个协调节点管理的;任何键的协调节点是一致性哈希环上被访问的第一个节点,当从环上键的位置开始按顺时针方向行走时([ LM10,页3 ])。
     多种复制策略由Cassandra提供:
  •      机架不敏感  是一种数据中心内的复制策略,一致性哈希环上协调节点后的N−1个节点被选择来复制数据。
  •      机架敏感(在一个数据中心内)和数据中心敏感  是两种复制策略,通过一种称为ZooKeeper的系统,一个领袖为集群选出,其负责维护“一种不变性,即环上没有节点负责超过N-1个键范围”。节点负责键范围的元数据被缓存在每个节点本地以及ZooKeeper系统上。已经崩溃并再次启动的节点因此可以确定他们负责的键范围。
     副本节点的选择,以及节点和键范围的分配也影响持久性:面对节点故障、网络分区甚至整个数据中心的失败,“一个键的优先列表被构造使得存储节点分布在多个数据中心上([ LM10,页3 ])。

集群成员和失败检测

     Cassandra集群中的服务器成员通过一种名为Scuttlebutt的类Gossip协议来管理([ vDGT08 ]),其被青睐因“非常高效的CPU利用率和非常高效的利用Gossip通道”,根据Lakshman和Malik。除了成员管理,Scuttlebutt也被用来在Cassandra集群内“传播其他的系统相关的控制状态”([ LM10,页3 ],[ Lak08 ])。
     Cassandra群集内的节点尝试本地检测另一个节点是启动或宕机,以避免对不可达节点的连接尝试。用于此目的的故障检测机制是基于“一个修改版本的Φ Accrual Failure Detector”。应计失效检测器背后的思想是,故障检测器不发出布尔值,而是“监测节点的怀疑的水平”,其表明他们启动或宕机的概率。Lakshman和Malik说,根据他们的经验“应计失效检测器在精度和速度上是非常好的,他们也对网络条件和服务器负载条件适应良好”([ LM10,页3 ],[ Lak08 ])。

     集群管理  根据Facebook的经验,Lakshman和Malik认为“节点故障很少意味着永久性的离开,因此不应该导致分区分配的重新平衡或修复那些不可及的副本”。因此,节点必须由管理员显式添加到集群或从集群删除([ LM10,页3 ])。

     引导节点  当节点添加到集群中时,它为其哈希环上的位置计算一个随机的令牌。这个位置以及其负责的键范围被存储在节点本地以及在ZooKeeper上。节点然后从配置中或通过类似ZooKeeper的服务检索群集中几个节点的地址,并向他们宣布自己的到达,这些节点依次传播这个信息到整个集群。通过这样的公告,成员信息在整个集群中传播,因此每个节点可以接收任何键的请求,并将其路由到适当的服务器。因为加入节点拆分了之前另一个服务器负责的键的一部分,这部分的键范围必须从后者转移到加入节点([ LM10,页3 ],[ Lak08 ])。

持久化

     与Bigtable及其衍生品相反,Cassandra持久化其数据到本地文件而不是一个分布式文件系统。然而,内存和磁盘上的数据表示以及处理读写操作是从Bigtable借鉴的([ LM10,页4 ]):
  •      写操作首先去一个持久性的提交日志,然后去一个内存数据结构。
  •      这个内存数据结构被保存到磁盘作为一个不可变的文件,如果它达到一定的尺寸阈值。
  •      所有写入到磁盘是线性的,并创建一个索引“为了基于行键的高效查询”(如同Bigtable使用的SSTables块索引)。
  •      磁盘上的数据文件被一个后台处理进程不时压缩。
  •      读操作考虑内存数据结构以及磁盘上保存的数据文件。“为了防止查找不包含键的文件,布隆过滤器将键汇总到文件中,也存储在每个数据文件中并保存在内存中”。它“首先被查阅以检查被查询的键是否确实存在于给定的文件中”。
     此外,Cassandra为列族和列维护索引,以“为列检索跳转到正确的磁盘块上”,避免扫描磁盘上所有列。鉴于写操作去一个只追加的提交日志且数据文件是不可变的,“Cassandra服务器实例实际上对读/写操作是无锁的”。

6.3.5 实现
Cassandra节点

     一个参与Cassandra集群的服务器运行提供以下功能的模块:
  •      分区
  •      集群成员和故障检测
  •      存储引擎
     这些模块是用java实现的,运行在一个事件驱动的软件层,其“消息处理管道和任务管道沿着SEDA架构的线被分割成多个阶段”([ WCB01 ])。集群成员和故障模块采用非阻塞I/O通信。“所有的系统控制消息依靠基于UDP的消息,应用相关的消息的复制和请求路由依赖TCP”([ LM10,页4 ])。
     请求路由通过存储节点上的状态机实现,其中包括以下状态,当一个请求到达时([ LM10,页4 ]):
1、识别负责所请求键的数据的节点
2、将请求路由到状态1中的节点,等待它们的响应
3、如果状态2中接触的节点在一个配置的时间段内不响应,请求失败并返回给客户端
4、“基于时间戳给出最近的响应”
5、“对任何副本调度一个数据修复,如果它们没有最新的数据”
     存储引擎模块可以进行同步或异步的写入,这是可配置的([ LM10,页4 ])。

提交日志

     Cassandra节点本地维护的提交日志必须从条目中被清除,如果条目已经被提交和持久化到磁盘的数据文件。Cassandra使用限制大小的滚动日志文件;在Facebook其阈值设置为128MB。提交日志还包含一个头,内存数据结构被转储到这个位中。如果一个提交日志在达到其大小限制时被清除,这些位被检查以确保所有提交都通过不可变的数据文件被持久化。
     提交日志可以被写入或者以普通模式(即同步地),或者以一个写被缓冲的快速同步模式;如果使用后者,内存数据也在写入磁盘前被缓冲。“这意味着,机器崩溃时有一个潜在的数据丢失”,Laksman和Malik说。

数据文件

     保存到磁盘的数据文件被划分成包含128个行键的数据块。这些块“被块索引划定”,其“捕获块内一个键的相对偏移量和数据的大小”。为了加速访问块,它们的索引被缓存在内存中。如果某个键需要被读而相应的块索引还没有在内存中,则数据文件以相反的时间顺序被读取,他们的块索引加载到内存中。

6.3.6 教训
     经过Facebook内对Cassandra三年的设计、实现和运行,Laksman和Malik提到在这一时期他们所学到的以下经验教训:
     谨慎的功能增加  对于加入新的功能,Laksman和Malik确认的经验也由Google的BigTable得到,即“不添加任何新的功能,如果不了解其使用于应用后的效果”。
     事务  Laksman和Malik进一步证实了Chang等人的声明,对于大多数应用程序,每行的原子操作是足够的,且通用事务主要被维护二级索引所需要——如果只是目的的话,这个可以通过专门的机制实现。
     故障检测  Laksman和Malik已经测试了各种故障检测的实现,发现检测故障的时间可以增加“超出可接受的极限当集群的大小成长”;在一个实验中,一个由100个节点组成的集群,一些故障检测器需要长达2分钟来发现一个失败的节点。然而,应计失效检测器提供了可接受的检测时间(在上述实验中:15秒)。
     监控  在Facebook的经验确认了Google的经验,监测是关键的对于“当提交给生产环境负载时了解系统的行为”以及检测错误如磁盘因非显而易见的原因而失败。Cassandra与分布式监测服务Ganglia集成。
     部分中心化  Waksman和Malik说,集中的组件如ZooKeeper是有用的由于“有一定数量的协调是必要的,使一些分布式特性的实现受控”。


相关内容推荐