Cassandra - A Decentralized Structured Storage System

Cassandra是一个分布式存储系统管理分布在很多商业服务器节点上的非常大量的结构化数据同时提供无单点失效的高可用服务Cassandra目标是在几百个基础节点上运行(可能分布在不同的数据中心)。在这个规模上,大大小小的组件经常失效。Cassandra对这些失败持久状态的管理方式促使软件系统的可靠性和扩展性依赖这一服务。虽然在许多方面Cassandra类似于一个数据库,并且共享很多设计和相关实现策略,但Cassandra并不支持完整的关系数据模型;相反,它为客户提供了一个支持动态控制数据布局并且格式简单数据模型。Cassandra系统设计目标是:运行在廉价商业硬件上,高写入吞吐量处理,同时又不用以牺牲读取效率为代价。

背景:

Facebook平台在性能、可靠性、效率以及支持持续增长平台所需的高扩展性等方面对操作有着严格的要求。我们的标准操作模式是在由几千个组件组成的基础设施上进行故障处理;任何时候都总有一些小型但非常重要的服务器或网络组件会发生故障。因此,软件系统需要建立处理故障(在这种情况下故障是一种常态而非异常)的机制。为了满足上述可靠性和可扩展性的需要,Facebook研发了Cassandra。

Cassandra使用了一系列总所周知的技术来实现可扩展性和可用性。Cassandra是为满足inbox搜索问题的存储需求而设计的。In- box搜索是facebook中的一项功能,允许用户通过他们Facebook的Inbox进行搜索。对于Facebook而言,这意味着系统需要处理一个非常高的写入吞吐量,数十亿计的每日写入量,以及相同规模用户量。由于用户从分布在不同地区的不同的数据中心获取数据,能够在不同数据中心复制数据是解决搜索延迟的关键。In-box搜索服务是在2008年6月推出的,到目前为止已经有累计超过2.5亿用户使用,Cassandra也达到了它的设计初衷。Cassandra现在已经被用作多个Facebook服务的备份存储系统

3. 数据模型

Cassandra中的表是一个分布式多维度由键索引的映射值是一个高度结构化的对象。表中的行键是一个没有大小限制的字符串,尽管通常情况下是16~36的字节长度。每个单行键下的操作都是一个原子副本,不管多少读取或写入了多少列。列被统一放在一个叫做列簇的集合中,这和Bigtable[4]系统的的工作机制很相似。Cassandra 有两种列簇——简单和超级列簇。超级列簇可以用一个列簇内又有一个列簇来形象化表示。

此外,应用可以在一个超列簇或普通列簇中指定列的排序。系统允许列按照时间或者名称对列进行排序。列的时间排序是特地为像inbox搜索这类结果需要按时间排序的应用提供的。任何列簇的列都要通过列簇访问规约(列和任何超列簇中的列都需使用列簇访问规约:super column : column)来进行访问。文中6.1小结给出了一个能够非常好的体现超列簇抽象能力的例子。通常的应用使用专用的Cassandra 集群,并作为其服务的一部分来管理。尽管系统支持多表的概念,但所有部署结构中都只有一个表。

4. API

Cassandra的API包括以下三个简单的方法.

insert(table; key; rowMutation)

get(table; key; columnName)

delete(table; key; columnName)

columnName可以是含有列族的一个特殊列, 一个列族,超列族,或者带有超列的一个特定列.

5. 系统架构

需要在产品设置中进行操作的存储系统架构非常复杂。除了实际的数据持久化组件之外,系统还需要有以下特性;可扩展性和强大的负载均衡,会员和故障检测,故障修复,副本同步,负载均衡,状态转移,并发和作业调度,请求编组,请求路由,系统监控和报警以及配置管理。每种解决方法详细描述都超出了本文讨论的范围,所以我们只会讨论Cassandra系统所使用的核心分布式技术:分区、复制、会员、故障处理以及扩展。所有的这些模块都需要同步处理读/写请求。通常一个键的读/写请求需要被发送到Cassandra集群中的任何一个节点上,然后节点判断是否为这个特定键的副本。对于写操作,系统将请求路由到副本并等待法定的副本数目的响应,以确认写操作的完成。对于读取,需要依据用户的一致性需求而定,系统要么将请求路由到最近的副本上或者路由到所有副本并等待法定的副本数目的响应。

5.1 分区

