分布式、集群概念汇总(三)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_36632174/article/details/102587916

大型分布式架构特点和注意

一、大型网站的特点

用户多,分布广泛

大流量,高并发
海量数据,服务高可用
安全环境恶劣,易受网络攻击
功能多,变更快,频繁发布
从小到大,渐进发展
以用户为中心
免费服务,付费体验

二、大型网站架构目标

高性能:提供快速的访问体验。(包括轮询巨大数据量等,最好控制在5秒之内)
高可用:网站服务一直可以正常访问。(做故障转移)
可伸缩:通过硬件增加/减少,提高/降低处理能力。
安全性:提供网站安全访问和数据加密,安全存储等策略。
扩展性:方便的通过新增/移除方式,增加/减少新的功能/模块。
敏捷性:随需应变,快速响应。

三、大型网站架构模式

分层:一般可分为,应用层,服务层,数据层,管理层,分析层;
分割:一般按照业务/模块/功能特点进行划分,比如应用层分为首页,用户中心。
分布式:将应用分开部署(比如多台物理机或多台云服务器),通过远程调用协同工作。
集群:一个应用/模块/功能部署多份(如:多台物理机或云服务器),通过负载均衡共同提供对外访问。
缓存:将数据放在距离应用或用户最近的位置,加快访问速度。
冗余:增加副本,提高可用性,安全性,性能。
安全:对已知问题有有效的解决方案,对未知/潜在问题建立发现和防御机制。
自动化:将重复的,不需要人工参与的事情,通过工具的方式,使用机器完成。
敏捷性:积极接受需求变更,快速响应业务发展需求。

四、高性能架构

以用户为中心,提供快速的网页访问体验。主要参数:    
①较短的响应时间    
②较大的并发处理能力    
③较高的吞吐量。    
④稳定的性能参数。    
系统的优化分为:    
a.前端优化:网站业务逻辑之前部分,浏览器的优化:减少HTTP请求数,使用浏览器缓存,启用压缩,css,js位置,js异步    
减少Cookie传输;CDN加速☆☆☆☆☆,反向代理;    
CDN(Content Delivery Network):即内容分发网络。其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性    
的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚    
拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求    
重新导向离用户最近的服务节点上。其目的是使用户可就近取得所需内容,解决Internet网络拥挤的状况,提高用户访问网站    
的响应速度。    
b.应用层优化:处理网站业务的服务器。使用缓存,异步,集群。    
c.代码层优化:合理的架构,多线程,资源复用(对象池,线程池等),良好的数据结构,JVM调优,单例,Cache等;    
d.存储层优化:缓存,固态硬盘,光纤传输,优化读写,磁盘冗余,分布式存储(HDFS),NOSQL等。

五、高可用架构

大型网站应该在任何时候都可以正常访问。正常提供对外服务。因为大型网站的复杂性,分布式,廉价服务器,开源数据库,    
操作系统等特点。要保证高可用是很困难的,也就是说网站的故障是不可避免的。    
如何提高可用性,就是需要迫切解决的问题。首先,需要从架构级别,在规划的时候,就考虑可用性。行业内一般用几个9    
表示可用性指标。比如四个9(99.99),一年内允许的不可用时间是53分钟。    
不同层级使用的策略不同,一般采用冗余备份和失效转移解决高可用问题。    
应用层:一般设计为无状态的,对于每次请求,使用哪一台服务器处理是没有影响的。一般使用负载均衡技术    
(需要解决Session同步问题),实现高可用。    
服务层:负载均衡,分级管理,快速失败(超时设置),异步调用,服务降级,幂等设计等。    
数据层:冗余备份(冷,热备[同步,异步],温备),失效转移(确认,转移,恢复)。数据高可用方面著名的理论基础是    
CAP理论(持久性,可用性,数据一致性[强一致,用户一致,最终一致])

六、可伸缩架构

只不改变原有架构基础上,通过添加/减少硬件(服务器)的方式,提高/降低服务器处理能力。
应用层:对应用进行垂直或水平切分。然后针对单一功能进行负载均衡(DNS,HTTP[反向代理],IP,链路层)。
服务层:与应用层类似;
数据层:分库,分表,NOSQL等;常用算法Hash,一致性Hash。

七、可扩展架构

可以方便的进行功能模块的新增/移除,提供代码/模块级别良好的可扩展性。
模块化,组件化:高内聚,内耦合,提高复用性,扩展性。
稳定接口:定义稳定的接口,在接口不变的情况下,内部结构可以“随意”变化。
设计模式:应用面向对象思想,原则,使用设计模式,进行代码层面的设计。
消息队列:模块化的系统,通过消息队列进行交互,使模块之间的依赖解耦。
分布式服务:公用模块服务化,提供其他系统使用,提高可重用性,扩展性。

八、安全架构

