超时处理模式
在服务化或者微服务架构里,传统的整体应用拆分成多个职责单一的微服务,微服务之间通过某种网络通信协议互相通信和交互,完成特定的功能,然而由于网络通信的不稳定,在设计系统时必须考虑到对网络通信的容错,特别是对调用超时问题的处理。
一、微服务的交互模式
1、同步调用模式
在同步调用模式中,服务A调用服务B,服务A的线程阻塞等待服务B的处理结果,如果服务B一直不返回处理结果,则服务A一直处于等待状态中一直到超时为止。
(同步调用模式图)
2、接口异步调用模式
在接口异步调用模式中,服务A请求服务B处理某项任务,服务B处理完后即刻返回给服务A处理结果,如果处理成功,则服务A继续干其他任务,而服务B异步处理这项任务,直到服务B处理完这项任务后,才反向通知服务A任务已完成,服务A再做后续的工作。
(接口异步调用模式)
接口异步调用模式适用于非核心链路上负载较高的处理环节,这个环节经常耗时较长并且对时效性要求不高。
3、消息队列异步处理模式
消息队列异步处理模式利用消息队列作为通信机制,在这种交互模式中,通常服务A只需将某种事件传递给服务B,而不需要等待服务B返回结果。在这种情况下,服务A与服务B可以充分解耦,并且在大规模、高并发的微服务系统中,消息队列对流量具有消峰的功能。
(消息队列异步处理模式)
消息队列异步处理模式与接口异步调用模式类似,多应用于非核心链路上负载较高的处理环节中,并且服务的上游不关心下游的处理结果,下游也不需要向上游返回处理结果。
以上这三种交互模式普遍应用于服务化和微服务架构中,它们之间没有绝对的好坏,只需要在特定场景下做出合适的选择。
二、同步于异步的抉择
1、尽量使用异步来替换同步操作
2、能用同步解决的问题,不要引入异步。
第一条原则是从业务功能的角度出发的,业就是从与用户或者使用方的交互模式出发的。如果业务逻辑允许,用户对产品的交互形态没有异议,则我们可以将一些耗时较长的、用户对响应没有特别要求的操作异步化,以此来减少核心链路的层级,释放系统的压力。
第二条原则是从技术和架构的角度出发的,这条原则应用的前提是同步能够解决问题,这隐含了一个含义:如果性能不是问题,或者所处理的操作是短小的轻量级处理逻辑,那么同步调用方式是最理想不过的,因为这样不需要引入异步化的复杂处理流程。
三、交互模式下超时问题的解决方案
1、同步调用模式下的解决方案
在同步模式下,对外的接口会提供服务契约,契约定义了服务的处理结果会通过返回值返回给对方,对返回的状态定义分为以下两种:
A)成功和失败(两状态的同步接口)
B)成功、失败和处理中(三状态的同步接口)
1)两状态的同步接口
服务契约中只规定了两种互斥的状态:成功和超时,服务处理结果必须是成功的或者失败的。在这种情况下可能发生两种同步调用超时。
第一种同步调用超时发生在使用方调用此同步接口的过程中
针对这个问题,我们需要服务的使用方使用前面架构杂谈中提到的查询模式,异步查询处理结果,在获得明确的处理结果后,得知处理结果是成功还是失败,然后做相应的处理。如果处理结果为成功,那么使用方可以继续下面的操作;如果结果为失败,那么调用方可以发起重试,请求再次进行处理。然而,这里有一个问题,如果查询模式的返回状态是未知请求,那么在这种情况下使用方超时,服务 1 实际上没有接收到或者还没有接收到一开始的处理请求,服务使用方需要使用同一个请求 ID 进行重试,服务 1 也必须实现请求处理的幕等性。
第二种同步调用超时发生在内部服务1调用服务2的过程中
在使用方调用服务 1,且服务 1 接收到请求后,同步调用服务 2,由于通信出现了问题, 所以服务 1得到超时的结果。这时服务 1 应该怎么做呢?是重试、取消还是快速失败?
我们看到上图的左面服务 1 对外接口的契约中包含两个返回状态 :成功或者失败,也就是对于使用方来讲,不允许有中间的处理中的状态,对于这种服务内部超时的场景,必须使用快速失败的策略 :针对这个超时错误,服务快速返回失败,同时在内部调用服务 2 的冲正接口,服务 2 的冲正接口可以判断之前是否接收到请求,如果接收到请求井做了处理,则应该做反向的回攘操作。如果服务 2 之前没有接收到处理请求,则忽略冲正请求,以此来实现服务的幕等性。
2)三状态的同步接口
对于上面的第 2 种定义,服务契约中规定了三种处理结果,状态值为:成功、失败和处理中,对于超时等系统错误的请求,其实可以认为是处理中状态的一个特例,在这种场景的应用里,超时被视为内部暂时的问题,随后可能被修复,因此,可能在一定的时间窗口内告知使用方在处理中,随后修复问题井补偿执行,达到最大化请求处理成功的目标,不至于让使用方重试,以提升用户体验 。
服务处理结果可能是成功或者失败,也可能是处理中,在这种情况下可能发生两种同步调用超时。
第1种同步调用超时发生在使用方调用此同步接口过程中,如下图所示:
这种场景和两状态同步调用的接口超时场景类似,使用方调用服务 1 的接口,由于网络等原因获得超时的结果,这时使用方应该将超时看作处理中的一个特例,使用服务 1 的查询接口后续补齐上一个请求的处理状态,可参照两状态同步调用的接口超时场景的方案。
第 2 种同步调用超时发生在内部服务 l 调用服务 2 的过程中,如下图所示。
在使用方调用服务 1, 且服务 1 接收到请求后,同步调用服务 2,由于通信出现了问题,所以服务 l 得到超时的结果,这时服务 1 又应该怎么做呢?
这和两状态同步调用 的内部超时场景不一样,两状态设计由于与使用方约定了契约,不是成功就是失败,所以必须在同步调用时给予一个明确的结果,然而,在三状态同步调用的内部超时场景下,可以返回给使用方一个中间状态,也就是处理中的结果,变相地把同步接口变成异步接口 ,达到最终一致的效果。
在这种场景下,我们更倾向于给用户更好的体验,尽最大努力成功处理用户发来的请求 。因此,针对在服务 1 调用服务 2 时超时,我们会返回给用户处理中的状态,随后系统尽最大努力补偿执行出错的部分,服务 1 需要通过服务 2 的查询接口得到最新的请求处理状态,如果服务 2 没有明确回复, 则可以尝试重新发送请求,当然,这里需要服务 2 也实现了操作的幕等性 。
2. 异步调用模式下的解决方案
在异步调用模式下,对外的接口也会提供服务契约,契约定义了服务的处理结果会通过返回值返回给使用方,返回的状态通常为两个:处理和未处理。和三状态同步调用接口不同的是异步调用模式还有异步处理返回结果的通知,状态包括处理成功和处理失败。
不同阶段的网络通信产生的超时和处理方案如下。
1)异步调用接口超时
异步调用接口超时发生在使用方调用服务 1 的受理接口时,同两状态同步调用接口超时及三状态同步调用接口超时的场景是一样的,需要通过查询来补齐状态,并根据状态来判断后续的操作,具体的解决方案参考两状态同步调用接口超时和三状态同步调用接口超时的解决方案。
2)异步调用内部超时
异步调用内部超时发生在服务 1 受理了使用方的请求后 ,服务 1 在处理请求时,在调用服务 2 的过程中超时,这和三状态同步调用内部超时的场景相似,由于异步调用模式使用的是受理模式,所以一旦受理,我们便应该尽最大努力将用户请求的操作处理成功,因此,在服务 1 调用服务 2 超时的场景下,服务 1需要根据服务 2 的查询接口获得最新状态,根据状态补偿后续的操作,这和三状态同步调用内部超时的解决方案一致,不同的是此场景下一旦处理成功,则需要异步回调通知使用方,而在三状态同步调用内部超时的场景下,只需要等待使用方查询,不需要通知,也无法实现通知。
3)异步调用回调超时
回调超时的问题在生产中经常出现,通常发生于这样的场景下:服务 1 受理后成功地调用了依赖服务 2,获得了明确的处理结果,但是在将处理结果通知使用方时出现超时。由于使用方有可能是公司内部的也可能是外部的 ,网络环境复杂多变,发生超时的概率很大,因此,大多数公司都会开发一个通知子系统,用来专门处理回调通知。
由于服务 1通过回调通知使用方,所以服务 1需要保证通知一定可送达,如果遇到超时,则服务 1 负责重新继续补偿,通常会设计一个通知时间按一定间隔递增的策略,例如 :指数回退,直到通知成功为止,通知是否成功以对方的回写状态为准。
3、消息队列异步处理模式的解决方案
消息队列异步处理模式多用于疏松祸合的项目,这些项目通常是在主流程中无法处理耗时的任务,恰好耗时的任务又不是核心流程的一部分,比如 :电商平台的物流、配送等。
这类交互使用消息队列进行解耦,电商交易系统成功处理交易后,需要发送消息到消息队列服务器,后续的流程由物流平台处理,也不需要将处理结果反馈给交易平台。
使用消息队列解耦后,处理流程被分为两个阶段:生产者投递和消费者处理,在不同的阶段会产生不同的超时问题,解决方案如下。
1)消息队列的生产者超时
2)消息队列的消费者超时
对于消息队列的处理机与消息队列之间的超时或者网络问题,通常可以通过消息队列提供的机制来解决。
一般消息队列会提供如下两种方式来消费消息。
1)、自动增长消费的偏移量:在一个消费者从消息服务器中取走消息后,消息队列的消息偏移量自动增加,即消息一旦被从消息队列中取走,则不再存在于服务器中,假如消息处理机对此消息处理失败,则也无法从消息服务器中找回。
2)、手工提交消费的偏移量 :在一个消费者从消息服务器中取走消息后,处理机先把消息持久到本地数据库中,然后告诉消息服务器己经消费消息,消息服务器才会移除消息,如果在没有告诉消息服务器己经消费消息之前,持久失败或者发生了其他问题,则消息仍然存在于消息服务器中,消息处理器下次还可以继续消费消息。
如果允许丢消息,则我们使用第1种处理方式,这种方式的并发量高、性能好,但是如果我们对消息处理的准确性要求较高,则必须采用第 2 种方式。
四、超时补偿的原则
1)服务1调用服务2,如果服务2响应服务1并且告诉服务1消息己接收,那么服务1的任务就结束了;如果服务2处理失败,那么服务2应该负责重试或者补偿。在这种情况下,服务2通常接收消息后先持久再告诉服务1接收成功,随后服务2才开始处理持久的消息,避免服务进程被杀掉而导致消息丢失。
2)服务1调用服务2,如果服务2没有给出明确的接收响应,例如网络超时,那么服务1应该持续进行重试,直到服务2明确表示己经接收消息。在这种情况下容易出现重复的消息,因此在服务2中通常要保证滤重或者幕等性。
说明:
1、参考书籍:《分布式服务架构:原理、设计与实战》
2、如有不合适的地方请反馈。综合后更改。