写点什么

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

作者:看山
  • 2021 年 12 月 12 日
  • 本文字数:2380 字

    阅读完需:约 8 分钟

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

你好,我是看山。


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



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


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


原因

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



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



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


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


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


  1. 想要在微服务架构中实现单体架构中模块间的关系;

  2. 想要在分布式应用中实现数据的强一致性。


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


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

方法

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


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


  1. 点对点主动通知,直接找到这个人,给这个人打电话,不通继续打,保证对方能够收到消息;

  2. 点对面广播通知,比如,群里发给公告、公司的告示栏等,这种方式需要消息接受者主动查看公告信息。


这两种方式对应了我们系统设计中消息传递的两个模式:指令(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 个。


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

文末总结

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


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。

发布于: 2021 年 12 月 12 日阅读数: 21
用户头像

看山

关注

公众号「看山的小屋」。 2017.10.26 加入

Java 后端研发,CSDN 博客专家,专注后端开发、架构相关知识分享,个人网站 https://howardliu.cn/。

评论

发布
暂无评论
微服务架构的陷阱:从单体到分布式单体