架构设计:系统间通信(34)——被神化的ESB(上)
1、概述
从本篇文章开始,我们将花一到两篇的篇幅介绍ESB(企业服务总线)技术的基本概念,为读者们理清多个和ESB技术有关名词。我们还将在其中为读者阐述什么情况下应该使用ESB技术。接下来,为了加深读者对ESB技术的直观理解,我们将利用Apache Camel一起搭建一个ESB技术的服务实现,虽然这个示例不能把目前主流的ESB服务实现中所有功能模块都保罗进来,但至少可以让读者看到ESB技术核心服务完整的工作方式。
2、为什么需要ESB
2-1、ESB与SOA
2-1-1、SOA
SOA(Service-Oriented Architecture)中文全称“面向服务的架构”。放在当下的技术环境(2015/2016年),SOA并不是一个新的概念。但是在SOA刚流行起来的2000年初(SOA的概念最初由Gartner Group在1996年提出),这个架构模型算就是非常流行了。本小节笔者试图使用最平实的语言向各位读者介绍SOA概念中的几个核心内容。
首先SOA是一种架构模式思想,主要围绕多个“服务”如何进行集成以达到某种目的进行讨论。那么SOA中所定义服务是什么意思呢?在业务系统中被发布出来供用户使用,能够完成一个完整业务过程的功能,就是服务。
服务着眼于完整的业务
从以上对“服务”的定义可以看出,服务的定义对象是业务系统中的完整业务功能。例如电商系统中“确认订单”这个功能就是一个服务、计费系统中“当月费用结算”功能就是一个服务;但是CRM系统中,需要完成“工单生成”功能所进行的“用户登录”动作就不是服务的定义,因为使用者进行“用户登录”是为了完成“工单生成”功能的权限验证步骤,并不是为了“登录”而登录。
服务的粒度虽然相对粗放,但却可控,目标是重用
接着讨论,“用户登录”这个功能在某些情况下也可以满足“服务”的定义:当用户中心系统为其他所有业务系统统一提供的“登录”服务被公布出来时。服务粒度的拆分完全依据业务系统中业务过程进行定义和分析,所以服务的粒度都相对粗放。就像上一段文字中所举例的“费用结算”服务那样:可能完成“费用结算”功能,在计费系统内部需要完成 用户身份确认->上月费用查询->套餐清单查询->费用明细生成 这几个过程。但是这些每一个过程都不会有任何其它业务系统进行单独使用,也就是说是否单独公布这些处理过程对于其他业务系统来说没有任何意义。
如果在后续的业务变更/业务重编时,业务设计人员发现某一个业务系统需要单独使用计费系统的“上月费用查询”功能,这时就需要计费系统将这个功能作为一个独立的服务向第三方业务系统公布出来。这时,这个功能就满足了“服务”的定义。
集成的目的是形成一个新的服务
对企业内部(或者企业间)的业务服务进行集成,被集成的业务服务称之为原子服务,集成的目的是重用这些原子服务形成一个新的服务。这样保证了技术团队/业务团队能以最小的代价,最高的效率使用既有服务,但同时也对实现SOA架构思想的软件提出了更高的要求。
SOA需保证屏蔽细节
使用SOA架构思想构建多个业务系统的集成关系,需要保证每个业务系统屏蔽细节。这些细节包括技术细节和业务细节。从技术细节层面看,无论业务系统使用哪种开发语言、哪种对外传输协议、哪种消息格式都可以使用SOA进行集成,并且能够在SOA架构的实现软件上完成不同传输协议的转换和不同消息格式的转换;从业务细节层面看,SOA需要屏蔽业务系统的功能步骤细节。也就是说第三方系统只需要知道调用某一个服务就可以达到业务目的,至于提供服务的业务系统如何实现业务过程则无需关心。
SOA让各业务系统保持松散
SOA架构模型为多个业务系统进行松散集成提供了一个良好的思路:通过屏蔽各业务系统技术细节和业务细节,兼容各业务系统的不同传输协议和不同消息格式,可以让通过SOA进行业务集成的各个业务系统保持低耦合状态。这是因为所有协议和消息格式都处于开放状态,业务集成时各业务系统不需要单独进行额外的转换工作,甚至不需要为基于SOA的业务集成进行任何额外工作,也无须知道对方系统的存在。
如果您所在企业或者客户的业务系统还没有达到一个较高的复杂等级,则不建议立即使用SOA架构模型进行业务集成。因为目前SOA架构模型的各种实现本身就具有一定的复杂性。
2-1-2、ESB
ESB(Enterprise Service Bus)全名:企业服务总线,是SOA架构思想的一种实现思路。既然是一种思路,就有这一实现思路的具体考虑,以及需要解决问题的实际环境:
企业的信息化建设一般经历很长时间的发展,少则5、6年多则10几年。所以我们看到某大型企业的信息系统最可能的情况是:存在着多个业务系统,甚至各业务系统负责的功能职责还存在重叠。这些系统采用不同时代的编程语言、编程框架、通讯协议、消息格式和存储方案。
例如,计费系统可能采用C++ 进行编写,对外调用功能采用CORBA;年久的CRM系统采用Delphi进行编写,同样使用CORBA发布调用功能,并且最近两年该企业刚对CRM系统使用C#语言进行了一次升级,但是由于数据存储层的设计原因,并没有将老系统的所有数据割接到新系统,所以目前两套CRM系统都在使用;最新开发的财务联动系统,采用JAVA语言开发,并且不再采用应用程序窗口,改为使用浏览器进行页面展示和用户操作。这个财务联动系统多数对外的服务采用HTTP协议对外公布,还有一部分服务采用Thrift RPC对外公布……
由此可见,由于各种可见的和不可见的原因,企业信息化系统的建设历史和现实存在往往纷繁复杂。如果这些系统需要进行服务集成,但是又没有一个成熟稳定、兼容易用的中间层进行协调,那么要达到以上的调用要求基本上不可能的(即使实现也相当难以维护和扩展)。
那么为了满足SOA架构思想的设计要点,达到既定的工作目标,ESB总线技术至少需要帮助这些业务系统完成以下工作:
- 多调用协议支撑和转换
无论业务系统向外部公布的服务使用哪种调用协议,都可以通过ESB技术进行兼容性转换。例如A业务系统的服务只接受Web Service SOAP形式的调用,B业务系统的服务却可以使用Thrift RPC进行调用(不必为了调用A业务系统而专门去适应A业务系统的协议)。在基于ESB服务的中间层帮助实现两种协议的转换。
- 多消息格式支撑和转换
无论调用协议携带哪一种消息描述格式,通过ESB中间层也可以实现相互转换。ESB中间层应该支持将JSON格式的信息描述转换成目标业务系统能够识别的XML格式,或者将XML描述格式转换成纯文本格式,又或者实现两种不同结构的XML格式的互相转换,等等……
- 服务监控管理(注册、安全、版本、优先级)
既然ESB要对原子服务进行集成,考虑的问题就比较多了。首先,业务系统提供的服务可能会以一定周期发生变化,例如周期性的升级;失控的业务系统甚至可能呈现完全无预兆无规律的服务变化,例如突发性数据割接导致服务接口变动。那么ESB的实现软件中应该有一套功能,能够保证在这样的情况下集成服务依然能够工作。其次,并不是业务系统所提供的所有服务都可以在ESB中进行集成,也并不是所有的服务都能被任何路由规则所编排。ESB应该有一套完整的功能来保证服务集成的安全性和权限。
作为被集成的业务系统,ESB中如何集成它提供的原子服务,前者是不需要关心的,
- 服务集成和编排
为了将多个服务通过ESB技术进行集成形成一个新的服务,ESB技术必须能够进行服务编排。服务编排的作用就是明确原子服务执行的先后顺序、判断原子服务执行的条件、确保集成后的新服务能够按照业务设计者的要求正常工作。下图示例了新服务“工单派发”通过多个业务系统提供的原子服务,按照设置的执行条件在ESB总线上进行工作的过程:
实际上ESB技术和本专题之前讲过的服务治理技术在架构层面属同一层:都是对SOA思想的实现思路。但是两者的应用场景和侧重点完全不一样。
2-2、ESB是EAI的进化
ESB企业服务总线技术是在SOA架构之后出现的,在这之前为了集成多个系统而使用最多的技术思路是EAI(Enterprise Application Integration):企业应用集成。EAI技术并没有一个统一的标准,而是对不同企业集成业务系统手段的统一称呼。
2-2-1、EAI的特征
从上图可以看出,EAI主要的作用还是完成各中消息格式的转换。由于EAI主要的使用场景是在上世纪八九十年代,所以如果从现在往回看EAI所支持的传输协议也是很有限的(不过肯定还是基于7层/5层网络协议的)。不过,在SOA架构思想出现之前,EAI技术确实为企业实现业务系统集成提供了一个可行的思路。
需要注意的是,EAI技术并不是SOA架构思想的一种实现。它出现在SOA架构思想之前,最重要的是它缺少SOA的基本要素——着眼业务服务,粒度粗放但却可控:由于EAI中并没有流程编排的功能,所以这些原子服务并不能有机的结合在一起,形成新的服务,也无法在EAI中重新梳理业务过程,以便要求原子服务进行相应的粒度拆分。
2-2-2、哪些特征得到了进化
- 在消息转换上的进化:
上一小节已经提到,由于EAI技术出现的时间比较早,在那个时候基本上还没有太多行业标准的传输协议和消息格式,使用最多的就是XML格式,还有半结构化的文本数据。所以,在公司内部实现EAI技术时,一般不会考虑太多的行业标准,使用公司内部自定制的传输协议和消息格式就能够搞定。虽然这样做也有一定的好处,就是公司内部的业务团队都清楚这些自定制的格式代表的业务意义,降低了一定的沟通成本。但这样做的问题也显而易见,如果日后需要和兄弟公司的业务系统进行集成,那么之前被节约的工作时间又会被浪费。
成熟的ESB产品中就不会这样考虑问题,一般采用开放性的传输协议和消息格式。例如使用HTTP传输协议携带查询请求、使用FTP传输协议携带上传的文件信息;采用XMPP消息格式描述IM即时通讯内容,采用MQTT消息格式描述物联网设备采集内容、使用AMQP消息格式描述MQ的内容。
- 在流程编排上的进化
EAI没有流程编排的硬性要求,也就是说他只面向数据转换过程,并不面向业务服务。所以各位读者可以这样看待这个问题:SOA架构思想出现后,伟大的技术屌丝团队立马发现了面向服务的概念对EAI软件的建设性作用,废寝忘食的为各种业已运行的EAI软件加入了各种面向服务的要素,最关键的就是加入了服务编排等面向服务的管理功能。实际上,以上情况就是现实中最真实的情况。
- 在服务管理上的进化
从EAI到ESB实际上是业务管理方法上的优化,但就像上文中提到的那样,并不是所有企业都适合使用基于SOA架构思想的ESB技术。目前大部分企业的对内部业务系统的集成手段还是更贴切于EAI技术的定义,这是因为这些企业的业务系统还没有达到较高的复杂度(出现最多的情况就是他们只有一套必须的财务系统)。
所以,EAI并不是淘汰品,ESB也不是什么“跨时代”的伟大发明。后者就是前者不断完善的产物,把两者之间的关系说成“基础版”和“升级版”更为贴切。一些网络文章一味贬低EAI提升ESB的地位,这是不正确的,有的企业或者平台,还没有复杂到必须使用ESB,那就可以不使用ESB技术。所以只有适合自己架构才是理想的架构。如果是某个ESB厂商的软文,目的是什么就仁者见仁智者见智了。
2-3、ESB与循环依赖
ESB技术保证了各个业务服务的低耦合性,间接避免了各业务系统在集成时技术团队有意无意制造的服务循环依赖问题。
2-3-1、什么是循环依赖
首先我们要讲清楚什么是循环依赖,以及循环依赖的在程序设计层面、软件产品设计层面、顶层架构设计层面上可能出现的场景。从概念模型上讲,只要两个或多个元素产生相互依赖关系,就可以看成产生了循环依赖:
上图是两个依赖关系正确的示例:A元素正常工作依赖于B元素的正常工作,或者A元素的正常工作依赖于B、C、D元素的正常工作。这里的A、B、C、D四个元素可以指代四段代码,也可以指代一个业务系统中四个功能模块,还可以指代顶层架构设计中的4个独立工作的业务系统。
循环依赖在逻辑层面上是一个有向循环图。上图中展示了两个错误的依赖关系实例:A元素正常工作依赖于B元素正常工作的同时,B元素正常工作又依赖于A元素的正常工作。那么究竟哪个元素能够首先正常工作起来呢(先有鸡还是先有蛋)?右侧图展示了三个元素的循环依赖,A元素依赖于C元素,C元素依赖于D元素,D元素又依赖于A元素。那么A、C、D三个元素究竟哪一个元素才是底层的基础元素呢?
- 代码层面的循环依赖:
代码层面的循环依赖是开发人员最容易出现的编码错误,不过有的时候也不能全怪开发人员,毕竟业务设计者从需求理解阶段可能就出现了问题。以下示例了代码层面的循环依赖:
/**
* 此类依赖于BusinessB
* @author yinwenjie
*/
public class BusinessA {
private BusinessB bb;
public BusinessA(BusinessB bb) {
this.bb = bb;
}
......
}
/**
* 此类依赖于BusinessC
* @author yinwenjie
*/
public class BusinessB {
private BusinessC bc;
public BusinessB(BusinessC bc) {
this.bc = bc;
}
......
}
/**
* 此类依赖于BusinessA
* @author yinwenjie
*/
public class BusinessC {
private BusinessA ac;
public BusinessC(BusinessA ac) {
this.ac = ac;
}
......
// 接下来我们试图实例化BusinessA...
public static void main(String[] args) {
// 怎么实例化BusinessA呢?
// new BusinessA(new BusinessB(new BusinessC(new BusinessA(!程序员已经发疯!))))
}
}
实际上按照这样的引用结构和构造函数要求,实例化BusinessA这件事情是永远无法完成的。
- 功能层面的循环依赖:
业务系统的功能间也可能出现循环依赖。相对于代码层面的循环依赖,功能模块层面的循环依赖更能够影响一款业务系统的设计质量。笔者曾参与的一款软件,客户方曾经提出过这样的一个业务需求:
货运系统中在创建新的“发车单”时,必须选择空闲的司机和空闲的货车(当然货车类型是要判断的)。空闲的司机和空闲货车缺少任何一样都不能完成“发车单”的创建。但同时为了记录某辆货车上一次对应的“发车单”,客户要求只能在创建新的发车单后,货车才能解除之前的“发车单”绑定关系,变成“空闲货车”。
那么问题来了,如果只有完成新的“发车单”创建后,货车才能解除和之前“发车单”的绑定关系,那么新创建“发车单”时,“空闲的货车”从哪里来呢?实际上客户方不懂技术,是我们在需求调研阶段遇到的算一个问题的问题,但关键看需求人员从哪个方面着手向用户解释引导用户对需求逻辑进行分析。不一定用技术语言直接告诉用户,他的需求在技术层面上不符合逻辑。
- 架构层面的循环依赖:
多个业务系统在进行集成时,他们也可能会出现循环依赖。特别是参与集成的业务系统越多,这种循环依赖的情况就越容易出现:
在系统数量还没有达到一定数量时(通常来说这个阀值为4),系统间的循环依赖最可能是由业务人员/技术人员无意造成。这时,系统间的依赖关系还处于一个可控级别,即使出现系统间循环依赖的情况,技术团队/业务团队也可以快速进行纠正。但是,当参与集成的业务系统数量超过可控制的阀值数量后,这个检查和纠正工作就不再是人工可及的范围了。如下图所示的5个系统进行集成时,很容易出现系统间循环依赖的情况:
2-3-3、避免循环依赖
- 依赖倒置原则预防循环依赖
依赖倒置原则可以帮助预防代码层面和功能模块层面的循环依赖。顶层架构层面的循环依赖问题,也可以遵循这个原则进行设计来避免。依赖倒置原则在很多书本、网络资料上都有很详细的介绍。基本上这个原则是在说两个点:高层次模块不应该依赖于低层次模块,模块的实现都应该依赖于抽象(接口),而抽象(接口)能够屏蔽功能设计上的细节。
- 对循环依赖的自动检测
如果您负责的产品是遗留产品。在经过多个设计人员更替后,产品内部设计或多或少出现了一些循环依赖问题,这时该怎么办?您要做的首先是检查产品的哪些模块出现了循环依赖,再思考修改方法。目前市面上有很多这样的工具,可以帮助您检查代码层面和系统模块层面的依赖关系是否良好,这里笔者推荐SonarQube。以下截图是笔者经历过的一个项目中,某个子模块通过SonarQube进行检测的结果:
呵呵,V0.0.9版本,看来还有若干个类的复杂度偏高,说明模块中使用的设计模式还有改进空间。好吧,这个子系统只是开了一个头,目前已经发展到V0.5.3的版本号,代码行数也快接近5万行了。不过作为这个子模块的作者之一,本人自认为包耦合度一直控制得很好,直到现在也没有出现包循环依赖的情况。
- 抽离底层能引导业务人员优化依赖结构
在上一小节“功能层面的循环依赖”中我们举了一个司机-货车-发车单三个元素在业务功能层面的循环依赖问题。现在我们接着这个问题继续讨论。客户之所以要在新的“发车单”创建后,才解除货车和历史“发车单”的绑定关系,是因为用户担心软件失去对历史“发车单”的跟踪能力,最后无法统计车辆使用率或者司机绩效情况。但实际上,客户完全无需担心出现这样的情况,了解客户的真实想法后,需求人员就能引导客户开辟一个新的日志模块,这个日志模块处于整个业务系统设计的更底层,专门跟踪各种历史行为,车辆的、人员的、财务的、货品的,都行:
我们可以用这种剥离循环依赖元素中底层能力的方式,解决循环依赖问题。这种解决思路不但适用于业务系统功能间的解耦,同样适用于代码级别或者系统顶层架构级别的解耦。
- 使用中间层/基础层对依赖元素进行隔离
ESB为各个业务系统的集成提供了一个理想的中间层/基础层。通过从中间层/基础层抽离的底层能力,我们将所有业务系统的集成工作交由ESB完成:
注意:虽然ESB承担了原本在系统内部完成的业务集成工作。但是根据依赖倒置设计原则,ESB也只是依赖于各业务系统注册在ESB总线上的调用接口,业务系统中功能的具体实现对于ESB来说是透明的。
==========================================
(接下文)