按照Enterprise Integration Pattern搭建服务系统

  在前一篇文章中,我们已经对Enterprise Integration Pattern中所包含的各个组成进行了简单地介绍。限于篇幅(20页Word以内),我并没有深入地讨论各个组成。但是如果要真正地按照Enterprise Integration Pattern搭建一个系统,仅仅是了解它们实际上还差得很远。因此在本文中,我将会对Enterprise Integration Pattern中较容易产生混淆的部分以及一些系统搭建时常常使用的一些方法进行介绍。

寻找最优的解决方案

  相信您在读前一篇文章时就已经能感觉到,在使用Enterprise Integration Pattern搭建一个系统时,我们常常可以通过不同的组成来满足类似的需求。例如在需要对消息进行转换时,我们常常可以使用Content Enricher或Content Filter等组成来添减内容,更可以使用Massaging Mapper等组成来完成类似的功能。那么我们应该什么时候使用Content Enricher或Content Filter,而什么时候使用Massaging Mapper呢?

  其实答案就存在于过滤器和Endpoint之间的不同。两者之间的不同主要在于,过滤器是Pipes and Filters模型之中的一个独立的过滤器,而Endpoint则是过滤器中的一个用来令过滤器内部的业务逻辑实现与消息系统关联的组成。例如我们有一个应用提供了一系列与消息系统不兼容的API。此时我们就需要使用一个Endpoint将其与消息系统关联。而它们则共同组成了一个过滤器:

  在了解了这点不同之后,相信您就会明白,为什么Endpoint中所介绍的那些功能与很多用来完成消息路由及转化的过滤器类似了:一类是单独存在的过滤器,一类则是过滤器中的组成。相较于使用一个独立的过滤器,Endpoint能够减少一次在管道中传输数据的消耗,从而提高了消息的处理速度:

  如上图所示,相较于经由过滤器处理,一个基于Endpoint的具有相同功能的组成可以减少一次消息通过管道进行传递的过程。因此基于Endpoint的解决方案拥有更好的性能。反过来,由于Endpoint是与其后的业务逻辑处于同一个过滤器中的,因此其无法将消息发送到其它子系统之中。也就是说,其灵活性有所下降。甚至说,每次对Endpoint之后的业务逻辑的更新同样需要对Endpoint进行维护,以保证其能正常工作。

  这就引出来了一个话题,在开发一个基于Enterprise Integration Pattern的系统,最需要考虑的是什么?

  通常情况下,这些考虑的因素主要有:性能,灵活度,可维护性,高可用性。而最终的解决方案则常常是这些因素相互平衡的产物。

  性能不必多说。在一个基于消息的系统中,对用户请求的处理最终会转化为一系列在管道中传递的消息。由于消息的传递是一个异步操作,因此对单个消息处理时的性能不会比同步操作更好。

  第一个使系统具有合适性能的前提就是使子系统拥有合适的粒度。如果子系统的粒度较小,那么对一个业务逻辑的处理就需要经由更多的子系统。这既增加了管道的数量,又增加了消息在管道中传输的次数。除此之外,子系统粒度过细也会给消息系统带来很大的压力。在前一篇文章中已经提到过,管道会将消息保存起来,因此每个管道都会占用一部分内存。如果子系统的粒度过细,那么整个系统就需要更多的管道,对消息系统所在的服务器造成更大的压力,也会提高系统出错的可能。

  一个拥有较粗粒度的系统最常出现的问题就是系统过载。这时我们该怎么做呢?答案就是对其进行横向扩展。在《服务的扩展性》一文中我们已经提到过,一个服务的扩展方式分为XYZ轴之分。而在处理系统过载问题上,我们常常需要执行X轴扩展,有时也需要进行Y轴扩展:

  X轴扩展相对简单:使用多个服务实例对消息进行处理。在这些服务之前,我们可以使用Message Dispatcher或Competing Consumers这类Endpoint,或者通过Dynamic Router等过滤器来完成对消息的分发。只是有时通过X轴扩展并不能完全地解决问题。就像上图显示的那样,对整个子系统进行扩展实际上会导致系统的某些组成利用率非常低。在这种时候,我们就可以将这个过载的子系统中的各个组成分离出来,并作为一个独立的基于消息的子系统,然后对其真正形成瓶颈的子系统进行扩展:

  上图中展示了如何在一个子系统实例遇到瓶颈的时候执行Y轴扩展。在一开始,该子系统是作为一个独立的系统存在的。在需要处理一个消息的时候,消息从其输入管道流入,并在处理完毕后从其输出管道流出。但是在该子系统成为整个系统的瓶颈时,我们就需要将该子系统分割为多个粒度稍小的子系统,并对其中成为瓶颈的子系统添加新的实例。这样,我们就解决了整个系统中的瓶颈。

  但是这样做的坏处则在于,一个消息常常需要经过更多的管道才能被处理完毕。就以上图所展示的子系统分割逻辑为例,可以看到,一个消息在被新系统处理时将首先需要经过绿色的结点,接下来还至少需要经过橘红色的结点。也就是说,对消息的处理至少多出了消息流经一个管道的时间。因此在分割一个对消息处理时间要求较高的系统时,我们常常需要考虑的是,如何使消息经过较少的管道。

  而在对消息处理吞吐量的要求超过对消息处理时间的要求时,我们则需要尽量地使每个实例最大程度地发挥它的处理能力。这是常识,所以我们不再深入讨论。

  在考虑系统性能时,我们也常常需要考虑这样一点,那就是有些消息系统提供了创建于内存中的管道。在该管道中传递消息的性能要比经由网络传递消息的性能高出很多。因此在设计基于消息系统的服务时,我们应尽可能地使用这种存在于内存中的管道。

  而从物理结构上来看,从原有子系统中所分割出来的各个子系统实例也需要和消息系统服务所在的实例进行交互。我们当然可以将管道直接添加到原有的消息系统服务上,而另一个较为常见的方法则是创建一个新的消息系统服务。这可以带来非常多的好处:使消息系统服务能够承担更多的负载,使得整个系统的物理拓扑逻辑变得更为清晰,更可以在不同的消息服务上使用不同的安全配置,例如将其设置为只接受从中心消息服务所发送的消息,进而提高整个系统的安全性:

  而一个与性能常常有冲突的地方就是系统的灵活度。我们可以回想一下前一篇文章中对Content-Based Router及Dynamic Router的讲解。两者之间的不同主要在于Dynamic Router可以令一个过滤器注册自身所能接收消息的条件,从而能够动态地加入或离开系统。而这也并非没有代价。为了提供这种灵活度,我们需要添加额外的管道,从而为消息服务带来了更多的负载。

  但是过多的灵活性反而也会导致问题的大量出现。例如对于某些服务,我们可以假设其可能会由于业务的快速增长而达到系统的瓶颈,因此为其设计较强的灵活性是有必要的。而对于某些系统,我们常常不应该设计有太大的灵活性。例如在当前需求仅仅是针对用户类型来提供相应推荐的推荐系统而言,当天的推荐实际上是固定的一系列推荐项的组合,因此也不存在什么需要根据用户偏好动态计算推荐项的功能。对于这样的一个服务,只要需求没有发生变化,整个系统的计算负载也不会高,因此也不必为它的处理能力扩展留下太多的灵活性。

  而在需求变化时,例如我们现在需要根据用户的浏览记录来推荐物品,那么我们就需要考虑系统的灵活性了。因为此时推荐系统的计算结果可能会随着用户的当前浏览而随时发生变化。随着用户的快速增多,这种负载将会越来越重,从而造成子系统的过载。

  况且,过多的灵活性也会导致可维护性变得更为困难。还是以Content-Based Router以及Dynamic Router为例。Dynamic Router之所以出现,就是因为当其中一个参与消息处理的子系统发生变化时,我们还需要更改Content-Based Router。这会导致Content-Based Router之后的整个子系统暂时不可用。

  这实际上就对参与消息系统中的各个组成之间的耦合性提出了要求。如果对某个组成的修改会导致我们更改其它一些组成,那么它们之间就是耦合的。对于不常变动的组成关系,这种耦合是正常的,而对于常常会发生变化的组成,尤其是在为整个系统设计高可用性,热插拔功能时,这些耦合就是相对致命的。

  可以这么说,对于一个重要的系统,如何让它在发生变化时不需要停止服务常常是其所最为看重的。高可用性是其中的一种需求,在维护时不需要停止服务也是一种非常重要的需求。因此在设计一个系统时,我们常常考虑的是:哪里可能会经常发生变化?发生变化之后我们需要更改哪些组成?如果系统的某个相关组成失效,整个系统是否能够继续正常提供服务?除此之外,是否有不必要的消息传递?

