AtomServer:数据分发的发布动力(第二部分)

在本系列的第一部分里我们介绍了AtomServer,它是一个可扩展的开源Atom存储框架。Atom存储是数据服务架构的新趋势,结合了Atom发布协议和GData风格扩展。

AtomServer是一个高负载、高可用的数据分发系统的精简版,该系统位于Homeaway.com。由于它抽取自一个与大量不同客户群交互的生产系统,AtomServer的演化重点绝对是以客户为中心的。这也催生了几个AtomPub规范扩展的诞生。其中最有必要一提的包括,自动标记(Auto-Tagging)、 批处理(Batching)和Feeds聚合(Aggregate Feeds)。

自动标记

类别是AtomPub里最有用的概念之一。一个Atom类别本质上是一个“名字-值”对(在Atom中被称为Scheme和Term),它作为附加元数据与一个Atom条目关联。这是一个很强大的概念,因为它使客户可以对数据进行分类,并在需要时应用新的关系,而这些根本不用操作原始数据源。

AtomServer自动维护条目的类别元数据。它还提供了一种机制可以对这些类别执行全部的布尔操作(如,AND和OR)。(GData也提供类似的机制,在本系列第一部分有详细阐述)。

鉴于类别在AtomServer当中扮演如此重要的角色,我们优先考虑的第一个特性就是对条目“自动标记”的能力。自动标记器会根据提交条目(PUT操作)的内容自行计算类别。

自动标记是通过实现EntryAutoTagger 接口来完成的。使用像Spring这样的IOC容器,可以实现将EntryAutoTagger注入AtomServer工作空间。

最常见的条目内容类型是XML,因此AtomServer提供了XPathAutoTagger实现。该EntryAutoTagger可以很容易被配置成根据条目内容执行XPath表达式来为条目产生类别。

举例来说,从以下XML内容:

<widget id="123">
    <color>red</color>
    <size>small</size>
</widget>

/widget/color/widget/size这样的XPath表达式可能会产生类别:(urn:color)red(urn:size)small。客户端不需要显式创建这些类别,甚至不必知道它们的存在。而是XPathAutoTagger来读取XML内容并自动生成类别。然后Feed阅读器就可根据给出的类别拉出所需的分类后feed。更进一步,系统不需要依赖于潜在不可靠的客户端来显式管理类别。

Feeds聚合

Atom存储常常包含了多个不同工作空间和(或)集合,它们的数据存在相互关联数据。例如,一个公司可能拥有一个容纳每个员工条目的Atom存储工作空间,同时还有另一个工作空间容纳各部分雇员之间的会议。工资计算部门可能会需要拉出员工Feed进行工资处理,而楼宇管理员需要监控会议预订情况。这两者都是最简单的AtomPub Feed实例。

另一个例子,假设项目经理们需要的一个Feed是:每个员工联结的所有会议。要求经理们拉出两个单独的Feed,并进行数据集的关联,这是常规的AtomPub做法。与此不同的是,AtomServer加入了Feed聚合的概念。Feed聚合充许任何条目,就算来自不同工作空间和集合,被联结成一个单一的数据Feed。

AtomServer利用强大的类别概念来构造聚合Feed。一个特定的Scheme(一个类别“Scheme/Term”对的第一部分)被选中作为聚合Feed的Join Scheme。需要被联结为聚合的两个或者以上的条目必须以同样的Join Scheme被标记为同一类别。

聚合Feed通过一个URI来指明,其中的工作空间通过$join来指明,集合通过Join Scheme来指明(即,$join/{Join Scheme})。针于该URI指明的Scheme所存在的每个唯一的Term,一个聚合Feed URI都包含了一个条目。这些条目作为aggregate元素(一个AtomServer特定的扩展元素)被聚合到了该条目的内容中,而该元素则包含了该聚合中合适的组件条目(即,以该聚合的Scheme/Term标记的所有条目)。

回到我们例子,设想该公司的Atom存储里有如下数据(以标准的AtomServer URI结构设计:{workspace}/{collection}/{entryId})。

/employees/acme/cberry.xml
    <employee name="Chris Berry" id="123" dept="dev"/>
/employees/acme/bjacob.xml 
    <employee name="Bryon Jacob" id="345" dept="dev"/>
/meetings/acme/standup.xml
    <meeting name="standup" time="Every Tuesday 9:15" >
       <employee id="123" />
       <employee id="345" />
         </meeting>

在项目经理们能够拉出“员工及其会议”的聚合Feed之前,我们需要在准备联结的每个条目上创建合适的Atom类别,其中唯一的类别scheme定义一个“聚合类别”,该Scheme中的每个类别term定义一个“聚合条目”。这最有可能通过上面介绍的自动标记器机制完成。

如果我们将Join Scheme设定为urn:EID,那么条目类别定义如下:

/employees/acme/cberry.xml <- (urn:EID)123
 /employees/acme/bjacob.xml <- (urn:EID)345
 /meetings/acme/standup.xml <- (urn:EID)123
                               (urn:EID)345 