对已知问题有有效的解决方案,对未知/潜在问题建立发现和防御机制。对于安全问题,首先要提高安全意识,建立一个安全的有
效机制,从政策层面,组织层面进行保障。比如:
a.服务器密码不能泄露、密码每月更新、并且三次内不能重复;
b.每周安全扫描;
c.登陆密码加密处理;
d.加入登陆拦截器,如果不登录其它URL不让访问(登陆、注册、找回密码除外)
e.logout情况下需要重新登陆
f.同一浏览器登陆一次默认不需要再登陆,一个账号PC端最多登陆一次,再登陆的账号会把第一次登陆的账号挤掉。APP端和PC端
可以同时登陆。
以制度化的方式,加强安全体系的建设。同时,需要注意与安全有关的各个环节。安全问题不容忽视。包括基础设施安全,应用
系统安全,数据保密安全等。
基础设施安全:
硬件采购,操作系统,网络环境方面的安全。一般采用,正规渠道购买高质量的产品,选择安全的操作系统,及时修补漏洞,安
装杀毒软件防火墙。防范病毒,后门。设置防火墙策略,建立DDOS防御系统,使用攻击检测系统,进行子网隔离等手段。
应用系统安全:
在程序开发时,对已知常用问题,使用正确的方式,在代码层面解决掉。防止跨站脚本攻击(XSS),注入攻击,跨站请求
伪造(CSRF),错误信息,HTML注释,文件上传,路径遍历等。还可以使用Web应用防火墙(比如:ModSecurity),进行安全漏
洞扫描等措施,加强应用级别的安全。
数据保密安全:
存储安全(存在在可靠的设备,实时,定时备份),保存安全(重要的信息加密保存,选择合适的人员复杂保存和检测等),
传输安全(防止数据窃取和数据篡改);
常用的加解密算法:
(单项散列加密[MD5,SHA],对称加密[DES,3DES,RC]),非对称加密[RSA]等。
AES加密算法参照:
https://blog.csdn.net/qq_28205153/article/details/55798628

九、敏捷性

网站的架构设计,运维管理要适应变化,提供高伸缩性,高扩展性。方便的应对快速的业务发展,突增高流量访问等要求。除上面介绍的架构要素外,还需要引入敏捷管理,敏捷开发的思想。使业务,产品,技术,运维统一起来,随需应变,快速响应。

十、大型架构样例

DNS:(Domain Name System,域名系统)    
因特网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取    
的IP数串。    
DNS解析是互联网绝大多数应用的实际寻址方式; 域名技术的再发展、以及基于域名技术的多种应用,丰富了互联网应用和协议。    
域名是互联网上的身份标识,是不可重复的唯一标识资源; 互联网的全球化使得域名成为标识一国主权的国家战略资源。    
以上框架样例分为七层:    
1.客户层:PC浏览器、移动端。支持PC浏览器和手机APP。差别是手机APP可以直接访问通过IP访问,反向代理服务器。    
2.前端优化层:使用DNS负载均衡,CDN本地加速以及反向代理服务;    
反向代理:前端后端的中转站例如nginx    
3.应用层:    
网站应用集群;按照业务进行垂直拆分,比如商品应用,会员中心等;(实现具体业务)    
4.服务层:    
提供公用服务,比如用户服务,订单服务,支付服务等;    
5.数据层:支持关系型数据库集群(支持读写分离),NOSQL集群,分布式文件系统集群;以及分布式Cache(缓存);    
6.大数据存储层:支持应用层和服务层的日志数据收集,关系数据库和NOSQL数据库的结构化和半结构化数据收集;    
7.大数据处理层:通过Mapreduce进行离线数据分析或Storm/Flink实时数据分析,并将处理后的数据存入关系型数据库。    
(实际使用中,离线数据和实时数据会按照业务要求进行分类处理,并存入不同的数据库中,供应用层或服务层使用)。

基于消息的分布式架构

一、集成模式中的消息模式

其实企业级的系统或平台就是对数据的处理和展现,数据为王,数据是软件、系统处理的原材料。而对于一个拥有多个子系统的企业应用系统而言,它的基础支撑无疑就是对消息的处理。与对象不同,消息本质上是一种数据结构(当然,对象也可以看做是一种特殊的消息),它包含消费者与服务双方都能识别的数据,这些数据需要在不同的进程(机器)之间进行传递,并可能会被多个完全不同的客户端消费。在众多分布式技术中,消息传递相较文件传递与远程过程调用(RPC)而言,似乎更胜一筹,因为它具有更好的平台无关性,并能够很好地支持并发与异步调用。对于Web Service与RESTful而言,则可以看做是消息传递技术的一种衍生或封装。在《面向模式的软件架构(卷四)》一书中,将关于消息传递的模式划归为分布式基础设施的范畴,这是因为诸多消息中间件产品的出现,使得原来需要开发人员自己实现的功能,已经可以直接重用。这极大地降低了包括设计成本、实现成本在内的开发成本。因此,对于架构师的要求也就从原来的设计实现,转变为对业务场景和功能需求的判断,从而能够正确地进行架构决策、技术选型与模式运用。基于消息的分布式架构是非常常用的

