企业集成模式-第七章
六、中场演练:简单的消息传递(略)
七、消息路由
7.1 引言
- 简单路由器:这是消息路由器的变种,它把消息从一个入站通道路由到一个或多个出站通道
- 组合路由器:把简单路由器结合到一起,建立更为复杂的消息流
- 架构模式:描述了基于消息路由器的架构风格
针对恰当的用途采用正确的路由器
7.2 基于内容的路由器
如果一个逻辑功能的实现需要跨越多个物理系统,这种情况应该如何应对?
基于内容的路由器首先检查消息的内容,并根据消息中包含的数据把消息路由到不同的通道上。可以根据许多标准来完成路由,如是否存在某些字段、是否有指定的字段值等实现基于内容的路由器时,由于可能会频繁维护路由器,所以要特别注意路由功能应当易于维护。在更复杂的集成环境中,基于内容的路由器可能会以可配置规则引擎的形式出现可以根据一组可配置的规则来计算目标通道。
1)减少依赖性
基于内容的路由器是更为通用的消息路由器的一种常用形式。它采用了预测路由机制(predictive routing),也就是说,它了解其他所有系统的功能。由于能将每条输出消息直接发送给正确的系统,因此能够高效地实现路由。其缺点是必须对所有可能的接收者及其功能了如指掌。每当增加、修改或删除接收者时,基于内容的路由器都要做相应的修改,这就给维护带来很大麻烦。
7.3 消息过滤器
如何避免组件接收到与之无关的消息?
消息过滤器是一种只有一个输出通道的消息路由器。如果输入消息的内容与消息过滤器指定的规则匹配,该消息将路由到输出通道。如果消息的内容与规则不匹配,这个消息则被丢掉。
消息过滤器可以描述为一种特殊的基于内容的路由器,它会把消息路由到输出通道或者一个空通道(null channel
)。发布到空通道的所有消息都会丢掉。这种通道的作用类似于许多操作系统中提供的 /dev/null
目标文件,或空对象(null obiect
)。
2)在消息传递系统中建立过滤功能
可以把促销信息发布到 wgco.update.promotion.widget
通道。然后订购者可以使用通配符来订购特定的消息子集。
例如,如果订购者在监听 wgco.update.*.widget 主题,他将接收到所有与零部件相关的更新消息(包括促销和价格变动消息)。
另一个订购者可能监听 wgco.update.promotion.*,该通道将发布所有与零部件和配件的促销活动相关的信息,但不包括价格变动信息。
基于通道层次结构,可以通过附加限定参数来明确通道的语义,客户可以指定额外的规则作为通道名的一部分来过滤消息,而不必订购所有的更新消息。不过,尽管层次式的通道命名可以提供一定的灵活性,但与消息过滤器相比灵活性仍然很有限。例如,消息过滤器可以根据价格变化是否达到 11.5%来决定是否传递价格变动消息,而利用通道名是很难表达这种含义的。
3)使用消息过滤器实现路由功能
基于内容的路由器 | 带消息过滤器的发布-订购通道 |
---|---|
每条消息只有一个消费者 | 多个消费者可消费同一条消息 |
集中控制和维护——预测型路由 | 分布式控制和维护——反应型过滤 |
路由器需要了解参与者。如果增加或删除了参与者,可能要更新路由器 | 不需要了解参与者,增加或删除参与者很方便 |
常用于商业事务处理,如订单处理 | 常用于事件通知或信息型消息 |
采用基于队列的通道通常效率更高 | 采用发布-订购通道效率更高 |
7.4 动态路由器
如何避免路由器对所有可能目标的依赖,同时还能保持其效率?
使用动态路由器,基于参与目标提供的特殊配置消息,这个路由器可以自行配置。
除了使用输入和输出通道,动态路由器还使用了一个附加的控制通道。系统启动时每个潜在的接收者(可能的接收者)都利用该控制通道向动态路由器发送一条特殊的消息宣布这个接收者的存在,并给出此接收者所处理消息的路由条件。动态路由器把每个参与者的偏好存储在一个规则基中。当有消息到达时,动态路由器会计算所有规则,并把消息路由给符合规则的接收者。它能实现了高效的预测型路由,而在维护动态路由器时不必依赖于各个潜在的接收者。
由于接收者彼此之间相互独立,动态路由器必须应对可能出现的规则冲突,如多个接收者都宣布对相同类型的消息感兴趣。动态路由器可以采用多种不同的策略解决这样的冲突:
- 忽略与已有消息存在冲突的控制消息。这个策略可以确保路由规则是无冲突的。但路由表的状态可能取决于潜在接收者的启动顺序。如果所有接收者在同一时刻启动,是,由于所有接收者同时向控制队列宣布自己的偏好,可能会导致不可预见的行为。
- 把消息发送给与规则匹配的第一个接收者。该策略允许路由表中包含相互冲突的接收者,但是在消息到达时可以解决这些冲突
- 把消息发送给与规则匹配的所有接收者。这种做法可容忍冲突的存在,但是它把动态路由器转换成了接收表。通常,基于内容的路由器采取的操作是为每条输入消息发布一条输出消息,而这个策略违背了这一原则。
动态路由器的主要缺点是这种解决方案比较复杂,并且调试动态配置的系统存在一定的困难。
7.5 接收表
如果接收者列表会动态变化,如何把消息路由给这样一组接收者?
为每个接收者定义一个通道。然后使用接收表模式检查输入消息,确定需要消息的接收者列表,并把消息转发给与列表中接收者关联的所有通道。
嵌入到接收表中的逻辑可以用两个单独的部分来描述,不过它们的实现一般都耦合在~起。第一个部分计算接收者列表。第二个部分只是遍历接收者列表,并将所接收消息的副本发送给各个接收者。类似于基于内容的路由器,接收表通常并不修改消息的内容。
接收表 | 带消息过滤器的发布-订购通道 |
---|---|
集中控制与维护——预测型路由 | 分布式控制与维护——反应型过滤 |
路由器要掌握参与者的信息。如果增加或删除了参与者,路由器可能需要更新(除非使用动态路由器,但是要以失去控制权为代价) | 不需要掌握参与者的信息。增加或删除参与者很方便 |
常用于商业事务处理,如请求报价信息 | 常用于事件通知或传递信息型通道 |
如果只采用基于队列的通道,效率会更高 | 如果采用发布——订购通道,效率会更高(具体由基础设施决定) |
7.6 分解器
如果消息中包含多个元素,每个元素要采用不同的方式处理,这种消息应该如何处理?
使用分解器把组合的消息分解为一系列单独的消息,每条消息只包含与某个商品相关的数据。
迭代分解器:把消息数据存储在一个树型结构中
静态分解器:分解的消息数量一般是固定的
7.7 聚合器
怎样把独立但相关的结果消息组合起来,从而使它们能作为一个完整的消息来处理?
使用有状态的过滤器,即聚合器,把单独的消息收集并存储起来,直到接收到所有相关的完整的消息集合,然后聚合器从各个单独消息中提取信息,组合成个消息后发布。
聚合器是一种特殊的过滤器(在管道和过滤器体系结构中),它接收一个消息流并识别出相互关联的消息。一旦接收到完整的消息集合 (后面会介绍如何确定消息集是完整的),聚合器就从每个相关的消息中收集信息,并把它们组合为一个聚合消息发布到输出通道上,以便进一步的处理。
设计一个聚合器时,需要指定以下属性:
- 相关性:哪些输入消息相互关联?
- 完成条件:何时才能发布结果消息?
- 聚合算法:怎样把接收到的消息组合成一个结果消息?
1)实现细节
由于消息传递系统是事件驱动的,聚合器可能在任意时刻以任意的顺序接收到相关的消息。为了组合消息,聚合器必须维护一个有效聚合列表,它是聚合器已经接收到的部分消息的集合。当聚合器接收到一条新消息时,要检查该消息是否属于已有消息集合的一部分。如果它与现有的消息集合无关,聚合器就创建一个新的消息集合,并把这个消息作为新集合中的第一条消息。如果它与现有的消息集合相关,聚合器就把它添加到相关的集合中。
完成消息添加后,聚合器会计算受影响消息集合的完成条件。如果条件满足,则从集合累积的消息中组合产生新的消息,并把新消息发布到输出通道上。如果条件不满足,则不发布任何消息,聚合器继续维护有效消息集合,等待新消息的到达。
为了增强整个解决方案的健壮性,也可以让聚合器监听一个特定的控制通道,从而允许手工地清空所有有效集合或者清空某个指定集合。如果我们想从错误条件下恢复同时又不想重启聚合器组件,就可以利用这一特性。同样的道理,如果允许聚合器根据请求把有效集合列表发布到一个特殊的通道上,这个特性对于调试是非常有用的。这两项功能充分了控制总线所具有的特性。
2)聚合策略
建立聚合器完成条件的策略有很多。现有的策略主要取决于我们是否知道需要多少个消息。如果聚合器接收到原始组合消息的副本,或者每个消息中都包含消息总数(在“分解器”示例中已经介绍过),聚合器就能知道需要多少个子消息。根据聚合器对消息流的了解程度,大多数常用的策略可以划分为如下几种:
- 等待所有消息
- 超时
- 第一个最好(first best):只等待接收第一个(最快的一个)响应。(适用于竞标和报价)
- 覆盖超时(timeout with override):等待指定的时间,或者接收到有预置最低分数的消息为止。
- 在这种情况下,如果发现非常满意的响应就中止等待,否则一直等到超时。
- 如果到超时还没有发现合适的消息,则对所有接收到的消息按分数排序
- 外部事件(external event):有时聚合是否结束是由外部业务事件的到达决定的。例如,在金融产业中,交易日的结束就意味着接收到的报价聚合的结束。
与完成条件的选择紧密相关的是聚合算法的选择。下面是将多个消息聚合为一个消息:
- 选择 "最好" 的答案:使用这种方法的前提是存在一个最好的答案,如某个商品的最低报价
- 精简数据:大容量数据源往往会带来很大的消息流量,聚合器可以用于减少消息流量。如果需要计算每个消息的平均结果,或者要把各个消息中的数字字段累加到一个消息中,此时就可以采用这种方法。如果每条消息代表的是一个数值,如已接收到的订单数。这种方法就是最理想的。
- 收集数据以便事后评估:对于如何选择最佳答案,要让聚合器做出决定一般是不可能的。在这些情况下,使用聚合器收集每条消息并把它们组合为一条消息仍然是有意义的。组合后的消息可能只是对每条消息中数据的简单汇集。可能在以后由另一个的组件或人来完成聚合决定
3)示例
- Bid 是一个便利类,用于存储与竞标有关的数据项。我们把输入的消息数据转换为一个 Bid 对象;这样就能通过一个强类型接口存取竞标数据,使 Auction 的逻辑完全与 JMS API 无关。
- Auction 是已接收的相关竞标消息的集合。Auction 类实现了聚集策略,例如,找出最低竞标价并确定聚合何时完成。
- AuctionAggregate 实现了 Aggregate 接口。该类是 Aggregate 接口与 Auction 类之间的适配器[GoF]。采用这种设置有利于 Auction 类独立于 JMS API。
7.8 重排器
如何把相互关联但是乱序的消息重新恢复成正确的顺序?
为了解决顺序不正确问题,有一个最明显的解决方案,这就是首先保持消息的有序性保持有序要比恢复有序容易得多。很多大学图书馆不让读者把书放回(已排序的) 书架上也正是出于同样的原因。通过对插入过程加以控制,任何时刻 (几乎) 都能保证正确的顺序。
搜集并重排消息,使消使用一个有状态的过滤器:重排器 (Resequencer),息能按指定的顺序发布到输出通道上。
序列号:如果想保持消息序列的顺序,应该定义一个单独的字段记录每条消息在序列中的位置。该字段一般位于消息的首部。a
要生成有序的编号,一般需要一个计数器为整个系统分配编号。在大多数情况下,不但要采用升序方式生成编号,编号之间还必须是连贯的,否则就很难识别出丢失的消息。如果在设计时不注意,序列号生成器很容易成为消息流的瓶颈。
重排器必须把有较高编号的消息存储起来,直到接收到所有“丢失”的较低编号消息为止。在这期间,它可能还会接收到其他乱序的消息,同样也要把它们存储起来。一旦缓冲区中包含了连续的消息,重排器就把这个消息序列发送到输出通道中,然后从缓冲区中删除已发送的消息(如下图所示)。
应该分配多大的缓冲区?如果要处理的消息序列很长,缓冲区就会变得相当大。
要避免缓冲区溢出,一种可靠的方法是使用主动确认机制来控制消息的生产者。
让重排器告诉生产者在它的缓冲区中还剩多少个空槽。消息的节流控制端可据此发送相应数量的消息,因为即使所有这些消息的顺序都不正确,重排器也能把它们放在缓冲区里并对其重排。这是一种折衷考虑效率和缓冲区需求的方法。但是,这要求我们能访问原始的有序消息流,只有这样才能把它们插入发送缓冲区并实现节流控制。
这种方法非常类似于_TCP/IP 网络协议的工作原理。TCP 协议的一个关键特性是确保数据包按序传送。在实际传输中,每个数据包可能按不同的网络路径路由,因此会经常发生包顺序不正确的情况。接收者维护着一个用作为滑动窗口的循环缓冲区。接收者和发送者在每次确认前要协商可发送的数据包数。由于发送者要等待接收者的确认,因此较快发送者的速度不能超过接收者,也就不会引起缓冲区溢出。
克服缓冲区溢出还有一种方法,可以为缺少的消息计算替代消息。如果接收者能接受“足够好”的消息数据,并且不要求每条消息都提供精确的数据,或者消息传递的速度比准确性更重要,就可以采用这种方法。例如,在利用IP 传送语音时,采用空白数据包代替手失的数据包而不是重新请求,能够明显减少语音流的延迟,使用户获得更好的体验。
7.9 复合消息处理器
如果要处理的消息包含多个元素,而且每个元素需要不同的处理,那么如何维持整个消息流?
在我们这个例子中,这意味着每个商品将路由给正确的库存系统进行检验。库存系统彼此之间是解耦合的,每个系统只接收它们能处理的商品。
采用目前这种设置的缺点是,我们不知道订购的所有商品是否都有库存并且可发货。我们还想得到所有商品的价格(考虑到大宗商品折扣),并把它们集中记录到一个发货单中。这就要求把订单作为单一的一个消息继续处理,尽管我们刚把它分解为多个子消息。
一种方法是把经某个特定库存系统处理的所有商品重新组装成一个单独的订单消息。在此之后,就可以作为一个整体来处理这个订单:订单可以履行和发货,并寄出相应的账单。每个子订单都采用独立的过程处理。
但是,采用这种方法并不能提供最佳的客户服务。客户可能会收到多批商品和多个发货单,而且很难解决商品退货或争议问题。对订购图书的消费者来说,这些还不算是大问如果每个商品彼此依赖,问题会更棘手。假设订单中包括许多家具商品,这些商品可顿,以组装成一套壁柜。如果收到许多包有家具材料的大箱子,但其中没有找到所需的装配零部件,而这些零部件要过些日子才能送到,客户肯定不愿意看到这种情形。
7.10 分散收集器
如果必须把消息发送给多个接收者,而且每个接收者都会返回应答,如何维护整个消息流?
前面介绍各种模式时,谈到了一个订单处理示例,当前库存中没有的商品都可由某个外部供货商提供。但是对于不同的商品,有的供货商有现货,有的可能没有;同一商品开出的价格不同,或者供货的日期不同。为了尽可能以最佳的方式满足订单需求,必须从所有供货商那里获得报价信息,从中选出能满足需求的最好的一家。
首先要能灵活地确定消息的接收者。为此,可以集中地确定哪些供货商符合要求,或让任何有兴趣的供货商都参与竞标。由于我们无法(或者几乎不能) 控制接收者,因此要做好准备只接收到部分响应,而不是全部响应。对竞标规则的修改不能影响解决方案的结构完整性。
有两种类型的分散收集器,它们使用了不同的机制把请求消息发送给目标接收者。
- 分配(Distribution)方式:利用一个接收表,采用分配方式,分散收集器可以控制接收者列表,前提是它必须知道每个接收者的消息通道。
- 拍卖(Auction)方式:采用拍卖方式,分散收集器使用发布-订购通道把请求广播给所有感兴趣的参与者。这种方式允许分散收集器使用单个通道,但是必须放弃对接收者的控制。
7.11 路由表
如果设计时不清楚消息处理的步骤,而且每条消息的处理各不相同,那么如何让消息通过一系列处理步骤连续地得到路由?
前提:必须事先确定处理步骤序列,并且该序列是线性的
管道和过滤器体系结构提供了一个很好的方法,可以把一系列处理步骤表示为用管道(通道)连接起来的独立的过滤器。在默认配置下,过滤器由固定的管道连接。如果想把消息动态路由给不同的过滤器,可以使用特殊的过滤器作为消息路由器。该路由器能动态确定路由该消息的下一个过滤器。
对于这个问题,要得到一个好的解决方案,关键需求可以总结为以下几点:
-
高效的消息流:消息只能流经必要的处理组件,不经过多余的组件。
-
高效地利用资源:该解决方案不能占用大量通道、路由器和其他资源
-
灵活性:每条消息的路由很容易改变。
-
易于维护:如果需要支持新的消息类型,最好不要因为某一点的维护带来新的错误
把路由表附加到每条消息中,指定处理步骤序列。采用特定的消息路由器包装每个组件,它读取路由表并把消息路由给表中的下一个组件。
缺点: -
首先,消息的大小有所增加
-
一旦开始消息的处理,消息的路径就无法改变(然而,在许多实际的业务处理过程中,消息流要根据中间处理结果改变。例如,要根据订单商品的可用性(由库存系统提供这方面的信息),把消息路由给不同的路径。这就要求集中的组件必须预先确定消息可能经过的所有处理步骤。与使用基于内容的路由器一样,这就从一定程度上降低了设计方案的可用性)
一般用法:
- 按顺序执行二元验证步骤
- 每个处理步骤均是无状态的转换
- 每个步骤只是搜集数据,但是不做路由决策
7.12 过程处理器
如果无法在设计时确定所需的处理步骤,或者处理步骤不是顺序执行的,应该如何通过多个处理步骤完成消息的路由?
使用中央处理单元(即过程管理器)维护序列的状态,并根据中间处理结果确定下一处理步骤。
首先要说明的是,过程管理器的设计与配置是一个非常宽泛的主题
使用过程管理器会得到一种所谓的集散式消息流(如上图所示)。输入的消息将初始化过程管理器。我们把该消息称为触发消息。根据过程管理器内部的规则,它把消息(1)发送给第一个处理步骤,该处理步骤由处理单元 A 实现。当单元 A 完成处理后,它把应答消息返回给过程管理器。过程管理器确定下一个要执行的处理步骤,并把消息 (2)发送给下一个处理单元。这样,所有的消息流都要经过这个中央“hub(集线器)”,因此我们把这种工作模式称为集散模式。采用中央控制元素的缺点是,过程管理器很容易成为性能瓶颈。
过程管理器的多功能既是它的最大优点,也是它的最大缺点。
过程管理器的一个主要功能是保持消息之间的状态
过程定义(模板),过程实例
比较过程管理器与其他模式
分布式管道和过滤器 | 路由表 | 中央过程管理器 |
---|---|---|
支持复杂的消息流 | 只支持简单的、线性消息流 | 支持复杂的消息流 |
难以改变消息流 | 容易改变消息流 | 容易改变消息流 |
不存在集中的故障点 | 存在潜在的故障点(计算路由表) | 存在潜在的故障点 |
高效的分布式运行时结构 | 大部分是分布式的 | 集中发散式结构可能导致瓶颈 |
不存在集中的管理和报告点 | 存在集中的管理点,不存在集中的报告点 | 存在集中的管理点和报告点 |
如果存在控制和状态管理的集中点,这也意味着它可能是故障的集中点或性能的瓶颈正因为如此,大多数过程管理器的实现会把过程实例的状态持久地存储在文件或数据库中从而利用企业级数据库系统特别提供的几余数据存储机制。并行地执行多个过程管理器实例也是很常见的情形。
7.13 消息代理
如何让消息的目标与消息的发送者解耦合,并保持对消息流的集中控制?
这个图说明,在每个应用之间建立直接的通道将导致通道数量的爆炸,并丧失使用通道完成消息路由所带来的许多优点。之所以会产生这种类型的集成体系结构,往往是一种解决方案随时间不断发展造成的。
使用一个集中的消息代理从多个目标接收消息,确定正确的目标,并把消息路由给正确的通道。消息代理的内部实现使用了其他消息路由器。
消息代理提供的集中维护是一个优点,但它也会变成缺点。所有消息都通过一个消息消息代理提供的集中维护是代理进行路由,这会使消息代理成为严重的性能瓶颈。