在微服务架构中,须要调用不少服务才能完成一项功能。服务之间如何互相调用就变成微服务架构中的一个关键问题。服务调用有两种方式,一种是RPC方式,另外一种是事件驱动(Event-driven)方式,也就是发消息方式。消息方式是松耦合方式,比紧耦合的RPC方式要优越,但RPC方式若是用在适合的场景也有它的一席之地.html
耦合的种类: 咱们总在谈耦合,那么耦合到底意味着什么呢?数据库
- 时间耦合:客户端和服务端必须同时上线才能工做。发消息时,接受消息队列必须运行,但后台处理程序暂时不工做也不影响。
- 容量耦合:客户端和服务端的处理容量必须匹配。发消息时,若是后台处理能力不足也没关系,消息队列会起到缓冲的做用。
- 接口耦合:RPC调用有函数标签,而消息队列只是一个消息。例如买了商品以后要调用发货服务,若是是发消息,那么就只需发送一个商品被买消息。
- 发送方式耦合:RPC是点对点方式,须要知道对方是谁,它的好处是可以传回返回值。消息既能够点对点,也能够用广播的方式,这样减小了耦合,但也使返回值比较困难。
下面咱们来逐一分析这些耦合的影响。 第一,时间耦合,对于多数应用来说,你但愿能立刻获得回答,所以即便使用消息队列,后台也须要一直工做。第二,容量耦合,若是你对回复有时间要求,那么消息队列的缓冲功能做用不大,由于你但愿及时响应。真正须要的是自动伸缩(Auto-scaling),它能自动调整服务端处理能力去匹配请求数量。第三和第四,接口耦合和发送方式耦合,这两个确实是RPC方式的软肋。编程
事件驱动(Event-Driven)方式:
Martin Fowler把事件驱动分红四种方式(What do you mean by “Event-Driven”),简化以后本质上只有两种方式。 一种就是咱们熟悉的的事件通知(Event Notification),另外一种是事件溯源(Event Sourcing)。事件通知就是微服务之间不直接调用,而是经过发消息来进行合做。事件溯源有点像记帐,它把全部的事件都记录下来,做为永久存储层,再在它的基础之上构建应用程序。实际上从应用的角度来说,它们并不该该分属一类,它们的用途彻底不一样。事件通知是微服务的调用(或集成)方式,应该和RPC分在一块儿。事件溯源是一种存储数据的方式,应该和数据库分在一块儿。api
事件通知(Event Notification)方式:
让咱们用具体的例子来看一下。在下面的例子中,有三个微服务,“Order Service”, “Customer Service” 和“Product Service”.服务器
图片来源架构
先说读数据,假设要建立一个“Order”,在这个过程当中须要读取“Customer”的数据和“Product”数据。若是用事件通知的方式就只能在“Order Service”本地也建立只读“Customer”和“Product”表,并把数据用消息的方式同步过来。并发
再说写数据,若是在建立一个“Order”时须要建立一个新的“Customer”或要修改“Customer”的信息,那么能够在界面上跳转到用户建立页面,而后在“Customer Service”建立用户以后再发”用户已建立“的消息,“Order Service”接到消息,更新本地“Customer”表。app
这并非一个很好的使用事件驱动的例子,由于事件驱动的优势就是不一样的程序之间能够独立运行,没有绑定关系。但如今“Order Service”须要等待“Customer Service”建立完了以后才能继续运行,来完成整个建立“Order”的工做。主要是由于“Order”和“Customer”自己从逻辑上来说就是紧耦合关系,没有“Customer”你是不能建立“Order”的。运维
在这种紧耦合的状况下,也可使用RPC。你能够创建一个更高层级的管理程序来管理这些微服务之间的调用,这样“Order Service”就没必要直接调用“Customer Service”了。固然它从本质上来说并无解除耦合,只是把耦合转移到了上一层,但至少如今“order Service”和“Customer Service”能够互不影响了。之因此不能根除这种紧耦合关系是由于它们在业务上是紧耦合的。dom
再举一个购物的例子。用户选好商品以后进行“Checkout”,生成“Order”,而后须要“payment”,再从“Inventory”取货,最后由“Shipment”发货,它们每个都是微服务。这个例子用RPC方式和事件通知方式均可以完成。当用RPC方式时,由“Order”服务调用其余几个服务来完成整个功能。用事件通知方式时,“Checkout”服务完成以后发送“Order Placed”消息,“Payment”服务收到消息,接收用户付款,发送“Payment received”消息。“Inventory”服务收到消息,从仓库里取货,并发送“Goods fetched”消息。“Shipment”服务获得消息,发送货物,并发送“Goods shipped”消息。
对这个例子来说,使用事件驱动是一个不错的选择,由于每一个服务发消息以后它不须要任何反馈,这个消息由下一个模块接收来完成下一步动做,时间上的要求也比上一个要宽松。用事件驱动的好处是下降了耦合度,坏处是你如今不能在程序里找到整个购物过程的步骤。若是一个业务逻辑有它本身相对固定的流程和步骤,那么使用RPC或业务流程管理(BPM)可以更方便地管理这些流程。在这种状况下选哪一种方案呢?在我看来好处和坏处是大体至关的。从技术上来说要选事件驱动,从业务上来说要选RPC。不过如今愈来愈多的人采用事件通知做为微服务的集成方式,它彷佛已经成了微服务之间的标椎调用方式。
事件溯源(Event Sourcing):
这是一种具备颠覆性质的的设计,它把系统中全部的数据都以事件(Event)的方式记录下来,它的持久存储叫Event Store, 通常是创建在数据库或消息队列(例如Kafka)基础之上,并提供了对事件进行操做的接口,例如事件的读写和查询。事件溯源是由领域驱动设计(Domain-Driven Design)提出来的。DDD中有一个很重要的概念,有界上下文(Bounded Context),能够用有界上下文来划分微服务,每一个有界上下文均可以是一个微服务。 下面是有界上下文的示例。下图中有两个服务“Sales”和“Support”。有界上下文的一个关键是如何处理共享成员, 在图中是“Customer”和“Product”。在不一样的有界上下文中,共享成员的含义、用法以及他们的对象属性都会有些不一样,DDD建议这些共享成员在各自的有界上下文中都分别建本身的类(包括数据库表),而不是共享。能够经过数据同步的手段来保持数据的一致性。下面还会详细讲解。
事件溯源是微服务的一种存储方式,它是微服务的内部实现细节。所以你能够决定哪些微服务采用事件溯源方式,哪些不采用,而没必要全部的服务都变成事件溯源的。 一般整个应用程序只有一个Event Store, 不一样的微服务都经过向Event Store发送和接受消息而互相通讯。Event Store内部能够分红不一样的stream(至关于消息队列中的Topic), 供不一样的微服务中的领域实体(Domain Entity)使用。
事件溯源的一个短板是数据查询,它有两种方式来解决。第一种是直接对stream进行查询,这只适合stream比较小而且查询比较简单的状况。查询复杂的话,就要采用第二种方式,那就是创建一个只读数据库,把须要的数据放在库中进行查询。数据库中的数据经过监听Event Store中相关的事件来更新。
数据库存储方式只能保存当前状态,而事件溯源则存储了全部的历史状态,于是能根据须要回放到历史上任何一点的状态,具备很大优点。但它也不是一点问题都没有。第一,它的程序比较复杂,由于事件是一等公民,你必须把业务逻辑按照事件的方式整理出来,而后用事件来驱动程序。第二,若是你要想修改事件或事件的格式就比较麻烦,由于旧的事件已经存储在Event Store里了(事件就像日志,是只读的),没有办法再改。
因为事件溯源和事件通知表面上看起来很像,很多人都搞不清楚它们的区别。事件通知只是微服务的集成方式,程序内部是不使用事件溯源的,内部实现仍然是传统的数据库方式。只有当要与其余微服务集成时才会发消息。而在事件溯源中,事件是一等公民,能够不要数据库,所有数据都是按照事件的方式存储的。
虽然事件溯源的践行者有不一样的意见,但有很多人都认为事件溯源不是微服务的集成方式,而是微服务的一种内部实现方式。所以,在一个系统中,能够某些微服务用事件溯源,另一些微服务用数据库。当你要集成这些微服务时,你能够用事件通知的方式。注意如今有两种不一样的事件须要区分开,一种是微服务的内部事件,是颗粒度比较细的,这种事件只发送到这个微服务的stream中,只被事件溯源使用。另外一种是其余微服务也关心的,是颗粒度比较粗的,这种事件会放到另一个或几个stream中,被多个微服务使用,是用来作服务之间集成的。这样作的好处是限制了事件的做用范围,减小了不相关事件对程序的干扰。详见"Domain Events vs. Event Sourcing".
事件溯源出现已经很长时间了,虽然热度一直在上升(尤为是这两年),但总的来讲很是缓慢,谈论的人很多,但生产环境使用的很少。究其缘由就是应为它对如今的体系结构颠覆太大,须要更改数据存储结构和程序的工做方式,仍是有必定风险的。另外,微服务已经造成了一整套体系,从程序部署,服务发现与注册,到监控,服务韧性(Service Resilience),它们基本上都是针对RPC的,虽然也支持消息,但成熟度就差多了,所以有很多工做仍是要本身来作。有意思的是Kafka一直在推进它做为事件驱动的工具,也取得了很大的成功。但它却没有获得事件溯源圈内的承认(详见这里)。 多数事件溯源都使用一个叫evenstore的开源Event Store,或是基于某个数据库的Event Store,只有比较少的人用Kafka作Event Store。 但若是用Kafka实现事件通知就一点问题都没有。总的来讲,对大多数公司来说事件溯源是有必定挑战的,应用时须要找到合适的场景。若是你要尝试的话,能够先拿一个微服务试水。
虽然如今事件驱动还有些生涩,但从长远来说,仍是很看好它的。像其余全新的技术同样,事件溯源须要大规模的适用场景来推进。例如容器技术就是由于微服务的流行和推进,才走向主流。事件溯源之前的适用场景只限于记帐和源代码库,局限性较大。区块链可能会成为它的下一个机遇,由于它用的也是事件溯源技术。另外AI从此会渗入到具体程序中,使程序具备学习功能。而RPC模式注定没有自适应功能。事件驱动自己就具备对事件进行反应的能力,这是自我学习的基础。所以,这项技术长远来说定会大放异彩,但短时间内(3-5年)大概不会成为主流。
RPC方式:
RPC的方式就是远程函数调用,像RESTFul,gRPC, DUBBO 都是这种方式。它通常是同步的,能够立刻获得结果。在实际中,大多数应用都要求马上获得结果,这时同步方式更有优点,代码也更简单。
服务网关(API Gateway):
熟悉微服务的人可能都知道服务网关(API Gateway)。当UI须要调用不少微服务时,它须要了解每一个服务的接口,这个工做量很大。因而就用服务网关建立了一个Facade,把几个微服务封装起来,这样UI就只调用服务网关就能够了,不须要去对付每个微服务。下面是API Gateway示例图:
服务网关(API Gateway)不是为了解决微服务之间调用的紧耦合问题,它主要是为了简化客户端的工做。其实它还能够用来下降函数之间的耦合度。 有了API Gateway以后,一旦服务接口修改,你可能只须要修改API Gateway, 而没必要修改每一个调用这个函数的客户端,这样就减小了程序的耦合性。
服务调用:
能够借鉴API Gateway的思路来减小RPC调用的耦合度,例如把多个微服务组织起来造成一个完整功能的服务组合,并对外提供统一的服务接口。这种想法跟上面的API Gateway有些类似,都是把服务集中起来提供粗颗粒(Coarse Granular)服务,而不是细颗粒的服务(Fine Granular)。但这样创建的服务组合可能只适合一个程序使用,没有多少共享价值。所以若是有合适的场景就采用,否侧也没必要强求。虽然咱们不能下降RPC服务之间的耦合度,却能够减小这种紧耦合带来的影响。
下降紧耦合的影响:
什么是紧耦合的主要问题呢?就是客户端和服务端的升级不一样步。服务端老是先升级,客户端可能有不少,若是要求它们同时升级是不现实的。它们有各自的部署时间表,通常都会选择在下一次部署时顺带升级。
通常有两个办法能够解决这个问题:
- 同时支持多个版本:这个工做量比较大,所以大多数公司都不会采用这种方式。
- 服务端向后兼容:这是更通用的方式。例如你要加一个新功能或有些客户要求给原来的函数增长一个新的参数,但别的客户不须要这个参数。这时你只好新建一个函数,跟原来的功能差很少,只是多了一个参数。这样新旧客户的需求都能知足。它的好处是向后兼容(固然这取决于你使用的协议)。它的坏处是当之后新的客户来了,看到两个差很少的函数就糊涂了,不知道该用那个。并且时间越长越严重,你的服务端可能功能增长的很少,但类似的函数却愈来愈多,没法选择。
它的解决办法就是使用一个支持向后兼容的RPC协议,如今最好的就是Protobuf+gRPC,尤为是在向后兼容上。它给每一个服务定义了一个接口,这个接口是与编程语言无关的中性接口,而后你能够用工具生成各个语言的实现代码,供不一样语言使用。函数定义的变量都有编号,变量能够是可选类型的,这样就比较好地解决了函数兼容的问题。就用上面的例子,当你要增长一个可选参数时,你就定义一个新的可选变量。因为它是可选的,原来的客户端不须要提供这个参数,所以不须要修改程序。而新的客户端能够提供这个参数。你只要在服务端能同时处理这两种状况就好了。这样服务端并无增长新的函数,但用户的新需求知足了,并且仍是向后兼容的。
微服务的数量有没有上限?
总的来讲微服务的数量不要太多,否则会有比较重的运维负担。有一点须要明确的是微服务的流行不是由于技术上的创新,而是为了知足管理上的须要。单体程序大了以后,各个模块的部署时间要求不一样,对服务器的优化要求也不一样,并且团队人数众多,很难协调管理。把程序拆分红微服务以后,每一个团队负责几个服务,就容易管理了,并且每一个团队也能够按照本身的节奏进行创新,但它给运维带来了巨大的麻烦。因此在微服务刚出来时,我一直以为它是一个退步,弊大于利。但因为管理上的问题没有其余解决方案,只有硬着头皮上了。值得庆幸的是微服务带来的麻烦都是可解的。直到后来,微服务创建了全套的自动化体系,从程序集成到部署,从全链路跟踪到日志,以及服务检测,服务发现和注册,这样才把微服务的工做量降了下来。虽然微服务在技术上一无可取,但它的流行仍是大大推进了容器技术,服务网格(Service Mesh)和全链路跟踪等新技术的发展。不过它自己在技术上仍是没有发现任何优点。。直到有一天,我意识到单体程序其实性能调试是很困难的(很难分离出瓶颈点),而微服务配置了全链路跟踪以后,能很快找到症结所在。看来微服务从技术来说也不全是缺点,总算也有好的地方。但微服务的颗粒度不宜过细,不然工做量仍是太大。
通常规模的公司十几个或几十个微服务都是能够承受的,但若是有几百个甚至上千个,那么毫不是通常公司能够管理的。尽管现有的工具已经很齐全了,并且与微服务有关的整个流程也已经基本上所有自动化了,但它仍是会增长不少工做。Martin Fowler几年之前建议先从单体程序开始(详见 MonolithFirst),而后再逐步把功能拆分出去,变成一个个的微服务。可是后来有人反对这个建议,他也有些松口了。若是单体程序不是太大,这是个好主意。能够用数据额库表的数量来衡量程序的大小,我见过大的单体程序有几百张表,这就太多了,很难管理。正常状况下,一个微服务能够有两、三张表到5、六张表,通常不超过十张表。但若是要减小微服务数量的话,能够把这个标准放宽到不要超过二十张表。用这个作为大体的指标来建立微程序,若是使用一段时间以后仍是以为太大了,那么再逐渐拆分。固然,按照这个标准创建的服务更像是服务组合,而不是单个的微服务。不过它会为你减小工做量。只要不影响业务部门的创新进度,这是一个不错的方案。
到底应不该该选择微服务呢?若是单体程序已经无法管理了,那么你别无选择。若是没有管理上的问题,那么微服务带给你的只有问题和麻烦。其实,通常公司都没有太多选择,只能采用微服务,不过你能够选择创建比较少的微服务。若是仍是无法决定,有一个折中的方案,“内部微服务设计”。
内部微服务设计:
这种设计表面上看起来是一个单体程序,它只有一个源代码存储仓库,一个数据库,一个部署,但在程序内部能够按照微服务的思想来进行设计。它能够分红多个模块,每一个模块是一个微服务,能够由不一样的团队管理。
用这张图作例子。这个图里的每一个圆角方块大体是一个微服务,但咱们能够把它做为一个单体程序来设计,内部有五个微服务。每一个模块都有本身的数据库表,它们都在一个数据库中,但模块之间不能跨数据库访问(不要创建模块之间数据库表的外键)。“User”(在Conference Management模块中)是一个共享的类,但在不一样的模块中的名字不一样,含义和用法也不一样,成员也不同(例如,在“Customer Service”里叫“Customer”)。DDD(Domain-Driven Design)建议不要共享这个类,而是在每个有界上下文(模块)中都建一个新类,并拥有新的名字。虽然它们的数据库中的数据应该大体相同,但DDD建议每个有界上下文中都建一个新表,它们之间再进行数据同步。
这个所谓的“内部微服务设计”其实就是DDD,但当时尚未微服务,所以外表看起来是单体程序,但内部已是微服务的设计了。它的书在2003就出版了,当时就颇有名。但它更偏重于业务逻辑的设计,践行起来也比较困难,所以你们谈论得不少,真正用的较少。直到十年以后,微服务出来以后,人们发现它其实内部就是微服务,并且微服务的设计须要用它的思想来指导,因而就又从新焕发了青春,并且此次更猛,已经到了每一个谈论微服务的人都不得不谈论DDD的地步。不过一本软件书籍,在十年以后还能指导新技术的设计,很是使人钦佩。
这样设计的好处是它是一个单体程序,省去了多个微服务带来的部署、运维的麻烦。但它内部是按微服务设计的,若是之后要拆分红微服务会比较容易。至于何时拆分不是一个技术问题。若是负责这个单体程序的各个团队之间不能在部署时间表,服务器优化等方面达成一致,那么就须要拆分了。固然你也要应对随之而来的各类运维麻烦。内部微服务设计是一个折中的方案,若是你想试水微服务,但又不肯意冒太大风险时,这是一个不错的选择。
结论:
微服务之间的调用有两种方式,RPC和事件驱动。事件驱动是更好的方式,由于它是松耦合的。但若是业务逻辑是紧耦合的,RPC方式也是可行的(它的好处是代码更简单),并且你还能够经过选取合适的协议(Protobuf+gRPC)来下降这种紧耦合带来的危害。因为事件溯源和事件通知的类似性,不少人把二者弄混了,但它们其实是彻底不一样的东西。微服务的数量不宜太多,能够先建立比较大的微服务(更像是服务组合)。若是你仍是不能肯定是否采用微服务架构,能够先从“内部微服务设计”开始,再逐渐拆分。
索引:
1 What do you mean by “Event-Driven”
[3] BoundedContext
[4] Domain Events vs. Event Sourcing
[5] Using Kafka as a (CQRS) Eventstore. Good idea?
[6] Evenstore
[7] MonolithFirst