常用消息模式

1.消息通道(Message Channel)模式

消息通道作为在客户端(消费者,Consumer)与服务(生产者,Producer)之间引入的间接层,可以有效地解除二者之间的耦合。只要实现规定双方需要通信的消息格式,以及处理消息的机制与时机,就可以做到消费者对生产者的“无知”。(不用在意生产者)该模式可以支持多个生产者与消费者。例如,我们可以让多个生产者向消息通道发送消息,因为消费者对生产者的无知性,它不必考虑究竟是哪个生产者发来的消息。虽然消息通道解除了生产者与消费者之间的耦合,使得我们可以任意地对生产者与消费者进行扩展,但它又同时引入了各自对消息通道的依赖,因为它们必须知道通道资源的位置。要解除这种对通道的依赖,可以考虑引入Lookup服务来查找该通道资源。例如,在JMS中就可以通过JNDI来获取消息通道Queue。若要做到充分的灵活性,可以将与通道相关的信息存储到配置文件中,Lookup服务首先通过读取配置文件来获得通道。消息通道通常以队列的形式存在,这种先进先出的数据结构无疑最为适合这种处理消息的场景。微软的MSMQ、IBM MQ、JBoss MQ以及开源的RabbitMQ、Apache ActiveMQ都通过队列实现了Message Channel模式。因此,在选择运用Message Channel模式时,更多地是要从质量属性的层面对各种实现了该模式的产品进行全方位的分析与权衡。例如:

消息通道对1.并发的支持2.在性能上的表现3.消息通道是否充分地考虑了错误处理4.对消息安全的支持5.关于消息持久化6.灾备(fail over)7.集群等方面的支持。

因为通道传递的消息往往是一些重要的业务数据,一旦通道成为故障点或安全性的突破点,对系统就会造成灾难性的影响。在下面的应用场景中会给出架构上的正确决策。

2.发布者-订阅者(Publisher-Subscriber)模式

一旦消息通道需要支持多个消费者时,就可能面临两种模型的选择:拉模型与推模型。拉模型是由消息的消费者发起的,主动权把握在消费者手中,它会根据自己的情况对生产者发起调用。如图所示:

拉模型的另一种体现则由生产者在状态发生变更时,通知消费者其状态发生了改变。但得到通知的消费者却会以回调方式,通过调用传递过来的消费者对象获取更多细节消息。在基于消息的分布式系统中,拉模型的消费者通常以Batch Job的形式,根据事先设定的时间间隔,定期侦听通道的情况。一旦发现有消息传递进来,就会转而将消息传递给真正的处理器(也可以看做是消费者)处理消息,执行相关的业务。在本文第二部分介绍的医疗卫生系统,正是通过引入Quartz.NET实现了Batch Job,完成对消息通道中消息的处理。推模型的主动权掌握在生产者的手中消费者被动地等待生产者发出的通知,这就要求生产者必须了解消费者的相关信息。对于推模型而言,消费者无需了解生产者。在生产者通知消费者时,传递的往往是消息(或事件),而非生产者自身。同时,生产者还可以根据不同的情况,注册不同的消费者,又或者在封装的通知逻辑中,根据不同的状态变化,通知不同的消费者。

拉模型推模型总结:

两种模型各有优势。拉模型的好处在于可以进一步解除消费者对通道的依赖,通过后台任务去定期访问消息通道。坏处是需要引入一个单独的服务进程,以Schedule形式执行。而对于推模型而言,消息通道事实上会作为消费者观察的主体,一旦发现消息进入,就会通知消费者执行对消息的处理。无论推模型,拉模型,对于消息对象而言,都可能采用类似Observer模式的机制,实现消费者对生产者的订阅,因此这种机制通常又被称为Publisher-Subscriber模式。

3.Publisher-Subscriber模式

