Enterprise Integration Pattern - 组成简介
近些年来,越来越多的Web应用正在逐渐向大型化的方向发展。它们通常都会包含一系列相互协作的子服务。在开发过程中,如何让这些子服务协同工作常常是软件开发人员所最为头疼的问题,如各个子服务之间的数据表示不一致,处理并发的能力不同,进行沟通的网络不稳定等。为了解决这些问题,世界各地的优秀程序员提出了一系列解决方案,并最终形成了一整套用来完成各个子服务之间沟通及集成所使用的解决方案。这些最佳实践最终由Gregor Hohpe以及Bobby Woolf整理成为《Enterprise Integration Pattern》一书。
从我个人的角度来讲,我还是希望您能够细细地品味这本书所给您带来的各种数据流组织方式以及它在讲解中所考虑的各种情况。当然,您也可以通过本文快速了解Enterprise Integration Pattern到底是什么。而在我的日程中,本文则是Cloud Orchestration的理论基础,并进而逐渐地将这些知识点铺开并最终向大家介绍什么是Application as a Service。
基于消息的集成
闲话少说,让我们直接介绍Enterprise Integration Pattern的最核心思想,那就是基于消息的集成。在这种方式下,各个组件之间的交互将不再使用远程调用等同步方式,而是通过向目标子系统发送一个消息来令该子系统执行某个功能。而在消息成功发送之后,调用方即可以开始对其它任务进行处理,而不再是同步调用过程中的等待。在使用这种处理方式时,一个系统的吞吐量可以大大增加:
在通过消息进行集成时,组成服务的各个子服务则可以通过消息完成彼此的隔离。这种隔离所带来的好处很多。首先,只要子服务之间能够在消息传递方式以及格式方面上达成一致,其它一系列有关实现的细节,如到底使用什么语言,什么类库编写服务并不会影响到相互通讯的另一方。这使得软件开发人员可以根据需求选择最合适的开发技术,进而降低开发成本,提高代码质量。同时只要相互沟通的各个子服务之间进行通讯的方式不变,这种隔离还能够让相互通讯的各个子服务独立地测试和升级。如果在服务运行过程中一个子系统实例产生了异常,那么该异常将只会影响到该子系统实例,而不会传播到其它与该子系统进行沟通的各个子系统中。与此同时,消息的发送方还可以将消息暂时缓存在本地,并可以在接收方重新恢复到正常运行状态之后再将该消息发送到接收方。
反过来,在子系统中传递消息不仅仅是一对一传递这么简单。例如在相互通讯的两类服务A和B之间,服务A的并发处理能力较服务B的并发处理能力强很多,那么我们就需要在消息传递给服务B之前对消息进行分发,以通过多个服务B的实例并行处理服务A所发出的消息。除此之外,我们还常常需要将一个消息拆分成为多个不同的消息,并由多种不同的服务来处理它们。甚至有时候,我们需要将一个消息广播到所有的目标类型服务实例上。因此在使用消息进行集成的时候,我们首先要理解的,就是对消息进行处理的多种不同方式。
当然,这些消息处理方式并不需要我们自己写代码来完成。很多消息系统(我这里不用名词消息中间件,即MOM,Message-oriented Middleware,因为有歧义)已经为我们提供了这些消息处理方式的通用实现。在使用时,我们只需要通过一系列配置文件来控制消息系统的运行方式即可。除了提供这些消息处理方式的通用实现之外,消息系统还为我们解决了一系列和网络传输相关的问题:在两个子服务之间发送消息时,发送方或接收方都有可能没有准备完毕,从而导致消息发送失败;抑或是在两个子服务都已经准备完毕的情况下,子服务之间的网络连接出现了问题,进而导致消息无法发送到目标子服务。在消息系统的帮助下,软件开发人员不再需要关心如何处理这一系列复杂的网络问题,而只需要在消息系统的帮助下将精力集中在业务逻辑的实现上即可。因此在以Enterprise Integration Pattern作为指导思想组织服务时,我们常常会使用一些较为成熟的消息系统来辅助集成。而在集成过程中,软件开发人员需要对消息系统以及业务逻辑的边界有一个清晰地认识:在消息处理的整个过程中,任何与消息分发传递相关的工作都需要由消息系统来完成,而对消息所包含信息的处理则是连接到消息系统上的各个子服务的责任。
也正是因为如此,一个消息常常分为两个部分:消息头和负载。消息头用来记录消息的元数据,以用来控制消息被如何处理,是需要由消息系统来使用的数据,与参与集成的各个子服务无关。而负载则用来记录消息的实际内容,是由参与集成的各个子服务所需要处理的数据,而与消息系统无关。
在一个子服务希望与其它子服务进行沟通时,其常常只需关心需要发送出去的消息的内容,而基本不需要关心如何生成这些消息的元数据:
从上图中可以看到,一个子服务在需要向另外一个子服务发送消息时,其常常只需要提供消息的负载,甚至都不需要考虑为消息系统提供该消息应该如何被处理的元数据。而在整个消息进行路由的过程中,消息系统则可以根据消息元数据决定如何路由这些消息。
在传递消息时,消息系统并不是将这些消息置于一个消息池中供各个子服务挑选,而是在各个需要沟通的子服务之间建立独立的通道。消息的生产者只需要将消息放入到特定的通道中即可返回,而并不会知道到底是由哪个消息的消费者来对其进行处理的。它只能肯定的一点是:消息肯定会准确地送达通道的另一端,进而被某些消息的消费者处理。而通道的内部实现则负责缓存该消息,并向通道的另一端发送。
所以就一个子服务的开发者而言,他所需要关心的仅仅是如何恰当地组织该子服务的业务逻辑,而不需要考虑如何对消息进行路由。而对于整个分布式服务的设计者而言,如何通过这些消息系统组织整个应用的数据流则会变得非常重要:通过消息路由的拓扑结构,我们需要防止服务的单点失效,同时也需要防止单个子服务出现过载等一系列问题。剩下的有关消息传递的细节,如如何对保证消息的可靠传递,如何保证消息的安全性等都留给消息系统来处理。
而Enterprise Integration Pattern所提出的一系列模式就可以帮助您解决在设计整个系统数据流时所可能遇到的一系列问题。和四人帮所提出的23种经典模式一样,它们并不能为您的问题提供一个整体的解决方案,但是通过灵活地使用并组合它们,您常常可以创建一个非常精巧稳定的数据流拓扑,从而使得您的服务在可用性,性能等众多非功能需求上表现得较为优异。
消息系统的组成
在了解消息系统对消息进行处理的各种模式之前,我们首先需要了解消息系统中的Pipes and Filters模型。就如其名字所描述的那样,该模型主要由两部分组成:用来传递消息的通道(Pipe)以及用来对消息进行处理的过滤器(Filter)。这些消息通道将过滤器串联起来,而消息自身则会沿着这些通道流动:
上图中所显示的就是对一个消息进行处理的最典型方式。在一个消息被发送到一个管道中后,过滤器将会在具有消息处理能力时从输入管道中接收该消息并开始处理。一旦该消息处理完毕,那么过滤器将会把处理结果放到输出管道中,并由与该管道相连接的各个过滤器完成消息的后续处理。一个过滤器可能对消息进行处理,也可能仅仅是对消息进行转发。
由于在经典的Pipes and Filters模型中,过滤器的输入及输出只能有一个,因此其所包含的各个过滤器并不可以被多条处理逻辑重用。因此Enterprise Integration Pattern放松了该约束,使得一个过滤器可以从多个管道接受消息,并向多个管道发送相同或不同的消息。在放松了该约束的情况下,我们才能真正地通过消息来组织我们的应用:您需要将业务逻辑分割为一系列顺序执行的彼此独立的步骤,然后才能通过Pipes and Filters模型对它们进行组织。对于一条独立的业务逻辑,您只需要将他们组织成为线形结构即可。而对于整个应用所包含的多种业务逻辑而言,这些分割出的步骤将可能被多个业务逻辑重用,进而组成一种较为复杂的拓扑结构:
上图展示了消息是如何在一个由管道和过滤器所组成的拓扑结构中进行传输及转发的。在一个消息到达该拓扑结构之后,第一个过滤器会根据自身的消息派发逻辑来选择到底是由右上的过滤器继续处理,还是由右下的过滤器来执行另一种业务逻辑。无论第一个过滤器选择的是哪个过滤器,其都将会继续产出消息并由后续的过滤器进行处理。同时您会发现,在该拓扑中心的两个过滤器则是可以由右上及右下两条处理逻辑所公用的。
这种放松的约束将使得对消息的处理方式变得更为多样,也更为灵活。在Pipes and Filters模型中,对消息的处理主要分为三步:消息从输入管道进入到过滤器中,由过滤器处理,再由输出管道流出。一个过滤器可能同时侦听多个输入管道所到来的消息。而对消息的继续派发也可能从多个输出管道中选择,或者是向输出管道进行广播。而在经过过滤器处理后,输出消息可能与输入消息相同,也可能与输入消息不再相同。
从多个输出管道中选择消息的派发目标通常是通过路由器(Router)来完成的。一个路由器可以连接多个输出管道,并根据正在处理的消息中所包含的信息来选择消息所需要派发到的管道:
输出消息和输入消息不同的情况则主要分为两种:业务逻辑处理之后所产生的输出,以及为了能够让前一个过滤器的输出可以被后一个过滤器所接受进行的转化。第一种实际上就是在执行业务逻辑,因此其输入和输出不同非常容易理解。而后一种情况则在不同应用进行集成时非常有用:两个能够相互集成的、原本独立的应用常常会对同一种事物建立模型,但是这些模型的定义则常常拥有不匹配的地方。而在集成时,我们就需要在这两种数据模型之间转化。
现在就剩下了最后一个问题,那就是应用是如何将消息放到管道中以及如何从管道中取出消息的呢?其实很简单:消息系统常常提供了一系列客户端,以允许应用通过该客户端向管道中插入消息或取出消息。由于很多消息系统都作为一个或多个独立的服务在运行,因此向管道插入消息的操作也就是向消息系统服务发送请求的过程。该请求的物理地址是目标消息服务所在的物理地址,而逻辑地址则是该服务中所包含的各个管道。因此刚刚所展示的拓扑结构也即是该应用的逻辑拓扑结构,而物理结构则常常是以消息服务为中心的星型结构,或者是基于星型结构的拓扑:
而逻辑拓扑中的各个管道则运行在消息服务内部。由此可见,消息传递的时间主要消耗在各个子服务与消息服务通讯的过程中:
因此在基于Enterprise Integration Pattern设计一个系统的时候,您对性能的考量不仅仅需要从逻辑拓扑上进行,还需要从系统的物理拓扑结构来考虑。
管道
OK。由于过滤器包含了很多种类型,并且需要覆盖对消息进行处理的绝大多数情况,因此我们从相对独立的管道开始说起。
首先要强调的一点就是,管道是有向的。在上面的讲解中就已经提到过,管道实际上是消息进行流通的路径。在需要从发送者传递到接收者时,一个消息就将会从管道的一个方向发送到另一个方向。由此可以看出,每个管道实际上都是一个消息池,而管道的使用者也就有了生产者和消费者之分。对于一个管道而言,如果与其连接的一个过滤器是生产者,那么它将只能向管道中放入消息,而不能从管道中取得消息,否则由该生产者所放入的消息将可能再次被自己所接收到。类似地,如果一个过滤器是消费者,那么它也只能从管道中取得消息,而不能向该管道发送消息。也就是说,管道是有向的。因此如果您希望两个过滤器之间能够双向通讯,那么您就需要在这两个过滤器之间创建两个单向的管道。
那到底一个管道可以拥有多少个生产者和消费者呢?答案是一个或者多个。通常情况下,管道的实现常常不会对生产者和消费者的个数进行限制。在一个具有较大吞吐量的网站中,常常有多个生产者和消费者在使用同一个管道:
而在管道中存在的消息则对于连接在同一个管道上的所有消费者是等效的。也就是说,在消息系统这个范畴之内,如果管道上的一个消费者可以接收一个消息,那么另一个消费者也应该可以接收该消息。
在管道的一个生产者生产出了一个消息之后,与该管道相连的接收者便可以从管道中取出消息并对该消息进行处理。那么到底有哪些消费者可以接收到这个消息呢?这取决于管道。一个管道可以有两种不同的消息分发方式:Point-to-Point以及Publish-Subscribe。在一个消息置于Point-to-Point管道中后,将只有一个消费者能够接收到该消息。而如果一个消息被放入Publish-Subscribe管道,那么所有的消费者都会接收到该消息:
当一个消息被置于管道中时,消息系统将会根据消息中所包含的元数据来决定如何对消息进行处理。而在过滤器接收到一个消息时,其需要能够知道消息负载所具有的格式以及如何对该负载进行解析。因此Enterprise Integration Pattern提供了Datatype Channel,以允许用户显式地标明在该管道中所传输的数据的类型。
如果需要在两个过滤器之间传输的数据类型分为很多种,那么我们是否需要在它们之间创建多个管道呢?答案是,我们可以这么做。只是由于每个管道都需要使用一个缓存来记录送入的各个消息,因此这会对消息系统造成较大的压力。另一种办法则是通过Selective Consumer来有选择地从一个管道上读取数据。有关Selective Consumer的使用我们会在后面的章节中讲解。
那么对消息进行处理的过程中出现了问题该怎么办?我们前面已经提到过,消息系统主要通过消息的元数据来决定如何对消息进行分发,而对消息负载的处理则由各个过滤器自身来完成。而这两部分都有可能发生一系列异常:消息可能由于网络的原因无法发送到接收方,一个消息被放到了错误的管道中,消息负载所包含的数据非法,进而无法通过验证等。
我们显然不能对这些异常情况置之不理。Enterprise Integration Pattern提供了两种用来处理异常的管道:Dead Letter Channel以及Invalid Message Channel。当消息系统无法根据当前消息中所包含的信息将消息发送到一个过滤器的情况下,其会尝试着将该消息放入Dead Letter Channel中。而如果一个消息能够正常地发送到过滤器中,但是过滤器无法对该消息进行处理,那么它就需要将该消息置于Invalid Message管道中。
通常情况下,一个消息系统会提供一个预设的Dead Letter Channel并提供对这些消息的默认处理。但对Invalid Message的处理则常常需要软件开发人员自行编写这些消息的处理逻辑。最为常见的一种处理方式便是在Invalid Message Channel的另一边放置一个用来接收这些非法消息的过滤器并在非法消息到达时向系统添加一条日志,同时在该条日志中记录有关消息的详细信息。
Dead Letter和Invalid Message之间的不同主要就在于其是否可以成功地发送到过滤器中。如果一个消息无法成功地发送到一个过滤器中,那么其将会被置于Dead Letter Channel中。而如果一个消息能够被成功发送,却无法被过滤器正确地处理,例如消息负载的格式不对,或者消息中缺少了必要的头,那么该消息就将被置于Invalid Message Channel中。这里需要注意的一点就是,如果错误出现在与业务逻辑相关的部分,如消息中没有给出足够的信息等,那么它应该归类为应用业务逻辑方面的错误。因此对该错误的处理应该由应用的业务逻辑完成,而不是置于消息系统的Invalid Message Channel中。
消息
看到这里,相信您的感觉一定和我一样:事情越来越复杂了。一个程序中的数据流动常常伴随着数据的分发,转换,合并,而且程序在执行过程中常常有不同的运行方式,如函数调用,事件广播等。为了能够支持这些功能,消息系统中的消息远比我们想象的复杂。
首先让我们从函数调用说起。一个普通应用中的函数调用常常具有如下形式:
1 result = function(param1, param2);
但是在一个基于消息的系统中执行跨子服务调用则不是那么容易的事。一个函数调用常常是同步的:调用方需要等到被调用方处理完毕并返回后才继续执行。而基于消息的调用则是异步的:如果调用消息发出后调用方即阻塞,那么调用方的吞吐量将受到很大的影响。因此在一个基于消息的系统中,跨子服务调用需要消息系统中的消息支持如下的功能:
- 能够通过消息来发送一个请求,以完成对另一个子系统中的功能的调用。该请求能够包含功能调用所需要的各个参数。而在请求发送完毕以后,发送方需要能够继续处理其它事务。
- 被调用的子系统能够通过消息将功能执行的结果返回给调用方。此时被调用的子系统需要知道如何将消息发送到调用方。
当然,如果一个跨子服务调用并没有任何返回值,那么消息系统只需要保证调用消息被发送到被调用方即可,而被调用的子系统就不再需要通过消息将执行结果返回给调用方。但是在很多时候,我们需要从子服务中得到执行结果。在这种情况下,我们不能简单地将包含结果的消息直接放到调用消息所发来的那个管道中。这是因为管道中消息的流动是有向的。为了返回子服务的执行结果,我们需要使用另外一个管道来传递结果消息:
是的,从图中您就可以看到,该消息调用的组织方式被称为Request-Reply。而实际情况则常常更为复杂。在一个具有较高吞吐量的系统中,发送方和接收方常常不止一个:
在被调用方需要将响应消息发送给对应的接收方时,它该如何知道传送响应消息所需要使用的那个管道呢?答案就是在请求消息中添加一个标示返回管道的信息(Return Address)。此时被调用方只需要查找请求消息中所记录的返回管道信息并使用相应的管道传输响应消息即可。
由于基于消息的调用是异步的,因此在接收到响应消息之前,调用方可能已经发送了许多请求。为了处理这种情况,消息的发送方需要知道当前到达的消息到底是哪个请求的响应。完成这个功能的消息组成就是Correlation ID。在调用方发送一个请求之前,其将为该请求添加一个ID。而在为该请求生成相应的响应消息时,被调用方会将请求的ID标示为响应的Correlation ID的值。这样在响应到达调用方的时候,调用方就可以通过查看响应中的Correlation ID来知晓该响应到底对应着哪个请求。当然,我们常常不需要担心如何设置及管理Correlation ID。消息系统常常已经帮我们处理了大部分工作。
另一个与消息的异步特性有关的事情就是消息的先后顺序。如果一个消息较为庞大,那么消息系统常常会将它分割为多个消息再进行传送。相应地,接收方则需要将这些消息组合到一起才能得到响应消息的全部内容。由于这一系列消息在传输过程中不可避免地存在着到达的先后顺序,因此接收方需要有一种方式知晓它们的先后顺序。消息系统常常提供了一种叫做Message Sequence的组成:在需要发送的数据较为庞大时,消息系统会将它切割成一系列较小的子消息,并在这些消息中记录这些消息的先后顺序以及总的子消息数量。在接收方接收到这些消息后,其可以通过其内部记录的先后顺序以及总的子消息数量判断到底是否所有的子消息都已经到达接收方,并在子消息全部到达后将它们组合起来。
除此之外,软件开发人员还可以标示消息的过期时间。由于该概念较容易理解,因此在这里不再赘述。
消息的路由
在前面一节对消息的讲解中,我们已经介绍了消息所包含的用来为各种消息处理方式进行支持所添加的各个域。在这些域的帮助下,消息系统能够在各个过滤器之间完成各种消息的传递,并将消息处理后的结果返回。但是这并不能满足一个复杂的基于消息的服务对于消息分发的需求:一个消息可能有多个可选的目标过滤器,而其需要被发送到一个或多个目标过滤器中;或者是一个消息需要分割为多个彼此独立执行的消息以交由不同的过滤器进行处理等。而在为消息设计它们如何在系统中路由时,您需要考虑子系统的性能,扩展性,高可用性等一系列需求,因此如何设计消息在系统中的路由常常是设计基于消息的服务中的重中之重。
在编写代码时,我们常常需要根据计算所得到的数据来决定到底执行哪一段逻辑:
1 if (condition) { 2 // Logic 1 3 } else { 4 // Logic 2 5 }
而在基于消息的系统中,我们也会遇到同样的问题。在前面对管道进行介绍的过程中我们提到过,由于管道需要保证在其中传输的消息能够到达接受方,因此其常常使用一个缓存来记录当前需要发送的消息。但是在一个系统中,需要传递的消息类型有很多种,甚至每种类型的消息所记录的内容差异非常大,因而需要由不同的子系统来进行后续处理。由此我们可以看出,为每种需要处理的情况创建一个独立的管道是一个不现实的设计,因为这种设计方式需要创建太多的缓存,并维护太多的管道,进而增加了消息系统中管道出现问题的概率。同时消息的发送方还需要知道消息的路由逻辑,而这本应该是消息系统的责任。一个较为贴近现实的情况则是:一个管道常常用来传递一类具有类似意义的消息,而由具有路由功能的过滤器来决定到底哪些过滤器可以用来接收并处理这些消息。这样消息的发送方就不需要知道消息的目标地址,从而使得消息的路由逻辑与业务逻辑分离。而在需要修改消息的路由方式时,我们也不必再在业务逻辑代码中寻找向管道发送消息的代码了。
通常情况下,消息系统都会提供一系列用来进行路由的过滤器实现。这些过滤器通常从一个管道中读取一个消息,并根据其所知的路由条件来决定到底向哪些管道中输出消息。
最常见的用于路由的过滤器就是基于内容的路由器:Content-Based Router。其有一个输入管道及多个输出管道。当一个消息从输入管道到达该路由器时,其会根据消息所包含的内容决定将这些消息派发到哪个管道中,从而由管道另一端的过滤器对消息进行处理:
而该路由器的一个缺点就是:其无法应付接收端所发生的各种变化。当我们向系统中添加、删除或修改一个接收端时,我们也同时需要修改Content-Based Router所包含的路由逻辑。这在一个接收端经常发生变动的系统中是一个非常显著的缺点。因此使用Content-Based Router的前提就是接收端不经常发生变化。
如果接收端经常发生变化怎么办?Enterprise Integration Pattern中提到的叫做Message Filter的过滤器或许能解决我们的问题。该类型的过滤器拥有一个输入管道和一个输出管道。当输入管道中传来的消息满足Message Filter中所标明的条件时,其将被传入输出管道,否则该消息将被直接丢弃:
如果将Message Filter与Publish-Subscribe管道结合,我们就可以处理接收端经常发生变化的问题了。在该方案中,所有到来的消息将被发送到一个Publish-Subscribe管道中,而每个接收端之前都会使用一个Message Filter连接到该管道上。当一个消息到达Publish-Subscribe管道时,所有的Message Filter都将接收到这个消息,而其所包含的筛选逻辑将会用来辨别到底哪些消息可以被Message Filter之后的接收端处理。一旦某个消息通过了该筛选,那么该消息将被送到Message Filter的输出管道,进而传入其后的接收端。当我们需要添加或删除一个接收端的时候,只需要添加或者删除相应的Message Filter即可。而如果一旦接收端可以接收的消息条件发生了变化,那么我们只需要修改相应的Message Filter即可:
但是相较于Content-Based Router,基于Message Filter的路由逻辑也有自己的问题。那就是可能有消息被多个Message Filter所通过,而另一些消息则不会被任何Message Filter通过。也就是说,该消息有可能被直接丢失或者重复处理。而这在某些情形下则是不被允许的。
一个解决该问题的方法就是让各个参与消息处理的接收端将自身的处理能力注册到一个路由器中,而当一个消息到达的时候,路由器会根据自身所记录的处理能力来对消息进行派发:
在系统启动时,各个消息处理端会通过Control Channel注册自身以及其可以处理的消息。一旦有消息到达,Dynamic Router就会根据其所记录的处理端的信息对消息进行派发。在这种运行模式下,Dynamic Router可以保证每个消息将只有一个接收端被处理,而且在没有接收端可以处理到来的消息时能够执行相应的运行逻辑,如在系统中添加一条记录等。而在系统运行过程中,接收端也可以通过Control Channel来通知Dynamic Router其自身的加入、离开以及侦听条件的修改。这样我们就可以在一个接收端失效后为系统添加该接收端的备份,从而保持整个系统的高可用性。
但是Dynamic Router并没有完全解决基于Message Filter的路由模式所导致的问题。从上面的叙述中可以看到,Dynamic Router,甚至是前面所讲到的Content-Based Router实际上都是将一个消息传递给一个接收端。而Message Filter的一个好处则是可以将消息发送给多个接收端。为此Enterprise Integration Pattern还包含了另外一种模式:Recipient List。
该模式会为每个接收端定义个管道。在一个消息到达之后,其将会根据各接收条件将消息放入符合接受条件的多个管道中:
而与Content-Based Router需要考虑接收端的添加和删除一样,我们也需要考虑如何处理Recipient List中接收端的添加和删除。解决方案也和Dynamic Router一样。那就是通过一个Control Channel来允许接收端的加入、离开以及侦听条件的修改:
当然,对消息进行处理的方式不仅仅局限于消息的分发。一个消息可能包含了多种组成的实例,而对单个消息组成的处理都可能会消耗大量的时间。如果我们能让这些组成被并行地处理,那么整个系统对单个消息的响应速度将会显著增加。举例来说,在一个监控软件中,我们常常需要将多个摄像头所记录的信息传递给图像分析系统。由于我们常常需要按照不同的方式来分析图像中所包含的信息,因此整个图像分析功能常常包含了不同的子分析系统。由于每种图像分析本身也是一个较为耗时的操作,因此每个子分析系统也常常会包含多个实例。在包含这些图像的消息到达图像分析系统之后,我们就需要将这些图像分发到这些子系统实例中,由这些子系统实例分别对这些图像进行处理。
因此在一个基于消息的系统中,我们常常需要将一个消息分割为多个独立的消息并由不同的过滤器实例进行处理。为此Enterprise Integration Pattern中提出了Splitter模式。该模式拥有一个输入管道和一个输出管道。如果一个消息从输入管道进入到Splitter,那么其将会根据Splitter的内部逻辑分割为多个子消息,并被置于输出管道中:
而在Splitter之后,我们常常需要通过一种叫Aggregator的过滤器将这些子消息的处理结果合并起来。其会在所有子消息处理结果到达之后才开始对它们进行处理,并生成输出消息。而对这些子消息进行处理的方式也分为很多种:Wait for All,即等待所有的子消息都到达;Time Out,即等待一段时间并根据这段时间之内到达的子消息响应生成最终的消息处理结果;First Best,即在第一个子消息的响应到达以后就忽略其它的子消息响应;Time Out with Override,即等待一段时间,并在某个满足要求的子消息响应到达后就生成最终的响应;External Event,即由一个外部消息来结束子消息的收集并由此开始生成最终的响应。
有时候,我们还希望某些消息能按照某种特征进行排列。此时我们就需要使用Resequencer。其通过输入管道接收到了一系列消息,并在其内部根据自身所设置的算法对它们进行排序,并将排好序的各个消息依次输出。
当然,一个基于消息的系统对消息进行分发的方式常常复杂得多。此时我们常常通过将这些简单模式组合在一起来完成对消息的处理。例如Enterprise Integration Pattern一书中提到了一系列常见的将他们组合在一起的方法:Composed Message Processor,Scatter-Gather,Routing Slip,Process Manager以及Message Broker等。
Composed Message Processor包含一个输入管道和一个输出管道。在一个复杂的消息到达输入管道后,其内部所包含的Splitter将会把该复杂消息切分为一系列粒度较小的子消息,并通过一个Router(Content-Based Router,Dynamic Router,Message Filter等)将消息分发到不同的子系统中。而在所有的子系统将这些子消息处理完毕以后,Composed Message Processor将通过Aggregator再将这些响应组织在一起:
一个与Composed Message Processor模式类似的模式则是Scatter-Gather模式。它们两者之间最主要的不同就是Scatter-Gather模式是将消息广播到一系列接受方,然后再由Aggregator进行汇总。也就是说,其同样只有一个输入管道和一个输出管道。当一个消息通过输入管道传递到Scatter-Gather管道之后,其内部将会把该消息分发到多个接受方。而后其会使用Aggregator将众多消息处理结果归结在一起:
上图中所列出的一个Scatter-Gather的实现就是通过一个Publish-Subscribe管道来完成的。在这种情况下,所有连接在该管道上的各个过滤器都将会接收到该消息。如果需要控制哪些过滤器会接收到该消息,那么我们就需要使用Recipient List来对消息进行分发:
另一种组合模式就是Routing Slip。当一个消息到达该模式实现时,其将首先决定该消息应该如何在各个过滤器之间路由,并将各路由路径附加到该消息上。接下来,该消息就会依次通过该路由信息所记录的各个过滤器:
该模式中存在着两个假设:消息的路由路径是固定的,而且需要被这些过滤器线性地处理。但是事情往往并不是如此美好:对一个消息的处理结果常常会决定消息下一步应该如何路由,同时一个消息常常需要被拆分为多个子消息并被并行处理。在这种情况下,Routing Slip模式并不能满足需求。因此Enterprise Integration Pattern又介绍了另外一种模式:Process Manager。在一个消息到达时,其将会根据消息中所包含的内容以及被各个过滤器处理的结果决定该消息应该如何被分发:
而其与Message Broker非常类似。只不过Message Broker则是从更高层次上来讨论消息到底应该如何组织的。
消息的转化
基于消息的应用常常需要处理的另外一个问题就是各个子系统之间所需要的数据并不一致。这里的不一致主要表现在几个方面:数据表现方式不一致,数据组成不一致,甚至有时还需要数据的加密和解密。
我们在前面已经提到过,一个消息主要分为消息头以及消息内容两部分。消息头主要由消息系统使用,而消息的内容则在各个系统之间交换数据。但是参与集成的系统则常常不知道消息系统所需要使用的各个消息头,以及消息内容的格式等信息。
解决这个问题的方法就是使用一个Envelope Wrapper。该组成会根据消息系统的要求对数据进行处理(如加密),并添加消息系统所需要的各个消息头。接下来,消息系统将根据消息头中所包含的信息对消息进行传递。而在到达接收端之后,这些消息头将被解析,接收端也会执行对消息内容的逆向操作,从而完成对消息的传递:
而另一种系统间无法顺利进行通讯的方式则是信息的缺失。例如在一个股票查询系统中,我们常常可以通过股票名称,股票名称缩写以及股票的代码对股票信息进行查询。但是由于股票的名称可能会根据主营业务的更改以及售壳等多种商业行为而发生改变,因此股票系统的内部实现常常只能使用股票的代码对相关信息进行查询。因此如果一个查询消息中只有股票名称或其缩写,却没有股票的代码,那么消息系统就需要通过一个外部系统将该查询消息中的股票代码部分补全。这种通过外部数据源将必要数据补全或执行数据转换的组成被称为Content Enricher:
反过来,我们也常常需要从一个消息中屏蔽一些信息。这些信息可能是一些机密信息,而不应该对后续的过滤器可见,或是一些数据量非常大却对后续的处理无用的信息等。在这些情况下,我们就需要创建一个新的消息。该新消息将只包含对后续处理有用的信息,却将那些不该被暴露的机密信息移除,进而减少单个消息所需要占用的带宽,提高安全性。如果原有的消息表现形式较为复杂,我们还可以借此机会来生成一个具有简化的表现形式的消息。而这个功能就是由Content Filter来完成的:
那如果我们只是在一段时间内不使用某些数据,而是在一些后续的环节使用它们,我们是不是就需要一直传输这些数据呢?答案是否定的。一个解决方案就是将这些数据暂时存储起来,并在消息中添加一个对应的Key。而在再次需要这些数据的时候通过该Key将它们取出:
从上图中可以看出,Claim Check的运行主要分为几个步骤:首先在消息到达Claim Check的时候由Check Luggage将特定数据提取并保存起来,并赋予该块数据一个特定的Key。该Key将被用来替换这些被提取出的数据,进而减小了需要传输的数据量。而在需要这些数据之前,我们则使用Data Enricher组成(实际上就是一个Content Enricher)将这些数据取回,以供后面的各个过滤器使用。
还有一个和消息转化相关的问题就是多个子系统对同一事物使用了不同的表现形式。在这种情况下,我们就需要通过Normalizer对消息进行转化。其内部使用了一个Router将传入的各个消息根据它们的类型转发给不同的转换器,并由这些转换器将消息中所包含的信息转化为具有同一表现形式的消息。其工作原理如下图所示:
但是如果一个系统需要集成太多的应用,那么Normalizer可能就已经不那么适用了。试想一下,如果一个系统包含了八个独立的子系统,我们就需要编写八个Normalizer,而每个Normalizer都需要能够转化其它七个子系统所传来的消息:
万一再增加一个新的子系统呢?在这种情况下,我们还需要编写一个Normalizer,而且对其它几个Normalizer进行更改,以能够接受这个新引入系统所使用的数据类型。可以看出,这种维护实际上是一个非常繁琐的过程。而该问题的解决方案就是为这些数据类型添加一个公用表示,即是Canonical Data Model:
Canonical Data Model为所有需要集成的各个应用定义了一种通用的数据表示。当需要与其它应用通信时,其将首先把自身的数据表示转化为Canonical Data Model,然后再将转化后的数据传递给其它应用。如果需要再向系统中添加一个子系统,我们只需要为其添加一个Transformer以能够在子系统所使用类型和Canonical Data Model之间转化即可。
Endpoints
接下来,我们要讨论的就是Enterprise Integration Pattern中的Endpoint。其用来定义一个应用应该如何与消息系统交互。之所以使用Endpoint这个英文单词,是因为我一直觉得中文中没有一个准确的词能够清晰地表示其所对应的概念。
首先我们要介绍的是与数据处理相关的各个Endpoint。这其中最常用的就是Messaging Gateway。其主要功能就是将参与系统的各个子系统转化包装成为对消息系统友好的组成,从而弥补两者之间的差距。试想一下,一个消息系统所接受的接入形式常常是:“向XX发送一个消息”,而一个子系统自身所提供的API则常常是一些具有明确意义的API,如getPrice()。而且由于基于消息的系统是一个异步的系统,而参与该系统的各个子应用常常是按照同步调用设计的。甚至有时子系统所提供的API的力度也会过细。因此通过为该子系统添加一个包装层来使得该它能够提供一个粒度合适,完全支持消息系统的API。而这就是Messaging Gateway要做的事情:
在Messaging Gateway的帮助下,子系统将不再拥有和消息系统相关的信息,从而完成了子系统和消息系统之间的解耦。
另一个拥有与之类似功能的Endpoint则是Messaging Mapper。其用来完成对数据的转换。我们在集成一个子系统时常常遇到这样的情况,那就是该子系统中所使用的数据并不符合要求。在前面对“消息的转化”一节中我们已经介绍过各个可以用来完成数据转换的各个组成。与它们不同的是,在使用消息转化器对消息内容进行转化时,应用需要向该消息转化器发送消息,而在转化完成后再由消息转化器向外发送消息。也就是说,该过程包含了两次消息的传递:
但是如果我们可以直接在子系统外围包装一层并完成对消息的转化,那我们不就减少了一次消息的传输,提高了效率么:
当然,这只是Messaging Mapper的一个作用。除此之外,我们还可以在Messaging Mapper中完成一系列其它工作,如解析数据中所包含的引用等。毕竟,在一个子系统中,其内部的表现形式可能并不适合使用消息进行传输。因此,将子系统的内部表现形式转化为适合消息传递的形式也是Messaging Mapper的一部分职责。
有时候,我们需要通过消息系统传递一系列相互关联的消息。由于任何消息的缺失都会导致这组消息失去意义,因此我们需要通过事务保证它们的完整传递。而这就是Transactional Client的职责。
在讲解了与数据处理有关的各个Endpoint之后,我们接下来要讲解的就是与消息发送/接收相关的各个Endpoint。
在一个基于消息的系统中,各个子系统在运行时将得到需要处理的消息,对消息进行处理,并在消息处理完毕之后将处理结果通过管道传递给其它需要的子系统。那么这些子系统是如何得到消息的呢?其中的一种方法就是通过一次调用从管道中主动读取数据。这被称为是Polling Consumer。如果管道中没有消息,那么从管道中读取数据的线程将被阻塞,直到新的消息到达。而另一种方法则是由消息的生成方将消息发送到子系统中。而这种方式被称为Event-Driven Consumer。
但是有时候,单个的子系统并不能及时处理所有的信息,因此我们就需要多个子系统协同工作来完成这些信息的处理。在这种情况下,我们需要令多个子系统连接到一个Publish-Subscribe管道上,并在消息到来时通过竞争决定需要处理该消息的子系统。这种处理消息的方式被称为是Competing Consumers:
然而此时这些子系统都需要添加一个额外的组成,以在一个消息到达管道时决定到底由谁来对该消息进行处理。而另外一种方式则是由一个组成对该消息进行分发,即是Message Dispatcher:
如果我们仅仅需要处理某个管道上的特定信息,那么我们就需要通过Selective Consumer来有选择地读取一些信息:
在一个Publish-Subscribe管道上,如果一个子系统由于某种原因暂时无法接收消息,那么该消息就会丢失。为了解决这个问题,Enterprise Integration Pattern提出了Durable Subscriber。其会在子系统无法接收消息时将这些消息保存起来,从而允许在该子系统恢复时重新读取这些消息。一个相反的问题则是对多次接收同一个消息的处理,Enterprise Integration Pattern则提出了Idempotent Receiver。该组成将可以保证在接收到重复消息的时候,这些冗余的消息将不被处理。
如果一个原本并不支持消息调用方式的服务既要求能够以消息的方式被调用,那么我们就需要使用一个Service Activator。在一个消息到达时,其内部将会转化为对该服务的调用。
好了。到此为止,我们已经将所有Enterprise Integration Pattern中所介绍的消息系统基本组成介绍完毕了。而在下一篇文章中,我们将对这些组成之间易发生混淆的各个组成加以比较,并介绍在消息系统中常见的一些解决方案。
转载请注明原文地址并标明转载:http://www.cnblogs.com/loveis715/p/5185332.html
商业转载请事先与我联系:silverfox715@sina.com
公众号一定帮忙别标成原创,因为协调起来太麻烦了。。。