选择合适的组成

  好。上一节我们已经介绍了在设计一个基于消息的系统时所需要考虑的各种因素。而在本节中,我们将对Enterprise Integration Pattern中所介绍的一些组成进行分析,从而使您更清楚地了解这些组成之间的优点和缺点,并最终能够正确地使用它们。

  就像Open-Close原则一样,我们在基于Enterprise Integration Pattern设计一个系统的时候也需要考虑这些系统中各个子系统之间的变与不变。变在这里主要分为两种:系统中的各个子系统之间的关系发生变化,以及路由过程中消息自身的路由方式发生变化。搞清系统中的变与不变能够提供较高的灵活性和可维护性。但是由于灵活性和可维护性常常需要引入一系列额外的组成,因此其常常会影响整个系统的性能。因此除了需要考虑整个系统的性能之外,我们还需要考虑各个组成的性能。在这两种思考方式下,消息系统各个组成之间的异同就会显得十分清晰明了。

  这些额外引入的组成常常意味着性能的下降以及维护成本的增加。例如就以使用一个Content-Based Router完成消息的分发这种最为简单的情况为例,它的好处就是能让我们把所有的路由逻辑都集中在一个组成中完成。这样只要消息中的数据发生了变化,或者有新的子系统添加到路由逻辑之中,我们只需要更改这些路由逻辑即可。这便是集中管理信息的好处,或者是SRP(笑,Single Responsibility Principle,思想类似,随便扯扯)。但是反过来,如果一个子系统所能接收的消息类型发生了变化,那么我们就需要同时修改该子系统以及相应的路由器。而这就是一种并不受待见的耦合,尤其是在一个接收端会经常发生变化的系统中,这种变化所带来的困扰远大于我们集中管理信息所带来的好处。

  反过来,很多消息系统也同样允许我们创建自定义的各个组成,例如自定义的路由器,自定义的消息转换逻辑等。在这些情况下,我们也可以通过一系列业界常用的思想来解决这些问题。

  就让我们从最先介绍的Content-Based Router说起。一个Content-Based Router会根据消息中所包含的信息来决定到底由哪个子系统对该消息进行处理。这也就是说,Content-Based Router知道到底有哪些子系统,同时它还知道如何去分析这些消息。那么一旦这些处理消息的子系统的可见性发生了变化,或者消息中所包含的信息发生了变化,那么我们就需要对Content-Based Router内的逻辑进行更改。

  而为了避免这些维护上的问题,Enterprise Integration Pattern提出了Dynamic Router。其允许各个接收消息的子系统向其注册处理问题的条件。那是不是Content-Based Router就没有任何价值了呢?不是的。相对于Dynamic Router,Content-Based Router是一个更轻量级的解决方案。因此在筛选条件不会发生变化而且参与消息分发的子系统不会发生变化的情况下,其反而是最佳的解决方案。除此之外,如何避免参与分发的各个子系统向Dynamic Router所注册的条件不会发生彼此相互重叠的情况也是一个需要讨论的问题。这也是Dynamic Router的这种灵活性所带来的副作用。

  你仔细想一想就会发现,实际上这就是一个依赖注入。只不过我们不是在具体编程过程中对其进行使用,而是在整个系统设计时候完成的。所注入的,则是Dynamic Router所需要的作为消息分发依据的逻辑。同样的,Service Locator也会帮我们解决一系列耦合的问题。在Enterprise Integration Pattern中,典型的借鉴Service Locator的组成似乎并不多,但是在实际使用中,我们也的确可以通过这样设计系统来完成各个系统之间的解耦。

  我说的意思实际是,虽然说不同层次上所常用的各种方法会存在着一些不同,例如我们无法像创建一个派生类一样对子系统进行派生,但是很多时候思想是通用的。

  OK,这段扯得有点远。我们拉回到如何区分并合理地使用各个组成这样一个话题中。我们在前面已经讲解过什么时候使用过滤器,什么时候使用Endpoint。因此在这里我们将会把精力主要集中在负责路由的各个过滤器上。因为这常常是很多人产生疑惑的地方。

  在Enterprise Integration Pattern一书中列入了如下的一个用来决定一个系统中所需要使用的路由器的判断逻辑图:

  但是我个人认为这个图是根据各个路由过滤器的特性来去分类的。而在我实际决策过程中,我更趋向于根据业务逻辑以及消息处理本身的需求来决定到底使用哪个路由器。该判断逻辑如下所示:

  因为我一直觉得,对一个消息如何进行处理才是与业务逻辑关联最密切的。业务逻辑以及某些非功能性需求决定了到底我们需要什么样的路由逻辑。而且在上图中,我也把Dynamic Router包含进了Content-Based Router中了。因为实际上,Dynamic Router就是一种特殊的Content-Based Router。当然,仁者见仁,智者见智。不是说原书中的决策逻辑不好,而仅仅是将我所使用的决策逻辑介绍给大家。

  而对于用来进行消息转化的Transformer以及各个Endpoint,由于我觉得它们实际上还是很容易区分的,因此在本文中就不再做细致的讲解了。