通常情况下,发布者和订阅者都会被注册到用于传播变更的基础设施(即消息通道)上。发布者会主动地了解消息通道,使其能够将消息发送到通道中;消息通道一旦接收到消息,会主动地调用注册在通道中的订阅者,进而完成对消息内容的消费。对于订阅者而言,有两种处理消息的方式。一种是广播机制,这时消息通道中的消息在出列的同时,还需要复制消息对象,将消息传递给多个订阅者。例如,有多个子系统都需要获取从CRM系统传来的客户信息,并根据传递过来的客户信息,进行相应的处理。此时的消息通道又被称为Propagation通道。另一种方式则属于抢占机制,它遵循同步方式,在同一时间只能有一个订阅者能够处理该消息。实现Publisher-Subscriber模式的消息通道会选择当前空闲的唯一订阅者,并将消息出列,并传递给订阅者的消息处理方法。有许多消息中间件都能够很好地支持Publisher-Subscriber模式,例如JMS接口规约中对于Topic对象提供的MessagePublisher与MessageSubscriber接口。RabbitMQ也提供了自己对该模式的实现。微软的MSMQ虽然引入了事件机制,可以在队列收到消息时触发事件,通知订阅者。但它并非严格意义上的Publisher-Subscriber模式实现。由微软MVP Udi Dahan作为主要贡献者的NServiceBus,则对MSMQ以及WCF做了进一层包装,并能够很好地实现这一模式。

4.消息路由(Message Router)模式

无论是Message Channel模式,还是Publisher-Subscriber模式,队列在其中都扮演了举足轻重的角色。然而,在企业应用系统中,当系统变得越来越复杂时,对性能的要求也会越来越高,此时对于系统而言,可能就需要支持同时部署多个队列,并可能要求分布式部署不同的队列。这些队列可以根据定义接收不同的消息,例如订单处理的消息,日志信息,查询任务消息等。这时,对于消息的生产者和消费者而言,并不适宜承担决定消息传递路径的职责。事实上,根据S单一职责原则,这种职责分配也是不合理的,它既不利于业务逻辑的重用,也会造成生产者、消费者与消息队列之间的耦合,从而影响系统的扩展。既然这三种对象(组件)都不宜承担这样的职责,就有必要引入一个新的对象专门负责传递路径选择的功能,这就是所谓的Message Router模式,如上图,通过消息路由,我们可以配置路由规则指定消息传递的路径,以及指定具体的消费者消费对应的生产者。例如指定路由的关键字,并由它来绑定具体的队列与指定的生产者(或消费者)。路由的支持提供了消息传递与处理的灵活性,也有利于提高整个系统的消息处理能力。同时,路由对象有效地封装了寻找与匹配消息路径的逻辑,就好似一个调停者(Meditator),负责协调消息、队列与路径寻址之间关系。    
除了以上的模式之外,Messaging模式提供了一个通信基础架构,使得我们可以将独立开发服务整合到一个完整的系统中。Message Translator模式则完成对消息的解析,使得不同的消息通道能够接收和识别不同格式的消息。而且通过引入这样的对象,也能够很好地避免出现盘根错节,彼此依赖的多个服务。Message Bus模式可以为企业提供一个面向服务的体系架构。它可以完成对消息的传递,对服务的适配与协调管理,并要求这些服务以统一的方式完成协作。

二、消息模式的应用场景

基于消息的分布式架构总是围绕着消息来做文章。例如可以将消息封装为对象,或者指定消息的规范例如SOAP,或者对实体对象的序列化与反序列化。这些方式的目的只有一个,就是将消息设计为生产者和消费者都能够明白的格式,并能通过消息通道进行传递。

场景一:基于消息的统一服务架构

在制造工业的CIMS系统中,我们尝试将各种业务以服务的形式公开给客户端的调用者,例如定义这样的接口:

public interface IService {
    IMessage Execute(IMessage aMessage);
    void SendRequest(IMessage aMessage);
}

之所以能够设计这样的服务,原因在于我们对业务信息进行了高度的抽象,以消息的形式在服务之间传递。此时的消息其实是生产者与消费者之间的契约或接口,只要遵循这样的契约,按照规定的格式对消息进行转换与抽取,就能很好地支持系统的分布式处理。在这个CIMS系统中,我们将消息划分为ID,Name和Body,通过定义如下的接口方法,可以获得消息主体的相关属性:

public interface IMessage:ICloneable
{
     string MessageID { get; set; }
     string MessageName() { get; set; }
     IMessageItemSequence CreateMessageBody();
     IMessageItemSequence GetMessageBody();
}

消息主体类Message实现了IMessage接口。在该类中,消息体Body为IMessageItemSequence类型。这个类型用于获取和设置消息的内容:Value和Item:

public interface IItemValueSetting {
     string getSubValue(string name);
     void setSubValue(string name, string value);  
}
public interface IMessageItemSequence:IItemValueSetting,ICloneable
{      
     IMessageItem GetMessageItem(string aName);
     IMessageItem CreateMessageItem(string aName);       
}

Value为字符串类型,它利用了HashTable存储Key和Value的键值对。Item则为IMessageItem类型,在IMessageItemSequence的实现类中,同样利用了HashTable存储Key和Item的键值对。IMessageItem支持消息体的嵌套。它包含了两部分:SubValue和SubItem。实现的方式和IMessageItemSequence相似。通过定义这样的嵌套结构,使得消息的扩展成为可能。一般的消息结构如下所示:

