你好,我是陈皓,网名左耳朵耗子。
前面所说的隔离设计通常都需要对系统做解耦设计,而把一个单体系统解耦,不单单是把业务功能拆分出来,正如前面所说,拆分完后还会面对很多的问题。其中一个重要的问题就是这些系统间的通讯。
通讯一般来说分同步和异步两种。同步通讯就像打电话,需要实时响应,而异步通讯就像发邮件,不需要马上回复。各有千秋,我们很难说谁比谁好。但是在面对超高吞吐量的场景下,异步处理就比同步处理有比较大的优势了,这就好像一个人不可能同时接打很多电话,但是他可以同时接收很多的电子邮件一样。
同步调用虽然让系统间只耦合于接口,而且实时性也会比异步调用要高,但是我们也需要知道同步调用会带来如下几个问题。
-
同步调用需要被调用方的吞吐不低于调用方的吞吐。否则会导致被调用方因为性能不足而拖死调用方。换句话说,整个同步调用链的性能会由最慢的那个服务所决定。
-
同步调用会导致调用方一直在等待被调用方完成,如果一层接一层地同步调用下去,所有的参与方会有相同的等待时间。这会非常消耗调用方的资源。因为调用方需要保存现场(Context)等待远端返回,所以对于并发比较高的场景来说,这样的等待可能会极度消耗资源。
-
同步调用只能是一对一的,很难做到一对多。
-
同步调用最不好的是,如果被调用方有问题,那么其调用方就会跟着出问题,于是会出现多米诺骨牌效应,故障一下就蔓延开来。
所以,异步通讯相对于同步通讯来说,除了可以增加系统的吞吐量之外,最大的一个好处是其可以让服务间的解耦更为彻底,系统的调用方和被调用方可以按照自己的速率而不是步调一致,从而可以更好地保护系统,让系统更有弹力。
异步通讯通常来说有三种方式。
异步通讯的三种方式
请求响应式
在这种情况下,发送方(sender)会直接请求接收方(receiver),被请求方接收到请求后,直接返回——收到请求,正在处理。
对于返回结果,有两种方法,一种是发送方时不时地去轮询一下,问一下干没干完。另一种方式是发送方注册一个回调方法,也就是接收方处理完后回调请求方。这种架构模型在以前的网上支付中比较常见,页面先从商家跳转到支付宝或银行,商家会把回调的URL传给支付页面,支付完后,再跳转回商家的URL。
很明显,这种情况下还是有一定耦合的。是发送方依赖于接收方,并且要把自己的回调发送给接收方,处理完后回调。
通过订阅的方式
这种情况下,接收方(receiver)会来订阅发送方(sender)的消息,发送方会把相关的消息或数据放到接收方所订阅的队列中,而接收方会从队列中获取数据。
这种方式下,发送方并不关心订阅方的处理结果,它只是告诉订阅方有事要干,收完消息后给个ACK就好了,你干成啥样我不关心。这个方式常用于像MVC(Model-View-Control)这样的设计模式下,如下图所示。

这就好像下订单的时候,一旦用户支付完成了,就需要把这个事件通知给订单处理以及物流,订单处理变更状态,物流服务需要从仓库服务分配相应的库存并准备配送,后续这些处理的结果无需告诉支付服务。
为什么要做成这样?好了,重点来了!前面那种请求响应的方式就像函数调用一样,这种方式有数据有状态的往来(也就是说需要有请求数据、返回数据,服务里面还可能需要保存调用的状态),所以服务是有状态的。如果我们把服务的状态给去掉(通过第三方的状态服务来保证),那么服务间的依赖就只有事件了。
你知道,分布式系统的服务设计是需要向无状态服务(Stateless)努力的,这其中有太多的好处,无状态意味着你可以非常方便地运维。所以,事件通讯成为了异步通讯中最重要的一个设计模式。
就上面支付的那个例子,商家这边只需要订阅一个支付完成的事件,这个事件带一个订单号,而不需要让支付方知道自己的回调URL,这样的异步是不是更干净一些?
但是,在这种方式下,接收方需要向发送方订阅事件,所以是接收方依赖于发送方。这种方式还是有一定的耦合。
通过Broker的方式
所谓Broker,就是一个中间人,发送方(sender)和接收方(receiver)都互相看不到对方,它们看得到的是一个Broker,发送方向Broker发送消息,接收方向Broker订阅消息。如下图所示。

