考虑这样一个场景:你为之工作的公司是由完全独立的站点组成的联邦,而且在各种不同平台上用了半打不同编程语言来实现。每个独立站点都有其自己的数据库与模式,被拥有不同技术集合的各个团队管理,分布在美国和欧洲的八个站点上。别忘了,公司还在扩张。
你的任务?使这些截然不同的系统能彼此之间方便快捷的共享关键数据。
你的设计准则是:
高传输能力——该服务在启动后每天需要传送大约1M的数据块。
事务的正确性——作为所有客户端数据的权威来源,该服务必须是精确的。
弹性——当数据因格式变更而重新发布时,该服务必须能够方便地进行无缝升级。
松耦合——对于如此多的系统,每个都必须能自我独立管理。
可采纳——对于以各种语言(Java、C#、PHP、Ruby和ColdFusion)实现的客户端而言,采纳该系统的门槛必须足够的低。
自适应性——该系统必须能够支持多种不同类型的数据并能按照需要进行扩展以增加数据类型。
一年之前,在我们俩服务的Homeaway.com公司,我们面对的正是以上问题。没过多久,我们就认识到了两条设计原则:首先,一个分布式的发布-订阅服务是应对子系统的弹性和松耦合的极佳选择;其次,对于需要高伸缩性、可扩展性和易采纳性的系统而言,构建RESTful服务(相对于SOAP等重量级协议而言)是自然之道。这两条原则直接将我们引向了Atom——一个RESTful发布协议——以及一种名为Atom存储的新型数据产生服务。我们去年一直都在为Homeaway实现Atom存储。我们从实际实现中抽取出了开源的Atom存储框架,命名为AtomServer(http://www.atomserver.org),本文将对其进行描述。
Atom由两份规范组成:Atom聚合格式,它基于XML定义了描述web feeds的语言;Atom发布协议,它描述了检索和操纵feeds的一种RESTful HTTP协议。
Atom被认为是RSS(Rich Site Summary,富站点摘要)的接替者,后者通常包含了人为撰写的内容,比如博客条目。因此,一个Atom条目或feed(XML元素和属性)内部结构传达了诸如作者、语言、标题等发布内容的语义。可别被这一点给骗了;Atom条目非常适合将所有类型数据作为有效负载进行传送。
Atom条目是数据记录个体,Atom feeds是条目的列表。因为Atom是RESTful协议,所以对资源的访问是通过对标识资源(这里指的是条目和feeds)的URI执行HTTP方法来完成的。举例来说,获取一个博客条目的feed可能是通过对http://your-atomserver/entries/myblog这样的URI执行GET方法来完成的,而响应可能会是:
<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<link rel="alternate"
href="http://your-blogserver/MyBlog"/>
<updated>2007-04-14T20:00:39Z</updated>
<title>My Weblog</title>
<entry>
<title>First Post</title>
<author>Chris Berry</author>
<link rel="edit"
href="http://your-blogserver/MyBlog/1234"/>
<updated>2007-04-14T20:00:00Z</updated>
<id>1234</id>
</entry>
<entry>
<title>Next Post</title>
<author>Bryon Jacob</author>
<link rel="edit"
href="http://your-blogserver/MyBlog/1235"/>
<updated>2007-05-01T17:00:00Z</updated>
<id>1235</id>
</entry>
</feed>
Atom还支持可被应用于条目的类别概念。类别是任意的字符串标签,可被应用于条目,使其成为某个分组的一部分。然后,可修改Feed URI包含过滤功能——只从feed返回那些应用了特定类别的条目。
有些术语在Atom上下文中具有特殊含义。条目被组织成固定的两级层次结构:工作空间和集合。工作空间包含着一定数量的集合,集合包含着一定数量的条目。一个feed的URI由其工作空间和集合组成。
http://your-atom-server/workspace/collection
而一个条目的URI就由工作空间、集合和条目id来组成。这一标识符在包含该条目的集合内必须是唯一的:
http://your-atom-server/workspace/collection/entryid
像RSS一样,Atom也为web聚合框架提供了基础。支持Atom的客户端有很多,包括浏览器,新闻阅读器,以及几乎每种流行语言中的可编程客户端。再加上,Atom实际上是建立在HTTP与XML之上的一个精简约定集合,因此几乎任何支持web的编程平台都可以很容易地访问它。
核心Atom协议描述了操纵feed和条目的基本操作,错误报告与处理的方法,并特别提供了扩展概念。Atom扩展是可在Atom XML文件中出现的附加XML元素和属性,以及可被应用于URI的附加HTTP请求参数,它可被用来修改支持Atom的服务器的行为。两个重要的扩展是:
OpenSearch——定义了一个搜索协议,包括确定支持哪类搜索的服务器内省方法。一个支持OpenSearch的服务将会将搜索结果作为Atom feed返回,其中每个结果被表示为一个条目。
Feed分页——为基于时间的数据提供了分页功能,定义了Atom feeds的下一页和上一页链接类型,这样客户端就能使用它在多页Atom feed中前后翻页。
或许最显而易见又影响深远的Atom应用就要数GData了,它是Google用来访问它们服务数据的Web API。GData包含了核心Atom规范和OpenSearch,以及为覆盖这些规范未涉及的附加特性而进行的大量扩展。
当你不再将Atom条目局限于博客与新闻feed等web内容,并将其扩展到一般性的数据管理时,你就得到了Atom存储;一个由互联Atom条目组成的通用数据存储,你可以用Atom发布协议来编辑它,用OpenSearch来搜索它。AtomServer正是在这样的期望中成长起来的:利用这一策略实现对我们数据的分布式访问。
使用Atom协议的好处之一就是其系统内含的分布式特性。在AtomServer里,我们将分页概念与向服务器轮询feed更新的机制相结合,限制了每次请求返回的条目数量。客户端最初从头请求feed,接着按页请求数据,直到没有新数据。之后,客户端需要定期检查新数据(例如,在上次成功处理的数据之后,是否有新的一页数据传送?)
每次变更的时候,Atom都会用一个递增的计数器来标记每一个条目,而客户端被要求为每个它请求的feed保存被处理的计数器的最后一值。在随后对feed的轮询时,下一页数据的起始位被设置成上一页的终止位。这一方法能够有效应对使用AtomServer来处理大容量、快速变化数据的场景。
当你不使用起始位“拉取”feed,它将默认从0开始。举例来说,
GET http://your-atom-server/widgets/acme
将返回http://atomserver.org/namespaces/1.0/名字空间中一个名为endIndex(终止位)的扩展标签作为这个feed的子元素。它将包含当前检索页的最后一个标志位:
<as:endIndex>23</as:endIndex>
在下次轮询时,这个数字将被作为start-index(起始位)查询参数传送:
GET http://your-atom-server/widgets/acme?start-index=23
当被请求页没有新数据了,服务器将会返回一个304 NOT MODIFIED(304未修改)响应。该信号表明,在请求下一页数据之前最好等待一段可配置的轮询间隔时间。
在Atom,每个条目在其所属工作空间和集合中都有一个ID是至关重要的——这三个部分加起来使条目的URI成为该条目的唯一标识符,将其与服务中的其它条目分别开来。AtomServer支持使用两种不同HTTP方法创建新条目:POST和PUT。
当条目是由POST方法创建时,所用的URI是该条目将要插入的集合的URI。在这种情况下,AtomServer将负责为新条目分配条目id,并在响应体中将这个id返回给POST调用者。
POST http://your-atom-server/widgets/acme
取而代之,当条目是由PUT方法创建时,为条目分配id的责任就落到了完成PUT操作的客户端头上。PUT要操作的URI就成了欲创建条目的URI。
PUT http://your-atom-server/widgets/acme/1000.xml
现有条目更新是通过对该条目的URI进行PUT操作来完成的——从这种意义来说,使用PUT创建一个条目就好像是“延迟更新”。如果没有这样的条目,就创建它,否则就对该条目进行更新。
为了在高度分布的Atom世界里确保一致、可预见的数据,AtomServer使用了乐观并发性(Optimistic Concurrency)管理对系统的写入。乐观并发性(Optimistic Concurrency)规定:AtomServer的写入者必须知道他编辑资源的当前修订号,并且应当在假定他的写入操作可以完成的前提下来进行操作,但要优雅地处理其他人同时对该资源进行写入操作的情形。
例如,假设系统A和B同时想要更改某个数据feed中的Acme部件。A先来到,它请求Acme部件 123的当前表述:
GET http://your-atom-server/widgets/acme/123.xml
然后feed响应返回了如下的“编辑链接”。
于是现在A着手对123.xml的表述开始编辑。此时B来到并请求123.xml的当前版本,并得到了同样的响应。B的编辑比A用时要短,因此B立即将更改写回了编辑链接:
PUT http://your-atom-server/widgets/acme/123.xml/2
操作成功,返回200 OK(200成功),让B知道其编辑已成功提交给AtomServer。此时,A完成了编辑并试图将其写入同一个编辑链接,但是由于B的编辑,修订号已经被改变了。因而,A将会收到一个409 CONFLICT(409冲突)HTTP错误,表示他尝试更新的资源自从他最近一次刷新其视图之后已被他人更改过了。遇到这种情况,A应该再次GET这个资源,这次会获取一个新的编辑链接,然后重复这个过程。注意,这使得A在B已经改动后的/widgets/acme/123.xml的副本上进行编辑,因此系统避免了A盲目地覆盖B的变更。
在许多系统中,对给定数据集合将只有单个权威的写入者。在那些场景中,为了降低乐观并发性(Optimistic Concurrency)带来的开销,可通过将星号(*)作为修订序号来屏蔽乐观并发性(Optimistic Concurrency):
PUT http://your-atom-server/widgets/acme/123.xml/*
然而,使用这一特性记住一点非常重要:只适于客户端知道给定资源仅有一个写入者的情形。
在Atom中,条目“类别”是由一对值来明确说明的:Scheme和Term。Scheme实质上是类别的“名字空间”,而Term是这一名字空间中一个特定的值。
借用GData对Atom的扩展,AtomServer支持一种特殊的feed语法,它可以让客户端基于应用于条目的“类别”来对feed进行过滤。比如,为了只获取Acme部件中被标记为“红”颜色的feed,请求可能是这样的形式:
GET http://your-atom-server/widgets/acme/-/(urn:colors)red
如果指明了多个类别,只有满足全部指定类别(逻辑与)的条目能被接受。例如,
GET http://your-atom-server/widgets/acme/-/(urn:colors)red/(urn:size)big
将会返回所有大号、红色的Acme部件。利用前缀表示法可指明使用“AND(与)”和“OR(或)”的任意类别组合:
GET http://your-atom-server/widgets/acme/-/OR/(urn:colors)red/AND/(urn:size)big/(urn:color)blue
这将返回所有要么是红色,要么是大号、蓝色的Acme部件。客户端应该将这些feed跟其它feed一视同仁:它们跟没有类别的feed一样同样可以被轮询与分页。
AtomServer是实现Atom存储的一款现成实现。它是以Java web应用实现的,应该部属于任何J2EE Serlet容器中。幕后,AtomServer使用了Apache的Atom协议开源实现——Abdera——处理RESTful动词与Atom的XML词汇。
就给现有应用加上Atom前端而言,Abdera是一款极其出色的类库。与之相比,AtomServer则是一个完整的Atom存储实现。它提供了开箱即用的存储和与Atom元数据(以及Atom条目本身内容)交互需要的所有组件。
AtomServer的协议尽可能地借鉴了GData的设计。在某些场合我们做了一些轻微的调整,以进一步提高URL的可读性,简化查询结构,或者实现GData规范所未能涉及的特性。
AtomServer在关系数据库中管理条目关联的所有Atom元数据;在关系数据库或文件系统中管理条目实际内容,这取决于你的具体需求。 AtomServer自动地为你处理Atom协议相关的每个方面(URI解释,解析Atom元素与扩展,更新时间戳,对条目分门别类),因此你只需要将改动发布到服务器并每隔一段时间轮询feed更新。
AtomServer使用非常方便。它可以部署为一个简单的WAR文件,或者作为替代,部署为一个独立的服务器,运行于其内部嵌入的Jetty服务器上。大多数的应用都只需要很少的配置(一些用于配置Atom工作空间和内容存储的Spring Bean)就可以用上AtomServer了。
AtomServer仍有一些重要、高级的特性我们还没有涉及到。这次就没有机会展开讨论了,在今后的文章我们将深入探讨:
Atom类别自动标记器:一种方便的配置机制,用于创建或更新条目时对其进行“自动标记”。其中内置了XPathAutoTagger,使得你能够“XPath”你的内容并根据条件将其与Atom类别关联。
批处理操作:全面支持创建,更新或者删除等请求混合而成的“批处理”操作。
Feed聚合:一种将来自不同集合或工作空间的不同条目聚合到一个条目的强大能力,使用了ATom类别。包括请求这些聚合feed的能力。因此它让你从与不同的feed 打交道——自行在后端将信息联结起来——中解脱出来,你只需监听一个单一的聚合feed,它将反映其内部任一部分的变化 。
AtomServer是看得见摸得着的。它正运用在我们公司的实际生产环境中,每天处理着上百万的请求,存储着几百万的条目。构建于Atom这样的 RESTful规范之上,同时利用诸如GData这样的现有服务的设计,保证了其构建根基是坚固的。我们希望你能下载一份拷贝,并告诉我们你的想法。你可以从这里得到它:http://www.atomserver.org,只需简单的步骤它就能很好的为你工作。
查看英文原文:AtomServer – The Power of Publishing for Data Distribution。
志愿参与InfoQ中文站内容建设,请邮件至[email protected]。也欢迎大家到InfoQ中文站用户讨论组参与我们的线上讨论。