IMessage——Name                
                     ——ID                
                     ——Body(IMessageItemSequence)                
                            ——Value                
                            ——Item(IMessageItem)                
                                   ——SubValue                
                                   ——SubItem(IMessageItem)                
                                          ——……   

在实现服务进程通信之前,我们必须定义好各个服务或各个业务的消息格式。通过消息体的方法在服务的一端设置消息的值,然后发送,并在服务的另一端获得这些值。例如发送消息端定义如下的消息体:

IMessageFactory factory = new MessageFactory();
IMessage message = factory.CreateMessage();
message.SetMessageName("service1");


IMessageItemSequence body = message.CreateMessageBody();
body.SetSubValue("subname1","subvalue1");
body.SetSubValue("subname2","subvalue2");

//Send Request Message
MyServiceClient service = new MyServiceClient("Client");
service.SendRequest(message);

在客户端引入了一个ServiceLocator对象,它通过MessageQueueListener对消息队列进行侦听,一旦接收到消息,就获取该消息中的name去定位它所对应的服务,然后调用服务的Execute(aMessage)方法,执行相关的业务。
ServiceLocator承担的定位职责其实是对存储在ServiceContainer容器中的服务进行查询。ServiceContainer容器可以读取配置文件,在启动服务的时候初始化所有的分布式服务(注意,这些服务都是无状态的),并对这些服务进行管理。它封装了服务的基本信息,诸如服务所在的位置,服务的部署方式等,从而避免服务的调用者直接依赖于服务的细节,既减轻了调用者的负担,还能够较好地实现服务的扩展与迁移。

在这个系统中,我们主要引入了Messaging模式,通过定义的IMessage接口,使得我们更好地对服务进行抽象,并以一种扁平的格式存储数据信息,从而解除服务之间的耦合。只要各个服务就共用的消息格式达成一致,请求者就可以不依赖于接收者的具体接口。通过引入的Message对象,我们就可以建立一种在行业中通用的消息模型与分布式服务模型。事实上,基于这样的一个框架与平台,在对制造行业的业务进行开发时,开发人员最主要的活动是与领域专家就各种业务的消息格式进行讨论,这样一种面向领域的消息语言,很好地扫清了技术人员与业务人员的沟通障碍;同时在各个子系统之间,我们也只需要维护服务间相互传递的消息接口表。每个服务的实现都是完全隔离的,有效地做到了对业务知识与基础设施的合理封装与隔离。

对于消息的格式和内容,我们考虑引入了Message Translator模式,负责对前面定义的消息结构进行翻译和解析。为了进一步减轻开发人员的负担,我们还可以基于该平台搭建一个消息-对象-关系的映射框架,引入实体引擎(Entity Engine)将消息转换为领域实体,使得服务的开发者能够以完全面向对象的思想开发各个服务组件,并通过调用持久层实现消息数据的持久化。同时,利用消息总线(此时的消息总线可以看做是各个服务组件的连接器)连接不同的服务,并允许异步地传递消息,对消息进行编码。这样一个基于消息的分布式,架构如图:

场景二:消息中间件的架构决策

一个医疗卫生系统中,我们面临了客户对系统性能/可用性的非功能需求。在我们最初启动该项目时,客户就表达了对性能与可用性的特别关注。客户希望最终用户在进行复杂的替换删除操作时,能够具有很好的用户体验,简言之,就是希望能够快速地得到操作的响应。问题在于这样的替换删除操作需要处理比较复杂的业务逻辑,同时牵涉到的关联数据量非常大,整个操作若需完成,最坏情况下可能需要几分钟的时间。我们可以通过引入缓存、索引、分页等多种方式对数据库操作进行性能调优,但整个操作的耗时始终无法达到客户的要求。由于该系统是在一个遗留系统的基础上开发,如果要引入Map-Reduce来处理这些操作,以满足质量需求,则对架构的影响太大,且不能很好地重用之前系统的某些组件。显然,付出的成本与收益并不成正比。通过对需求进行分析,我们注意到最终客户并不需要实时获得结果,只要能够保证最终结果的一致性和完整性即可。关键在于就用户体验而言,他们不希望经历漫长的等待,然后再通知他们操作究竟是成功还是失败。这是一个典型需要通过后台任务进行异步处
理的场景。
在企业应用系统中,我们常常会遭遇这样的场景。我们曾经在一个金融系统中尝试通过自己编写任务的方式来控制后台线程的并发访问,并完成对任务的调度。事实证明,这样的设计并非行之有效。对于这种典型的异步处理来说,基于消息传递的架构模式才是解决这一问题的最佳办法。因为消息中间件的逐步成熟,对于这一问题的架构设计,已经由原来对设计实现的关注转为如何进行产品选型和技术决策。例如,在.NET平台下,架构师需要重点考虑的是应该选择哪种消息中间件来处理此等问题?这就需要我们必须结合具体的业务场景,来识别这种异步处理方式的风险,然后再根据这些风险去比较各种技术,以求寻找到最适合的方案。通过分析业务场景以及客户性质,我们发现该业务场景具有如下特征:

