在2014年,Sam Newman,Martin Fowler在ThoughtWorks的一位同事,出版了一本新书《Building Microservices》。该书描述了如何按照Microservice架构模式设计及搭建一个具有良好扩展性并可持续开发的系统。除此之外,该书还将基于该模式的系统演化流程与Continuous Delivery等当前甚为流行的开发流程结合在了一起,使得Microservice架构模式看起来非常具有吸引力。基于这些原因,该架构模式迅速被业界所熟知,并在多个产品中被尝试着使用。这其中就包含了我们公司的产品vRA。
在这一年多的时间里,我们不但真正地体会到了Microservice所具有的一系列优点,也犯过一系列错误。因此在这篇文章里,我会对Microservice架构模式进行简单地介绍,并将我们所得到的经验和教训介绍给大家。
Monolith
网上对Microservice进行介绍的文章常常以Monolith作为开头,我也不会例外。原因是,知道了Monolith的不便之后才能更容易地理解Microservice架构模式所具有的各种优点。
首先请回想一下我们所开发的服务是什么样子的。通常情况下,这个服务所对应的代码由多个项目所组成,各个项目会根据自身所提供功能的不同具有一个明确的边界。在编译时,这些项目将被打包成为一个个JAR包,并最终合并在一起形成一个WAR包。接下来,我们需要将该WAR包上传到Web容器中,解压该WAR包,并重新启动服务器。在执行完这一系列操作之后,我们对服务的编译及部署就已经完成了:
这种将所有的代码及功能都包含在一个WAR包中的项目组织方式被称为Monolith。在项目较小的情况下,这种代码组织方式还是可以接受的:更改完代码后,软件开发人员可以趁着编译器编译代码的时候冲杯咖啡,并在回到座位后花费一分钟部署刚刚编译出来的WAR包以便测试自己刚刚所做的更改。但随着项目的逐渐变大,整个开发流程的时间也会变得很长:即使在仅仅更改了一行代码的情况下,软件开发人员需要花费几十分钟甚至超过一个小时的时间对所有代码进行编译,并接下来花费大量的时间重新部署刚刚生成的产品,以验证自己的更改是否正确。
如果应用的部署非常麻烦,那么为了对自己的更改进行测试,软件开发人员还需要在部署前进行大量的环境设置,进而使得软件开发人员的工作变得繁杂而无趣:
从上面的示意图中可以看到,在应用变大之后,软件开发人员花在编译及部署的时间明显增多,甚至超过了他对代码进行更改并测试的时间,效率已经变得十分低下。
在变得越来越大的同时,我们的应用所使用的技术也会变得越来越多。这些技术有些是不兼容的,就比如在一个项目中大范围地混合使用C++和Java几乎是不可能的事情。在这种情况下,我们就需要抛弃对某些不兼容技术的使用,而选择一种不是那么适合的技术来实现特定的功能。
除此之外,由于按照Monolith组织的代码将只产生一个包含了所有功能的WAR包,因此在对服务的容量进行扩展的时候,我们只能选择重复地部署这些WAR包来扩展服务能力,而不是仅仅扩展出现系统瓶颈的组成:
但是这种扩展方式极大地浪费了资源。就以上图所展示的情况为例:在一个服务中,某个组成的负载已经达到了90%,也就是到了不得不对服务能力进行扩容的时候了。而同一服务的其它三个组成的负载还没有到其处理能力的20%。由于Monolith服务中的各个组成是打包在同一个WAR包中的,因此通过添加一个额外的服务实例虽然可以将需要扩容的组成的负载降低到了45%,但是也使得其它各组成的利用率更为低下。
可以说,所有的不便都是由于Monolith服务中一个WAR包包含了该服务的所有功能所导致的。而解决该问题的方法就是Microservice架构模式。
Microservice架构模式
简单地说,Microservice架构模式就是将整个Web应用组织为一系列小的Web服务。这些小的Web服务可以独立地编译及部署,并通过各自暴露的API接口相互通讯。它们彼此相互协作,作为一个整体为用户提供功能,却可以独立地进行扩容。就以下图所示的WikiPedia服务架构为例:
从上图中可以看到,WikiPedia包含了一系列服务,如数据访问服务Databases,搜索服务Search等。这些服务都包含了数量不等的服务实例,以确保能在不同负载的情况下为用户提供优质的服务。在用户的请求到达时,它们将协同工作,一起完成对用户请求的响应。
在使用Microservice架构模式的情况下,软件开发人员可以通过编译并重新部署单个子服务的方式来验证自己的更改,而不再需要重新编译整个应用,从而节省了大量的时间。同时由于每个子服务是独立的,因此各个服务内部可以自行决定最为合适的实现技术,使得这些子服务的开发变得更为容易。最后如果当前系统的容量不够了,那么我们只需要找到成为系统瓶颈的子服务,并扩展该子服务的容量即可:
Microservice经验谈
以上就是对Miscroservice架构模式的介绍,是不是很简单?实际上,这是一个正在发展的架构模式。在众多讨论中,关于该模式的标准实现,以及最佳实践等众多话题并没有完全达成一致。因此我在这里介绍的,是各个论坛讨论中基本达成一致意见的一系列经验。而各位在实现自己的Microservice架构模式时,一方面可以借鉴这些经验,另一方面也可以根据项目本身需求调整Microservice架构模式的实现方法。
转变你的视角
无论是在编写一个服务,还是在编写一个桌面应用,我们常常会首先尝试将需要实现的功能分割为一系列组件,并围绕着这些组件设计完成业务逻辑所需要的工作流及数据流。这种设计方法将导致实现业务逻辑的所有组件都运行在同一个进程之内,并且各个业务逻辑的实现也在同一个进程之内运行:
但是在Microservice架构模式中,我们需要更高一层的分割:在尝试将需要实现的功能分割成为一系列组件之前,我们首先需要考虑如何将需要实现的功能交由彼此相互独立的一系列服务来完成。例如在一个电子商务网站中,对用户购买商品这一业务流程的支持就可以交由三个服务来完成:在用户浏览商品的时候,其使用的是商品浏览服务;在用户将商品添加到购物车并生成订单的时候,其使用的是订单服务;而在用户进行网上支付的时候,其使用的则是付款服务。根据这种分割思路,我们的应用将运行在三个独立的进程之中:
同时这三种服务各自的侧重点并不相同:商品浏览服务中,对数据库的读操作比写操作多得多,因此对读操作进行优化将非常显著地提高服务的运行性能;而订单服务则是写操作居多,因此我们需要对订单的写入性能进行优化;付款服务涉及到用户的财产,因此其对安全要求会偏高一些。这种差异可能导致最适合实现这三个服务的技术各不相同。由于这些服务是完全独立的,因此我们完全可以根据子服务的需求来决定所需要使用的技术,而不再需要考虑这些类库是否与已有系统兼容。
使用最合适的技术所带来的优点就是,服务的代码会变得非常清晰明了,甚至在有些情况下可以达到简洁优雅的程度。在一些讨论中,有些人甚至建议一个服务只需要10到100行代码(他们常用简写LoC,即Lines of Code)。再加上服务已经独立出来,而不再与其它服务混合在一起,因此正确地使用Microservice架构模式大大提高了代码的维护性以及新人上手的速度,也有助于技术人员在日常工作中进行技术集的更新及转换。
但是这种对于服务的分割和组件之间的分割并不相同。最重要的一点就是在各个服务之间进行通讯的消耗相对于在同一个进程中而言是非常大的。在设计一个组件的时候,我们需要考虑该组件所给出的接口能够尽可能地满足当前及今后的一系列可以预见的需求,这便要求该组件所提供的API具有一定的前向兼容性,并拥有一系列其它特性,如灵活性,扩展性等等。这常常导致该组件所提供的API具有较细的粒度。在程序运行时,对该组件所提供的API的调用就在当前进程中进行,速度非常快,因此频繁地对该细粒度API进行调用并没有太大的问题。但是一个跨服务调用所需要的时间则比进程内调用的时间长很多。如果在处理一个请求的时候需要太多的跨服务调用,那么整个应用的性能将变得无法忍受。因此我们在执行服务分割时定义的API需要是粗粒度的API。
就让我们以一个电子商务网站为例。在为用户生成订单时,电子商务网站常常需要列出各个商品的主要信息,商品的价格,优惠幅度,并通过库存系统检验该商品的库存,从而得到整个订单的内容。如果每次与其它服务沟通都需要100毫秒,而且整个订单包含了20件货物,那么系统准备订单的时间就会达到8秒(100ms * 4次调用 * 20件商品)。这从用户的角度来说是不可以接受的性能。而且随着订单中所包含商品数量的增多,系统准备订单的时间会线性增长,进而使得系统的性能更加不可忍受。
究其原因,实际上还是因为准备订单所调用的API的粒度太细了。如果订单系统能够一次性地把一件商品的主要信息,价格,优惠幅度以及库存信息从商品服务中取回来,那么其效率就将提高四倍。如果订单系统不需要为每件商品依次发送请求,而是可以通过一次性地服务间调用就能取回所有需要的信息,那么系统准备订单的时间将不会再随着订单的增大而增长。因此在Microservice架构模式中,各个服务应该提供可以被灵活使用的粗粒度API,以减少各种跨服务调用的消耗。
除了各个服务所提供的API的粒度,服务分割的粒度也是在服务分割过程中需要考虑的因素。如果一个服务的粒度太小,那么它所提供的API的粒度也不会高。一个较为普遍的看法是,在Microservice架构模式中,一个服务需要能够独立地完成特定的业务逻辑,至少是某个独立资源的CRUD操作。例如在电子商务网站中,我们需要一个服务能够独立地完成对商品相关信息的读取,如商品的主要信息,商品的价格,参与的优惠活动等。
这里有一个例外,那就是公共功能的处理。试想在一个应用中,我们常常需要一个权限管理组件来管理用户所具有的各个权限。权限管理组件常常实现了一种公用的安全模型(Security Model),如ACL(Access control list),RBAC(Role-based access control)等。在每次访问一个子服务的时候,这些服务都需要检查用户所具有的权限:
发现问题了么?是的,每次对一个产品系统及订单系统的调用都需要从权限系统中得到当前用户的权限,才能决定用户是否能够访问特定信息。如果这样的公共服务很多,那么该系统的性能将会变得非常差。
解决该问题的一种方法就是在各个系统中将下次还能够使用的信息缓存起来,也就是在这些系统中为用户创建一个会话。由于每个系统可能由多个服务实例所组成,为了能够重复利用会话中所储存的信息,减少向公共服务发送请求的次数,我们需要通过负载平衡技术让系统中的同一个服务实例处理同一个用户的请求。有关如何实现该功能,请见我的另一篇文章《企业级负载平衡简介》。
除了性能问题之外,公共服务还会与各个服务产生一种逻辑上的依赖关系。让我们继续通过权限系统这个例子进行讨论。当权限管理的组成存在于各个服务中的时候,我们可以直接通过传入用户的信息以及需要访问的资源就能判断出到底用户是否能够访问特定资源。也就是说,从权限管理组成所返回的实际上就是一个布尔类型的数据。但如果权限管理不再是一个组成,而是一个服务,那么为了避免每次都调用权限管理服务,我们需要在用户的会话中记录用户所具有的权限。在用户下次访问该服务的时候,我们可以通过直接检查该用户所具有的所有权限就能决定其是否能够访问特定资源了。这些在用户会话中记录的权限常常具有其特定的表现方式,例如特定形式的字符串,而这种字符串表示需要同时被服务和权限管理服务所理解,从而造成了这两个服务之间的耦合:
但是这种方式为子服务增强了对权限系统的依赖性。和组件之间的耦合一样,增大的耦合性会导致服务的重用性下降。
所以说,如何对服务进行分割实际上是Microservice架构模式中最需要技巧的事情。在分割过程中,服务的总体性能是至关重要的,而各个服务的独立性也是大家所最为关心的特性。当然,Microservice架构模式仍在逐渐发展中,因此相信会有越来越多的实践经验被大家所发掘出来,进而指导我们更好地对服务进行分割。
共享服务
在前面一节中,我们已经提到了公共服务。实际上,这是Microservice架构模式中最需要技巧的一部分。
实际上,Microservice架构模式实现中常常需要一系列公有服务以辅助整个应用的运行。除了我们刚刚提到的权限管理服务,我们还需要能够监控各个服务实例的服务状态,服务实例的添加删除升级管理等等。这些服务在各个子服务的服务实例之间共享,甚至可以在其它应用中被重用。
只是很多人拥有一个这样的误区,那就是Microservice架构模式可以让服务的开发变得更容易。而实际情况则恰好相反。在刚开始使用Microservice架构模式开发应用的时候,其效率是明显低于通过Monolith进行开发的:
从上图中可以看到,在刚开始的阶段,使用Microservice架构模式开发应用的效率明显低于Monolith。但是随着应用规模的增大,基于Microservice架构模式的开发效率将明显上升,而基于Monolith模式开发的效率将逐步下降。
为什么呢?这是因为Microservice是一个架构模式,而不是一个特定的技术解决方案。其并不会将开发中的各个难点全部转移,而只是允许通过更为合适的技术来适当简化单个子服务的开发,或者绕过开发中可能遇到的部分难点。但是为了支持各个子服务的运行,我们还需要创建一系列公共服务。这些公共服务需要在编写第一个子服务的同时进行。这是导致Microservice架构模式在开发初期会具有较低效率的一个原因。
然而使用特定技术并不会绕过开发中所能遇到的所有难点。由于在Microservice架构中,各个子服务都集中精力处理本身的业务逻辑,而所有的公共功能都交由公共服务来完成,因此公共服务在保持和各个子服务的松耦合性的同时还需要提供一个足够通用的,能够在一定程度上满足所有当前和未来子服务要求的解决方案。而这也是导致Microservice架构模式在开发初期会具有较低效率的另外一个原因。
而在开发的后期,随着Monolith模式中应用的功能逐渐变大,增加一个新的功能会影响到该应用中的很多地方,因此其开发效率会越来越差。反过来,由于Microservice架构模式中的各个子服务所依赖的公共服务已经完成,而且子服务本身可以选择适合自己的实现技术,因此子服务的实现通常只需要关注自身的业务逻辑即可。这也是Microservice架构模式在后期具有较高效率的原因。
当我们再次通过Microservice架构模式搭建应用的时候,其在开发时的效率劣势也将消失,原因就是因为在前一次基于Microservice架构模式开发的时候,我们已经创建过一次公共服务,因此在这个新的应用中,我们将这些公共服务拿来并稍事改动即可:
从上图中可以看到,虽然我们仍然需要花一些时间来对公共服务进行一些修改,但是此时所导致的效率下降已经不再那么明显了。也就是说,就算是在前期,我们已经拥有了较高的开发效率。
而且随着Microservice架构模式的不断流行,在网络上会有越来越多的用户共享自己的公共服务解决方案。那么第一次按照Microservice架构模式编写应用所导致的性能下降也会逐渐变得越来越小。
模型匹配
OK。在介绍了共享服务之后,我们就可以讨论Microservice架构模式中的另外一个问题:模型匹配了。在Microservice中,各个服务是彼此独立的,而且是关注于自身业务逻辑的。因此在看待一个事物的时候,Microservice可能拥有不同的视角,进而造成了各个子服务中的对应模型并不匹配。
例如在一个IaaS云中,一个用户所具有的角色可能会根据他所拥有的职责来划分:云上拥有一系列用于监控的用户,用来完成对云的整体运行监控等工作(不包含查看用户数据,这是个安全问题)。同时云上的用户又可以分为帐号管理员,Tenant管理员,资源管理员以及普通用户等。而在其上运行的应用即服务(AaaS,Application as a Service)中,其用户的职责划分可能是另一个样子:在AaaS上定义应用的是应用架构师,负责应用部署及维护的则是运维人员。在应用架构师设计一个应用的时候,其并需要拥有IaaS云上访问资源的权限,却并不需要分配资源的权限,但是运维人员需要拥有该权限以对应用进行部署和维护。
也就是说,IaaS云中的权限划定和AaaS服务中的权限划定并不一样。通常情况下,我们常常在业务集成时执行一次对权限的匹配:
从上图中可以看出,由于AaaS服务是运行在IaaS之上的,因此为了能够操作IaaS中所包含的各个资源,AaaS服务需要将自己的用户角色匹配到IaaS所定义的角色上。例如应用架构师需要能够在定义应用的时候需要知道IaaS上所具有的资源,并需要能够指定到底哪些人可以使用这些应用,因此其需要拥有IaaS的Tenant管理员,资源管理员及普通用户三种角色。而AaaS上的运维人员则只需要在部署和维护时察看IaaS上所拥有的资源,因此其只需要资源管理员及普通用户两种角色。
但是这么做有两点不好的地方:如果Microservice中只包含了几个服务,而且这种服务之间的依赖关系并不是很多,那么这种服务匹配还能够解决,但是如果整个系统之间各个子服务的沟通很多,那么在各个子服务之间进行角色匹配将变成一个噩梦:
解决该问题的方法就是使用我们上节所介绍的公共服务对它们进行管理。在提供一个集中的公共服务的情况下,我们就不再需要处理这么多的模型转化了:
除此之外,仅仅简单地对角色进行匹配实际上并不那么合适:就应用架构师而言,其需要的是查看当前的已有资源,却不需要对资源进行分配。因此其需要的是对资源的读权限。而运维人员则不仅仅需要能够读取资源信息,更需要对资源进行分配,因此其需要的是资源的读写权限。如果仅仅像上面那样在IaaS层为应用架构师赋予对资源的读写权限,那么应用架构师就可能拥有了错误的权限,进而执行了错误的操作。而相对地较为合适的方式则是对这些权限进行细分,即在权限中区分读写权限等:
因此在集中的公共服务中,我们需要使用较为细粒度的模型。该细粒度模型需要具有较高的灵活性,以能够无损地表示各个服务中的相应模型。
相信您现在已经能够看出,虽然说Microservice架构模式将单个子服务的实现简化了,但是复杂化了数据的处理。因此相较于我们以往所编写的应用,Microservice架构模式会在数据相关的一些特性上遇到一系列麻烦。
一个较为常见的麻烦就是保持多个子服务之间数据的一致性。我们知道,在服务中,保持一致性的工作常常是由事务来完成的。而如果希望在Microservice架构模式实现中保持子服务之间数据的一致性,我们可能就需要使用分布式事务了。但是分布式事务本身就是一个非常复杂并且难以操作的东西,因此就现在而言,这种问题实际上是非常难以解决的。但是反过来讲,事务本身也是表示一种逻辑上的强耦合,因此我们需要真正反思的则是这些需要使用事务来保持数据一致性的子服务是否应该属于同一个服务。当然,我们可以在某种程度上借鉴NoSQL数据库中的一些做法。例如在一个服务更新了数据以后,我们使用一种异步机制来保持数据的一致性,就好像很多NoSQL数据库不保证用户的数据立即可读一样。
另一个较为常见的麻烦就是粒度的问题。我们在前面已经说过,在Microservice的各个子服务之间进行服务间调用效率是十分低下的。为了减少多次服务间调用,各个子服务所提供的API的粒度需要尽量地粗,却需要尽量地保持灵活性。最好的情况就是可以通过一次服务间调用来得到所有想要的信息。
项目管理
除了上面所讨论的一系列技术因素之外,Microservice架构模式的开发还存在着一系列项目管理上的难题。
首先,由于Microservice架构模式中的各个子服务可能使用了不同的技术搭建,例如有些子服务是由Java开发的,有些则是由Python开发的,而且它们所使用的Servlet容器并不相同,因此由Microservice架构模式所搭建的应用可能需要非常复杂的环境设置。这对于传统的运维人员来说是非常困难的一个任务。而相对于这些运维人员而言,负责各个子服务开发的开发人员才是有关该服务运行及部署的专家。因此在Microservice架构模式中,开发及运维的职责均发生了变化:开发人员不仅仅需要负责子服务代码的编写,还需要考虑该子服务的日常运维。而运维人员需要向开发人员给出一些运维相关的建议,并在总的方向上掌控产品的日常运维。
这样做的好处则在于:开发人员会直接接触到生产环境,可以快速地跟踪并解决问题,而不再需要通过客户及运维人员的转述等步骤才开始处理问题,也避免了在转述过程中出现的偏差。除此之外,开发人员也能更清楚地了解用户到底是如何使用他们所创建出来的产品的,进而创建出来更容易被使用及管理的子服务。
但是这也会导致项目管理出现一些困难。首先,不论是开发人员还是管理者都需要了解并处理一系列运维相关的问题。这会分散他们的注意力,使得开发效率的降低。其次,由于一个子服务常常同时包含前端,后台,数据库,测试,甚至运维相关的一些任务,因此子服务的开发人员常常需要了解服务开发的大部分组成。这种人才在中国市场上并不多见,因此比较抢手。而且由于一个开发人员需要接触太多的功能和技术,因此很多时候没有办法深入地研究它们。由此所导致的问题则是,在遇到较为困难的问题时,软件开发人员需要花费较多的时间来分析并解决该问题。如果该问题较为严重,那么它将会严重影响整个组的开发进度。从项目管理的角度来讲,这实际上是一件非常危险的事情。
一个理想的解决方案就是,当前子服务所使用的各个技术都有一个专家。但是一个全栈开发人员,还需要是某一方面的技术专家,雇佣该人的成本可想而知。
除此之外,我们还需要在按照Microservice架构模式开发的时候使用一系列标准化的开发及测试流程。其中和Microservice最自然契合的就是现在最为流行的Continuous Delivery,或被称为是DevOps。在这些自动化流程的帮助下,软件开发人员可以快速地完成一次迭代:在对代码更改完毕以后,软件开发人员可以直接开始对自己的更改进行编译,运行单元测试及功能测试。接下来,系统将会把刚刚编译好的代码自动进行部署,并在整个系统中执行集成测试。在集成测试完毕之后,质量管理人员或软件开发人员自己会在该系统中进行一次测试,并在完成测试后进行复杂的性能测试,并在通过性能测试后进行部署。
所有这一切实际上都和使用Monolith开发时所使用的流程类似。唯一不同的是,在基于Microservice架构模式的开发中,这种自动化的流程变得更为重要了。因为基于Microservice架构模式所搭建的应用常常使用了不同的逻辑,因此部署一个完整的环境就会变得非常复杂。所以由这些自动化流程来负责测试环境的部署则大大地减轻了软件开发人员的负担,也是提高软件开发人员工作效率的基础。
同时由于软件开发人员需要随时执行应用程序的部署来测试自己刚刚所做的更改,因此其需要能够随时分配到其所需要的各个资源,如部署应用所需要的计算资源,内存以及存储等。而这种功能则正是云这种商业模式所提供的功能。因此在开发基于Microservice架构模式的应用时,我们则尽量基于某些云来开展我们的持续开发流程。
Microservice实现
在本节中,我们将对实现Microservice架构模式时所常用的一些方法进行讲解。
相信大家的第一个问题就是,Microservice架构模式中各个子服务应该如何相互协作以向用户提供服务的呢?按照上面我们的讲解,Microservice架构模式中各个子服务应该是独立的,否则它们之间将产生耦合,进而带来一系列问题:这些子服务彼此不独立,需要使用分布式事务保持其数据一致性,子服务不易被重用等。但是如果这些子服务绝对独立,甚至不包含一点点逻辑上的耦合,那么它们之间也将无法进行协作。因此在论坛讨论中常常出现的问题就是,这些子服务之间哪里可以出现耦合?可以出现什么程度的耦合?
这个问题实际上非常简单,那就是UI。我们知道,在一个BS服务中,服务端和客户端之间存在着一定程度的耦合。两者通过服务所暴露的API进行沟通。而基于Microservice架构模式的服务也不例外:
既然运行在用户浏览器中的UI需要与其它各个子服务进行交互,那么它完全可以作为一个中介者来完成各个子服务之间的交互。例如在显示产品页面的时候,该页面逻辑会向产品服务及库存服务同时发送请求,以并行地得到产品的详细信息以及该产品的当前库存。
因此在一个基于Microservice架构模式的服务中,常常会出现一个前端服务。该服务所提供的页面会与各个服务沟通。但是它实际上与各个子服务之间却不需要通讯:
或许您会说,在这种情况下,我们的各个子服务就没有UI了。而UI服务不仅仅需要处理所有的前端业务逻辑,而且随着时间的推移,其可能会变成另外一个庞然大物。除此之外,如果希望整个平台能够允许第三方服务接入,那么这种打包在一起的UI服务将变成整个平台扩展性的阻碍。
是的。如果需要解决这个问题,那么您就需要在应用中尝试借鉴Service Locator模式。此时我们需要的则是一个UI框架,其允许用户通过特定方式在应用中插入各个子服务所提供的UI,并允许您通过一些机制来发现已经在平台中注册的具有特定功能的API,并允许您对该API进行调用。我相信,随着Microservice架构模式的不断发展,会有越来越多的支持这种扩展方式的UI类库出现。
另外一种模式则是Message Broker。简单地说,Message Broker就是一个消息的中转平台。该平台允许其它组成向其中注册消息,也允许其它组成侦听消息。当一个组成将一个消息发送到了Message Broker之上后,其它侦听该消息的各个组成则会根据消息中所包含的信息更新自己的状态。
反过来,如果您的服务需要支持移动设备,如手机,iPad等,我们就不能让这些移动设备一个一个地访问子服务了。这是因为这些移动设备的带宽一般来说都非常小,而且用户常常处于信号不是很好的地方,因此在向这些子服务一个个地发送请求将快速消耗掉它们所拥有的有限的带宽。为了解决这个问题,我们常常需要在这些子服务前搭建一个代理服务。该代理服务会将用户请求根据业务逻辑拆分为对各个子服务的请求,并将各个子服务所返回的结果归纳为一个响应返回给用户:
当然,上面所介绍的仅仅是当前论坛讨论中所常常提到的一种搭建基于Microservice架构模式应用的方式。或许在不久的将来,您会看到设计得越来越精巧的各种模式出现。
在讲解完这些子服务该如何展现给用户之后,我们就来讲解一下如何创建各个子服务所需要的公共服务。之前我们已经提到过,由于对公共服务的调用是一个跨进程调用,因此其相较于进程内调用效率非常低下。在这种情况下,我们需要尽量避免对该公共服务的重复调用。为了达到该目标,我们需要尽量使用户访问同一个子服务实例,并且在该用户的会话中缓存从公共服务中所得到的信息。
因此在同一个子服务的各个服务实例上,我们需要尽量使用负载平衡服务的Sticky Session的功能,并在一次公共服务调用中取得多项信息。例如在查看用户的权限时,我们不是返回用户是否具有特定权限,而是该用户拥有哪些权限。当然,这不仅仅需要从Microservice这种架构模式的方面来考虑,还需要同时兼顾安全,维护性等一系列问题。
简单地说,在兼顾其它方面的情况下,我们需要将公共服务API的粒度定得粗一些,同时也需要具有一定的灵活性,从而通过减少服务间调用来避免整个服务的性能瓶颈。
既然说到了API的粒度,那我们就需要讨论一下各个子服务所提供的API了。和公共服务一样,各个子服务所暴露的API也应该具有较粗的粒度以及较大的灵活性。除此之外,我们还需要让这些子服务所暴露的API具有尽量一致的样式,如定义一系列RESTful的API。在这种情况下,与这些服务进行交互的组成,如网页的UI,才能具有可以接受的维护性。
一个经验性的观点则是,Microservice架构模式中的“开”是各个服务的内部实现,而其中的“闭”则是各个服务之间相互沟通的方式。
如果您需要从头开始搭建一个服务,那么您需要首先考虑如何对这些服务进行划分,并在创建第一个服务的时候开始搭建出各个公共服务的雏形,同时确定各个服务之间沟通所需要遵守的协议。当越来越多的子服务被创建出来之后,您需要逐渐丰富各个公共服务所提供的功能,使其逐渐变为功能强大的,可重用的服务。
如果您已经拥有一个Monolith服务,并且希望通过采用Microservice架构模式来缓解当前Monolith模式服务所具有的一系列问题,那么您首先需要创建一个独立的服务,并通过一个粘合层来与该Monolith服务交互。在该过程中,您可能需要将Monolith服务的内部接口逐渐暴露出来,以供这个新的服务使用。而这就是在抽象公共服务的过程。
接下来,您就需要根据上一步中所得到的接口来逐步将Monolith服务中的公共服务剥离。在剥离过程中,您脑中需要记得的一句话还是:粗粒度,灵活的API。而其内部实现到底是什么样的,实际上并不会影响到您的剥离结果。
最后就是再从Monolith中剥离其它服务了。此时我们最需要考虑的就是在服务中具有鲜明特点的各个服务,如对资源的要求与整个Monolith服务格格不入,或者使用了和Monolith很难兼容的技术等。
最后一种情况就是多个服务集成的情况。在产品的逐渐迭代过程中,我们常常会遇到需要将多个产品集成成为一个产品以提高整体竞争力的情况。这常常发生在盈利产品和其它非盈利产品之间。而这正是实践Microservice架构模式的绝佳机会。此时我们仅仅需要暴露一系列Monolith服务中的接口并创建粘合层即可。
Microservice的优点与劣势
好,在前面我们已经讲解了很多有关Microservice架构模式的经验性方法和相关知识。那我们现在回顾一下Microservice所具有的一系列优点和劣势,以使您能够在采用Microservice架构模式之前全面地衡量该方案所可能得到的好处及遇到的困难。
首先,由于Microservice架构模式中的每个子服务都可以独立于其它服务执行,因此其常常具有更好的服务边界。而这个明确的服务边界则会带来一系列好处:在Microservice架构模式中,各个子服务执行所需要的业务逻辑都相对集中于子服务内。因此其实现代码相对容易理解,并且便于维护。另外各个子服务所具有的结构,运行流程及数据模型都能够更贴近于子服务所表示的业务逻辑,因此在代码的开发速度和维护性上得到了大大地增强。同时各个子服务可以选择最适合实现业务逻辑的技术,进而使得各个服务的开发变得更为容易。同时在出现新的更适合的技术时,我们可以较为容易地在各个子服务内部对原有的实现技术进行替换。
独立性也意味着扩展性的增强。在Microservice架构模式中,各个子服务可以根据自身的负载独立地进行扩容,如Scale Up或Scale Out等。不仅如此,我们还可以根据子服务自身的特性为其准备特定的硬件设备,使得其运行在更适合的服务器上。同时这种独立性还可以使得各个子服务可以被重用。
同时这种独立性也可以增加整个服务的容错能力。例如如果一个子服务由于种种原因无法继续提供服务,其它子服务仍然可以独立地处理用户的请求。
另外,各个子服务的独立部署能力也可以大大地提高Continuous Delivery的运行效率。毕竟在这种情况下,软件开发人员只需要重新部署更改过的子服务就可以了。
由于Microservice架构模式中的各个子服务无论是在代码量方面还是最终生成的WAR包方面都较Monolith架构所搭建的服务小,因此在IDE支持,启动速度方面都具有相当的优势。同时,这种小粒度的服务已经可以由一个几个人所组成的小组来完成,而不再需要通过来自世界各地的不同小组协同开发,进而大大降低了沟通成本,提高了开发的效率。
但是反过来,Microservice架构模式中各个子服务的独立性也会导致一系列问题。最明显的就是需要多个子服务相互配合的情况。由于这些子服务是不同的进程,因此在这些进程之间保持数据的一致性,或添加一个新的跨子服务的用户用例实际上都是一件非常麻烦的事情。而且对这些独立服务在整个系统中是否能够工作的测试需要运行大量的集成测试。而如果需要快速地对这些子服务进行开发和迭代,那么我们就需要每个开发人员都能够专业并高效地使用一系列自动化工具。这实际上也是一个不低的要求。
除此之外,基于性能考虑,各个子服务所提供的接口将是粗粒度的,却具有较高灵活性的API。但是这种API拥有一个较明显的缺陷,那就是越灵活的API,其使用起来的难度就越大。因此对于服务的用户而言,其上手的难度则相对增加了。
另外,如何规范化各个子服务之间的沟通协议也是一个非常具有挑战性的事情。因为在Microservice架构模式中,我们常常需要创建一系列公共服务。这些公共服务常常暴露特定样式的接口以供其它服务调用。因此我们需要在这些接口上保持一致性,进而才能够更自然地编写各个子服务的内部逻辑并暴露适当的接口。但是反过来,一致的接口样式常常会导致各个服务的自然实现需要向这些标准进行妥协。因此我们常常需要在两者之间平衡。
这些平衡方法包括标准化各个服务所暴露的接口,使用固定的几种方式对子服务进行集成,保持数据模型格式的一致性等。这些实际上都是我们自由编写各个子服务的障碍。对此采取多么严格的规范实际上是需要通过经验累积来完成的,因此这大大提高了使用Microservice架构模式失败的概率。