引子
我们都知道软件开发的中,不仅仅要解决正常的业务逻辑,更重要的是对异常状态的处理,这关系到我们程序的稳定性和容错性,在引入我们的微服务后我们的错误处理机制又面临了新的挑战,如图所示,微服务中,多个服务之间可能存在着依赖关系,而底层的服务可能被多个服务所依赖,从而一个服务的失效可能导致多个服务不可用,从而进一步导致整个系统的不可用,面对这个问题,选择正确的服务容错处理方案就显得格外重要了,今天我们就来讨论服务容错的设计和响应的几种模式.
设计原则
我们再来思考一下,容错在我们设计上需要的功能,容错的处理并非一个通用的模式,所以在面对不同的场景的时候,我们就应该在设计上避免底层不可用带来的影响,让依赖的服务的故障不影响用户的正常体验,比如搜索功能故障,可以暂时禁用,并给予友好提示,而不应该因此造成整个系统的不可用.其次应该同时让系统能应对这个错误,并具有恢复能力,比如故障的服务可能在一段时间后会恢复正常后,对应的依赖服务应有所感知并进行恢复.
经典的容错模式
当然经过多年的实践,业界已经存在了一些优秀可靠的设计模式,下面简单介绍一下,我们可以根据我们的场景选择正确的模式
-
超时重试
超时 这个模式是我们比较常见的,比如在HTTP请求中我们就会设置一下超时时间,超过一定时间后我们就后断开连接,从而防止服务不可用导致请求一直阻塞,从而避免服务资源的长时间占用.
重试 这个模式一般和超时配合出现,一般使用在对下层服务强依赖的场景,否则不建议使用.利用重试来解决网络异常带来的请求失败的情况,超时次数不应该太多,超时时间的时间也比较关键,不能太长最好是 根据服务的正常响应时间来定,否则可能会导致长时间无响应,拖垮系统.
实现方式比较简单,通过设置请求时间和记录请求次数来判断是否需要重试即可,框架实现有Spring retry
-
限流
我们应用的异常情况不仅仅是内部原因引起的,每个应用的处理请求的能力是有限的,如果外部流量过大也会导致我们的服务不可用,这时候就需要限流了.常见的限流有控制并发数量和限制访问速率.
控制并发,属于常用的限流方式,比如有1000个请求,但是同时我们只允许100个请求进行处理,而其他的请求可以直接返回不进行处理,这样保证了我们的并发数和我们的设计的负载水平相一致.在实现中我们可以使用Java的信号量来控制进入的请求数量.
控制速率 控制访问的流量,效果上也是一样的,实现上有所区别,一般使用令牌桶算法实现,简单来说就是每秒会有N个令牌放入桶中,并且桶的大小是固定的也就是我们要限制流量的大小,多余的令牌会丢弃,每个请求会消耗对于请求的字节大小的令牌,如果令牌不足了请求就会被丢弃,从而实现访问控制.
-
舱壁隔离
这个模式借鉴造船行业的经验,在造船业人们往往利用舱壁将不同的船舱隔离起来,这样如果一个船舱破了进水,只损失一个船舱,其它船舱可以不受影响.
** 线程隔离** 就是了这个模式的思想,在开发过程中我们对每个不同的服务分配不同的线程池,比如serviceA分配了10个serviceB分配了20个,如果serviceA的线程用完了对serviceB也不会有影响,并不用占用另一个的资源,如果没有这种隔离,serviceA出现异常后就可能占用了其他服务的资源,从而影响整个系统.
-
熔断器
熔断器模式类似我们家里的电路保险丝,如果出现异常熔断器会打开,所有的请求都会直接返回,而不会等待或阻塞.在我们的项目当中,如果服务不可用会导致大量的请求等待,阻塞重要的系统资源CPU内存等,并进一步消耗资源使系统的其他服务也不可用,而熔断器会对异常状态直接返回,从而减少这种资源的浪费,同时如图所示,熔断器还可以有一种半开的状态,当熔断器打开后,会定时接受一部分请求来检测系统是否恢复,然后正常就会关闭熔断器,恢复正常使用了.
实际使用中我们经常使用熔断器模式来实现微服务的优雅降级,Netflix开源的组件[Hystrix](https://www.oschina.net/p/hystrix)就很好的实现了这一模式.
-
回退
以上所有的模式都会出现异常,针对这些异常的处理方式也是一种模式 叫做回退模式, 回退的策略主要有
- 快速失败 直接抛出异常,适应于非数据类的服务和非强依赖的服务.
- 故障沉默 返回空数据或者默认值,适用于可降级的场景,比如推荐系统直接返回空也不影响业务
- 自定义处理 根据实践情况定义处理的策略,是返回缓存数据还是使用备用方案等,适用于系统的关键流程
总结
系统的容错性对于高可用的系统架构格外重要,在我们的实践业务中应用这些模式的可单独使用也可灵活组合,当然发现容错处理后及时发现系统异常的监控也格外重要.