Shuttle(飞梭)服务总线是一个免费的.NET 开源软件项目,它为开发面向消息的事件驱动架构(EDA[1])系统提供了一种新方法。尽管它仍处于起步阶段,不过它已被应用于生产系统之中。
相关要点如下:
- 用 C#(基于.NET 3.5)开发而成
- 核心功能不依赖于任何第三方产品或项目
- 既支持命令消息,又支持事件消息(Pub/Sub)
- 具有集成的消息分发功能
- 包含一个命令行管理程序,以便轻松应对各种操作要求
- 广泛使用接口,以便替换或扩展功能
- 通过自动重试提供容错能力
为何要使用服务总线?
尽管在使用服务总线(service bus)时需要做一些思维模式的转变,不过这么做却是大有裨益的。通过在你的系统中设计一条服务总线,让其在特定终结点(endpoint)上执行非常明确的功能,这样你就可以专心设计在隔离环境下运行的那一小部分软件。可以对此类终结点进行独立地版本控制及维护,而前提是你对此类终结点所做的解耦工作已达到所需的程度。
基本上是说,你最终会在不同组件之间发送消息,并对这些消息进行 _ 异步 _ 处理。由于这会导致出现“即发即弃(fire-and-forget)”的情况,因此需要认真考虑系统的工作方式。而由此带来的用户体验可能完全不同于传统实现,即那种立即处理对用户所请求的全部操作的方式。
比如就是简单地发送一封电子邮件。你可以先设置好某个终结点去处理与之有关的命令消息类型(command message type)。然后,当你需要发送电子邮件时,你就可以使用如下代码:
Bus.Send(<span>new</span> SendEMailCommand { From = "someone@from.com", To = "someone@to.com", Subject = "<span>testing e-mail</span>", Body = "Hello" });
你可能想知道,这么做与你自己直接发送此邮件的到底有何不同之处:
- 这条消息发送调用是即时的,而我们不用等着此调用执行完成
- 由于请求会被排入队列,因此就避免了瓶颈
- 要是此邮件发送失败,就会自动重发此邮件
- 只要得到的数据正确无误,就可以保证此邮件最终会被发出去;或者,至少在发生问题时可以采取手动操作进行处理
因此,客户端代码不用关心该终结点到底是如何发送数据的。而终结点可能会用简单邮件传输协议(SMTP)发送数据,甚至还可能会用某种自定义的网络服务(web-service)发送数据。
Shuttle 是如何发挥其魔力的?
Shuttle 服务总线依赖于两件事:
- 消息(Message),及
- 队列(Queue)
队列基础设施可以是任何实现。通常情况下,你会想使用某种 _ 真正 _ 的队列技术,例如微软消息队列(MSMQ)。目前 Shuttle 提供直接现成支持的队列有,微软消息队列(MSMQ)及 Sql Server 基于表的队列(table-based queues)。如果你想使用任何其他实现,要解决的问题就是实现有关接口,因此你会干得不错。Shuttle 使用统一资源标识符(URI)的结构去表示队列,例如:
- msmq://{machine}/{queue}
- sql://{connection-name}/{table}
为了实现你自己的队列,你只需简单挑选一种方案,并在你的队列实现中解析这种结构。
为了组织你的终结点,Shuttle 使用了通用服务主机去简化部署。让一个新的终结点生效,就是解决该终结点所用队列的配置问题,然后启动你的服务即可,正如以下配置文件及代码片段所示:
<span><?</span><span>xml</span> <span>version</span><span>=</span>"<span>1.0</span>"<span>?></span> <span><</span><span>configuration</span><span>></span> <span><</span><span>configSections</span><span>></span> <span><</span><span>section</span> <span>name</span><span>=</span>"<span>serviceBus</span>" <span>type</span><span>=</span>"<span>Shuttle.ESB.Core.ServiceBusSection, Shuttle.ESB.Core</span>"<span>/></span> <span></</span><span>configSections</span><span>></span> <span><</span><span>serviceBus</span><span>></span> <span><</span><span>inbox</span> <span>workQueueUri</span><span>=</span>"<span>msmq://./inbox-work</span>" <span>journalQueueUri</span><span>=</span>"<span>msmq://./inbox-journal</span>" <span>errorQueueUri</span><span>=</span>"<span>msmq://./shuttle-error</span>" <span>/></span> <span></</span><span>serviceBus</span><span>></span> <span></</span><span>configuration</span><span>></span> <span>public class</span> <span>ServiceBusHost</span> : <span>IHost</span>, <span>IDisposable</span> { <span>private</span> <span>IServiceBus</span> bus; <span>public void</span> Dispose() { bus.Dispose(); } <span>public void</span> Start() { bus = <span>ServiceBus</span> .Default() .Start(); } }
应该注意的是,通用主机既能以控制台应用程序的方式运行,又能作为服务去安装。这样就让调试终结点变得特别容易,因为当你在 Visual Studio 中调试时,你可以将通用主机指定为启动应用程序。
一旦通用主机从收件箱队列中获取到一条特定的消息,通用主机就会尝试找到处理程序,并将消息传送给此处理程序以供处理。至于那些找不到处理程序的消息,或被移至错误队列,或被丢弃,采取何种处理方式就要根据配置文件中所指定的信息而定了。
命令消息与事件消息
由于命令(command)是一个要求执行特定功能的明确请求,因此命令会被发送至单独的终结点。这意味着,为了发送消息,你需要知道某个终结点实现了特定的行为。因此,命令导致了更高程度的行为耦合。而如果在发送消息时,那个接收消息的终结点是可配置的,那么便可随时更改终结点。
譬如,你可能有如下命令:
- SendEMailCommand
- ConvertDocumentCommand
- DeleteFileCommand
- CancelOrderCommand
刚好相反,事件(event)可能没有或有多个订阅者(subscriber)。通常情况下,事件应该被明确定义,因为从业务角度看必需这么做。因此,按理说,除非是那种根据未来需求定义的事件,否则事件至少会有一个订阅者。一旦事件被发布出来,每个订阅者都会收到一份事件消息副本。
这不同于消息分发(message distribution),因为一条分布式消息只会被发送至一个工作者的收件箱队列。
譬如,你可能有如下事件:
- EMailSentEvent
- DocumentConvertedEvent
- FileDeletedEvent
- OrderCancelledEvent
为了在消息中添加任何必需的即席数据(ad-hoc[2] data),Shuttle 服务总线允许你为消息指定消息头。此外,还可以使用相关 ID(correlation ID)去对有关消息进行分组。
可伸缩性(Scalability)
Shuttle 使你获得了不少的可伸缩性(scalability[3]),由于消息被排入队列,因此也就不会出现刻不容缓的紧迫瓶颈。即使有些终结点可能会被配置成多线程的,但是这并不意味着某个特定的终结点不会由于接收过量消息而导致性能下降。每个终结点都要有将消息分发给其他终结点的内置能力,一旦任何其他终结点通知 _ 分配器(distributor)_ 它有可执行工作的空闲线程,就可以运用此能力分发消息。而要做到这一点,只需配置一下就好了。
模块(Modules)
Shuttle 使用了一种可观测的管道(pipeline)结构。各种事件以特定顺序被注册到管道中,而观察者(observer)也可以被注册到管道中去响应各自的事件。
为了保持可扩展性(extensibility[4]),你可以把自己的模块实现注册到 Shuttle 之中。通过添加响应各自管道事件的观察者,这些模块通常会与特定的管道相连。而且为了随需应变,你甚至可以把自定义事件添加到管道中。
对于依赖注入及日志记录的支持怎么样?
由于 Shuttle 为了解耦而如此广泛地使用了接口,因此你就可以根据个人喜好,自由接入任何依赖注入(DI)或日志记录的实现。默认实现不依赖于任何第三方组件,不过目前还有作为依赖注入实现的 Castle Windsor 及作为日志记录实现的 Log4Net 可供选用。
生产环境下的 Shuttle 案例
Shuttle 在南非某大型短期保险公司的实施已大获成功,取代了陈旧的文档索引系统。
客户通过电子邮件发送与索赔有关的文档,而邮件会被 Lotus Domino 电子邮件系统接收到。然后,FileNet Email Manager(电子邮件管理器)应用程序会将这些电子邮件提取出来,并放置到 IBM FileNet Content Engine(内容引擎)中。内容引擎经过配置,用以响应任何电子邮件分类为已提交的新文档。内容引擎被设计用于处理这些送达邮件,并写出一份包含相关数据的 XML 文件。
从那里开始,Shuttle Content Engine(内容引擎)终结点会拾取这些 XML 文件,然后发布事件以表明新内容已被放置到了内容引擎中。由于此终结点的结构并没有具体到索引过程,因此就有可能重用此终结点,用于发布任何新内容。而且应由此内容引擎负责简单写出相关的 XML 文档。
Shuttle Indexing(索引)终结点订阅那些新的内容消息,而且消息一送达,终结点就开始跟踪与特定邮件一起送达的所有文档,因为每封邮件都有唯一标示符,而且此标示符已被附加到每份文档的内容引擎元数据中。一旦搜集到与一封邮件相关的所有文档,命令消息就会被发送至 Shuttle Document Conversion(文档转换)终结点,用以将 HTML 格式邮件正文及全部 JPG 格式文件转换为 TIFF 格式文档。
文档转换终结点对于索引过程一无所知,而仅仅执行各种文档转换。一旦某个文档转换完成,就会发布事件以通知某个文档转换已成功或失败。任何系统需要文档转换的系统都会订阅这些事件消息。为了既能建立针对于特定系统的消息,也能建立不针对于特定系统的消息,请求转换的系统可以在转换请求命令中使用相关 ID(correlation ID),和 / 或将名 / 值对头添加到传出的转换请求命令消息中。这些头信息始终会被附加到由 Shuttle 所发送的任何相关消息中。
一旦完成了所有需要的文档转换,就会给 Shuttle OvaFlo 终结点发送一条命令消息,用以在 IBM FileNet Process Engine(流程引擎)中创建索引工作流实例。 OvaFlo 是由 Ovations Group 公司开发的一款元工作流(meta-workflow)框架产品。
为了执行实际的索引,用户会访问基于网络的索引应用程序。此应用程序会从 OvaFlo 终结点中得到下一个可用索引工作流实例,并显示相关文档的分类。每份文档会被链接到一个具体的索赔、及输入的任何其他的相关索引数据。此外,也可以用于处理系统用户所提出的将某些文档转换为 TIFF 格式的请求。当把各种分门别类的文档放置到一个文件中时,此功能就特别有用。一旦用户觉得对数据很满意,就可以提交该任务以示完成。这一步是通过发出一条命令消息后异步处理的,以便让用户可以立即继续处理下一索引任务。
在某些情况下,会发现来自 web 前端的工作却在后台系统工作中排队。譬如文档转换就是这种情况,其中在从前端发起请求之前,来自送达邮件的所有需要的转换都在被处理中。我们通过通用主机,使用相同的编译程序集 ,就可以简单安装一个单独的 Shuttle Document Conversion(文档转换)终结点,不过我们要修改此终结点的配置,以便使用其自身队列。然后前端将转换请求发送给这个高优先级终结点,接着那些转换会得到及时地处理。而所有的后台转换仍会被发送至原有的终结点。
因此,在这个例子中,Shuttle 被用于将不同的系统粘合到一起。由于该系统具有所需的内置容错能力,因此它比前一系统要稳定得多。而且由于没有积压工作被创建出来,因此系统性能非常出色。
结论
Shuttle 为你在实现企业服务总线(ESB)[5] 时提供了另一种自由选择。这个项目托管在 CodePlex 上:
优点:
- 新项目
- 高度可扩展
- 免费开源软件
- 命令行管理程序
缺点:
- 新项目
- 必须手动处理进程状态数据
随着时间的推移和社区的支持,出现了各种不同的实现可用于扩展,其中包括更多的队列、依赖注入(DI)、及其他选择。
关于作者
Eben Roux 作为一名开发者、咨询师、以及许多行业的架构师,他不仅在专业领域拥有近 20 年的经验,而且还提供了各种策略及解决方案,为各种不同系统的成功实施作出了贡献。他是免费开源 Shuttle 服务总线项目的所有者,并且他坚信要开发高质量的软件,就要给予用户完成其工作的权力。
Eben 有着 VB 背景,他于 1998 年初次成为了微软认证专家(MCP),而后,到 2003 年,又完成了 3 个微软认证解决方案开发专家(MCSD)的认证(VB5, VB6, VB.NET )。自从 2007 年专攻 C#开发以来,他一直致力于以面向消息的中间件为基础,在事件驱动架构内部去实现领域驱动设计。
译注
[1] EDA(Event-Driven Architecture),事件驱动架构是一种软件架构模式,它提倡事件的生产、检测、消费、及应对。可以把一个事件定义为“一个显著的状态变化”。例如,当消费者购买汽车时,汽车的状态会从“待售”变为“已售出”。汽车经销商的系统架构可以将这种状态变化作为一个事件去处理,只要发生此事件就可以让该架构中的其他应用程序都知道。从正式的角度来看,被生产、发布、传播、检测或消费的是(通常是异步的)被称为事件通知(event notification)的消息(message)而不是事件本身,其中状态变化触发了消息发送。事件不会传播,它们只是发生而已。然而。事件这个术语通常会被换喻用于表示通知消息本身,这可能会导致一些混淆。更多内容参阅维基百科。
[2] ad-hoc,译为“即席”,意指在未经规划或准备的情况下,出于某一特定目的才做的。例如,即席查询,一般是指用户根据自己的需求定义查询条件,即席查询可理解为立马生效的查询。
[3] scalability,可伸缩性。在电子学(包括硬件、通信及软件)中,可伸缩性(Scalability)是指系统、网络或过程以妥善的方式去处理越来越多的工作量的能力,或者说是其为适应这种增长的放大能力。例如,它可以指,当添加资源(通常是硬件)时,系统随着负载不断增长,其总吞吐量也会相应增长的能力。要是将这个词用于经济背景下,就会有某种类似的意思,公司的可伸缩性意味着,该公司的基础商业模式为其经济增长提供了潜力。更多内容参阅维基百科。
[4] extensibility,可扩展性。在软件工程中,可扩展性是一条系统设计原则,即在实现中要考虑到未来的增长。它是对扩展系统的能力及实现扩展所需努力程度的系统衡量指标。既可以通过添加新功能实现扩展(Extension),也可以通过修改现有功能实现扩展。其主旨就是,当系统发生变化时——通常是增强系统——使对现有系统功能的影响最小化。更多内容参阅维基百科。
[5] ESB(Enterprise Service Bus),企业服务总线是一种软件架构模型,用于在面向服务的架构(SOA,Service-Oriented Architecture)中,为相互作用的软件应用程序之间的交互进行设计及实现。作为一种分布式计算的软件架构模型,它是更为普遍的客户端服务器软件架构的专业变体。其主要用途是在异构及复杂情况下的企业应用集成(EAI,Enterprise Application Integration)。更多内容参阅维基百科。
[6] 维基(Wiki)是指允许访问者添加或修改资料的网站。
评论