转载:http://www.ibm.com/developerworks/cn/opensource/os-riak1/index.html
简介
典型的现代关系数据库在某些类型的应用程序中表现平平,难以满足如今的互联网应用程序的性能和可扩展性要求。因此,需要采用不同的方法。在过去几年中,一种新的数据存储类型变得非常流行,通常称为 NoSQL,因为它可以直接解决关系数据库的一些缺陷。Riak 就是这类数据存储类型中的一种。
Riak 并不是惟一的一种 NoSQL 数据存储。另外两种较流行的数据存储是 MongoDB 和 Cassandra。尽管在许多方面十分相似,但是它们之间也存在明显的不同。例如,Riak 是一种分布式系统,而 MongoDB 是一种单独的系统数据库,也就是说,Riak 没有主节点的概念,因此在处理故障方面有更好的弹性。尽管 Cassandra 同样是基于 Amazon 的 Dynamo 描述,但是它在组织数据方面摒弃了向量时钟和相容散列等特性。Riak 的数据模型更加灵活。在 Riak 中,在第一次访问 bucket 时会动态创建这些 bucket;Cassandra 的数据模型是在 XML 文件中定义的,因此在修改它们过后需要重启整个群集。
Riak 的另一个优势是它是用 Erlang 编写的。而 MongoDB 和 Cassandra 是用通用语言(分别为 C++和 Java)编写,因此 Erlang 从一开始就支持分布式、容错应用程序,所以更加适用于开发 NoSQL 数据存储等应用程序,这些应用程序与使用 Erlang 编写的应用程序有一些共同的特征。
Map/Reduce 作业只能使用 Erlang 或 JavaScript 编写。对于本文呢,我们选择使用 JavaScript 编写 map
和 reduce
函数,但是也可以用 Erlang 编写它们。虽然 Erlang 代码的执行速度可能稍快一些,然而我们选择 JavaScript 代码的理由是它的受众更广。参阅 参考资料 中的链接,详细了解 Erlang。
回页首
开始
如果您希望尝试本文中的一些示例,则需要在您的系统中安装 Riak(参阅 参考资料)和 Erlang。
您还需要构建一个包含三个节点的群集并在您的本地机器上运行它。Riak 中保存的所有数据都被复制到群集的大量节点中。数据所在的 bucket 的一个属性 (n_val) 决定了将要复制的节点的数量。该属性的默认值为 3,因此,要完成本示例,我们需要创建一个至少包含三个节点的群集(之后您可以创建任意数量的节点)。
下载了源代码后,您需要进行构建。基本步骤如下:
$ tar xzvf riak-1.0.1.tar.gz
$ cd riak-1.0.1
$ make all rel
这将构建 Riak (./rel/riak)。要在本地运行多个节点,则需要生成 ./rel/riak 的副本,对每个额外的节点使用一个副本。将 ./rel/riak 复制到 ./rel/riak2、./rel/riak3 等地方,然后对每个副本执行下面的修改:
-name [email protected]
现在依次启动每个节点,如 清单 1 所示。
$ cd rel $ ./riak/bin/riak start $ ./riak2/bin/riak start $ ./riak3/bin/riak start |
最后,将节点连接起来形成群集,如 清单 2 所示。
$ ./riak2/bin/riak-admin join [email protected] $ ./riak3/bin/riak-admin join [email protected] |
您现在应该创建了一个在本地运行的 3 节点群集。要进行测试,运行如下命令: $ ./riak/bin/riak-admin status | grep ring_members
。
您应当看到,每个节点都是刚刚创建的群集的一部分,例如 ring_members : ['[email protected]','[email protected]','[email protected]']
。
回页首
Riak API
目前有三种方式可以访问 Riak:HTTP API(RESTful 界面)、Protocol Buffers 和一个原生 Erlang 界面。提供多个界面使您能够选择如何集成应用程序。如果您使用 Erlang 编写应用程序,那么应当使用原生的 Erlang 界面,这样就可以将二者紧密地集成在一起。其他一些因素也会影响界面的选择,比如性能。例如,使用 Protocol Buffers 界面的客户端的性能要比使用 HTTP API 的客户端性能更高一些;从性能方面讲,数据通信量变小,解析所有这些 HTTP 标头的开销相对更高。然而,使用 HTTP API 的优点是,如今的大部分开发人员(特别是 Web 开发人员)非常熟悉 RESTful 界面,再加上大多数编程语言都有内置的原语,支持通过 HTTP 请求资源,例如,打开一个 URL,因此不需要额外的软件。在本文中,我们将重点介绍 HTTP API。
所有示例都将使用 curl 通过 HTTP 界面与 Riak 交互。这样做是为了更好地理解底层的 API。许多语言都提供了大量客户端库,在开发使用 Riak 作为数据存储的应用程序时,应当考虑使用这些客户端库。客户端库提供了与 Riak 连接的 API,可以轻松地与应用程序集成;您不必亲自编写代码来处理在使用 curl 时出现的响应。
API 支持常见的 HTTP 方法:GET
、PUT
、POST
、DELETE
,它们将分别用于检索、更新、创建和删除对象。我们稍后将依次介绍每一种方法。
存储对象
您可以将 Riak 看成是创建键(字符串)与值(对象)的分布式映射。Riak 将值保存在 bucket 中。在保存对象之前,不需要显式地创建 bucket;如果将对象保存到一个不存在的 bucket 中,则会自动创建该 bucket。
Bucket 在 Riak 中是一个虚拟概念,主要是为了对相关对象分组而存在。bucket 还具有其他一些属性,这些属性的值定义了 Riak 对存储在其中的对象的处理。下面是 bucket 属性的一些示例:
n_val
:对象在群集内进行复制的次数 allow_mult
:是否允许并发更新您可以通过对 bucket 发出 GET
请求查看 bucket 的属性(及其当前值)。
要存储对象,我们将对 清单 3 所示的其中一个 URL 执行 HTTP POST
。
POST -> /riak/<bucket> (1) POST -> /riak/<bucket>/<key> (2) |
键可以由 Riak (1)自动分配,或由用户 (2) 定义。
当使用用户定义的键存储对象时,也可以向 (2) 执行一个 HTTP PUT
操作来创建对象。
Riak 的最新版本还支持以下 URL 格式:/buckets/<bucket>/keys/<key>,但是在本文中,我们将使用更旧的格式来维持与早期 Riak 版本的向后兼容性。
如果没有指定键,Riak 会自动为对象分配一个键。例如,我们将在 bucket “foo” 中存储一个明文对象,并且不会显式指定键(参见 清单 4)。
$ curl -i -H "Content-Type: plain/text" -d "Some text" \ http://localhost:8098/riak/foo/ HTTP/1.1 201 Created Vary: Accept-Encoding Location: /riak/foo/3vbskqUuCdtLZjX5hx2JHKD2FTK Content-Type: plain/text Content-Length: ... |
通过检查 Location 标头,您可以看到 Riak 分配给对象的键。这样做不容易记忆,因此另一种选择是让用户提供键。让我们创建一个艺术家 bucket,并添加一个叫做 Bruce 的艺术家(参见 清单 5)。
$ curl -i -d '{"name":"Bruce"}' -H "Content-Type: application/json" \ http://localhost:8098/riak/artists/Bruce HTTP/1.1 204 No Content Vary: Accept-Encoding Content-Type: application/json Content-Length: ... |
如果使用我们指定的键成功存储了对象,我们将从服务器得到一个 204 No Content 响应。
在本例中,我们将对象的值保存为 JSON,但是它既可以是明文格式,也可以是其他格式。在存储对象时,需要注意正确设置 Content-Type 标头。例如,如果希望存储一个 JPEG 图像,那么您必须将内容类型设置为 image/jpeg。
检索对象
要检索已存储的对象,使用您希望检索的对象的键对 bucket 运行 GET
方法。如果对象存在,则会在响应的正文中返回对象,否则服务器会返回 404 Object Not Found 响应(参见 清单 6)。
GET
方法
$ curl http://localhost:8098/riak/artists/Bruce HTTP/1.1 200 OK ... { "name" : "Bruce" } |
更新对象
在更新对象时,和存储对象一样,需要用到 Content-Type 标头。例如,让我们来添加 Bruce 的别名,如 清单 7 所示。
$ curl -i -X PUT -d '{"name":"Bruce", "nickname":"The Boss"}' \ -H "Content-Type: application/json" http://localhost:8098/riak/artists/Bruce |
如前所述,Riak 自动创建了 bucket。这些 bucket 拥有一些属性,其中一个属性为 allow_mult,用于确定是否允许执行并发写操作。默认情况下,该属性被设置为 false;但是,如果允许进行并发更新,则需要向每个更新发送 X-Riak-Vclock 标头。应该将该标头的值设置为与客户端最后一次读取对象时看到的值相同。
Riak 使用向量时钟 (vector clock) 判断修改对象的原因。向量时钟的工作原理超出了本文的讨论范围,但是,在允许执行并发写操作时,可能会出现冲突,这时需要使用应用程序来解决这些冲突(参阅 参考资料)。
删除对象
删除对象的操作使用了一个与前面的命令类似的模式,我们只需要对希望删除的对象所对应的 URL 执行一个 HTTP DELETE
方法: $ curl -i -X DELETE http://localhost:8098/riak/artists/Bruce
。
如果成功删除对象,我们会从服务器获得一个 204 No Content 响应;如果试图删除的对象不存在,那么服务器会返回一个 404 Object Not Found 响应。
回页首
链接
目前为止,我们已经了解了如何通过将对象与特定键相关联来存储对象,稍后可以使用此特定键来检索对象。如果能够将这个简单的模型进行扩展以表示对象如何(以及是否)与其他对象相关,那么这会非常有用。我们当然可以实现这一点,并且 Riak 是使用链接实现的。
那么,什么是链接?链接允许用户创建对象之间的关系。如果熟悉 UML 类图的话,您可以将链接看作是对象之间的某种关联,并用一个书签说明这种关系;在关系数据库中,该关系被表示为一个外键。
通过 “Link” 标头,以将链接 “依附” 到对象上。下面演示了链接标头看起来是什么样子。例如,关系的目标(即我们准备进行链接的对象)是尖括号中的内容。关系内容(本例中为 “performer”)是通过 riaktag 属性来表示的:Link: </riak/artists/Bruce>; riaktag="performer"
。
现在让我们添加一些专辑,并将它们与专辑的表演者艺术家 Bruce 关联起来(参见 清单 8)。
$ curl -H "Content-Type: text/plain" \ -H 'Link: </riak/artists/Bruce> riaktag="performer"' \ -d "The River" http://localhost:8098/riak/albums/TheRiver $ curl -H "Content-Type: text/plain" \ -H 'Link: </riak/artists/Bruce> riaktag="performer"' \ -d "Born To Run" http://localhost:8098/riak/albums/BornToRun |
现在我们已经设置了一些关系,接下来要通过 link walking 查询它们,link walking 是一个用于查询对象关系的进程。例如,要查找表演 River 专辑的艺术家,您应当这样做:$ curl -i http://localhost:8098/riak/albums/TheRiver/artists,performer,1
。
末尾的位是链接说明。链接查询的外观就是这个样子。第一个部分(artists
)指定我们应当执行查询的 bucket。第二个部分(performer
)指定了我们希望用于限制结果的标签,最后的 1
部分表示我们希望包含这个查询阶段的结果。
还可以发出过渡性查询。假设我们在专辑和艺术家之间建立了关系,如 图 1 所示。
通过执行下面的命令,可以发出 “哪些艺术家与表演 The River 专辑的艺术家合作过” 之类的查询:$ curl -i http://localhost:8098/riak/albums/TheRiver/artists,_,0/artists,collaborator,1
。链接说明中的下划线的作用类似于通配符,表示我们不关心具体的关系是什么。
回页首
运行 Map/Reduce 查询
Map/Reduce 是一个由 Google 推广的框架,用于在大型数据集上同时运行分布式计算。Riak 还提供 Map/Reduce 支持,它允许对群集中的数据运行功能更强大的查询。
Map/Reduce 函数包括一个 map 阶段和一个 reduce 阶段。map 阶段应用于某些数据并生成 0 个或多个结果;这在编程中类似于通过列表中的每一项映射函数。map 阶段是并行发生的。reduce 阶段将获取 map 阶段的所有结果,并将它们组合起来。
例如,计算某个单词在大量文档中出现的次数。每个 map 阶段都将计算每个单词在特定文档中出现的次数。这些中间计数在计算完后将发送到 reduce 函数,然后计算总数并得出在所有文档中的次数。参见 参考资料,获得有关 Google 的 Map/Reduce 文章的链接。
回页首
示例:分布式 grep
对于本文,我们将开发一个 Map/Reduce 函数,该函数将对 Riak 中存储的一组文档执行一次分布式 grep。和 grep 一样,最终的输出是一些匹配所提供模式的行。此外,每个结果还将表示文档中出现匹配时所在位置的行号。
要执行一个 Map/Reduce 查询,我们将对 /mapred 资源执行 POST
操作。请求的内容是查询的 JSON 表示;和前面的例子一样,必须提供 Content-Type 标头,并且始终将其设置为 application/json。清单 9 显示了我们为执行分布式 grep 而做的查询。后面将依次讨论查询的每一个部分。
{ "inputs": [["documents","s1"],["documents","s2"]], "query": [ { "map": { "language": "javascript", "name": "GrepUtils.map", "keep": true, "arg": "[s|S]herlock" } }, { "reduce": { "language": "javascript", "name": "GrepUtils.reduce" } } ] } |
每个查询都包含若干输入,例如,我们希望对之执行计算的文档,在 map 和 reduce 阶段运行的函数的名称。也可以直接在查询中包含 map
和 reduce
函数的源代码,只需要使用源属性替代名称即可,但是我在本例中没有这样做;然而,要使用指定的函数,则需要对 Riak 的默认配置进行一些修改。将清单 9 中的代码保存到某个目录中。对于群集中的每个节点,找到文件 etc/app.config,打开它并将属性 property js_source_dir 设置为您用于保存代码的目录。您需要重启群集中的所有节点使变更生效。
清单 10 中的代码包含将在 map 和 reduce 阶段执行的函数。map
函数将查看文档的每一行,确定是否与提供的模式(arg
参数)匹配。本例中的 reduce
函数并不会执行太多操作;它类似于一个恒等函数,仅仅用于返回输入。
var GrepUtils = { map: function (v, k, arg) { var i, len, lines, r = [], re = new RegExp(arg); lines = v.values[0].data.split(/\r?\n/); for (i = 0, len = lines.length; i < len; i += 1) { var match = re.exec(lines[i]); if (match) { r.push((i+1) + “. “ + lines[i]); } } return r; }, reduce: function (v) { return [v]; } }; |
在运行查询之前,我们需要一些数据。我从 Project Gutenberg Web 站点下载了 Sherlock Holmes 电子图书(参见 参考资料)。第一个文本存储在键 “s1” 下的 “documents” bucket 中;第二个文本位于同一个 bucket 中,键为 “s2”。
清单 11 展示了如何将这类文档上传到 Riak。
$ curl -i -X POST http://localhost:8098/riak/documents/s1 \ -H “Content-Type: text/plain” --data-binary @s1.txt |
上传文档后,我们现在可以对文档执行搜索。在本例中,我们想输出匹配常规表达式 "[s|S]herlock"
(参见 清单 12)的所有行。
$ curl -X POST -H "Content-Type: application/json" \ http://localhost:8098/mapred --data @-<<\EOF { "inputs": [["documents","s1"],["documents","s2"]], "query": [ { "map": { "language":"javascript", "name":"GrepUtils.map", "keep":true, "arg": "[s|S]herlock" } }, { "reduce": { "language": "javascript", "name": "GrepUtils.reduce" } } ] } EOF |
查询中的 arg
属性包含我们希望在文档中对其执行 grep 查询的模式;该值被作为 arg
参数传递给 map
函数。
清单 13 中显示了对样例数据运行 Map/Reduce 作业所产生的输出。
[["1. Project Gutenberg's The Adventures of Sherlock Holmes, by Arthur Conan Doyle","9. Title: The Adventures of Sherlock Holmes","62. To Sherlock Holmes she is always THE woman. I have seldom heard","819. as I had pictured it from Sherlock Holmes' succinct description,","1017. \"Good-night, Mister Sherlock Holmes.\"","1034. \"You have really got it!\" he cried, grasping Sherlock Holmes by" …]] |
回页首
流化 Map/Reduce
在关于 Map/Reduce 的最后部分中,我们将简单地了解 Riak 的 Map/Reduce 流化 (streaming) 特性。该特性对于包含 map 阶段并需要花一些时间完成这些阶段的作业非常有用,因为对结果进行流化允许您在生成每个 map 阶段的结果后立即访问它们,并且在执行 reduce 阶段之前访问它们。
我们可以对分布式 grep 查询应用这个特性。本例中的 reduce 步骤并没有多少实际操作。事实上,我们完全可以去掉 reduce 阶段,只需要将每个 map 阶段的结果直接发送到客户端即可。为了实现此目标,需要对查询进行修改,删除 reduce 步骤,将 ?chunked=true
添加到 URL 末尾,表示我们希望对结果进行流化(参见 清单 14)。
$ curl -X POST -H "Content-Type: application/json" \ http://localhost:8098/mapred?chunked=true --data @-<<\EOF { "inputs": [["documents","s1"],["documents","s2"]], "query": [ { "map": { "language": "javascript", "name": "GrepUtils.map", "keep": true, "arg": "[s|S]herlock" } } ] } EOF |
在完成 map 阶段后,会将每个 map 阶段的结果(在本例中为匹配查询字符串的行)返回给客户端。该方法可用于需要在查询的中间结果可用时就对它们进行处理的应用程序。
回页首
结束语
Riak 是基于 Amazon 的 Dynamo 文件中记载的规则的一种开源的、高度可扩展的键值存储库。Riak 非常易于部署和扩展。可以无缝地向群集添加额外的节点。link walking 之类的特性以及对 Map/Reduce 的支持允许实现更加复杂的查询。除了 HTTP API 外,Riak 还提供了一个原生 Erlang API 以及对 Protocol Buffer 的支持。在本系列的第 2 部分中,我们将探讨各种不同语言中的大量客户端库,并展示如何将 Riak 用作一种高度可扩展的缓存。
参考资料
学习
获得产品和技术