Cassandra 一个重要的设计特性就是持续扩展的能力。这要求动态将数据分区到集群中各个节点(即存储主机)的能力。Cassandra 整个集群上的数据分区用的是一致性哈希[11],但是使用了保序哈希函数来达到这一点。在一致性哈希算法中,一个散列函数输出范围被当作一个固定的圆形空间或‘环’来对待(即最大散列值之后为最小散列值)(有点类似Dynamo系统)。系统中的每一个节点被赋予一个在这一空间内表示其圆环的位置的随机值。每一个数据项通过键来标识,通过散列数据项的键来确定环上的位置,然后分配到圆环上第一个离大于该条目位置最近的节点。这个节点被视为此键的坐标。应用对此键进行特化处理并且Cassandra使用它来进行请求路由。因此,每个节点只需对圆环上它与前任节点之间的区域负责。一致性哈希最主要的优点是离开或到达一个节点只会影响期直接毗邻节点,而其他节点不受影响。基础的一致性散列算法面临一些挑战。首先,每个节点圆环上随机位置的分配导致数据和负载分布的不均匀。第二,基础算法无视节点性能的不均匀性。通常解决这个问题有两个方法:一个是将一个节点分配给环中多个位置(就像Dynamo的做法),第二种就是对环上的负载信息进行分析,优先路由负载小的节点以减轻负载较重的节点负担(正如[17]中讲述的)。Cassandra采用后一种方法,因为它能够使得设计和实现易于处理,并且有助于负载均衡做出确定的选择。

5.2复制

Cassandra采用复制策略来达到高可用性和耐受性。每个数据节点都会被复制到N个主机上,这里N是指复制因子,通过per-instance配置来设置。每个键k都被赋给了一个协调节点(上节已经提到)。由协调节点负责它自身范围内数据项的复制。除了协调节点本地存储了它自身区域内的每个键之外,协调节点还会将这些键复制到环上其他N-1个节点上。Cassandra提供了众多的复制策略,例如”机架不可知”(rack unaware)、”机架可知”(rack aware)(同一个数据中心内)与”数据中心可知”(data-center aware)。应用选择的复制策略决定了副本的数量。如果应用选择了”机架不可知”(rack unaware)策略,那么系统将通过环上N-1个继任协调节点来确定非协调副本个数。对于”机架可知”(rack aware)和”数据中心可知”复制策略时复制的算法要稍微复杂一点.Cassandra使用一个叫做Zookeeper[13]的系统从所有节点中选取一个领导节点。所有加入集群的节点时由领导者告知它们负责哪个环上哪个范围的副本,并且领导节点需要保持协调一致,使得节点不需要对环上N-1之外的区域负责。节点所需负责区域范围的元数据被缓存在每个节点本地,并在Zookeeper内做容错处理,这样当一个节点崩溃并返回的时候就可以知道它到底负责哪个范围。借用Dynamo的说法,我们认为负责给定范围的节点是这个范围的“最优清单”。

正如在5.1章节中所阐述的那样, 每一个节点都能感知到系统内其它任意一个节点的存在,因此也知道它们这个系统所负责的范围。正如在5.2章节中描述的那样,通过放宽对设备的规定数目,即使面对节点失效和网络分区,Cassandra也能提供对持久化的保证。数据中心失效通常是由于电力中断、降温失效、网络中断以及自然灾害等。Cassandra被配置成把每一条数据都进行跨数据中心的复制。实质上,构建了一个针对某个key的引用列表,来使得存储节点可以跨越多个数据中心。这些数据中心通过高速网路进行连接。这种跨多个数据中心进行复制的方案,使得我们能够掌控整个数据中心而无任何中断。

5.3会员

Cassandra中的集群成员基于Scuttlebutt[19],一种非常有效的反熵Gossip(anti-entropy Gossip,一种Gossip算法)机制。Scuttlebutt最显著的特性是它对CPU利用率非常高效并且非常高效的使用Gossip频道。Gossip并不仅仅用于Cassandra系统成员内部之间,同样能传播其他系统相关的控制状态

5.3.1故障检测

故障检测是一种节点可以在本地确定系统中其他节点是死是活的机制。在Cassandra中,故障检测还被用来避免在多个操作中与不可达节点的进行通讯。Cassandra使用的是Φ Accrual故障检测器[8]的一个改良版本。Accrual故障检测原理是故障探测模块不产生一个布尔值来代替节点的生死状态,相反故障检测模块为每个被监控节点产生一个代表其怀疑级别的数值,该值被定义为Φ.其基本的思路是用Φ的值来表示一个范围,可以动态对其进行调整以反映监控节点上的网络与负载情况.

