来源:http://www.ibm.com/developerworks/cn/opensource/os-apache-cassandra/
NoSQL 存储提供了关系数据库的一个灵活的、可扩展的替换物,而且在众多诸如此类的存储中,Cassandra 是广受欢迎的选择之一。本文将超越众所周知的一些细节,探讨与 Cassandra 相关的不太明显的细节。您将检查 Cassandra 数据模型、存储模式设计、架构,以及与 Cassandra 相关的潜在惊喜。
Apache Cassandra 简介在数据库历史文章 “What Goes Around Comes Around”中,Michal Stonebraker 详细描述了存储技术是如何随着时间的推移而发展的。实现关系模型之前,开发人员曾尝试过其他模型,比如层次图和有向图。值得注意的是,基于 SQL 的关系模型(即使到现在也仍然是事实上的标准)已经盛行了大约 30 年。鉴于计算机科学的短暂历史及其快速发展的步伐,这是一项非凡的成就。关系模型建立已久,以至于许多年来,解决方案架构师很容易为应用程序选择数据存 储。他们的选择总是关系数据库。
诸如增加系统、移动设备、扩展的用户在线状态、云计算和多核系统的用户群之类的开发已经导致产生越来越多的大型系统。Google 和 Amazon 之类的高科技公司都是首批触及规模问题的公司。他们很快就发现关系数据库并不足以支持大型系统。
为了避免这些挑战,Google 和 Amazon 提出了两个可供选择的解决方案:Big Table 和 Dynamo,他们可以由此放松关系数据模型提供的保证,从而实现更高的可扩展性。Eric Brewer 的 “CAP Theorem”后来官方化了这些观察结果。它宣称,对于可扩展性系统,一致性、可用性和分区容错性都是权衡因素,因为根本不可能构建包含所有这些属性的系统。不久之后, 根据 Google 和 Amazon 早期的工作,以及所获得的对可扩展性系统的理解,计划创建一种新的存储系统。这些系统被命名为 “NoSQL” 系统。该名称最初的意思是 “如果想缩放就不要使用 SQL”,后来被重新定义为 “不只是 SQL”,意思是说,除了基于 SQL 的解决方案外,还有其他的解决方案。
有许多 NoSQL 系统,而且每一个系统都缓和或改变了关系模型的某些方面。值得注意的是,没有一个 NoSQL 解决方案适用于所有的场景。每一个解决方案都优于关系模型,且针对一些用例子集进行了缩放。我的早期文章 “在 Data Storage Haystack 中为您的应用程序寻找正确的数据解决方案” 讨论了如何使应用程序需求和 NoSQL 解决方案相匹配。
Apache Cassandra是其中一个最早也是最广泛使用的 NoSQL 解决方案。本文详细介绍了 Cassandra,并指出了一些首次使用 Cassandra 时不容易发现的细节和复杂之处。
Apache Cassandra
Cassandra 是一个 NoSQL 列族 (column family) 实现,使用由 Amazon Dynamo 引入的架构方面的特性来支持 Big Table 数据模型。Cassandra 的一些优势如下所示:
- 高度可扩展性和高度可用性,没有单点故障
- NoSQL 列族实现
- 非常高的写入吞吐量和良好的读取吞吐量
- 类似 SQL 的查询语言(从 0.8 起),并通过二级索引支持搜索
- 可调节的一致性和对复制的支持
- 灵活的模式
这些优点很容易让人们推荐使用 Cassandra,但是,对于开发人员来说,至关重要的一点是要深入探究 Cassandra 的细节和复杂之处,从而掌握该程序的复杂性。
Cassandra 根据列族数据模型存储数据,如 图 1 所示。
图 1. Cassandra 数据模型
Cassandra 数据模型包括列、行、列族和密钥空间 (keyspace)。让我们逐一进行详细介绍它们。
- 列:Cassandra 数据模型中最基本的单元,每一个列包括一个名称、一个值和一个时间戳。在本文的讨论中,我们忽略了时间戳,您可以将一个列表示为一个名称值对(例如 author="Asimov")。
- 行:用一个名称标记的列的集合。例如,清单 1 显示了如何表示一个行:
清单 1. 行的示例
"Second Foundation"-> { author="Asimov", publishedDate="..", tag1="sci-fi", tag2="Asimov" }
Cassandra 包括许多存储节点,并且在单个存储节点内存储每一个行。在每一行内,Cassandra 总是存储按照列名称排序的列。使用这种排序顺序,Cassandra 支持切片查询,在该查询中,给定了一个行,用户可以检索属于给定的列名称范围内的列的子集。例如,范围 tag0 到 tag9999 内的切片查询会获得所有名称范围在 tag0 和 tag9999 内的列。
- 列族:用一个名称标记的行的集合。清单 2 显示了样例数据的可能形式:
清单 2. 列族示例
Books->{ "Foundation"->{author="Asimov", publishedDate=".."}, "Second Foundation"->{author="Asimov", publishedDate=".."}, … }
人们常说列族就像是关系模型中的一个表格。如下例所示,相似点将不复存在。
- 密钥空间:许多列族共同形成的一个组。它只是列族的一个逻辑组合,并为名称提供独立的范围。
最后,超级列位于一个列族中,该列族对一个密钥下的多个列进行分组。正如开发人员不赞成使用超级列一样,在此,我对此也不作任何讨论。
Cassandra 与 RDBMS 数据模型
根据以上对 Cassandra 数据模型的描述,数据被放入每一个列族的二维 (2D) 空间中。要想在列族中检索数据,用户需要两个密钥:行名称和列名称。从这个意义上来说,尽管还存在多处至关重要的差异,关系模型和 Cassandra 仍然非常相似。
- 关系列均匀分布在表中的所有行之间。数据项之间通常有明显的纵向关系,但这种情况并不适用于 Cassandra 列。这就是 Cassandra 使用各个数据项(列)来存储列名称的原因。
- 有了关系模型,2D 数据空间就完整了。2D 空间内的每一个点至少应当拥有存储在此处的 null 值。另外,这种情况不适用于 Cassandra,Cassandra 可以拥有只包括少数项的行,而其他行可以拥有数百万个项。
- 有了关系模型,就可以对模式进行预定义,而且在运行时不可以更改模式,而 Cassandra 允许用户在运行时更改模式。
- Cassandra 始终存储数据,这样就可以根据其名称对列进行排序。这使得使用切片查询在列中搜索数据变得很容易,但在行中搜索数据变得很困难,除非您使用的是保序分区程序。
- 另一个重要差异是,RDMBS 中的列名称表示与数据有关的元数据,但绝不是数据。而在 Cassandra 中,列名称可以包括数据。因此,Cassandra 行可以拥有数百万个列,而关系模型通常只有数十个列。
- 关系模型使用定义良好的不可变模式来支持复杂的查询,这些查询中包括 JOIN 和聚合等。使用关系模型,用户无需担心查询就可定义数据模式。Cassandra 不支持 JOIN 和大多数 SQL 搜索方法。因此,模式必须满足应用程序的查询要求。
为了探讨上述差异,可以考虑一个书籍评分站点,用户可以在该站点添加书籍(作者、等级、价格和链接)、评论(文本、时间和名称),对这些添加的内容进行标记。应用程序需要支持用户的以下操作:
- 添加书籍
- 添加书籍评论
- 添加书籍标记
- 列出按等级排序的书籍
- 列出给定一个标记的书籍
- 列出给定一个书籍 ID 的评论
使用关系模型实现以上应用程序几乎微不足道。图 2 展示了数据库设计的实体关系 (ER) 图。
图 2. 书籍评分站点的 ER 模型
让我们看看使用 Cassandra 数据模型如何实现此项操作。清单 3 展示了 Cassandra 的可能模式,其中第一行表示 “Book" 列族(拥有多个行),每一行拥有和列相同的书籍属性。<TS1> 和 <TS2> 表示时间戳。
清单 3. 用于书籍评分的 Cassandra 模式样例
Books[BookID->(author, rank, price, link, tag<TS1>, tag<TS2> .., cmt+<TS1>= text + "-" + author) …] Tags2BooksIndex[TagID->(<TS1>=bookID1, <TS2>=bookID2, ..) ] Tags2AuthorsIndex[TagID->(<TS1>=bookID1, <TS2>=bookID2, ..) ] RanksIndex["RANK" -> (rank<TS1>=bookID)]
表 1 是按照模式表示的样例数据集。
表 1. 书籍评分站点的样例数据
列族名称 | 样例数据集 |
---|---|
Books | "Foundation" -> ("author"="Asimov", "rank"=9, "price"=14, "tag1"="sci-fi", "tag2"="future", "cmt1311031405922"="best book-sanjiva", "cmt1311031405923"="well I disagree-srinath") "I Robot" -> ("author"="Asimov", "rank"=7, "price"=14, "tag1"="sci-fi" "tag2"="robots", "cmt1311031405924"="Asimov's best-srinath", "cmt1311031405928"="I like foundation better-sanjiva") |
RanksIndex | "Rank" -> (9="Foundation", 7="I Robot") |
Tags2BooksIndex | "sci-fi" -> ("1311031405918"="Foundation", "1311031405919"="I Robot" "future" -> … |
Tags2AuthorsIndex | "sci-fi" -> (1311031405920="Asimov") "future" -> … |
本示例展示了关系模型和 Cassandra 模型之间的几个设计差异。Cassandra 模型在一个名为 “Book" 的单个列族内存储书籍数据,而其他三个列族是构建用来支持查询的索引。
请仔细看一下 “Books” 列族,该模型使用了一个行来表示书籍名称是行 ID 的每本书。有关书籍的细节被表示为存储在行中的列。
再 仔细看看,您可能会发现,已存储的数据项(比如评论、与书籍关系为 1:M 的标记)也位于单个行中。为了实现这一点,可以将时间戳附加在列名称上,以便进行标记和评论。这种方法在同一列中存储所有的数据。这样的操作避免了必须执 行 JOIN 才可检索数据的问题。Cassandra 弥补了通过此方法支持 JOIN 的不足。
这也提供了一些优势。
- 通过使用单个查询读取完整行的方法,您可以读取书籍的所有数据。
- 您可以通过使用切片查询来检索评论和标记,无需执行 JOIN,该切片查询的起始范围和终止范围分别为 cmt0-cmt9999 和 tag0-tag9999。
由 于 Cassandra 存储按照其列名称排序的列,这就使得切片查询非常快就能完成。值得注意的是,在单个行中存储所有的数据细节并使用排序顺序是 Cassandra 数据设计时最重要的理念。大多数 Cassandra 数据模型根据这些理念的某些形式进行设计。用户在存储数据和构建索引时可以使用排序顺序。例如,给列名称附加时间戳的另一个副作用是:就像列名称按照排序 顺序进行存储一样,评论也有使用时间戳后缀的列名称,并按照创建它们的顺序进行存储,且搜索结果也具有相同的顺序。
Cassandra 不支持基础设计的任何搜索方法。尽管其支持二级索引,这些方法还是通过使用后来构建的索引来提供支持,而且二级索引有一些局限性,不支持范围查询。
因 此,要实现 Cassandra 数据设计最好的结果,需要用户通过构建定制索引并使用列和行排序顺序来实现搜索。其他三个列族(Tags2BooksIndex、 Tags2AuthorsIndex 和 RankIndex)也这样做。由于用户需要搜索具有给定标记的书籍,通过将标记名称存储为行 ID,并将使用该标记进行标记的所有书籍存储为该行下的列,“Tags2BooksIndex” 列族构建了一个索引。如该例所示,时间戳被添加为列密钥,但是也是将要提供的惟一的列 ID。通过按照标记名称查找行并通过读取存储在该 rowID 内的所有列来找到匹配项,搜索实现仅读取索引。
表 2 讨论了应用程序要求的每个查询是如何使用上述 Cassandra 索引来实现的。
表 2. 查询实现的比较
查询描述 | SQL 查询 | Cassandra 实现 |
---|---|---|
列出根据等级存储的书籍 | 运行查询"Select * from Books order by rank" ,然后在每个结果上执行 "Select tag from Tags where bookid=?" and "Select comment from Comments where bookid=?" | 在 “RankIndex” 列族上进行切片查询,接收已排序的书籍列表,并在 “Books” 对每一个书籍执行切片查询,以便读取书籍的详细信息。 |
给定一个标记,查找具有给定标记的书籍的作者。 | Select distinct author from Tags, Books where Tags.bookid=Books.bookid and tag=? | 使用切片查询在 Tags2Authors 中读取给定标记的所有列。 |
给定一个标记,列出具有给定标记的书籍。 | Select bookid from Tags where tag=? | 使用切片查询在 Tags2BooksIndex 中读取具有给定标记的所有列。 |
给定一个书籍,创建评论时,按时间的排序对列出该书籍的评论进行排序。 | Select text, time, user from Comments where bookid=? Order by time | 在 “Books” 列族中,在与给定书籍对应的行中执行切片查询。它们是按排序顺序的,这是因为将时间戳用作了列名称。 |
尽管上述设计可以高效支持由书籍评分站点要求的查询,但它只能支持为专用查询设计但不支持专用查询的查询。例如,如果没有构建新的索引,它就不能支持以下查询。
Select * from Books where price > 50;
Select * from Books where author="Asimov"
将设计更改为支持这些和其他查询是有可能的,通过构建适当的索引或编写代码来遍历所有数据即可实现此操作。但是,需要定制代码来支持新的查询,与关系模型相比,这是一种局限性,因为在关系模型中添加新查询通常不需要更改模式。
在 0.8 发行版中,Cassandra 支持次级索引,用户可以在此根据给定属性指定搜索,而且 Cassandra 可以自动构建索引来根据该属性进行搜索。但是,该模型的灵活性不大。例如,次级索引不支持范围查询,也没有为结果的排序顺序提供保证。
在 Java 环境中使用 Cassandra
Cassandra 具有许多用不同语言编写的客户端。本文将重点介绍 Hector 客户端(参阅 参考资料),这是最广泛用于 Cassandra 的 Java 客户端。用户可以通过向应用程序类路径添加 Hector JAR 向应用程序添加 Hector 客户端节点。清单 4 展示了一个样例 Hector 客户端。
首先,连接到 Cassandra 集群。然后使用 Cassandra Getting Started Page(参阅 参考资料)中的指令来建立一个 Cassandra 节点。除非更改了配置,否则通常在端口 9160 之上运行该指令。其次,要定义一个密钥空间,这可以通过客户端或 conf/cassandra.yaml 配置文件来完成。
清单 4. Cassandra 的样例 Hector 客户端节点
Cluster cluster = HFactory.createCluster('TestCluster', new CassandraHostConfigurator("localhost:9160")); //define a keyspace Keyspace keyspace = HFactory.createKeyspace("BooksRating", cluster); //Now let's add a new column. String rowID = "Foundation"; String columnFamily = "Books"; Mutator<String> mutator = HFactory.createMutator(keyspace, user); mutator.insert(rowID, columnFamily, HFactory.createStringColumn("author", "Asimov")); //Now let's read the column back ColumnQuery<String, String, String> columnQuery = HFactory.createStringColumnQuery(keyspace); columnQuery.setColumnFamily(columnFamily).setKey(”wso2”).setName("address"); QueryResult<HColumn<String, String> result = columnQuery.execute(); System.out.println("received "+ result.get().getName() + "= " + result.get().getValue() + " ts = "+ result.get().getClock());
在 Download 中查找书籍评分示例的完整节点。包括切片查询的样例和其他复杂的操作。
Cassandra 架构
查看过 Cassandra 的数据模型之后,让我们返回到 Cassandra 的架构,从分布式系统的角度了解它的优缺点。
图 3 展示了 Cassandra 集群的架构。首先观察到得是 Cassandra 是一个分布式系统。Cassandra 包括多个节点,并跨这些节点来分发数据(用数据库的术语来说就是,将数据分成很多份)。
图 3. Cassandra 集群
Cassandra 使用一致的散列算法给节点分配数据项。简言之,Cassandra 使用一个散列算法来计算存储在 Cassandra (例如,列名称和行 ID)中的每个数据项的密钥散列。散列范围或所有可能的散列值(又称为密钥空间)是在 Cassandra 集群中的节点之间进行分配的。然后,Cassandra 向该节点分配每一个数据项,而该节点负责存储或管理数据项。论文 “Cassandra - A Decentralized Structured Storage System”(参阅 参考资料)提供了有关 Cassandra 架构的详细讨论。
由此产生的架构提供了以下属性:
- Cassandra 在其节点之间分发数据,这对用户是透明的。任何节点可以接收任何请求(读取、写入或删除)并将请求路由至正确的节点,即使数据没有存储在该节点中。
- 用户可以定义所需的副本的数量,而且 Cassandra 会透明地处理副本的创建和管理。
- 可 调节的一致性:在存储和读取数据时,用户可以选择所期望的每项操作的一致性级别。例如,如果 “quorum” 一致性级别是在执行写入或读取操作时使用,那么可以对来自集群中一半以上的节点的数据进行写入和读取操作。支持可调节的一致性使用户能够选择最适合其用例 的一致性级别。
- Cassandra 提供非常快速的写入速度,比每个节点以每秒 80-360MB 的速度传输数据时的读取速度还要快。它通过使用两项技术实现这一目的。
- Cassandra 在其负责的节点上保留内存中的大多数数据,而且所有更新都在内存中完成,并以一种懒惰的方式写入永久存储(文件系统)。但是,为了避免丢失数 据,Cassandra 将所有的事务写入磁盘中的提交日志。与在磁盘中更新数据不同,向提交日志写入数据是追加数据,因此在向磁盘写入输入时可以避免旋转延迟。有关磁盘驱动性能 特征的更多信息,请参阅 参考资料。
- 除非要求写入操作是完全一致的,否则 Cassandra 无需解决任何数据不一致性(只在首次读取时解决不一致性)问题即可将数据写入足够多的节点。这个过程称作 “读取修复”。
由 此产生的架构具有高可缩放性。您可以构建一个具有数十至数百个节点的 Cassandra 集群,能够处理数 TB 到数 PB 字节的数据。分布式系统有一个权衡,而且缩放几乎从来不会免费提供。如前所述,用户在从关系数据库迁移到 Cassandra 时会遇到许多惊喜。下一部分将讨论这些问题。
Cassandra 可能带来的惊喜
从关系数据库迁移到 Cassandra 时要意识到这些差异。
不支持事务,就不支持 JOIN
众所周知,Cassandra 不支持 ACID 事务。尽管其有一个批处理操作,但还是不能保证批处理操作内的子操作是以原子的方式进行的。在 失败操作可能产生变更 中会对此进行详细讨论。
此外,Cassandra 不支持 JOIN。如果用户需要连接两个列族,就必须以编程方式检索和连接数据。对于大型数据集来说,这通常代价高昂且非常耗时。Cassandra 通过在同一行中存储尽更多的数据来巧妙地避免这种局限性,如示例中所述。
没有任何外键和键是不可变的
Cassandra 不支持外键,所以 Cassandra 不可能代表用户来管理数据的一致性。因此,应用程序应当处理数据一致性。此外,用户不能更改键。推荐使用具有需要对多个键进行更改的用例的代理键(生成多个键而非一个键,并将键作为属性进行管理)。
键必须是惟一的
每个键(例如行键和列键)在此范围内都必须是惟一的,而且如果同一个键使用过两次,则需要重写数据。
对 于这个问题有两种解决方案。第一个是,您可以使用一个组合键。也就是说,通过组合多个字段来创建键,而且这个解决方案通常和行键一起使用。第二个解决方案 是,当出现同一个键被使用两次的危险时,使用任意值或时间戳作为该键的后缀。在索引将某个值存储为列名称并且使用这些索引时,通常会发生这种情况。例如, 在书籍评分应用程序中,等级用作列名称。为了避免有两个条目因具有相同的等级而具有相同的列名称,时间戳作为后缀添加到等级中。
失败的操作可能导致发生更改
正如前面已经解释过的,Cassandra 不支持原子操作。相反,它支持幂等操作。不论执行多少次操作,幂等操作都将系统保持为相同的状态。所有的 Cassandra 操作都是幂等的。如果操作失败,您可以果断进行重试。这就提供了一种从暂时性故障中恢复的机制。
Cassandra 还支持批处理操作,但也不提供任何原子性保证。因为操作是幂等的,所以客户端可以一直重试,直到所有批处理操作成功完成为止。
幂 等操作并不等同于原子操作。如果一个操作成功了,一切都很顺利,结果与原子操作是相同的;如果某个操作失败,客户端可以进行重试,如果重试成功了,那么再 次一切顺利。但是,如果在重试后操作仍然失败(与原子操作不同),则会产生副作用。不幸的是,在使用 Cassandra 的时候,这是程序员必须亲自处理的一项复杂事物。
搜索变得复杂
搜索并没有构建为 Cassandra 架构的核心,而且如前所述,搜索机制使用了排序顺序,划分在分层结构的顶部。Cassandra 支持次级索引,系统可以利用一些受限的功能在此自动构建次级索引。当次级索引不工作时,用户必须了解数据模型,并使用排序顺序和切片来构建索引。
与构建搜索方法相关的三种类型的复杂领域:
- 在一定程度上,构建定制搜索方法需要程序员了解索引和存储的细节。因此,Cassandra 需要的是更高水平的技能熟练的开发人员,而不是关系模型。
- 定制索引很大程度上取决于排序顺序,而且被复杂化。有两种类型的排序顺序:第一,列始终根据名称进行排序,第二,行排序顺序只在使用保序分区程序(参阅 参考资料)时起作用。
- 添加一个新查询通常需要新的索引以及与关系模型不同的代码更改。这就要求开发人员先分析查询,然后再存储数据。
不赞成使用超级列和保序分区程序
Cassandra 超级列在建模多层数据时非常有用,它可以向层次结构再添加一个级别。然而,可以与超级列一起建模的任何事物也通过列进行支持。因此,超级列不提供附加能 力,也不支持次级索引。因此,Cassandra 开发人员不赞成使用超级列。尽管没有固定的中断支持的日期,但这种情况会发生在将来的版本中。
Cassandra 中的分区程序决定了以何种方式在 Cassandra 节点之间分发(分开)数据,而且有多种实现方式。如果使用保序分区程序,那么 rowID 会根据排序顺序进行存储,而且 Cassandra 也可以跨各个 rowID 进行切片(搜索)。然而,该分区程序并不是在其节点之间均匀地分发数据,如果使用大数据库,一些节点可能负担很重,而其他节点则是空载的。因此,开发人员 也不赞成使用保序分区程序。
手动执行故障修复
如果 Cassandra 集群中的一个节点已经失败,如果您有副本的话,则该集群将继续工作。完整恢复,用来重新分发数据并弥补缺少的副本,是一项通过名为节点工具(参阅 参考资料)执行的手动操作。而且,执行手动操作时,系统是不可用的。
记得删除
Cassandra 被设计为:即使节点发生故障(或断开连接)但随后又恢复,节点仍会继续工作,不会出现任何问题。其中一个结果就是这使得数据删除复杂化。例如,假设一个节 点出现故障。出现故障时,数据项已经从副本中删除。当不可用的节点恢复的时候,如果 Cassandra 记得该数据项已经删除,它会在同步流程中再次引入已删除的数据项。
因此,Cassandra 必须记得该数据项已经删除。在 0.8 发行版中,Cassandra 记得所有数据(即使已经删除)。这就使得进行集中更新操作时磁盘使用率持续增长。Cassandra 不需要记得所有已删除的数据,但事实恰好已经删除了一个数据项。在以后的 Cassandra 发行版中会执行此项修复。
结束语
本 文深入研究了在考虑 Cassandra 时不太明显的一些细节。我描述了 Cassandra 数据模型,将它与关系数据模型进行了比较,并演示了一个使用 Cassandra 设计的典型模式。其中一个重要的观察结果是,与关系模型有所不同,Cassandra 将数据分解成许多表格,并试图在同一行内保留尽可能多的数据,从而避免连接该数据进行检索。
您还可以看看基于 Cassandra 的方法的一些局限性。但是,这些局限性对于大多数 NoSQL 解决方案来说很常见,而且常常是支持高可扩展性时需要留意的一些设计权衡。
同类文章: