当我们在为一个软件设计架构的时候,我们不仅仅要确保所做出来的架构要满足系统的业务需求,更加要确保做出来的架构要满足可维护性,安全,稳定性的非业务行的需求。
另外一个非常重要的非功能性需求就是性能。性能涉及到很多方面的关注点,例如吞吐量,延迟等。SOA的很多的设计原则和一些指导从来没有告诉我们如何去解决SOA中遇到的性能问题,因为SOA的关注点不在这里,其实SOA天生就是会导致很差的性能的:因为SOA涉及到了分布式,这就决定了它有很多不得不要的延时和不同的层之间的通信问题。
任何问题,都有其对应的解决方法或者缓解的措施,我们在接下来的文章中会给给出一些解决方案来实现如何在SOA应用中提升性能,扩展性,还有可用性的问题。
在SOA应用中,提升性能的一个常用的策略就是使用负载均衡,这一点我们在后面详细讲述。如果负载均衡使用的合适,它确实会在很大的程度上面提升服务的可用性。
很大的技术朋友感到性能,可用性,扩展性很容易通过使用很大的硬件设备来达到。其实很多的情况下,事情都不是这么容易的,尤其是当新的技术和开发的方法引入的时候。
在SOA应用中解决上面提到的那些问题的时候,我们不是一头雾水,而是有很多的现存方法和经验可以借鉴。所以,我们现在面临的挑战不是说“找不到方法”,而是“如何更好的,恰当的使用这些经验和方法”。
因为我们在后面会讲述一些常用的模式,其实我们知道:模式就是对一类问题的解决方法,但是很多的人会把模式滥用,误用。
言归正传,下面我们就进入详细的讲述。
提出问题
为了使得大家更加清楚的知道这个模式的使用场景,我这里还是通过一个示例来进行说明。现在假设我们有一个在线的音乐网站。我们这个音乐网站购买一些唱片或者下载一些歌曲。这个音乐网站运行了一个后台的服务,专门用来批量的购买唱片的订单。另外,这个站点还运行了一个实时单向的处理订单的服务:就是用户下了订单之后,然后立刻就处理。
大家肯定会想:为什么要搞两个这样的服务呢?
1. 这是一个假定的场景,为了说明一些问题。
2. 在现实中,有时候确实存在这样的情况:如果实时的订单处理是在忙不过来,那么就把订单保存起来,在后台运行服务进行处理。
在图中,左边的请求量就反应了在一般的正常的情况下,音乐站点每天的业务情况。此时,一般是由服务1还处理用户提交的订单。如果站点业务是在太忙,一部分的订单处理就会分摊到服务2。
在图中,右边的请求量就反应了一个比较极端的情况:某一天某段时间内,大量的用户在站点购买唱片,此时导致服务1要处理的清单极多,而此时,这些订单的相关还没有及时的保存到数据库中,导致服务2无法为服务1分担任务。
所以,我们的问题就是: 如何设计服务,使得其花费的成本最小(经济方面),同时又使得服务可以同时处理一般情况和一些比较极端的高负载情况的请求,而且还不会挂掉 。
当然,我们为了可能想砸钱购买一些很强大,很多的设备来应付,但是如果这些高负载情况出现的不频繁,那么大部分时候,这些设备可能就是闲置了,那钱就花得有点冤枉。现在,另外一个比较不错的选择就是使用一些“云”,在高负载的情况下,我们付钱购买更好的设备和相关的支持,一旦过了高峰期,我们就不使用这些额外的设备和支持,也就是 按需花钱 。
如果我们不用“云”,我们如何设计呢?
解决方案
采用Decoupled Invocation模式,将请求与回复进行分离:服务的边界收到请求的数据包,然后将其放在一个可靠的队列中,然后从队列中读取数据包,采用负载均衡将这些请求按照优先级分发到不同的处理组件上面。
我们看到下面的这个图,在图中我们可以知道:Decoupled Invocation模式的实现需要三个基本的组件:一个处理者,一个队列,还有一个分发器。
看到下图:
下面我们就介绍一下这个模式的实现是如何工作的:
1. 首先 处理者 负责监听发送到服务的请求。
2. 新的请求到达之后, 处理者 发送一个应答的响应给发送者(如客户端),告诉发送者,我们已经收到了。
3. 处理者 的主要的职责就是初始化请求处理,或者进行消息的转换等。不管怎么样,这个初始化的处理的动作要尽可能的小,目的就是为了使得处理过程更快,更快的处理下一个请求和发送应答响应。
4. 处理者 把请求消息发送到队列中。队列是这个模式需要实现的第二个组件,用来保存接受到的消息。
队列的实现方式有很多,我们在实现队列的过程中,把保存的请求消息持久化,以免出现丢失数据的问题。如果队列支持事务相关的功能,那么我们可以在借用之后要讲述的Transaction Service模式来配合,从而增强可用性与健壮性。
分发器是第三个需要实现的组件。它主要是用来创建多个消息读取器的,然后不同的读取器将消息发到了对应的业务组件。分发器可以把接收到的消息按照一定的优先级来划分(优先级的定制可以按照业务或者相关的资源来考虑)。分发器在整个Decoupled Invocation模式实现中其中至关重要的位置,因为它可以延时对请求的处理。
这里有一点需要注意的就是有关服务发送应答消息给请求发送方 。
我们刚刚说过,服务端这边接收到了请求之后,其实还没有处理,而是保持请求消息,以便后面去处理。所以此时服务这边只是发送了一个很简单的响应应答消息给发送方,告诉它们消息已经收到了。但是很多的时候,事情不是这么简单。例如,电子商务站点中把订单请求消息发送之后,我们希望得到一个订单的编号和一些处理的信息,而不是简单的Yes或No。
所以,在使用这个Decoupled Invocation模式的时候,一定要把不同的消息区分开来,不能把所有的消息都按照这个方式来实现。特别是那些涉及到业务处理才能回应的请求要特别的处理:需要不同的边界来发送响应。这可能不太好理解,我这里还是解释一下。看到下面的图:
在上图中:
1. 如果请求的消息只是一般的消息,可以延迟的处理,并且发送方只要知道消息是否被接受了,不需要额外的信息,此时我们可以让处于第一个边界的处理者直接处理(把请求保存,然后发送响应)。
2. 如果请求的消息的发送需要更加详细的应答响应信息,那么处理者就不能草率的发送一个简单的响应了,而是要把消息进一步的传递到更深的层次,发送业务组件,然后业务组件处理之后,发送响应给客户端。
到这里,朋友们就会发现: 如果是第二类的消息,这个模式似乎就没有作用了:因为消息还是要实时的处理了。不否认,存在这个问题,或者说,我们这里讲述的这个模式可以很好的处理第一类消息,对于第二类消息的处理,也不是没有作用的 。
我们可以把消息再次的分析, 分为浅层处理的消息,深层处理的消息 。所谓的浅层处理的消息,这主要是根据请求方的期望来分的。如果我们把一个消息完完全全的处理看出是100%的话,或者说,请求发期望的响应只有把服务方把整个消息彻底的处理完之后才能得到,例如,订单的处理,如果请求发要知道处理的完全的详细信息,例如编号,配送日期,配送厂家,发货员等等。
其实这样的情况在现实中有,但是不多见,就拿我们现在的几家大型的电子商务站点而言,我们提交了订单之后,只要知道订单的编号,是否开始配送就行了,我们不是希望立刻马上得到100%的全部信息。所以,现实中的订单处理的请求消息都是浅层处理消息,我们可以定期的去查看状态。
所以,一般而言,我们还是可以使用这个模式来处理消息的。如果消息需要进一步的处理才能返回响应,我们可以在分发器这里把这些消息放在一个高优先级的对象中,尽可能快速的处理。
也不是说,我们非要在消息处理的时候要用这个模式才能实现高性能。这个模式最好的使用场景就是在 请求-回调 (这一点要和我们常常看到的请求-应答区分)。
所谓的请求-回调就是:发送请求消息之后,不需要立刻得到完全的响应,可以在之后由服务器回调客户端,发送消息。很多的时候,我们可以采用异步的方式来实现。客户端发送方不用等处理完之后才返回。而我们常见的“请求-应答”方式,就是客户端发送方把消息发送之后,就等着服务端处理完成之后,才进行后续的操作。
技术实现
下面,我们就看看如何使用现有的技术实现这个模式。首先,要实现这个模式,我们最好是采用成熟的解决方案和组件。很多的企业级的消息组件都可以用来帮助我们。例如,微软的MSMQ,Java中的WebSphere MQ, Progress SonicMQ,还有Apache ActiveMQ,另外还有基于AMQP的队列RabbitMQ。
另外,在实现的时候,需要考虑是否把请求消息持久化(很多的队列都可以配置是否持久化消息),如果不持久化,也就是把消息放在内存中,那么业务逻辑组件和前面的边界组件都要在一个进程中。如果持久化了,那么使用消息的各个组件就可以运行在不同的进程中,从而达到高性能。在.NET中,我们借助WCF采用上述的思想,可以实现高性能的消息处理的。代码示例这里就不举例了,掌握了上面的思想和介绍的消息组件,应该是没有问题的。
备注:本文已经投稿IT168并且出版,转载请标注出处!