Φ有以下含义:给定一个Φ的阈值,当Φ=1时我们决定怀疑节点A(出现故障),然后我们将会犯错(即这个决定在将来可能由于心跳接收延迟而被证明是错误的)的可能性为10%;当Φ=2时,可能性为1%;当Φ=3时可能性为0.1%,以此类推。系统中每个节点都维护了一个其他节点发出的gossip消息的内部到达时间的滑动窗口。根据内部到达时间间隔的分布,计算Φ值。尽管初稿中说分布近似为高斯分布(Gaussian distribution),但是我们发现其实它更接近于指数分布(Exponential Distribution),因为gossip 频道本身的特性和它对延迟的影响,所以它更符合指数分布。据我们所知,我们的权责故障检测实现在基于Gossip的配置中还属首创。权责故障探测器的准确性和速度都非常好并且它们可以很好的适应不同的网络状况和服务器负载状况。

5.4 引导

当一个节点第一次启动时,它为它所在环上的位置随机选择一个令牌。出于容错,映射被持久化到本地硬盘和Zookeeper中。令牌信息然后在集群中传播开来。我们就是通过令牌来了解集群中的所有节点以及它们在环上所在的位置的,通过它任何一个节点都可以将一个键(key)的请求路由到集群中的合适的节点。在引导情况下,当一个节点需要加入到集群中时,它需要读取自身包含集群中的联络点的列表的配置文件,我们把这些初始化联络点称为集群种子。种子也可以来自于像Zookeeper之类的配置服务。

在facebook现实环境中(由于故障或维护引起)的节点中断通常只是暂时的,但有些也可能持续一段很长的时间。引发故障的形式也多种多样,比如磁盘故障、CPU损坏等。一个节点的断开很少意味着它会永久断开,因此不应该导致重新平衡分区指派或维护不可到达的副本。同样,人为错误可能导致启动了新的非初始化Cassandra节点。为此,每条信息都包含了集群中每个Cassandra实例的名称。如果配置中的人为错误导致节点试图加入一个错误的Cassandra 实例,它将会因为集群名称错误而失败。出于这些原因,使用显示机制来从Cassandra实例上初始化添加和删除节点是合适的。管理员使用命令行工具或浏览器连接到一个Cassandra 节点,并发出成员加入或离开集群的变更

5.5 集群扩展

当一个新的节点被添加到系统中时,它将被分配一个令牌,这样它可以缓解高负荷节点(的压力)。这将导致节点可以将之前负责的区域拆分给到新节点。Cassandra 引导算法是用命令行操作或Cassandra网络仪表从系统中其他节点初始化的。放弃这部分数据的节点的数据通过内核-内核拷贝技术转移到新的节点。运行经验表明数据可以在单个节点中以40MB/秒的速率进行传输。我们正在通过让多个副本来参与并行化引导传输的方式来努力改善这一效率,就像Bit torrent(所做的一样)。

5.6 本地持久化

Cassandra的数据持久化需要依赖本地文件系统。数据用一种高效读取格式存放在硬盘上。出于耐受性和可恢复性考虑,通常写操作将会涉及到提交日志写入并且更新到一个内存数据结构中。写入内存数据结构仅仅在写入提交日志成功后才会进行。我们每台机器上都有个专门磁盘用来提交日志,因为所有写入提交日志是连续的,所以可以最大限度的利用磁盘吞吐量。当内存数据结构大小(根据数据大小和数量计算得出)超过一定阈值,它将转储到磁盘上。这个写操作是在每台机器配备的许多廉价磁盘中的一个上进行的。所有写入操作写入到磁盘都是有序的,并且生成了一个基于行键可进行快速检索的索引。这些索引通常是数据文件在一起持久化的。随着时间的推移,在磁盘上可能会存在很多这样的文件,后台会有一个合并进程将这些文件合并成一个文件。这个进程和Bigtable系统中的压缩进程非常相似。

一个典型的读取操作在读取磁盘上文件之前首先将查询内存数据结构。文件是以文件的新旧来进行排序的。当进行磁盘检索时,我们可能需要检索磁盘上的多个文件的关键字。为了避免查找到不包含关键字的文件,我们用布隆过滤器来汇总文件中的关键字,它同样也存储在每个数据文件中并常驻内存。(检索时)首先将咨询布隆过滤器来检查搜索的关键字是否在给定的文件中。一个列簇中的关键字可能会包含很多列。所以当检索的列距离键较远时还需要利用一些特殊的索引。为了防止搜索时搜索磁盘上的每一列,我们维护列索引来帮组我们直接跳到磁盘上所取列的正确块上。由于指定键的列已经被序列化并写入到磁盘,所以我们按照每块256K的范围来生成索引。边界的大小是可以配置的,但是我们发现在实际产品负载环境下中,256K大小工作良好。