a.在一些特定情形下,可能会集中发生批量的替换删除操作,使得操作的并发量达到高峰;例如FDA要求召回一些违规药品时,就需
要删除药品库中该药品的信息;
b.操作结果不要求实时性,但需要保证操作的可靠性,不能因为异常失败而导致某些操作无法进行;
c.自动操作的过程是不可逆转的,因此需要记录操作历史;
d.基于性能考虑,大多数操作需要调用数据库的存储过程;
e.操作的数据需要具备一定的安全性,避免被非法用户对数据造成破坏;
f.与操作相关的功能以组件形式封装,保证组件的可重用性、可扩展性与可测试性;
g.数据量可能随着最终用户的增多而逐渐增大;

针对如上的业务需求,我们决定从以下几个方面对各种技术方案进行横向的比较与考量:

并发:选择的消息队列一定要很好地支持用户访问的并发性;
安全:消息队列是否提供了足够的安全机制;
性能伸缩:不能让消息队列成为整个系统的单一性能瓶颈;
部署:尽可能让消息队列的部署更为容易;
灾备:不能因为意外的错误、故障或其他因素导致处理数据的丢失;
API易用性:处理消息的API必须足够简单、并能够很好地支持测试与扩展;

先后考察了MSMQ、Resque、ActiveMQ和RabbitMQ,通过查询相关资料,以及编写Spike代码验证相关质量,我们最终选择了RabbitMQ。选择放弃MSMQ,是因为它严重依赖Windows操作系统;它虽然提供了易用的GUI方便管理人员对其进行安装和部署,但若要编写自动化部署脚本,却非常困难。同时,MSMQ的队列容量不能查过4M字节,这也是我们无法接收的。Resque的问题是目前仅支持Ruby的客户端调用,不能很好地与.NET平台集成。此外,Resque对消息持久化的处理方式是写入到Redis中,因而需要在已有RDBMS的前提下,引入新的Storage。我们比较倾心于ActiveMQ与RabbitMQ,但通过编写测试代码,采用循环发送大数据消息以验证消息中间件的性能与稳定性时,我们发现ActiveMQ的表现并不太让人满意。至少,在我们的询证调研过程中,ActiveMQ会因为频繁发送大数据消息而偶尔出现崩溃的情况。相对而言,RabbitMQ在各个方面都比较适合我们的架构要求。

在灾备与稳定性方面,RabbitMQ提供了可持久化的队列,能够在队列服务崩溃的时候,将未处理的消息持久化到磁盘上。为了避免因为发送消息到写入消息之间的延迟导致信息丢失,RabbitMQ引入了Publisher Confirm机制以确保消息被真正地写入到磁盘中。它对Cluster的支持提供了Active/Passive与Active/Active两种模式。例如,在Active/Passive模式下,一旦一个节点失败,Passive节点就会马上被激活,并迅速替代失败的Active节点,承担起消息传递的职责。

在并发处理方面,RabbitMQ本身是基于erlang编写的消息中间件,作为一门面向并发处理的编程语言,erlang对并发处理的天生优势使得我们对RabbitMQ的并发特性抱有信心。RabbitMQ可以非常容易地部署到Windows、Linux等操作系统下,同时,它也可以很好地部署到服务器集群中。它的队列容量是没有限制的(取决于安装RabbitMQ的磁盘容量),发送与接收信息的性能表现也非常好。RabbitMQ提供了Java、.NET、Erlang以及C语言的客户端API调用非常简单,并且不会给整个系统引入太多第三方库的依赖。例如.NET客户端只需要依赖一个程序集。选择了RabbitMQ,但仍有必要对系统与具体的消息中间件进行解耦,这就要求我们对消息的生产者与消费者进行抽象,例如定义如下的接口:

public interface IQueueSubscriber        
    {        
        void ListenTo<T>(string queueName, Action<T> action);        
        void ListenTo<T>(string queueName, Predicate<T> messageProcessedSuccessfully);        
        void ListenTo<T>(string queueName, Predicate<T> messageProcessedSuccessfully, bool requeueFailedMessages);        
    }        
public interface IQueueProvider        
    {        
        T Pop<T>(string queueName);        
        T PopAndAwaitAcknowledgement<T>(string queueName, Predicate<T> messageProcessedSuccessfully);        
        T PopAndAwaitAcknowledgement<T>(string queueName, Predicate<T> messageProcessedSuccessfully, bool requeueFailedMessages);        
        void Push(FunctionalArea functionalArea, string routingKey, object payload);        
    }        