通过将这些类别应用于条目,项目经理就可以基于scheme-urn:EID来拉出一个聚合Feed了。因为所有聚合Feed的工作空间是$join,一个合适的URL会是:

http://your.atomserver/$join/urn:EID

对于urn:EID scheme中每个唯一的term,它都会返回包含一个条目的聚合Feed。

每个条目的内容是一个<aggregate>元素,它为每个“真正”映射到类别上的条目都包含了一个条目XML集合。就我们例子来说,一个聚合Feed响应就像下面一样。注意,出于简短说明的目的,很多XML元素都未经整理。

 <feed xmlns="http://www.w3.org/2005/Atom"
          xmlns:as="http://atomserver.org/namespaces/1.0/">
    <as:endIndex>16573</as:endIndex>
    <id>tag:atomserver.org,2008:v1:urn:EID</id>
    <entry>
       <id>/atomserver/v1/$join/urn:EID/345.xml</id>
       <as:entryId>345</as:entryId>
       <content type="application/xml">
          <aggregate 
             xmlns="http://schemas.atomserver.org/atomserver/v1/rev0">
             <entry xmlns="http://www.w3.org/2005/Atom">
                <id>/atomserver/v1/employees/acme/bjacob.xml</id>
                <as:entryId>bjacob</as:entryId>
                <as:workspace>employees</as:workspace>
                <as:collection>acme</as:collection>
                <content type="application/xml">
                   <employee
                      xmlns="http://schemas.atomserver.org/examples" 
                      name="Bryon Jacob" id="345" dept="dev" />
                </content>
             </entry>
             <entry xmlns="http://www.w3.org/2005/Atom">
                <id>/atomserver/v1/meetings/acme/standup.xml</id>
                <as:entryId>standup</as:entryId>
                <as:workspace>meetings</as:workspace>
                <as:collection>acme</as:collection>
                <content type="application/xml">
                   <meeting 
                         xmlns="http://schemas.atomserver.org/examples"
                         name="standup" time="Every Tuesday 9:15">
                      <employee id="123" />
                      <employee id="345" />
                   </meeting>
                </content>
             </entry>
          </aggregate>
       </content>
    </entry>
    <entry>
       <id>/atomserver/v1/$join/urn:EID/123.xml</id>
       <as:entryId>123</as:entryId>
       <content type="application/xml">
          <aggregate
             xmlns="http://schemas.atomserver.org/atomserver/v1/rev0">
             <entry xmlns="http://www.w3.org/2005/Atom"
                <id>/atomserver/v1/employees/acme/cberry.xml</id>
                <as:entryId>cberry</as:entryId>
                <as:workspace>employees</as:workspace>
                <as:collection>acme</as:collection>
                <content type="application/xml">
                    <employee 
                       xmlns="http://schemas.atomserver.org/examples"
                       name="Chris Berry" id="123" dept="dev" />
                </content>
            </entry>
            <entry xmlns="http://www.w3.org/2005/Atom">
               <id>/atomserver/v1/meetings/acme/standup.xml</id>
               <as:entryId>standup</as:entryId>
               <as:workspace>meetings</as:workspace>
               <as:collection>acme</as:collection>
               <content type="application/xml">
                  <meeting 
                     xmlns="http://schemas.atomserver.org/examples" 
                     name="standup" time="Every Tuesday 9:15">
                     <employee id="123" />
                     <employee id="345" />
                  </meeting>
               </content>
            </entry>
         </aggregate>
      </content>
   </entry>
 </feed>

对于聚合Feed ,有几点重要的地方需要注意:

  • 一个聚合Feed的流水号( <as:endIndex>所返回的数字,AtomServer用以保持Feed分页的一致性)等于它的“子条目”流水号中最大值。这就产生了一个重要的结果,只要一个聚合的任何组件发生了改变,这一聚合将在下一次拉出聚合Feed时被返回。
  • 一个聚合条目的类别集是它所有组件的类别的并集。
  • 聚合Feed也可以接受类别搜索。请求/$join/urn:EID/-/(urn:department)dev,将会拉出Feed中具有(urn:department)dev类别定义的所有聚合条目。
  • 指明一个本地化聚合Feed的方式跟常规Feed一样(如,/$join/urn:EID?locale=en_US)一个本地化聚合条目存在的唯一条件是,在指定locale中至少存在一个它的本地化组件。如果没有本地化条目,则这一聚合不会在返回的feed中。

聚合Feed对<aggregate>元素中的条目定义了三个新XML元素。

  • <as:workspace>:包含组件条目的工作空间。
  • <as:collection>:包含组件条目的集合。
  • <as:locale>:包含了组件条目的locale(如果有的话)。

这些元素可以让聚合feed的消费者能轻易通过程序判定一个聚合的每个组件的特征。

批处理