5.7 实现细节

Cassandra 在一台机器上的运行过程主要包括以下抽象模块:分区模块,集群会员管理和故障检测模块以及存储引擎模块所有这些模块都依赖于一个事件驱动的底层模块,它按照SEDA[20]架构设计,将消息处理管道与任务管道切分成了多个阶段。这些模块均采用java实现。集群成员管理和故障检测模块建立在使用非堵塞IO的网络层之上。所有系统控制消息依赖于基于UDP协议的消息传输,而复制与请求路由等应用相关的消息则依赖于TCP协议。请求路由模块使用一个固定状态机来实现。当一个读/写请求到达任何集群中的节点时状态机将会在以下几个状态切换:(i)验证节点是否拥有给定键的数据(ii)将请求路由到节点并等待响应返回(iii)如果副本在配置超时时间内没有到达,将请求设置为失败并返回客户端(iv)根据时间戳分辨出最后到达的响应(v)如果副本的数据并不是最新,安排副本进行数据修复。出于论述目的,我们在这里不讨论失败的情况。系统写入机制既可以配置成同步也可以配置成异步。对于特定高吞吐量需求的系统我们依赖异步复制策略,这种情况下,系统的写入远远超过系统读取。在同步复制的情况下,我们在指定仲裁数目的响应返回后才将结果返回到客户端。

在任何日志系统中都需要有一个清除提交日志条目的机制。在Cassandra中,我们采用滚动提交日志——即在一个旧的提交日志超过一个特定的(可配置)大小后自动开启一个新的日志文件。我们发现每128MB滚动提交日志在生产环境中负载良好。每个提交日志都有一个基本上是一个大小固定的位向量的头信息,其大小通常超过一个系统可能处理的列簇的个数。在我们的实现中,对于每个列簇,我们都会生成一个内存数据结构以及一个数据文件。每当一个特定的列簇的内存数据结构转储到磁盘,我们都会在提交日志中记录它对应的位,说明这个列簇已经被成功地持久化到磁盘。这就表明这条信息已经被提交。每份提交日志都有一份这些位向量同时也在内存中维护一份。每当发生提交日志滚动的时候,它的位向量,以及它之前滚动的提交日志的位向量都会进行检查。如果认为所有数据已经被成功持久化到磁盘之后这些日志将会被删除。写到提交日志的操作既可以是正常模式也何以是快速同步模式。在快速同步模式下,写到提交日志将会开启缓冲。这就意味着当机器崩溃时存在数据丢失的潜在风险。在这种模式下内存数据结构转储到磁盘上这一过程同样也采用了缓冲。传统数据库并不是被设计成来处理特别高的写入吞吐量。Cassandra将所有的写入操作都转换成有序的写操作以最大限度地利用磁盘的写入吞吐量。由于转储到磁盘的文件不再会被修改,从而在读取它们的时候也不需要加锁,Cassandra 服务器读/写操作实际上并没有加锁,因此我们不需要处理在以B-Tree实现的数据库中存在的并发问题。

Cassandra系统根据主键来索引所有数据。磁盘上的数据文件被分成了一系列块。每块至多包含了128个主键并由一个块索引来进行界定。块索引抓取块内键的相对偏移量和数据的大小。当内存数据结构转储到磁盘上时,(系统)将会产生一个块索引并把它的偏移量作为索引写入磁盘。在内存中也同样维护一份索引以便进行快速访问。通常读取操作总是首先在内存数据结构中检索数据。如果检索到数据,将会把数据返回应用,因为内存数据结构包括了任意键的最新数据。如果没有找到(对应的数据),我们则会使用磁盘I/O按照时间逆序对所有磁盘上的数据文件进行搜索。由于我们总是查询的是最新的数据,所以我们首先在最新文件进行查找一旦找到就返回所查找数据。随着时间的推移,磁盘上的数据文件的数量将会增加。我们使用一个压缩进程,非常类似于Bigtable系统中所做的一样——将多个文件合并成一个,对一系列有序数据文件进行合并排序。系统始终总是压缩和大小彼此相近的文件,例如,永远不会出现一个100GB的文件与另一个小于50GB的文件进行合并的情形。每隔一段时间就会运行一个主压缩线程来将所有相关数据文件压缩成一个大文件。这个压缩进程是一个磁盘I/O密集型操作,因此需要对此做大量的优化以做到不影响后续的读请求。

