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

阅读数:513 2008 年 11 月 12 日 21:20

在本系列的第一部分里我们介绍了 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 中文站内容建设,请邮件至 editors@cn.infoq.com 。也欢迎大家到 InfoQ 中文站用户讨论组参与我们的线上讨论。

评论

发布