因为针对每个条目分别发起服务器请求存在隐式的双向开销,把多个操作组打包成单个请求(POST、PUT或DELETE)的批操作会提升系统性能。遗憾的是,AtomPub未提供批处理机制,所以我们为AtomServer加进了批处理能力(灵感来自GData的类似功能)。

一种更RESTful的作法可能是使用HTTP内置的Multipart能力来实现批处理。然而,对于客户端来说,这一技巧过于复杂,特别是考虑到客户端使用多种不同语言的时候。作为替代,我们选择了基于URL的模式。

我们还短暂地考虑过使用一个自定义的HTTP动词,如BATCH,来表明一个批处理被写入。然而,因为同样的原因,为了最大化客户端互操作性,我们选择坚持“标准”的HTTP方法集,使用一个不同的URI来表示批处理操作。

AtomServer中的批处理操作是通过对在集合中的一个EntryId名为$batch的“虚拟”条目执行PUT操作来完成的。例如,下面这个URL:

PUT http://your-atom-server/widgets/acme/$batch

表示对widgets工作空间的acme集合执行一个批处理操作。注意,这一URI的结构暗示了一个特定的批处理仅适用于一个特定工作空间和集合的条目。

一个批处理PUT请求的XML内容是一个Atom Feed,它包含了这一批处理每一项的Atom条目。条目的组织与作为单个请求时没有区别,并且增加了一些可能的扩展。

AtomServer在http://atomserver.org/namespaces/1.0/batch名字空间里声明了一个新的XML扩展元素,<asbatch:operation>,以允许客户指明这一Feed里的每个条目是update (PUT)、insert (POST),还是 delete (DELETE)。这些元素可以是完全应用于整个批处理Feed,也可以单独应用于每个条目。

例如,如下批处理请求:

<feed xmlns="http://www.w3.org/2005/Atom"
 xmlns:asbatch="http://atomserver.org/namespaces/1.0/batch">
   <entry>
     <asbatch:operation type="update" />
     <link href="/widgets/acme/123.xml/*" rel="edit" />
     <content type="xhtml">
       <div xmlns="http://www.w3.org/1999/xhtml">
         <widget id="123>
           <color>red</color>
           <size>small</size>
         </widget>
       </div>
     </content>
   </entry>
   <entry>
     <asbatch:operation type="delete" />
     <link href="/widgets/acme/234.xml/*" rel="edit" />
   </entry>
 </feed>

试图更新条目123同时删除条目234

如果该操作被理解并处理,HTTP对该批处理Feed的响应将是200 OK,就算该批处理的个别条目有错。

XML响应的内容依然是Atom Feed元素,包含一个条目对应原始批处理请求的每个条目。这些条目的顺序和在相应批处理请求中的顺序一样。

在这些响应的每个条目里,AtomServer加入了一个自定义元素表示HTTP的状态代码,如若条目作为单独操作被提交,该代码将会被返回。对于成功的条目,响应将会是:

<asbatch:status code="200" reason="OK"/>

或者

<asbatch:status code="201" reason="CREATED"/>

如若发生错误,将会给出错误代码和原因。例如:

<asbatch:status code="404" reason="NOT FOUND"/>

或是

<asbatch:status code="409" reason="Optimistic Concurrency Error"/>

另外,在Feed层级有一个<asbatch:results>元素,它表示发生了多少insertsupdatesdeleteserrors。所以,就上例我们会得到:

<asbatch:results
 inserts="0" updates="1" 
 deletes="1" errors="0"/>

检查这一“汇总”报告,客户就可仅在错误报告不为零时再来发掘特定的错误。

展望

我们基于AtomPub构建AtomServer是因为它的RESTful设计能支持伸缩性与互操作性。基于标准进行构建使我们得以利用Atom社区的群 体智慧来解决我们的问题。通过参考GData激发的灵感,我们加入了几个有用的扩展,比如批处理与乐观并发(optimistic concurrency)。随着我们不断扩展AtomServer的用处,对于自动标记和Feed聚合这样的附加特性的需求也日渐显现。多亏了Atom标 准的扩展天性,这些特性的增加十分容易且不会对既有Atom客户端的互操作性造成影响。当我们得以加入这些强劲的特性而不用牺牲我们与大多数客户都满意的 简单交互时,Atom的强大之处真正的体现了出来。对于二者这都达到了最佳的双赢。

自从AtomServer于2008年5月开源以来,引起了相关的关注。我们希望更多的人会关注AtomServer,使用AtomServer,并告诉我们还缺少什么!你可以在http://www.atomserver.org下载到AtomServer,几分钟就可以让它跑起来!

查看英文原文:AtomServer – The Power of Publishing for Data Distribution – Part Two。

相关阅读:AtomServer:数据分发的发布动力。

志愿参与InfoQ中文站内容建设,请邮件至[email protected]。也欢迎大家到InfoQ中文站用户讨论组参与我们的线上讨论。

你可能感兴趣的:(AtomServer:数据分发的发布动力(第二部分))