这是完全的解耦。所有的服务都不需要相互依赖,而是依赖于一个中间件Broker。这个Broker是一个像数据总线一样的东西,所有的服务要接收数据和发送数据都发到这个总线上,这个总线就像协议一样,让服务间的通讯变得标准和可控。
在Broker这种模式下,发送方的服务和接收方的服务最大程度地解耦。但是所有人都依赖于一个总线,所以这个总线就需要有如下的特性:
- 必须是高可用的,因为它成了整个系统的关键;
- 必须是高性能而且是可以水平扩展的;
- 必须是可以持久化不丢数据的。
要做到这三条还是比较难的。当然,好在现在开源软件或云平台上Broker的软件是非常成熟的,所以节省了我们很多的精力。
事件驱动设计
上述的第二种和第三种方式就是比较著名的事件驱动架构(EDA – Event Driven Architecture)。正如前面所说,事件驱动最好是使用Broker方式,服务间通过交换消息来完成交流和整个流程的驱动。
如下图所示,这是一个订单处理流程。下单服务通知订单服务有订单要处理,而订单服务生成订单后发出通知,库存服务和支付服务得到通知后,一边是占住库存,另一边是让用户支付,等待用户支付完成后通知配送服务进行商品配送。

每个服务都是“自包含”的。所谓“自包含”也就是没有和别人产生依赖。而要把整个流程给串联起来,我们需要一系列的“消息通道(Channel)”。各个服务做完自己的事后,发出相应的事件,而又有一些服务在订阅着某些事件来联动。
事件驱动方式的好处至少有五个。
-
服务间的依赖没有了,服务间是平等的,每个服务都是高度可重用并可被替换的。
-
服务的开发、测试、运维,以及故障处理都是高度隔离的。
-
服务间通过事件关联,所以服务间是不会相互block的。
-
在服务间增加一些Adapter(如日志、认证、版本、限流、降级、熔断等)相当容易。
-
服务间的吞吐也被解开了,各个服务可以按照自己的处理速度处理。
我们知道任何设计都有好有不好的方式。事件驱动的架构也会有一些不好的地方。
-
业务流程不再那么明显和好管理。整个架构变得比较复杂。解决这个问题需要有一些可视化的工具来呈现整体业务流程。
-
事件可能会乱序。这会带来非常Bug的事。解决这个问题需要很好地管理一个状态机的控制。
-
事务处理变得复杂。需要使用两阶段提交来做强一致性,或是退缩到最终一致性。
异步通讯的设计重点
首先,我们需要知道,为什么要异步通讯。
-
异步通讯最重要的是解耦服务间的依赖。最佳解耦的方式是通过Broker的机制。
-
解耦的目的是让各个服务的隔离性更好,这样不会出现“一倒倒一片”的故障。
-
异步通讯的架构可以获得更大的吞吐量,而且各个服务间的性能不受干扰相对独立。
-
利用Broker或队列的方式还可以达到把抖动的吞吐量变成均匀的吞吐量,这就是所谓的“削峰”,这对后端系统是个不错的保护。
-
服务相对独立,在部署、扩容和运维上都可以做到独立不受其他服务的干扰。
但我们需要知道这样的方式带来的问题,所以在设计成异步通信的时候需要注意如下事宜。
-
用于异步通讯的中间件Broker成为了关键,需要设计成高可用不丢消息的。另外,因为是分布式的,所以可能很难保证消息的顺序,因此你的设计最好不依赖于消息的顺序。
-
异步通讯会导致业务处理流程不那么直观,因为像接力一样,所以在Broker上需要有相关的服务消息跟踪机制,否则出现问题后不容易调试。
-
因为服务间只通过消息交互,所以业务状态最好由一个总控方来管理,这个总控方维护一个业务流程的状态变迁逻辑,以便系统发生故障后知道业务处理到了哪一步,从而可以在故障清除后继续处理。
这样的设计常见于银行的对账程序,银行系统会有大量的外部系统通讯,比如跨行的交易、跨企业的交易,等等。所以,为了保证整体数据的一致性,或是避免漏处理及处理错的交易,需要有对账系统,这其实就是那个总控,这也是为什么银行有的交易是T+1(隔天结算),就是因为要对个账,确保数据是对的。
- 消息传递中,可能有的业务逻辑会有像TCP协议那样的send和ACK机制。比如:A服务发出一个消息之后,开始等待处理方的ACK,如果等不到的话,就需要做重传。此时,需要处理方有幂等的处理,即同一条消息无论收到多少次都只处理一次。
小结
好了,我们来总结一下今天分享的主要内容。首先,同步调用有四个问题:影响吞吐量、消耗系统资源、只能一对一,以及有多米诺骨牌效应。于是,我们想用异步调用来避免该问题。
异步调用有三种方式:请求响应、直接订阅和中间人订阅。最后,我介绍了事件驱动设计的特点和异步通讯设计的重点。下节课,我们讲述幂等性设计。希望对你有帮助。
也欢迎你分享一下你在分布式服务的设计中,哪些情况下使用异步通讯?是怎样设计的?又有哪些情况使用同步通讯?
文末给出了《分布式系统设计模式》系列文章的目录,希望你能在这个列表里找到自己感兴趣的内容。
- 弹力设计篇
- 管理设计篇
- 性能设计篇
精选留言
2018-02-28 08:41:00
2018-02-27 12:09:12
2018-05-11 07:21:15
2020-06-01 10:37:49
(评论虽略显油腻,但是也是出自真心觉得陈皓老师写得好~)
2018-03-13 08:30:26
发送方为 消息生产者 将消息发送到 Q 中
接收方为 消息消费者 将消息从Q中取出
2019-04-16 08:02:28
2018-03-21 08:14:54
2018-03-01 08:35:45
2019-12-25 14:38:59
2018-03-01 09:08:43
2018-07-28 17:12:16
2020-09-28 20:18:26
2018-02-27 09:26:22
2023-06-16 09:36:14
2023-03-14 06:39:39
有没有什么情况是必须同步调用的?能发邮件、短信、微信的情况下,就不要打电话。
异步通讯的三种方式
请求响应式,发送方依赖于接收方,要把回调发给接收方
通过订阅的方式,接收方依赖于发送方,需要向发送方订阅事件
通过 Broker 的方式,发送方和接收方都依赖于中间件 Broker
以前看到过事件驱动架构,但是不知道使用在哪里,今天明白原因了。
事件驱动的优点:
1. 服务解耦
2. 服务的开发、测试、运维、故障处理隔离
3. 服务间增加 Adapter 相当容易
我觉的服务间不会相互 block 以及吞吐的解耦与前面三项有些重复。
事件驱动的缺点:
1. 业务流程架构复杂,需要可视化工具
2. 事件可能乱序,需要状态机控制
3. 事务处理变得复杂,需要两阶段处理来做强一致性,或者退缩到最终一致性
asynchronous adj. Pertaining to, being, or characteristic of something that is not dependent on timing. For example, asynchronous communications can start and stop at any time instead of having to match the timing governed by a clock.
adj. asynchronous computer processes happen at different times or rates.
2020-06-03 09:15:41
2020-04-27 23:40:00
2020-04-20 07:54:39
回调式响应 通过回调地址解决
订阅者与发布者 通过消息推送实现
通过中间broker来解藕
2020-01-21 11:35:13
异步可以实现一定程度解耦,提升吞吐量。
不同实现方式也有不同特性。
常见的有请求后轮询或者回调;直接向消息发送者订阅消息;或者通过中间人订阅。
中间人订阅解藕最彻底,但也会存在时间事件无序,状态管理困难,问题定位困难等情况。
2019-07-26 16:29:46