在这两个接口的实现类中,我们封装了RabbitMQ的调用类,例如:        
public class RabbitMQSubscriber : IQueueSubscriber        
    {        
        public void ListenTo<T>(string queueName, Action<T> action)        
        {        
            using (IConnection connection = _factory.OpenConnection())        
            using (IModel channel = connection.CreateModel())        
            {        
                var consumer = new QueueingBasicConsumer(channel);        
                string consumerTag = channel.BasicConsume(queueName, AcknowledgeImmediately, consumer);        
        
                var response = (BasicDeliverEventArgs) consumer.Queue.Dequeue();        
                var serializer = new JavaScriptSerializer();        
                string json = Encoding.UTF8.GetString(response.Body);        
                var message = serializer.Deserialize<T>(json);        
        
                action(message);        
            }        
        }               
    }        
    public class RabbitMQProvider : IQueueProvider        
    {        
        
        public T Pop<T>(string queueName)        
        {        
            var returnVal = default(T);        
            const bool acknowledgeImmediately = true;        
        
            using (var connection = _factory.OpenConnection())        
            using (var channel = connection.CreateModel())        
            {        
                var response = channel.BasicGet(queueName, acknowledgeImmediately);        
        
                if (response != null)        
                {        
                    var serializer = new JavaScriptSerializer();        
                    var json = Encoding.UTF8.GetString(response.Body);        
                    returnVal = serializer.Deserialize<T>(json);        
                }        
            }        
        
            return returnVal;        
        }    }        

我们用Quartz.Net来实现Batch Job。通过定义一个实现了IStatefulJob接口的Job类,在Execute()方法中完成对队列的侦听。Job中RabbitMQSubscriber类的ListenTo()方法会调用Queue的Dequeue()方法,当接收的消息到达队列时,Job会侦听到消息达到的事件,然后以同步的方式使得消息弹出队列,并将消息作为参数传递给Action委托。因此,在Batch Job的Execute()方法中,可以定义消息处理的方法,并调用RabbitMQSubscriber类的ListenTo()方法,如下所示(注意,这里传递的消息事实上是Job的Id):

public void Execute(JobExecutionContext context)            
        {            
            string queueName = queueConfigurer.GetQueueProviders().Queue.Name;            
            try            
            {            
                queueSubscriber.ListenTo<MyJob>(queueName, job => request.MakeRequest(job.Id.ToString()));            
            }            
            catch(Exception err)            
            {            
           Log.WarnFormat("Unexpected exception while processing queue '{0}', Details: {1}", queueName, err);            
            }        }           

队列的相关信息例如队列名都存储在配置文件中。Execute()方法调用了request对象的MakeRequest()方法,并将获得的消息(即JobId)传递给该方法。它会根据JobId到数据库中查询该Job对应的信息,并执行真正的业务处理。在对基于消息处理的架构进行决策时,除了前面提到的考虑因素外,还需要就许多设计细节进行多方位的判断与权衡。例如针对Job的执行以及队列的管理,就需要考虑如下因素:

对Queue中Job状态的监控与查询;    
对Job优先级的管理;    
能否取消或终止执行时间过长的Job;    
是否能够设定Job的执行时间;    
是否能够设定Poll的间隔时间;    
能否跨机器分布式的放入Job;    
对失败Job的处理;    
能否支持多个队列,命名队列;    
能否允许执行Job的工作进程对应特定的队列    
对Dead Message的支持。

三、选择基于消息机制架构的时机

大概需要满足如下几个条件:
对操作的实时性要求不高,而需要执行的任务极为耗时;存在企业内部的异构系统间的整合;服务器资源需要合理分配与利用;对于第一种情况,我们常常会选择消息队列来处理执行时间较长的任务。此时引入的消息队列就成了消息处理的缓冲区。消息队列引入的异步通信机制,使得发送方和接收方都不用等待对方返回成功消息,就可以继续执行下面的代码,从而提高了数据处理的能力。尤其是当访问量和数据流量较大的情况下,就可以结合消息队列与后台任务,通过避开高峰期对大数据进行处理,就可以有效降低数据库处理数据的负荷。前面提到的医疗卫生系统正是这样一种适用场景。

四、面临的困难

由于消息模式自身的特殊性,我们在运用消息模式建立基于消息的分布式架构时,常常会面临许多困难。

问题一:

