服务隔离与降级
1.什么是服务隔离
简单来说,当系统某个服务或组件出现故障时,能隔离这些故障并且能做到优雅地服务降级。
比如,在图片分享应用中,当出现故障时,用户可能无法上传图片,但他们依然能浏览、编辑和分享已上传的图片。
2.为什么做服务隔离
我们在做系统设计的时候,必须有一个清楚的认知是:任何软件系统,故障是不可避免的,并且大多数还是不可预测的,因此,我们只能在系统的设计之初就充分的考虑好应对措施,如何在故障发生时,去尽最大可能的止损和减少故障范围。 把系统分离成子服务,将子服务进行一定程度隔离的做法,能保证在有不可预测的故障发生时,缩小故障范围的最佳手段。
上图一个典型的分布式服务实现, 当大多数人在使用Tomcat时,多个HTTP服务会共享一个线程池,假设其中一个HTTP服务访问的数据库响应非常慢,这将造成服务响应时间延迟增加,大多数线程阻塞等待数据响应返回,导致整个Tomcat线程池都被该服务占用,甚至拖垮整个Tomcat。因此,如果我们能把不同HTTP服务隔离到不同的线程池,则某个HTTP服务的线程池满了也不会对其他服务造成灾难性故障.
上面只是个简单的例子,在大中型分布式系统中,通常系统很多依赖(HTTP,hession,Netty,Dubbo等),在高并发访问下,这些依赖的稳定性与否对系统的影响非常大,但是依赖有很多不可控问题:如网络连接缓慢,资源繁忙,暂时不可用,服务脱机等。
当依赖阻塞时,大多数服务器的线程池就出现阻塞(BLOCK),影响整个线上服务的稳定性,在复杂的分布式架构的应用程序有很多的依赖,都会不可避免地在某些时候失败。高并发的依赖失败时如果没有隔离措施,当前应用服务就有被拖垮的风险,最终导致系统崩溃,又称雪崩效应;
解决上诉雪崩效应,一般有3种方案;
(1)熔断模式:这种模式主要是参考电路熔断,如果一条线路电压过高,保险丝会熔断,防止火灾。放到我们的系统中,如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
(2)隔离模式:这种模式就像对系统请求按类型划分成一个个小岛的一样,当某个小岛被火少光了,不会影响到其他的小岛。例如可以对不同类型的请求使用线程池来资源隔离,每种类型的请求互不影响,如果一种类型的请求线程资源耗尽,则对后续的该类型请求直接返回,不再调用后续资源。这种模式使用场景非常多,例如将一个服务拆开,对于重要的服务使用单独服务器来部署,再或者公司最近推广的多中心。
(3)限流模式:上述的熔断模式和隔离模式都属于出错后的容错处理机制,而限流模式则可以称为预防模式。限流模式主要是提前对各个类型的请求设置最高的QPS阈值,若高于设置的阈值则对该请求直接返回,不再调用后续资源。这种模式不能解决服务依赖的问题,只能解决系统整体资源分配问题,因为没有被限流的请求依然有可能造成雪崩效应。
1.怎么做服务隔离
服务隔离是一个很大的概念,具体到实现上,有方方面面;下面先谈谈隔离的维度上有哪些;
1.线程隔离
线程隔离主要有线程池隔离,在实际使用时我们会把请求分类,然后交给不同的线程池处理,当一种业务的请求处理发生问题时,不会将故障扩散到其他线程池,从而保证其他服务可用。
2.进程隔离
在公司发展初期,一般是先进行从0到1,不会一上来就进行系统的拆分,这样就会开发出一些比较大而全的系统,系统中的一个模块/功能出现问题,整个系统就不可用了。首先想到的解决方案是通过部署多个实例,然后通过负载均衡进行路由转发,但是这种情况无法避免某个模块因BUG而出现如OOM导致整个系统不可用的风险。因此此种方案只是一个过渡,较好的解决方案是通过将系统拆分为多个子系统来实现物理隔离。通过进程隔离使得某一个子系统出现问题不会影响到其他子系统。
3.集群隔离
随着系统的发展,单实例服务无法满足需求了,此时需要服务化技术,通过部署多个服务,形成服务集群来提升系统容量,如下图所示
随着调用方的增多,当秒杀服务被刷会影响到其他服务的稳定性,此时应该考虑为秒杀提供单独的服务集群,即为服务分组,从而当某一个分组出现问题不会影响到其他分组,从而实现了故障隔离,如下图所示
4.机房隔离
随着对系统可用性的要求,会进行多机房部署,每个机房的服务都有自己的服务分组,本机房的服务应该只调用本机房服务,不进行跨机房调用;其中一个机房服务发生问题时可以通过DNS/负载均衡将请求全部切到另一个机房;或者考虑服务能自动重试其他机房的服务从而提升系统可用性。
5.读写隔离
通过主从模式将读和写集群分离,读服务只从从Redis集群获取数据,当主Redis集群出现问题时,从Redis集群还是可用的,从而不影响用户访问;而当从Redis集群出现问题时可以进行其他集群的重试。
6.动静隔离
当用户访问如结算页时,如果JS/CSS等静态资源也在结算页系统中时,很可能因为访问量太大导致带宽被打满导致出现不可用。因此应该将动态内容和静态资源分离,一般应该将静态资源放在CDN上
方案实现,
1.先在系统设计上,做到服务拆分, 让每个小业务成为孤岛,即便是其中一个业务挂掉,其他服务依然可以正常运行。
2.服务隔离目前业内应用比较广泛和成熟的的方案是Hystrix,
hystrix是netflix开源的一个容灾框架,解决当外部依赖故障时拖垮业务系统、甚至引起雪崩的问题
Hystrix的服务隔离主要有两种,常用的是线程隔离,对热点接口建立单独的线程池避免对主程序的干扰 ,比如 Tomcat默认是一个线程池150个线程,如果单个热点接口的请求过多,就会造成其他功能没有线程可用甚至直接程序崩溃的问题
另外一种是信号量,应用场景不是太多,两者的区别 其实就是一个增大系统的开销,一个则直接限制了线程总的并发数,开销更小一些;
Hystrix基本使用方法,参考
https://www.imooc.com/article/75502
http://blog.51cto.com/developerycj/1950881
有了服务隔离后,谈谈服务熔断
服务熔断的三个模块,熔断请求判断算法、熔断恢复机制、熔断报警;
对于Hystrix的实现
(1)熔断请求判断机制算法:使用无锁循环队列计数,每个熔断器默认维护10个bucket,每1秒一个bucket,每个blucket记录请求的成功、失败、超时、拒绝的状态,默认错误超过50%且10秒内超过20个请求进行中断拦截。
(2)熔断恢复:对于被熔断的请求,每隔5s允许部分请求通过,若请求都是健康的(RT<250ms)则对请求健康恢复。
(3)熔断报警:对于熔断的请求打日志,异常请求超过某些设定则报警
熔断之后,就要对服务进行降级处理了;所谓降级,就是当某个服务熔断之后,服务器将不再被调用,此时客户端可以自己准备一个本地的fallback回调,返回一个缺省值。 也可以理解为兜底;
这样做,虽然服务水平下降,但好歹可用,比直接挂掉要强
方案1:dubbo降级
Dubbo开发中,可能由于服务没有启动或者网络不通,调用中会出现RpcException,也就是远程调用失败。如果是服务启动顺序的问题,可能加上check=”false”的配置可以得到很好的解决.但是,如果是服务宕掉或者并发数太高导致的RpcException该如何处理?
查看dubbo的官方文档,可以发现有个mock的配置,mock只在出现非业务异常(比如超时,网络异常等)时执行。mock的配置支持两种,一种为boolean值,默认的为false。如果配置为true,则缺省使用mock类名,即类名+Mock后缀;另外一种则是配置”return null”,可以很简单的忽略掉异常,示例如下,当接口超时异常后,会自动返回null降级;
<dubbo:reference id="iUser" interface="com.dubbosample.iface.IUser" timeout="10000" check="false" mock="return null">
check=true,时, 需要自定义一个接口名+Mock的类名的降级接口,此时如果调用失败会调用Mock实现
方案2: hystrix降级
引入Hystrix依赖后,通过在需要降级的方法上加注解实现降级
@HystrixCommand(
fallbackMethod = "fallback",
threadPoolProperties = { @HystrixProperty(name = "coreSize", value = "20"), @HystrixProperty(name = "maxQueueSize", value = "100"), @HystrixProperty(name = "queueSizeRejectionThreshold", value = "20")},
commandProperties = { @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "30000"), @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20")
其中
fallbackMethod指降级后的兜底方法,特别注意fallback方法的返参和入参必须和该方法相同;