微服务架构的陷阱:从单体到分布式单体

你好,我是看山。

前面咱们聊了架构的演进过程,提到单体架构、SOA 架构、微服务架构、无服务架构。整个过程如下图:

架构演进过程:单体架构、早期服务化、SOA 架构、微服务架构

目前无服务架构还未成熟,只能满足一些简单场景。所以大家在设计软件架构时,首选还是微服务架构。然后我们又聊了聊如何把单体架构改造为微服务架构,推荐采用绞杀模式,一步一步的实现系统微服务化。

在这个过程中,我们会碰到微服务架构的一个大坑:分布式错觉,即将分布式当成了微服务的全部(充要条件)。

原因

出现分布式单体的主要原因在于,只是用进程间的远程调用替换进程内的方法调用。

模块 A 与模块 B 之间的通信方式

从上图可以看出,单体架构在模块 A 与模块 B 之间的请求是通过进程内通信(通常是方法调用)实现的;在微服务架构中,两者之间是通过 REST 或 RPC 调用。抛开进程和消息通知机制的差异,两种架构中模块 A 与模块 B 之间的通信形式完全一致:

单体/微服务架构中模块间通信方式

在这种情况下,模块 A 与模块 B 耦合在一起,任何一方变更请求契约(方法签名或接口参数),另外一个都必须同步修改。更糟糕的是,由于微服务架构服务之间是通过网络通信,由于其不可靠性和不稳定性,大大增加了出错的概率,使模块之间的调用关系更加脆弱。

模块 A 与模块 B 之间的网络请求是同步调用,请求过程中会占用一个网络连接和至少一个线程,如果模块 A 与模块 B 所在的服务的承压能力不同,很有可能模块 B 所在服务被打满,后续模块 A 的请求会阻塞等待,直到请求超时。

那又是什么原因让大家没有意识到这种方式不妥呢?原因有两个:

  1. 想要在微服务架构中实现单体架构中模块间的关系;
  2. 想要在分布式应用中实现数据的强一致性。

针对于微服务架构中的数据一致性问题,可以参考 关于微服务系统中数据一致性的总结

下面我们重点说说如果解决第一个问题。

方法

对于模块之间的关系,主要在于通信模式,对于查询请求,由于数据依赖,模块之间的耦合是天然的,我们这里要解耦的是数据变更(增、删、改)时的模块调用。

类比一下现实,我们如果想要通知某些人一个消息,会怎么处理?一般来说,有两种方式:

  1. 点对点主动通知,直接找到这个人,给这个人打电话,不通继续打,保证对方能够收到消息;
  2. 点对面广播通知,比如,群里发给公告、公司的告示栏等,这种方式需要消息接受者主动查看公告信息。

这两种方式对应了我们系统设计中消息传递的两个模式:指令(Command)、事件(Event)。

指令(Command)和事件(Event)

command 与 event,指令与事件

指令(Command)是表示从发起者(source)向执行者(destination)传递(send)一个必须执行某个动作(action)的请求(request)。这个模式有如下特点:

  • 明确的知道发起者和执行者,发起者依赖执行者;
  • 请求发送方式一般是点对点同步请求,一般是 RPC 请求;
  • 动作已发生或即将发生,有可能由于执行者拒绝执行而取消;
  • 执行者有可能拒绝执行;
  • 执行者会很明确的告知发起者指令执行情况:拒绝、成功、失败等。
  • 为了保证指令的有效触达,发起者在网络超时时会重复调用执行者,所以执行者需要实现请求幂等;
  • 执行者可能会成为下一个指令的发起者。

事件(event)是表示由生产者(producer)发布(public)一个已经发生的事情,表示行为(action)已经发生,某些状态(status)发生了改变,消费者(consumer)订阅这些事件,然后做出响应。这个模式有如下特点:

  • 事件有明确的生产者,但是消费者不明确,甚至可能不存在;
  • 一般借助消息中间件实现事件发送、存储、传递等;
  • 行为已经发生,不可改变,不可逆,事件是对已经发生事情的客观描述;
  • 消费者根据消息选择处理方式:执行、抛弃等;
  • 消费者处理完成后,不需要回复生产者;
  • 一般消息中间件采用“至少一次”通知机制,所以消费者需要实现消息处理的幂等;
  • 消费者可能会成为下一个事件的生产者。

指令与事件之间的区别

由于请求模式的不同,在依赖关系上就会发生改变:

在指令模式中,模块 A 调用模块 B,属于直接调用,模块 A 需要依赖模块 B;在事件模式中,模块 A 把事件发送给消息中间件,其他需要订阅事件的服务,直接从消息中间件获取,这种会产生依赖倒置,模块 B 依赖模块 A。这是解耦模块 A 与模块 B 很好的方式。

重新定义微服务

我们再回过头来看看我们的问题:

单体/微服务架构中模块间通信方式

此时我们会比较清晰,由于全系统中使用了指令模式,上次调用者依赖下层,由于是同步请求,依赖会发生传递,这种依赖传递,将整个系统耦合在一起,一处修改,处处变动,也就是我们在抨击单体架构时常说的牵一发而动全身。

此时,我们就可以借助事件模式,将依赖链条打断。但是需要注意,不要矫枉过正的全部改为事件模式,那将会是另一个火坑。一般我们会将系统改造成下面的样子:

重新定义微服务

根据业务具体情况,我们可以归纳一下改造结果:

  1. 服务 A 接到的请求可能是事件或指令;
  2. 服务 A 会向服务 B 发送指令,也会向消息中间件发送事件;
  3. 服务 B 接到指令后开始执行,执行完毕后,可能会向消息中间件发送事件;
  4. 服务 C 定于事件,从消息中间件接到消息后处理,它可能发送事件或指令。

需要注意的是,每个服务内部还有有一些操作。抽象一下,整个系统中的指令、事件、操作如下图:

指令和事件的定义

  • 输入:以一个指令或事件作为输入,开始整个业务执行;
  • 服务内部操作:服务内部会有执行逻辑,比如操作数据库、访问缓存服务等。可选,0-N 个;
  • 指令调用:同步调用依赖服务,发送指令,获得结果。可选,0-N 个;
  • 发布事件:以消息形式发布事件,一般发布到消息中间件,其他发布订阅消息中间件,执行事件需要的行为。可选,事件一般是 0-1 个。

架构设计的过程不是非此即彼,全部指令会造成耦合,全部事件会致使开发难度提升以及边界不清。我们需要理性的看待两种模式,做到不偏不倚。

文末总结

本文从分布式单体陷阱展开,讲述了分布式错觉带来的问题,然后通过事件、指令两种模式相结合的方式解决问题。微服务是目前比较完善的架构风格,从单体到微服务架构,是要实现架构的升级,所以调用模式不会一成不变。这个陷阱,也是我们在做新系统时需要避免的。

推荐阅读


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。

个人主页:https://www.howardliu.cn
个人博文:微服务架构的陷阱:从单体到分布式单体
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:微服务架构的陷阱:从单体到分布式单体

👇🏻欢迎关注我的公众号「看山的小屋」,领取精选资料👇🏻

公众号:看山的小屋