微服务调用方式

简介

在微服务架构中,大多数场景中需要调用多个服务才能完成一项功能。服务之间如何互相调用就变成微服务架构中的一个关键问题。服务调用有两种方式: RPC方式和事件驱动方式。

耦合的种类:

  • 时间耦合:客户端和服务端必须同时上线才能工作。发消息时,接受消息队列必须运行,但后台处理程序暂时不工作也不影响。对于多数应用来讲,你希望能马上得到回答,因此即使使用消息队列,后台也需要一直工作。

  • 容量耦合:客户端和服务端的处理容量必须匹配。发消息时,如果后台处理能力不足也不要紧,消息队列会起到缓冲的作用。如果你对回复有时间要求,那么消息队列的缓冲功能作用不大,因为你希望及时响应。真正需要的是自动伸缩(Auto-scaling),它能自动调整服务端处理能力去匹配请求数量

  • 接口耦合:RPC调用有函数标签,而消息队列只是一个消息。例如买了商品之后要调用发货服务,如果是发消息,那么就只需发送一个商品被买消息。

  • 发送方式耦合:RPC是点对点方式,需要知道对方是谁,它的好处是能够传回返回值。消息既可以点对点,也可以用广播的方式,这样减少了耦合,但也使返回值比较困难。

事件驱动(Event-Driven)方式

Martin Fowler把事件驱动分成四种方式(What do you mean by “Event-Driven”),简化之后本质上只有两种方式:事件通知(Event Notification)和事件溯源(Event Sourcing)。

事件通知就是微服务之间不直接调用,而是通过发消息来进行合作。事件溯源有点像记账,它把所有的事件都记录下来,作为永久存储层,再在它的基础之上构建应用程序。

实际上从应用的角度来讲,它们并不应该分属一类,它们的用途完全不同。事件通知是微服务的调用(或集成)方式,应该和RPC分在一起。事件溯源是一种存储数据的方式,应该和数据库分在一起。

事件通知(Event Notification)方式

让我们用具体的例子来看一下。在下面的例子中,有三个微服务,“Order Service”, “Customer Service” 和“Product Service”。

在这里插入图片描述

读数据:假设要创建一个“Order”,在这个过程中需要读取“Customer”数据和“Product”数据。如果用事件通知的方式就只能在“Order Service”本地也创建只读“Customer”和“Product”表,并把数据用消息的方式同步过来。

写数据:如果在创建一个“Order”时需要创建一个新的“Customer”或要修改“Customer”的信息,那么可以在界面上跳转到用户创建页面,然后在“Customer Service”创建用户之后再发”用户已创建“的消息,“Order Service”接到消息,更新本地“Customer”表。

再举一个购物的例子。用户选好商品之后进行“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里了(事件就像日志,是只读的),没有办法再改。

事件溯源和事件驱动比较:

事件通知只是微服务的集成方式,程序内部是不使用事件溯源的,内部实现仍然是传统的数据库方式。只有当要与其他微服务集成时才会发消息。而在事件溯源中,事件是一等公民,可以不要数据库,全部数据都是按照事件的方式存储的。

RPC方式

RPC的方式就是远程函数调用,像RESTFul,gRPC, DUBBO 都是这种方式。它一般是同步的,可以马上得到结果。在实际中,大多数应用都要求立刻得到结果,这时同步方式更有优势,代码也更简单。

服务网关(API Gateway)

熟悉微服务的人可能都知道服务网关(API Gateway)。当UI需要调用很多微服务时,它需要了解每个服务的接口,这个工作量很大。

于是就用服务网关创建了一个Facade,把几个微服务封装起来,这样UI就只调用服务网关就可以了,不需要去对付每一个微服务。下面是API Gateway示例图:

在这里插入图片描述

服务网关(API Gateway)不是为了解决微服务之间调用的紧耦合问题,它主要是为了简化客户端的工作。其实它还可以用来降低函数之间的耦合度。

服务调用

把多个微服务组织起来形成一个完整功能的服务组合,并对外提供统一的服务接口。但这样建立的服务组合可能只适合一个程序使用,没有多少共享价值。

降低紧耦合的影响

什么是紧耦合的主要问题呢?就是客户端和服务端的升级不同步。服务端总是先升级,客户端可能有很多,如果要求它们同时升级是不现实的。它们有各自的部署时间表,一般都会选择在下一次部署时顺带升级。

