深度:微服务如何解耦,对于已经紧耦合的架构如何重构?
作者:人月神话,新浪博客同名
简介:多年SOA规划建设,私有云PaaS平台架构设计经验,长期从事一线项目实践
今天准备谈下微服务架构下各个微服务间如何解耦,以及对于已经紧耦合的微服务如何进行重构。在谈这个内容前,可以先看下我前两天发布的微服务模块和粒度如何划分才更加合理的一篇文章,这篇文章对于微服务拆分有比较详细的描述。
可以参考:中台规划中微服务粒度究竟应该如何划分?你可以从以下几点考虑
要明白实际上微服务后续出现的诸多问题往往都是一开始微服务模块划分就不合理导致,对于具体的模块划分方法和原则,我在上面文章里面给出了以下几点。
原则1:划分为<10个微服务模块
原则2:强数据关联模块不要拆分
原则3:以数据聚合驱动业务功能聚合
原则4:从纵向功能划分思路到横向分层思路转变
原则5:高内聚,松耦合的基础原则
对于具体的内容在这篇文章不再重复给出。可以看到对于微服务模块拆分更多的是属于业务建模和系统分析方面的内容,而今天谈的微服务解耦重点是想从可用的技术手段入手来谈下可用的以下解耦方法和策略。
问题综述
最近几年对于微服务架构,企业中台构建,组件化和服务化,平台+应用构建模式,包括Docker容器化,DevOps等都越来越受到传统企业的关注,也可以看到很多企业传统架构也在朝这个目标进行演进和转型。对于微服务架构本身的优点和缺点,包括传统企业实施微服务架构的演进路线等,在我前面很多微服务架构相关的文章中都有所介绍,今天主要谈下在微服务架构下的解耦问题。
要明白,企业实施微服务架构后,原来所有内部的接口调用,内部的完整事务都会变成微服务模块间的跨域的接口服务调用,传统事务也变成了分布式事务,这些本身是增加了系统复杂度。
原来一个系统就能够完成的事情,现在要依赖底层技术组件服务,其它业务微服务模块多个Http Rest API接口调用往往才能够完成。只要其中任何一个API接口出现问题,都会直接影响到前端的业务功能使用。
微服务间的雪崩效应:在采用微服务架构后,各个微服务间存在大量的API接口服务调用,相互之间还形成了服务调用链,比如A-》B-》C,那么如果C服务出现故障就将直接影响到B服务无法正常访问和服务阻塞,同时B的故障又将进一步传导到A服务的消费和使用。
对于互联网企业实施微服务架构,其中有几个关键点。
其一就是微服务架构可以更好的进行平台的性能扩展和高伸缩要求。
其二就是互联网应用本身业务规则相对简单,模块间容易解耦。
其三就是大的互联网企业IT技术积累更强,有更好的技术能够搭建高可用的技术平台,也有更好的技术能够实现微服务架构实施后的自动化运维和监控。
而这些往往都是传统企业在实施微服务架构所欠缺的,比如有些企业一开始实施微服务架构没发现问题,结果最终上线后却发现后续的系统运维和性能监控,故障问题分析和排查等能力跟不上,无法及时响应客户需求并快速的定位和解决问题。
即前面经常说到的,传统企业的IT治理和团队技术能力跟不上,直接影响到微服务架构实施成败。
那么回到正题,今天希望讨论和分析下,企业实施微服务架构后,如何尽量减少微服务模块的耦合导致的单个微服务模块功能实现和运行故障,简单来说就是一个微服务模块中业务功能的运行,如何做到最小化的依赖外部微服务模块Http API服务接口的可用性。即使外部模块挂点,当前模块也能够正常使用,或者说能够不影响到当前模块核心功能的使用。
对于该问题,我们可以分开从几个方面进行讨论。
同步调用转为异步调用
一说到解耦,我们一定会首先想到消息中间件来实现异步,即将同步转为异步,通过异步来实现解耦。我们可以先想消息发送给消息中间件,只要消息中间件是高可用性的没有宕机,整个接口集成过程就是OK的,而消息中间件再异步方式分发消息给目标系统,同时支持重试。
消息中间件的采用
消息中间件(message oriented middleware)是指支持与保障分布式应用程序之间同步/异步收发消息的中间件。消息是分布式应用之间进行数据交换的基本信息单位,分布式应用程序之间的通信接口由消息中间件提供。其中,异步方式指消息发送方在发送消息时不必知道接收方的状态,更无需等待接收方的回复,而接收方在收到消息时也不必知道发送方的目前状态,更无需进行同步的消息处理,它们之间的连接完全是松耦合的,通信是非阻塞的,这种异步通信方式是由消息中间件中的消息队列及其服务机制保障的。
消息中间件实现了发布者和订阅者在时间、空间和流程三个方面的解耦:
时间解耦—-发布方和订阅方无需同时在线就能够进行消息传输,消息中间件通过存储转发提供了这种异步传输的能力;
空间解耦——发布方和订阅方都无需知道对方的物理地址、端口,甚至无需知道对方的逻辑名字和个数;
流程解耦——发布方和订阅方在发送和接收数据时并不阻塞各自的控制流程。
从消息中间件的基本功能来看,无论是点对点消息中间件还是消息代理,其体系结构都是非常清晰简单的。但由于分布式应用及其环境的多样性和复杂性,导致了消息中间件的复杂性。
当前的消息中间件仍然分为两类,一类是基于AMQP高级消息协议的,一类是基于JMS消息协议的。对于互联网使用较多的RabbitMQ,Kafka等基本都基于AMQP高级消息协议。而对于Weblogic JMS,IBM MQ则是基于JMS消息协议的消息中间件产品。
对于Weblogic而言是一个企业级的应用服务器中间件,同时Weblogic JMS也是企业级的消息中间件产品,该产品是一个企业级消息中间件产品,具备了高可靠,高可用,高扩展,高性能基础特性。支持主流的各种消息模型,消息发布订阅,消息持久化,事务处理,集群等核心特性。
消息中间件的使用场景,具体包括了如下几个方面:
1.消息通知:单据状态变化后的事件通知,数据传输完成后的事件通知
2.异步集成:服务消费方只需要将数据送到OSB即实时返回,通过异步集成实现彻底解耦
3.目标系统削峰:大并发数据导入而目标系统处理性能受限的场景
4.消息发布订阅:基础主数据通过JMS实现1对多的实时数据分发
5.高可靠性场景:确保在数据集成中不出现任何丢失的情况
对于采用Weblogic JMS来实现消息集成,具体过程如下图:
基于事件驱动的业务分析
而要做到同步转异步,我们必须从业务需求分析开始就转变思维,即从传统的业务流程需求分析方法转到事件驱动分析方法,这个在我很早的EDA事件驱动架构内容整理的时候专门谈到过,今天摘录部分内容供大家参考。
事件驱动框架(EDA)里事件可传输于松散耦合的组件和服务之间。一个事件驱动系统典型地由事件消费者和事件产生者组成。事件消费者向事件管理器订阅事件,事件产生者向事件管理器发布事件。当事件管理器从事件产生者那接收到一个事件时,事件管理把这个事件转送给相应的事件消费者。如果这个事件消费者是不可用的,事件管理者将保留这个事件,一段间隔之后再次转送该事件消费者。
EDA架构往往具备如下特征:
广播通信:参与通信的系统将事件广播给任何对该事件感兴趣的参与者。
实时性:在业务事件发生时候,EDA架构下可以实时的发送事件给消费方,而无需等待
异步:事件发布系统不用等待事件接收系统来处理事件,发送到EDA模块即可返回。
细粒度:只要具备独立的业务价值,即可以发布为细粒度的事件,而不是传统服务下的粗粒度。
复杂事件处理:根据业务流程需求,事件间可以聚合和组装,形成事件链满足复杂事件处理。
并行运行:多个事件可以同时运行,单个事件可以同时分发给多个订阅方。
非阻塞:EDA本身提供MQ等消息持久化机制,不会在事件大并发下出现事件阻塞情况。
简单来讲,消息集成,异步,彻底解耦,消息发布订阅,事件链是EDA整个架构的核心。但是在EDA包括CEP复杂事件处理,在使用的时候首先还是应该了解清楚其和传统流程驱动在业务分析方法上的区别。简单来说,流程驱动和事件驱动的一个简单比较可以用下图描述:
基于EDA的核心业务分析思路说明
在事件驱动架构下,业务分析的核心就是事件的识别。而对于传统方法往往则是关键流程和活动即可。在总体分析思路上的变化来说,传统分析方法只分析到第2-3级业务流程,识别业务活动和交互点,而EDA需要业务分析时候分析到L4级的最底层EPC事件流程图,并识别关键业务事件和事件分解,聚合关系。
在具体分析内容上的变化来说,传统方法只关心业务活动,而不关系业务活动具体的启动机制,业务活动完成后产生的业务事件。基于EDA业务分析方法,需要打开业务活动,识别业务活动的前者触发条件和业务活动引起的业务对象状态的变化,往往状态变化点都是关键的事件识别点。
具体可以用下图进行描述:
简单事件-基于业务需求用例分析和识别
业务事件的识别可以从业务需求用例入手,分析业务用例中的业务前置触发条件,分析业务对象的状态流转过程和后续操作,以找寻业务活动的事件输入和事件产生。
从下图里面也可以看到,对于事件的识别往往比用例的识别更加细化,需要详细的分析业务用例中的基本流,扩展流,业务规则,特别是关注其中核心的业务对象和单据状态的变化。同时对于用例分析中的触发条件也需要重点分析,这些触发条件往往是事件链形成,或者说触发消息事件订阅的来源。
复杂事件-基于事件识别形成事件链
传统的基于流程的业务分析方法往往只会分析到业务流程,具体的业务活动,而不关心具体业务活动执行前或执行后产生的业务事件,这和接口平台前期重点关注数据集成有关系。而为了保证业务实时响应需求,必须准确的识别业务事件,才能进一步设计基于业务事件的处理和响应机制。
基于EPC事件流程链分析思路,需要对传统分析流程进行细化,增加红色事件识别点和事件分解聚合关系。在事件链的形成过程中往往存在一些复杂场景需要分析,包括了事件的一对多分发和订阅,也包括了多个事件聚合,在满足某个特定的业务规则后才触发下一个新的业务活动和新事件。这些都是在复杂事件分析中必须考虑的内容之一。
从EDA事件驱动到CQRS
顾名思义,CQRS即命令查询职责分离,将CUD操作和R查询操作分离,对于CUD操作仍然参考传统的领域模型建模思路来实现,但是在命令中增加了消息事件机制,实现CUD操作变更通过消息事件异步写入到数据库。
在CQRS中,查询方面,直接通过方法查询数据库,然后通过DTO将数据返回,这个方面的操作相对比较简单。而命令方面,是通过发送具体Command,接着由CommandBus来分发到具体的CommandHandle来进行处理,CommandHandle在进行处理时,并没有直接将对象的状态保存到外部持久化结构中,而仅仅是从领域对象中获得产生的一系列领域事件,并将这些事件保存到Event Store中,同时将事件发布到事件总线Event Bus进行下一步处理;接着Event Bus同样进行协调,将具体的事件交给具体的Event Handle进行处理,最后Event Handler把对象的状态保存到对应Query数据库中。
对于CQRS,最容易想到的还是在数据库层面做的读写分离模式,可以看到CQRS本身和数据库的读写分离模式可以更好的匹配,由于采用事件驱动和消息订阅模式,对于R读库我们可以更加容易对数据变更信息进行更新,达到读库数据的及时同步更新。同时读库既可以采用读写分离数据库,也可以采用类似Solr,Nosql等分布式,非结构化数据来实现弹性水平扩展能力。
在命令查询职责没有分离的时候,可以看到一方面是模型本身的扩展性受到影响,另外一方面是原有的领域模型本身偏重,而且Entity实体本身也通过完整的DTO对象进行传输,这样在一些特殊的只需要更新或查询个别字段的时候,整个模型仍然偏重。
通过命令查询职责的解耦,不仅仅是提升整个框架模型的扩展性,更加重要是将两类业务规则和实现彻底的解耦开,方便后续的功能开发和运维,特别是在整个业务场景和逻辑实现复杂的情况下,这种解耦会使整个开发架构更加清晰简单。
同时也可以看到有一Command命令都是采用异步事件的方式进行写入,因此不存在同步和长连接占用的问题,有利于提升整个平台在大并发下的整体响应性能。
当然,采用CQRS模式最大的一个问题点就是无法实现命令和查询两部分内容的强一致性保障,即很可能你界面上查询到的数据不是最新的持久化数据库里面的数据,这个本身和消息管道异步写入的实时性有关系。
其次在使用CQRS模式的时候,有一个重要假设就是,在事件和命令发出后,无特殊情况在事件接收方都必须要能够接收事件成功处理,否则就存在大量的异常错误消息的异步回写,反而增加系统的复杂度。举个简单例子来说:
当我们在电商平台购买一个商品的时候,只要订单提交成功,那么这个订单就一定能够生效,也一定有库存能够发运和配送,而不是在后续到了配送环节的时才发现没有库存而导致订单取消。如果这样的话就极大的降低了系统本身的易用性。
即在异步命令和事件发送场景,当命令发送成功时候,虽然我们没有及时接收到处理方的事件处理结果信息,但是我们默认是接收方能够成功处理事件。但是我们也看到在CQRS场景框架下,只要命令事件发出,我们并不需要等待任何反馈信息。
另外还有一种CQRS实现场景,即虽然在内部对Command命令处理的时候是基于事件机制,异步响应,但是客户在前端的操作是同步等待返回。在这种情况下我们就可以保持前端连接,但是是否后端的类似DB连接等。
在CQRS模型下,由于职责分离,可以看到我们通过事件和消息的订阅,可以实现多个读库的订阅,这些读库既可以是结构化数据库,也可以是非结构化数据库;既可以用来实现业务功能本身的查询读,也可以用来做海量数据本身的分布式全文检索。
对于CQRS框架的实施,不是简单的设计模式使用问题,更加重要的仍然是是否能够接受最终一致性要求,同时在该要求下将传统的同步请求下业务功能和逻辑处理机制转变为异步事件价值下的事件链驱动模式。要实现这种转变就必须能够拆分出独立,自治的命令和事件,同时确保这些事件在朝后端业务功能和逻辑模块发送的时候能够处理成功(即该做的校验必须提前做完)。
将同步接口调用转为本地消息缓存
这个类似消息中间件的功能,举例来说我们设计了一个同步发送订单到ERP系统的接口,如果在同步实时调用这个接口服务的时候出现异常,那么我们可以首先将消息存储到本地,然后设置定时任务和重试机制,通过重试方式将消息发送到目标系统。
即对于业务功能来说不用关心实时是否发送成功,而由业务系统自身机制来完成消息发送的重试。
而要做到这点,在接口功能设计时候,最好要做到单据业务完整性校验接口和实际的数据发送接口分离,即先调用接口进行完整性校验,在校验没有问题后再进行消息发送。以确保最终发送的消息不会因为数据完整性的原因导致无法发送成功。
查询数据的本地化缓存或落地
memcached是一套分布式的高速缓存系统,由LiveJournal的Brad Fitzpatrick开发,但被许多网站使用。这是一套开放源代码软件,以BSD license授权发布。
memcached的API使用三十二比特的循环冗余校验(CRC-32)计算键值后,将数据分散在不同的机器上。当表格满了以后,接下来新增的数据会以LRU机制替换掉。由于memcached通常只是当作缓存系统使用,所以使用memcached的应用程序在写回较慢的系统时(像是后端的数据库)需要额外的代码更新memcached内的数据。
对于实时查询类接口,将查询的基础数据进行本地化缓存,即如果在实时查询出现异常的时候,我们可以直接查询本地缓存的数据,减少对业务功能使用的影响。
比如查询供应商接口服务,如果主数据系统提供的接口出现异常,我们可以直接查询本地缓存的供应商数据。这种模式对于变更不频繁的数据基本都适应,同时本身也减少实时调用接口带来的性能损耗。
如果是接口服务注册在API网关或ESB服务总线上面,我们还可以考虑在ESB服务总线上启用缓存能力,即对于调用过的接口,在同样参数重复调用的时候能够通过缓存数据获取,这样即使在源端业务系统不可用的情况下,也不好影响到当前接口服务的成功调用。
可以适度考虑数据落地
在微服务架构里面,我们一直在强调一点,即数据实时需要实时访问,不进行底层数据库的数据集成和同步,这既满足了数据的高一致性,也满足了数据实时性的要求。
但是带来的问题就是强耦合,如果数据提供方出现异常,那么导致消费方业务功能也无法使用。
因为我们可以适量考虑数据落地方式的数据集成在整体微服务架构实施过程中,对于变化不频繁的数据适度落地到微服务模块本地。这样本身可以减少实时的业务接口服务调用,增加单个微服务模块的可用性和可靠性。
对于已经出现强耦合如何重构
如果微服务已经实施完成并出现了大量紧耦合的情况,那么我们就需要在后期考虑对微服务架构进行重构,具体重构的方法可以从如下几点考虑。
两个微服务本身紧耦合
如果两个微服务间出现大量接口相互调用,即可以认为紧耦合。
或者我原来的判断标准,即两个微服务对应的后台数据表,其中30%以上都需要两个微服务交叉访问,那么就认为两个微服务本身耦合性极强。
在这种情况下处理措施就是原来微服务划分的太细了,需要对两个微服务进行合并。
交叉依赖变为共性依赖
要知道在传统软件开发里面往往是不允许两个组件交叉依赖的。
但是在新的IOC和微服务开发里面,大量都是反射调用,两个组件相互依赖不会有问题。但是这本身也不是一种很好的设计方法。
如果两个微服务或多个微服务相互依赖内容本身具备共性。那么最好的做法就是将共性内容全部移出,下沉为一个共性基础微服务模块再朝上提供服务。
即交叉依赖转变为对底层的共性依赖。
对某个微服务实现单元进行迁移
为什么出现这种场景?
简单来说就是原来的微服务模块划分和业务功能划分不合理。比如上图中的微服务A中的A1部分。这个部分内容需要大量被微服务B调用,但是A1实际依赖微服务A中其它部分的内容却很少。
这种就是典型的A1部分功能划分位置不合理。
最好的做法就是将A1功能从微服务A迁移到微服务B,实现对原有业务划分不合理的纠正。
将细粒度服务转变为粗粒度服务
服务本身应该具备粗粒度属性,暴露仅仅需要暴露的内容。
比如微服务A实现客户信用检查和评级。微服务B需要客户信用。有两种做法
第一种是B调用A多个接口,把客户基本信息,客户交易信息,客户违约信息全部查询过来,然后自己计算客户信用。
第二种即是只需要输入客户编码,微服务A返回最早的信用评级。
对于后者就是我们常说的粗粒度接口或领域服务,服务间的交互应该以领域服务和粗粒度服务为主,避免掉完全的数据库表的CRUD类服务接口。
来源:
https://www.toutiao.com/i6851016633579028996/
申明:感谢原创作者的辛勤付出。本号转载的文章均会在文中注明,若遇到版权问题请联系我们处理。