管理基于消息的系统

  在前面的讲解中,我们只介绍了应该如何通过Enterprise Integration Pattern所提及的各种组成搭建一个系统。但是除了业务逻辑之外,我们还需要令我们的系统满足一定的非功能性需求,例如高可用性,可测试性等。因此在搭建了一个系统之后,我们还需要做一系列的工作,才能让我们的系统稳定持续地提供服务。

  但是对这些非功能性需求的保证则没有那么简单。例如,在一个基于消息的系统中,消息的生产者和消费者并不知道彼此,同时对消息的传送常常是一个异步的调用,其只对消息的可靠传递进行了保证,却没有对消息的传递时间进行保证。因此如何满足这些非功能性需求则是更为困难的一件事。

  Enterprise Integration Pattern一书中提供了一系列用以提供这些非功能性需求的解决方案。在本节中,我们就将对这些解决方案进行简单地介绍。

  首先要介绍的就是Control Bus。在该方案中,Control Bus将使用独立的管道与系统中的各个子系统关联,以动态地监控各组成的运行状态,如子系统是否正常工作,与其运行相关的统计数据,其是否过载,消息的处理是否有较高的延迟等。甚至在监控到了某些异常状态之后,其还需要通过这些管道向这些子系统发送消息,以更改这些子系统的配置:

  那么我们应该如何通过这些消息来判断一个子系统是否正常工作呢?简单地说,我们可以令子系统向管道中送入一系列心跳消息的方式来解决。这种心跳消息可能仅仅是一个简单的通知消息,更可以在消息中包含子系统当前的状态信息,如处理了多少消息,每个消息的处理时间,整个系统的状态等。

  但是这些信息仅仅用来描述子系统的当前运行状态。我们怎么判断子系统的业务逻辑是否正常执行呢?此时我们就需要使用Enterprise Integration Pattern中所介绍的Test Message方案:

  从上图中可以看到,Test Message主要包含了四个组成:Generator将首先生成测试消息,接下来,该测试消息将会通过Injector与实际的业务消息发送到子系统中。在子系统处理完毕之后,Separator将会把这些测试消息对应的处理结果分离,并将这些处理结果发送给Verifier进行验证。而这些验证的结果将被发送到Control Bus中,以方便Control Bus管理这些子系统。

  好了,现在我们已经知道了如何探测一个子系统是否在正常工作。下一步则是为我们追踪及调试系统作准备。为了能够完成这些功能,我们首先需要能够侦听在两个子系统之间所传递的消息,才能通过这些侦听到的消息来进行调试。当然,在一个Publish-Subscribe管道上侦听消息是非常简单的:我们只需要侦听该管道上的消息即可。但是由于Point-to-Point管道将只能对消息进行点对点传输,因此我们不能简单地对该管道上的消息进行侦听。为了解决这个问题,Enterprise Integration Pattern则提出了Wire Tap方案。该方案会将Point-to-Point管道的一端连接到Wire Tap上,然后由其向目标子系统以及侦听方转发该消息:

  好的,现在我们能够侦听这些消息了,下一步则是找到一个地方把它们存起来。该功能是通过Message Store来完成的:

  从上图中可以看到,Message Store会要求各个子系统在向输出管道放置消息时也向消息的存储发送一个相同的消息,从而完成对这些消息的持久化。但是我们怎么才能知道一个消息到底是如何在系统中流动的呢?答案是通过Message History来记录消息所经过的各个子系统:

  而为了重现并调试某些出错的情况,我们则需要让某个消息能够经过一系列特殊的子系统,从而允许软件开发人员对出错的情况进行调试。此时我们就需要使用Detour方案。该方案会使用一个Context-Based Router判断某个消息是否满足特殊条件,如果是,那么将其传递给特定的输出管道:

  但是这里有一个问题,那就是我们更改了消息的路由路径。这明显会影响Request-Reply类型的消息的执行。为了解决这个问题,我们需要使用一个Smart Proxy。该组成能够缓存原消息的Return Address。这样当一个消息经过该组成时,其将首先缓存该消息的Return Address,并使用自己的响应输入管道地址替换消息中的返回地址。当消息从该管道返回时,Smart Proxy则会找到原有的Return Address并将消息送回。

  至于Enterprise Integration Pattern中所提到的最后一个组成Channel Purger则非常容易理解。由于消息是在消息系统中缓存的。当我们重新启动某个子系统,或者对某个子系统进行调试时,其管道中所存留的消息将会明显地影响我们的调试。Channel Purger则会帮助我们解决这个问题:其会将管道中的不需要的消息移除。

