微服务:交互模式下超市问题的解决方案。

本文介绍交互模式下可能遇到的超时问题,并对每个问题给出相应的方法、模式和解决方案。

同步调用模式下的解决方案

在同步调用模式下,对外的接口会提供服务契约,契约定义了服务的处理结果会通过返回值返回给使用方,对返回的状态定义分为以下两种。

  • 成功和失败。
  • 成功、失败和处理中。

我们将第1种定义称为两状态的同步接口,将第2种定义称为三状态的同步接口。

两状态的同步接口

对于上面的第1种定义,服务契约中只规定了两种互斥的状态:成功和超时,服务处理结果必须是成功的或者失败的,在这种情况下可能发生两种同步调用超时。

第1种同步调用超时发生在使用方调用次同步接口的过程中,如下图所示。

微服务:交互模式下超市问题的解决方案。

针对这个问题,我们需要服务的使用方使用查询模式,异步查询处理结果,在获得明确的处理结果后,得知处理结果是成功还是失败,然后做相应的处理。如果处理结果为成功,那么使用方可以继续下面的操作;如果结果为失败,那么调用方可以发起重试,请求再次进行处理。然而,这里有一个问题,如果查询模式的返回状态是未知请求,那么在这种情况下使用方超时,服务1实际上没有接收到或者还没有接收到一开始的处理请求,服务使用方需要使用同一个请求ID进行重试,服务1也必须实现请求的幂等性。

第2种同步调用超时发生在内部服务1调用服务2的过程中,如下图所示。

微服务:交互模式下超市问题的解决方案。

在使用方调用服务1,且服务1接收到请求后,同步调用服务2,由于通信出现了问题,所以服务1得到超时的结果。这时服务1应该怎么做呢?是重试、取消还是快速失败?

我们看上图的左面,服务1对外接口的契约中包含两个返回状态:成功或者失败,也就是对于使用方来讲,不允许有中间的处理中的状态,对于这种服务内部超时的场景,必须使用快速失败的策略:针对这个超时错误,服务快速返回失败,同时在内部调用服务2的冲正接口,服务2的冲正接口可以判断之前是否接收到请求,如果接收到请求并做了处理,则应该做反向的回滚操作。如果服务2之前没有接收到处理请求,则忽略冲正请求,以此来实现服务的幂等性。

三状态的同步接口

对于上面的第2种定义,服务契约中规定了三种处理结果,状态为:成功、失败和处理中,对于超时等系统错误的请求,其实可以认为是处理中状态的一个特例,在这种场景的应用里,超时被视为内部暂时的问题,随时可能被修复,因此,可能在一定的时间窗口内告知使用方在处理中,随后修复问题并补偿执行,达到最大化请求处理成功的目标,不至于让使用方重试,以提升用户体验。

服务处理结果可能是成功或者失败,也可能是处理中,在这种情况下可能发生两种同步调用超时。

第1种同步调用超时发生在使用方调用次同步接口的过程中,如下图所示。

微服务:交互模式下超市问题的解决方案。

这种场景和两状态同步调用的接口超时场景类似,使用方调用服务1的接口,由于网络等原因获得超时的结果,这时使用方应该将超时看作处理中的一个特例,使用服务1的查询接口后续补齐上一个请求的处理状态,可参照两状态同步调用的接口超时场景的方案。

第2种同步调用超时发生在内部服务1调用服务2的过程中,如下图所示。

微服务:交互模式下超市问题的解决方案。

在使用方调用服务1,且服务1接收到请求后,同步调用服务2,由于通信出现了问题,所以服务1得到超时的结果,这时服务1又应该怎么做呢?

这和两状态同步调用的内部超时场景不一样,两状态设计由于与使用方约定了契约,不是成功就是失败,可以返回给使用方一个中间状态,也就是处理中的结果,变相的把同步接口变成异步接口,达到最终一致的效果。

在这种场景下,我们更倾向于给用户更好的体验,尽最大努力成功处理用户发来的请求。因此,针对在服务1调用服务2时超时,我们会返回给用户处理中的状态,随后系统尽最大努力补偿执行出错的部分,服务1需要通过服务2的查询接口得到最新的请求处理状态,如果服务2没有明确恢复,则可以尝试重新发送请求,当然,这里需要服务2也实现了操作的幂等性。

