服务雪崩
分布式系统环境下,服务间类似依赖非常常见,一个业务调用通常依赖多个基础服务。如下图:
如果各个服务正常运行,那大家其乐融融,高高兴兴的,但是如果其中一个服务崩坏掉会出现什么样的情况呢?如下图:
当Service A的流量波动很大,流量经常会突然性增加!那么在这种情况下,就算Service A能扛得住请求,Service B和Service C未必能扛得住这突发的请求。
此时,如果Service C因为抗不住请求,变得不可用(服务可能会宕机,比如内存溢出,也可能请求连接数满了,请求不到服务)。那么Service B的请求不到Service C,这时候所有的请求都会积压在B服务这里,积压时间过长B也会阻塞,慢慢耗尽Service B的线程资源,Service B就会变得不可用。紧接着,Service A也会不可用。
So,简单地讲。一个服务失败,导致整条链路的服务都失败的情形,我们称之为服务雪崩
服务雪崩的原因和三个阶段及解决方案
原因大致有四:
- 硬件故障;
- 程序Bug;
- 缓存击穿(用户大量访问缓存中没有的键值,导致大量请求查询数据库,使数据库压力过大);
- 用户大量请求;
服务雪崩三阶段:
- 第一阶段:服务不可用;
- 第二阶段:调用端重试加大流量(用户重试/代码逻辑重试);
- 第三阶段:服务调用者不可用(同步等待造成的资源耗尽);
解决方案
- 应用扩容(扩大服务器承受力)
加机器、升级硬件
- 流量控制(超出限定流量,返回类似重试页面让用户稍后再试)
限流、关闭重试
- 缓存
将用户可能访问的数据大量的放入缓存中,减少访问数据库的请求。
- 服务降级
服务接口拒绝服务、页面拒绝服务、延迟持久化、随机拒绝服务
- 服务熔断
我们今天来说一下服务熔断和降级
服务熔断
当下游的服务因为某种原因突然变得不可用或响应过慢,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用
服务熔断的原理:
原理: 当远程服务被调用时,断路器将监视这个调用,如调用时间太长,断路器将会介入并中断调用。此外,断路器将监视所有对远程资源的调用,如对某一个远程资源的调用失败次数足够多,那么断路器会出现并采取快速失败,阻止将来调用此远程资源的请求.
最开始处于closed状态,一旦检测到错误到达一定阈值,便转为open状态;
这时候会有个 reset timeout,到了这个时间了,会转移到half open状态,尝试放行一部分请求到后端,一旦检测成功便回归到closed状态,即恢复服务;
想不通的小伙伴可以参考你家的电保险
断路器的实现有两种:
- 阿里公司出的Sentinel
- 网飞公司出的的Hystrix
Hystrix中熔断的常用配置:
//默认值20.意思是至少有20个请求才进行errorThresholdPercentage错误百分比计算。比如一段时间(10s)内有19个请求全部失败了。
//错误百分比是100%,但熔断器不会打开,因为requestVolumeThreshold的值是20. 这个参数非常重要
circuitBreaker.requestVolumeThreshold
//过多长时间,熔断器再次检测是否开启,默认为5000,即5s钟
circuitBreaker.sleepWindowInMilliseconds
//设定错误百分比,默认值50%,例如一段时间(10s)内有100个请求,其中有55个超时或者异常返回了,那么这段时间内的错误百分比是55%,大于了默认值50%,这种情况下触发熔断器-打开。
circuitBreaker.errorThresholdPercentage
按照以上配置的熔断器如下:
每当20个请求中,有50%失败时,熔断器就会打开,此时再调用此服务,将会直接返回失败,不再调远程服务。直到5s钟之后,重新检测该触发条件,判断是否把熔断器关闭,或者继续打开
服务降级
服务降级主要有两种场景:
- 当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,增加响应速度!
- 当下游的服务因为某种原因不可用,上游主动调用本地的一些降级逻辑,避免卡顿,迅速返回给用户!
聪明人已经看出来,第二个场景怎么和服务熔断的概念有异曲同工之妙,没错,服务熔断可视为降级方式的一种!
第一种场景我个人理解更像是业务层面的解决方案,比如:
双十一的时候,我们买东西是不是都不允许修改购物地址,不允许发起退货,不允许退款还有很多服务都不可以用,只允许用户选择商品加入购物车付钱。那天只有一个目的就是让一些不是很重要的服务所占用的cpu资源都让出来,给购物,付款这样的核心服务。这样的话,用户付款的速度就会越来越快,毕竟前多少名支付的用户是有免单机会的(大家都会挤在0点那一刻去付款)。服务降级主要用于当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时,为了保证重要或基本的服务能正常运行,将一些 不重要 或 不紧急 的服务或任务进行服务的 延迟使用 或 暂停使用。
降级就是为了解决资源不足和访问量增加的矛盾。
服务降级方式
- 延迟服务:定时任务处理、或者mq延时处理。比如新用户注册送多少优惠券可以提示用户优惠券会24小时到达用户账号中,我们可以选择再凌晨流量较小的时候,批量去执行送券。
- 页面降级:页面点击按钮全部置灰,或者页面调整成为一个静态页面显示“系统正在维护中”等。
- 关闭非核心服务:比如电商关闭推荐服务、关闭运费险、退货退款等。保证主流程的核心服务下单付款就好。
- 写降级:比如秒杀抢购,我们可以只进行Cache的更新返回,然后通过mq异步扣减库存到DB,保证最终一致性即可,此时可以将DB降级为Cache。
- 读降级:比如多级缓存模式,如果后端服务有问题,可以降级为只读缓存,这种方式适用于对读一致性要求不高的场景。
服务熔断和服务降级的区别与联系
联系
- 目的很一致:都是从可用性可靠性着想,为防止系统的整体缓慢甚至崩溃,采用的技术手段,都是为了保证系统的稳定。
- 终表现类似:对于两者来说,最终让用户体验到的是某些功能暂时不可达或不可用;
- 粒度一般都是服务级别
区别
- 触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑;
- 管理目标的层次不太一样,熔断其实是一个框架级的处理,每个微服务都需要(无层级之分),而降级一般需要对业务有层级之分(比如降级一般是从最外围服务开始)熔断是降级方式的一种体现。
- 降级时可能会调用熔断机制,而熔断时通常不会调用降级机制。
题外话
当然,某些框架如 Sentinel,它早期在 Dashboard 控制台中可能叫“降级”,但在新版中新版本又叫“熔断”,如下图所示
使用Hystrix实现解决雪崩问题
<!-- 支持使用hystrix断路器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
在启动类上使用@EnableCircuitBreaker启动断路器
需要注意的是,被熔断的方法需要和熔断方法的请求参数一致才可以
@RestController
@RequestMapping("/hystrixTest")
public class TestHystrixController {
private final Logger logger = LoggerFactory.getLogger(TestHystrixController.class);
@Autowired
private SsoFeign ssoFeign;
@GetMapping("/test")
//定义熔断注解,指定熔断处理方法
@HystrixCommand(fallbackMethod = "hystrixFallback")
public ResponseMsg<String> test(){
ResponseMsg<String> web = null;
try {
web = ssoFeign.getVerificationCode("web");
} catch (Exception e) {
logger.error("调用服务异常:{}",e.getMessage());
throw e;
}
return web;
}
public ResponseMsg<String> hystrixFallback(){
return ResponseUtil.buildResponseMsg("服务熔断生效!");
}
}
类中有多个方法的话,我们可以在类上写一个默认的熔断方法,这个类中所有的请求接口都会执行这一个熔断方法
@RestController
@RequestMapping("/hystrixTest1")
@DefaultProperties(defaultFallback = "defaultFallback")
public class TestHystrix1Controller {
private final Logger logger = LoggerFactory.getLogger(TestHystrix1Controller.class);
@Autowired
private SsoFeign ssoFeign;
@GetMapping("/test")
//定义熔断注解,指定熔断处理方法
//@HystrixCommand(fallbackMethod = "hystrixFallback")
@HystrixCommand
public ResponseMsg<String> test(){
ResponseMsg<String> web = null;
try {
web = ssoFeign.getVerificationCode("web");
} catch (Exception e) {
logger.error("调用服务异常:{}",e.getMessage());
throw e;
}
return web;
}
public ResponseMsg<String> hystrixFallback(){
return ResponseUtil.buildResponseMsg("服务熔断生效!");
}
public ResponseMsg<String> defaultFallback(){
return ResponseUtil.buildResponseMsg("默认服务熔断生效!");
}
}
我们可以在配置中配置熔断启动规则:
//自身调用超时10秒后才启动熔断逻辑
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
//休眠时长,默认是5000毫秒,
//这个可以这么理解:我们调用服务超时,那么熔断逻辑执行。那么在这十秒内,就算服务不再超时,熔断逻辑依然会执行,等十秒后,若首次调用成功,则熔断不再执行
hystrix.command.circuitBreaker.sleepWindowInMilliseconds=10000
Hystrix实现服务熔断的两种规则
默认情况下,Hystrix使用线程池模式。
- 线程池模式:
Hystrix为每个依赖服务调用分配一个小的独立线程池,用户的请求将不再直接访问服务,而是通过线程池中 的空闲线程来访问服务,如果线程池已满调用将被立即拒绝,否则使用线程来处理请求,可以在主线程中设置超时时间,超过这个时间如果子线程还没有执行完成任务或者子线程执行出现异常,则会进行服务降级,什么是服务降级?参考文章《什么是服务熔断和服务降级》
服务降级:优先保证核心服务,而非核心服务不可用或弱可用
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息) 。 服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池 中的资源,对其它服务没有响应。
触发Hystix服务降级的情况:
- 线程池已满
- 请求超时
但是在正常情况下不设置线程,那么被@HystrixCommand注解修饰的方法都会在同一个线程池中,如果方法A连续被调用十次,将线程池中的线程沾满后,方法B将会被熔断无法被调用。因此出现了舱壁模式
信号量模式:
如果要使用信号量模式,需要配置参数execution.isolation.strategy=ExecutionIsolationStrategy.SEMAPHORE.
在该模式下,接收请求和执行下游依赖在同一个线程内完成,不存在线程上下文切换所带来的性能开销,所以大部分场景应该选择信号量模式,但是在下面这种情况下,信号量模式并非是一个好的选择。
比如一个接口中依赖了3个下游:serviceA、serviceB、serviceC,且这3个服务返回的数据互相不依赖,这种情况下如果针对A、B、C的熔断降级使用信号量模式,那么接口耗时就等于请求A、B、C服务耗时的总和,无疑这不是好的方案。
另外,为了限制对下游依赖的并发调用量,可以配置Hystrix的execution.isolation.semaphore.maxConcurrentRequests,当并发请求数达到阈值时,请求线程可以快速失败,执行降级。
实现也很简单,一个简单的计数器,当请求进入熔断器时,执行tryAcquire(),计数器加1,结果大于阈值的话,就返回false,发生信号量拒绝事件,执行降级逻辑。当请求离开熔断器时,执行release(),计数器减1。
Hystrix的舱壁模式
上面讲的线程池模式下,被@HystrixCommand注解修饰的方法都会在同一个线程池中,如果方法A连续被调用十次,将线程池中的线程沾满后,方法B将会被熔断无法被调用。因此出现了舱壁模式。
正常情况:
舱壁模式:
我们使用threadPoolKey设置线程的名字,使用threadPoolProperties设置线程池数量和大小,给每个方法加上这些注解,那么每个方法的线程池将会被隔离开来互不影响了。
使用Feign自带的Hystix机制完成服务熔断
#feign本身也支持Hystix,只不过默认是关闭的,开启服务熔断机制,配合feign远程调用熔断机制
feign.hystrix.enabled=true
方式一
/**
* @Author:wangpeng
* @Description:rabbitmq接口远程调用失败,hystrix容错介入
* @Date:
**/
@Component
public class RabbitMqFeignHystrixFallBackFactory implements FallbackFactory<RabbitMqFeign> {
@Override
public RabbitMqFeign create(Throwable cause) {
return new RabbitMqFeign() {
@Override
public ResponseMsg<String> directOrTopicHelloMq(String exchangeName, String routingKey, String message) {
return ResponseUtil.buildResponseMsg("远程调用失败,hystrix容错机制介入:" + cause.getMessage());
}
};
}
}
在feign中使用熔断工厂:
@FeignClient(name = "sky-rabbitmq-server", path = "/rabbitMq/business", fallbackFactory = RabbitMqFeignHystrixFallBackFactory.class)
方式二:
/**
* @Author:wangpeng
* @Description:使用继承的方式
* @Date:
**/
public class SsoFeignHystrixFallBackFactory implements SsoFeign {
@Override
public ResponseMsg<String> getVerificationCode(String loginSource) {
return ResponseUtil.buildResponseMsg("容错机制启用");
}
}
/**
* @Author:wangpeng
* @Description:
* @Date:
**/
@FeignClient(name = "sky-sso-server", path = "/login",fallback = SsoFeignHystrixFallBackFactory.class)
public interface SsoFeign{
@RequestMapping("/getVerificationCode")
ResponseMsg<String> getVerificationCode(@RequestParam("loginSource")String loginSource);
}
这种方式是对所有的接口都适用的配置,如果某个接口要单独适配,需要修改default为接口名#方法名(参数类型)
#休眠时长,默认是5000毫秒,
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=10000
#触发熔断的最小请求次数,默认20
hystrix.command.default.circuitBreaker.requestVolumeThreshold=10
#触发熔断的失败请求最小占比,默认50%
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50