首先是系统集成的问题。由于系统之间的通信靠消息进行传递,就必须保证消息的一致性,同时,还需要维护系统之间(主要是服务之间)接口的稳定性。一旦接口发生变化,就可能影响到该接口的所有调用者。即使服务通过接口进行了抽象,由于消息持有双方服务规定的业务数据,在一定程度上违背了封装的要义。换言之,生产与消费消息的双方都紧耦合于消息。消息的变化会直接影响到各个服务接口的实现类。然而,为了尽可能保证接口的抽象性,我们所要处理的消息都不是强类型的,这就使得我们在编译期间很难发现因为消息内容发生变更产生的错误。在我之前提到的汽车零售商管理系统就存在这样的问题。当时我负责的CRM模块需要同时与多个子系统进行通信,而每个子系统又是由不同的团队进行开发。团队之间因为沟通原因,常常未能及时地同步接口表。虽然各个子系统的单元测试和功能测试都已通过,但直到对CRM进行集成测试,才发现存在大量消息不匹配的集成问题,这些问题的起因都是因为消息的变更。
问题一解决方案:
解决的方案是引入充分的集成测试,甚至是回归测试,并需要及时运行这些测试,以快速地获得反馈。我们可以将集成测试作为提交代码的验证们,要求每次提交代码都必须运行集成测试与指定的回归测试 。这正是持续集成的体现。通过在本地构建与远程构建运行集成测试与回归测试,有效地保证本地版本与集成后的版本不会因为消息的改变使得功能遭受破坏。一旦遭受破坏,也能够及时获得反馈,发现问题,即刻解决这些问题,而不是等到项目后期集中进行集成测试。

问题二:

后台任务的非实时性带来的测试困难。由于后台任务是定期对消息队列中的消息进行处理,因而触发的时机是不可预测的 。对于这种情况,我们通常会同时运用两种方案,双管其下地解决问题。首先,我们会为系统引入一个同步实现功能的版本,并通过在配置文件中引入toggle的开关机制,随时可以在同步功能与异步功能之间进行切换。如果我们能够保证消息队列处理与后台任务执行的正确性,就可以设置为同步功能,这样就能快速而准确地对该任务所代表的功能进行测试,并及时收获反馈。同时,我们可以在持续集成服务器上建立一个专门的管道(pipeline),用以运行基于消息处理的异步版本。这个管道对应的任务可以通过手动执行,也可以对管道设置定时器,在指定时间执行(例如在凌晨两点执行一次,这样在第二天开始工作之前可以获得反馈)。我们需要为该管道准备特定的执行环境,并将后台任务的侦听与执行时间修改为可以接受的值。这样既能够及时了解功能是否正确,又能保证基于消息的系统是工作正常的。

问题三:

分布式系统还存在解析消息、网络传递的性能损耗。对于这些问题,需要架构师审慎地分析业务场景,正确地选择架构方案与架构模式。相比较本地系统而言,分布式系统的维护难度可能成倍递增。这既需要我们在进行架构决策与设计时,充分考虑系统架构的稳定性,同时还需要引入系统日志处理。更好的做法是为日志处理增加错误通知的功能,只要发生消息处理的错误信息,就通过邮件、短信等方式通知系统管理员,及时地处理错误。因为只有在发生错误的当时查询错误日志,才能够更好对问题进行定位。同时,还可以为系统引入Error Message Queue以及Dead Message Queue,以便于处理错误和异常情况。

问题四:

对于分布式系统而言,还需要考虑服务执行结果的一致性,尤其是当某个业务需要多个服务参与到一个会话中时,一旦某个服务发生故障,就可能导致应用出现状态不一致的情况,因为只有所有参与者都成功执行了任务,才能视为完全成功。这就牵涉到分布式事务的问题,此时任务的执行就变成了事务型的:即任务必须是原子的,结果状态必须保持一致。在任务处理过程中,状态修改是彼此隔离的,成功的状态修改在整个事务执行过程中是持久的。这就是事务的ACID(Atomic,Consistent,Isolated与Durable)属性。

五、总结

一种方案是引入分布式事务协调器,即DTC(Distributed Transaction Coordinator),将事务分为两段式甚至三段式提交,要求整个事务的所有参与者以投票形式决定事务是完全成功还是失败。另一种方案是降低对结果一致性的要求。根据eBay的最佳实践,考虑到分布式事务的成本,获得分布式资源即时的一致性是不必要的,也是不现实的。在Randy Shoup的文章《可伸缩性最佳实践:来自eBay的经验》中提到了Eric Brewer的CAP公理:分布式系统的三项重要指标——一致性(Consistency)、可用性(Availability)和 分区耐受性(Partition-tolerance)——在任意时刻,只有两项能同时成立。我们应该根据不同的应用场景,权衡这三个要素。在不必要保证即时的一致性前提下,我们可以考虑合理地划分服务,尽量将可能作用在同一个事务范围的业务操作部署在同一个进程中,以避免分布式部署。如果确实需要多个分布式服务之间保持执行结果的一致,可以考虑引入数据核对,异步恢复事件或集中决算等手段。

猜你喜欢

转载自blog.csdn.net/qq_36632174/article/details/102587916
今日推荐