适当地使用EIP

  最后一节,我们则主要用来讨论您应该如何在合适的时机以合适的方式使用Enterprise Integration Pattern所提供的各种功能。

  首先要明白的就是什么时候使用Enterprise Integration Pattern。试想一下,如果一个系统对一个用户请求的处理需要5秒钟,那么一个浏览器用户需要很长时间才能完成对页面上所有数据的加载。对于不同的任务,用户对该行为的忍受能力其实并不相同。例如如果用户加载一个服务的首页都需要2分钟,那么他极有可能放弃使用该服务。但是如果一个功能是在后台做了非常耗时的操作,如部署虚机并在其上安装运行服务所需要的软件,那么对该请求的处理耗时10分钟都不足为过。此时我们只需要提供给用户一个界面并定时地刷新任务的执行状态,以通知我们的系统正在工作既可。

  因此,一个原本就需要较长时间耗时的,或者是至少用户能够理解为较为耗时的功能,才能使用Enterprise Integration Pattern对其进行组织。很多直接面向用户的功能,如电子商务,博客,很少直接使用到这些需要长时间耗时的操作,因此使用Enterprise Integration Pattern来组织这些功能只会让您的服务质量变得更差。

  那么我们应该在什么时候使用Enterprise Integration Pattern呢?答案实际上就存在于Enterpriese这一个词上。很多企业级应用常常包含一系列非常耗时的操作。就以现在最流行的云来说吧。我做的产品就是一个云管理软件。这个软件能让用户通过简单地拖拽就能定义其在特定云上所需要部署的服务。接下来,用户只要点击一下部署,在几十分钟后,该应用就将被部署完毕。

  让我们想一想这个云管理软件在部署时做了哪些事情呢?从Amazon上请求资源,对资源进行配置,在这些资源上部署服务所需要的各个软件,配置这些软件,并最终启动服务。可以想象到的是,这里面的每一步都是一个较为耗时的操作。而且它是一个非常典型的按照Pipes and Filters模型组织的业务逻辑:

  而为了能让用户能够知道我们的应用正在正常工作,我们则会将当前部署任务的状态回填到数据库中。这样用户在请求查看当前任务的运行状态时,我们只需要从数据库中将该状态读出返回既可。因此,虽然我们的部署服务所需要消耗的时间较长,但是用户在请求查看时,我们就能非常快速地返回,不是么?

  其实这是业内非常常见的一种对耗时任务的一种展示方法。只是由于这可能涉及到我们公司产品的内部实现,因此为了避免一些不必要的麻烦,我会找机会在介绍其它公司的产品,例如Amazon的CloudFormation,Beanstalk或OpsWorks等再对它的内部执行逻辑进行讲解。

  而且从云这个领域来看,其实现在对云服务提供Enterprise Integration Pattern的原生支持这一要求的呼声也是很高的。这也就是所谓的Cloud Orchestration的一个重要的组成部分。当然啊,这玩艺挺大也挺虚的。我尽量把它们一步步细化地讲解掉,毕竟我这一系列和Web Service的文章都是一步步地向着这个目标前进的。从前面的负载平衡,后面的扩展性,然后还有以后要讲的高可用性(尤其是基于云的),Amazon云所提供的功能等,我都会抽出时间写成博客。

转载请注明原文地址并标明转载:http://www.cnblogs.com/loveis715/p/5185353.html

商业转载请事先与我联系:silverfox715@sina.com

公众号一定帮忙别标成原创,因为协调起来太麻烦了。。。

原文地址:https://www.cnblogs.com/loveis715/p/5185353.html