异步调用模式下的解决方案

在异步调用模式下,对外的接口也会提供服务契约,契约定义了服务的受理结果会通过返回值返回给使用方,返回的状态通常为两种:受理和未受理。和三状态同步接口不同的是,异步调用模式还有异步处理返回结果的通知,状态包括处理成功和处理失败。

不同阶段的网络通信产生的超时和处理方案如下。

异步调用接口超时

异步调用接口超时如下图所示。

微服务:交互模式下超市问题的解决方案。

异步调用接口超时发生在使用方调用服务1的受理接口时,同两状态同步调用接口超时及三状态同步调用接口超时的场景一样的,需要通过查询来补齐状态,并根据状态来判断后续的具体,具体的解决方案参考两状态同步调用接口超时和三状态同步调用接口超时的解决方案。

异步调用内部超时

异步调用内部超时如下图所示。

微服务:交互模式下超市问题的解决方案。

异步调用内部超时发生在服务1受理了使用方的请求后,服务1在处理请求时,在调用服务2的过程中超时,这和三状态同步调用内部超时的场景相似,由于异步调用模式使用的是受理模式。所以一旦受理,我们便应该尽最大努力将用户请求的操作处理成功,因此,在服务1调用服务2超时的场景下,服务1需要根据服务2的查询接口获得最新状态,根据状态补偿后续的操作,这和三状态同步调用内部超时的解决方案一致,不同的是此场景下一旦成功,则需要异步回调通知使用方,而在三状态同步调用内部超时的场景下,只需要等待使用方查询,不需要通知,也无法实现通知。

异步调用回调超时

异步调用回调超时如下图所示。

微服务:交互模式下超市问题的解决方案。

回调超时的问题在生产中经常出现,通常发生于这样的场景下:服务1受理后成功的调用了依赖服务2,获得了明确的处理结果,但是在将处理结果通知使用方时出现超时。由于使用方有可能是公司内部的也可能是外部的,网络环境复杂多变,发生超时的概率很大,因此,大多数公司都会开发一个通知子系统,用来专门处理回调通知。

由于服务1通过回调通知使用方,所以服务1需要保证通知一定可送达,如果遇到超时,则服务1负责重新继续补偿,通常会设计一个通知时间按一定间隔递增的策略,例如:指数回退,直到通知成功为止,通知是否成功以对方的会写状态为准。

消息队列异步处理模式的解决方案

消息队列异步处理模式多用于疏松耦合的项目,这些项目通常是在主流程中无法处理耗时的任务,恰好耗时的任务又不是核心流程的一部分,比如:电商平台的物流、配送等。

这类交互使用消息队列进行解耦,电商交易系统成功处理交易后,需要发送消息到消息队列服务器,后续的流程由物流平台处理,也不需要将处理结果反馈给交易平台。

使用消息队列解耦后,处理流程被分为两个阶段:生产者投递和消费者处理,在不同的阶段会产生不同的超时问题,解决方案如下。

消息队列的生产者超时

消息队列的生产者超时如下图所示。

微服务:交互模式下超市问题的解决方案。

对于这种场景,可参考可靠消息模式部分的内容。

消息队列的消费者超时

消息队列的消费者超时如下图所示。

微服务:交互模式下超市问题的解决方案。

对于消息丢列的处理机与消息队列之间的超时或者网络问题,通常可以通过消息队列提供的机制来解决。
一般消息队列会提供如下两种方式来消费消息。

  • 自动增长消费的偏移量:在一个消费者从消息服务器中取走消息后,消息队列的消息偏移量自动增加,即消息一旦被从消息队列中取走,则不再存在于服务器中,假如消息处理机对此消息处理失败,则也无法从消息服务器中找回。

  • 手工提交消费的偏移量:在一个消费者从消息服务器中取走消息后,处理机先把消息持久到本地数据库中,然后告诉消息服务器已经消费消息,消息服务器也会移除消息,如果在没有告诉消息服务器已经消费消息之前,持久失败或者发生了其他问题,则消息仍然存在于消息服务器中,消息处理器下次还可以继续消费消息。

如果允许丢消息,则我们使用第1种处理方式,这种方式的并发量高、性能好,但是如果我们对消息处理的准确性要求较高,则必须采用第2种方式。