6. 实践经验

在Cassandra的设计、实现以及维护过程中我们积累了很多有用的经验也学到了许多教训。其中一个非常基础的教训就是在没有了解应用使用的效果之前不要急着添加新功能。最棘手的情况不仅仅只来自于节点崩溃和网络分区。我们将在此分享一些有趣的应用场景。

在应用的收件箱搜索(进行搜索之前)我们必须对超过1亿用户大约7TB的收件箱数据进行索引,将他们保存在我们的MySQL[1]基础设备上,然后将其加载到Cassandra系统中。整个过程涉及到对MySQL数据文件进行Map/Reduce[7]调度;建立索引,然后将逆序索引保存到Cassandra中。实际上,M/R进程是作为Cassandra的客户端运行的。我们为M/R进程开放后端通道,使其可以对每位用户的反向索引进行聚合,并将将序列化的数据传递给Cassandra实例,以避免序列化/范序列化的额外开销。这样Cassandra实例的瓶颈就只剩下网络带宽了。

大多数应用只需要每个键每个副本的原子级操作。然而,任然有许多应用需要事务的支持——主要是出于维护二级索引的考虑。大多数有几年RDBM开发经验的开发者都认为这是一个非常实用的功能。我们正在研究一种机制开放此类原子操作。

我们尝试了各种实现的故障检测器,比如在 [15] 和 [5]讲到的那些。我们的试验表明随着集群规模的增长,检测到故障的时间也会出现增长,超出了我们的接受限度。在一个特定的包含100个节点的实验中,检测一个故障节点竟然耗费大约2分钟的时间。这在我们实际的运行环境中是行不通的。采用accrual故障检测器并设置一个稍显保守的PHI(Φ)值(设置为5),在上面的实验中检测到故障的平均时间大约为15秒。

不要对监控想当然。Cassandra系统很好的集成了Ganglia[12]——一个分布式性能监测工具。我们向Ganglia开放了各种系统级别的指标,这在我们将Cassandra部署到生产环境时,帮助我们对这个系统的行为有了更深的理解。磁盘无缘无故的发生故障,当磁盘出现故障,引导算法中有一些异常分支可以修复节点,然而这实际上是一个管理操作。

7. 结论

我们已经构建,实现并且操作了一个存储系统,它提供了可扩展性,具有高性能并且具有广泛的适用性。我们通过使用经验展示了Cassandra可以支持在吞吐量上有一次大的提升同时降低传输延时。下一步的工作包括:添加数据压缩,支持跨key的原子操作能力,支持二级索引。

尽管Cassandra是一个完全分散的系统,我们已经意识到为了使得某些分布式特性更加可控,大量的协调必不可少。例如Cassandra集成了Zookeeper,它可以在大型可扩展分布式系统中被用来协调各种任务。我们打算对一些关键特性使用Zookeeper抽象,这实际上同使用Cassandra作为存储引擎的应用的关系不大。

6.1 搜索Facebook收件箱

对于facebook收件箱来说,我们建议每个用户都应该检索下那些发送人和接收人都互换了的邮件. 目前启用了两种搜索的功能: 短语搜索- 输入一个用户名就能返回所有有关他曾经收到和发送过的邮件.这种模式一般是两列组成. 对于a查询来说,某个用户的ID就是组成顶级列邮件的关键字和关键词.个人邮件会在顶级列中标记出包含关键字的邮件. 对于b查询来说,某个用户的ID也是关键字,而且顶级列就变成接收者的ID看. 对于每个顶级列来说,个人邮件标识就是列. 这主要是为了实现缓存智能数据以便更好的让搜索变得快速和准确. 比如,当用户点击进入搜索工具栏的时候就会准确的从缓存中根据关键字异步显示出匹配关键字的邮件. 这样,要是真选择了某个可供选择的选项,那么查出来的结果就好像早就缓存在内存中一样. 目前系统可以按150节点字符串缓存大约50+TB的数据, 这些数据会在东西海岸数据中心同步储存和展开. 下面展示了测试大量读取数据的结果.

你可能感兴趣的:(分布式系统)