有两个办法可以解决这个问题:

  • 同时支持多个版本:这个工作量比较大,因此大多数公司都不会采用这种方式。

  • 服务端向后兼容:这是更通用的方式。例如你要加一个新功能或有些客户要求给原来的函数增加一个新的参数,但别的客户不需要这个参数。这时你只好新建一个函数,跟原来的功能差不多,只是多了一个参数。这样新旧客户的需求都能满足。

服务向后兼容缺点

它的坏处是当以后新的客户来了,看到两个差不多的函数就糊涂了,不知道该用那个。而且时间越长越严重,你的服务端可能功能增加的不多,但相似的函数却越来越多,无法选择。

它的解决办法就是使用一个支持向后兼容的RPC协议,现在最好的就是Protobuf gRPC,尤其是在向后兼容上。

它给每个服务定义了一个接口,这个接口是与编程语言无关的中性接口,然后你可以用工具生成各个语言的实现代码,供不同语言使用。函数定义的变量都有编号,变量可以是可选类型的,这样就比较好地解决了函数兼容的问题。

就用上面的例子,当你要增加一个可选参数时,你就定义一个新的可选变量。由于它是可选的,原来的客户端不需要提供这个参数,因此不需要修改程序。

微服务的数量

  • 微服务数量多的缺点:

比较重的运维负担。单体程序大了之后,各个模块的部署时间要求不同,对服务器的优化要求也不同,而且团队人数众多,很难协调管理。

  • 解决办法:

微服务建立了全套的自动化体系,从程序集成到部署,从全链路跟踪到日志,以及服务检测,服务发现和注册,这样才把微服务的工作量降了下来。

可以用数据库表的数量来衡量程序的大小,我见过大的单体程序有几百张表,这就太多了,很难管理。正常情况下,一个微服务可以有两、三张表到五、六张表,一般不超过十张表。但如果要减少微服务数量的话,可以把这个标准放宽到不要超过二十张表。用这个做为大致的指标来创建微程序,如果使用一段时间之后还是觉得太大了,那么再逐渐拆分。

  • 单体服务还是微服务

到底应不应该选择微服务呢?如果单体程序已经没法管理了,那么你别无选择。如果没有管理上的问题,那么微服务带给你的只有问题和麻烦。其实,一般公司都没有太多选择,只能采用微服务,不过你可以选择建立比较少的微服务。如果还是没法决定,有一个折中的方案,“内部微服务设计”。

内部微服务设计

这种设计表面上看起来是一个单体程序,它只有一个源代码存储仓库,一个数据库,一个部署,但在程序内部可以按照微服务的思想来进行设计。它可以分成多个模块,每个模块是一个微服务,可以由不同的团队管理。

在这里插入图片描述

用这张图做例子。这个图里的每个圆角方块大致是一个微服务,但我们可以把它作为一个单体程序来设计,内部有五个微服务。

每个模块都有自己的数据库表,它们都在一个数据库中,但模块之间不能跨数据库访问(不要建立模块之间数据库表的外键)。

“User”(在Conference Management模块中)是一个共享的类,但在不同的模块中的名字不同,含义和用法也不同,成员也不一样(例如,在“Customer Service”里叫“Customer”)。

DDD(Domain-Driven Design)建议不要共享这个类,而是在每一个有界上下文(模块)中都建一个新类,并拥有新的名字。

虽然它们的数据库中的数据应该大致相同,但DDD建议每一个有界上下文中都建一个新表,它们之间再进行数据同步。这个所谓的“内部微服务设计”其实就是DDD,但当时还没有微服务,因此外表看起来是单体程序,但内部已经是微服务的设计了。

这样设计的好处是它是一个单体程序,省去了多个微服务带来的部署、运维的麻烦。但它内部是按微服务设计的,如果以后要拆分成微服务会比较容易。至于什么时候拆分不是一个技术问题。如果负责这个单体程序的各个团队之间不能在部署时间表,服务器优化等方面达成一致,那么就需要拆分了。

结论

  • 微服务之间的调用有两种方式,RPC和事件驱动。事件驱动是更好的方式,因为它是松耦合的。但如果业务逻辑是紧耦合的,RPC方式也是可行的(它的好处是代码更简单),而且你还可以通过选取合适的协议(Protobuf gRPC)来降低这种紧耦合带来的危害。

  • 由于事件溯源和事件通知的相似性,很多人把两者弄混了,但它们实际上是完全不同的东西。微服务的数量不宜太多,可以先创建比较大的微服务(更像是服务组合)。

  • 如果你还是不能确定是否采用微服务架构,可以先从“内部微服务设计”开始,再逐渐拆分。

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大张哥儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值