背景
公司的服务都是微服务之前的调用,现在A服务需要使用B服务的功能,但是B服务处理业务是需要一定的时间的。为了提高服务间的吞吐采用异步的方式执行。
同步调用和异步调用
- 同步调用带来的坏处
- 同步调用需要被调用方的吞吐不低于调用方的吞吐。否则会导致被调用方因为性能不足而拖死调用方。换句话说,整个同步调用链的性能会由最慢的那个服务所决定。
- 同步调用会导致调用方一直在等待被调用方完成,如果一层接一层地同步调用下去,所有的参与方会有相同的等待时间。这会非常消耗调用方的资源。因为调用方需要保存现场(Context)等待远端返回,所以对于并发比较高的场景来说,这样的等待可能会极度消耗资源。
- 同步调用只能是一对一的,很难做到一对多。同步调用最不好的是,如果被调用方有问题,那么其调用方就会跟着出问题,于是会出现多米诺骨牌效应,故障一下就蔓延开来。
- 所以,异步通讯相对于同步通讯来说,除了可以增加系统的吞吐量之外,最大的一个好处是其可以让服务间的解耦更为彻底,系统的调用方和被调用方可以按照自己的速率而不是步调一致,从而可以更好地保护系统,让系统更有弹力。
异步调用的几种方式:
- 请求响应式
- 通过订阅的方式
- 通过 Broker 的方式
具体方式解读:
https://time.geekbang.org/column/article/3926
项目中的应用
第一版方案
-
第一版方案比较原始,采用的是定时任务跑JOB的方式来主动获取异步结果(如下图)。
-
遇到的问题:
- 这个定时任务设置的时间如何来把控(这真的很难设定)。我们使用的是根据用户每秒的上传量来进行设定的。然后根据每次处理的数据量做一个计算保证不产生数据堆积。而产生很多中间状态
- 还有就是这个job的时间也限制了被调用服务的吞吐了。比如我上传300条数据处理,这300条在很短时间处理完成了,而我的客户端job设置了1分钟跑一次,切每次处理100,那这就得3分钟了。所以完全限制了服务的吞吐。
- 还有就是如果服务来个突然猛增,那这就GG了,之前的job参数就有问题了。我们在实际情况汇总中就遇到了这个问题,导致发起回来后,
第二版方案
-
采用回调的方式进行交互
-
回调的过程有ACK机制,当只有A服务确定收到了且成功处理了B服务的回调结果才会成功,不然B服务会有重试机制(有限次数)。
-
相对于定时job的优势:通过回调的方式解决了刚开始定时job方式在吞吐量方面的瓶颈,还有就是当数据量很大的时候,在进行扫描表的时候也会浪费一些时间。
-
实际情况遇到问题: A服务吧任务给B服务,B服务产生任务编号,直接同步返回给A服务,B服务处理完任务回调给A A是根据这个任务号进行更新的,就在这时出现问题了。
回调的结果比taskID落库快。 这下完了,回调回来。没有这个taskID,然后迅速回调了三次都挂掉了。导致任务一直处于中间状态。
第三版方案(解决第二版方案问题)
-
采用延时队列,当没有这个taskID的时候放入延时队列,当落库后消费延时队列的消息。
-
采用定时任务,做兜底如下:
- 关于扫描表时间长的问题,采用请求的时当前时间前1分钟的前1小时的数据。
- 真的时B服务处理没有成功没有获取到对应的taskID数据,那就直接更新为失败任务。
-
可以发现这是第一个方案和第二个方案的结合,但是如果我们从根源解决问题呢?出问题的原因就是在更新TaskID上,我们一方面让taskID变快,另一方面就是提早的将taskID落库。那就选择能不能把taskId生成的地方放在发起的服务上呢?
感觉没有什么问题啊。但是总是感觉有点怪,但是并不知道是哪里怪。还请大家出出建议。
总结
- 同步调用有四个问题:影响吞吐量、消耗系统资源、只能一对一,以及有多米诺骨牌效应。于是,我们想用异步调用来避免该问题。
- 异步调用有三种方式:请求响应、直接订阅和中间人订阅。
- 但是一定要注意接口幂等问题,防止数据不能保证最终一致性