前言
复杂分布式体系结构中的微服务存在着依赖关系,每个依赖关系在某些时候将不可避免的出现差错,将会导致服务调用者被阻塞,进而导致整个分布式系统面临瘫痪的风险,我们称这种问题叫做服务雪崩。
服务雪崩
服务雪崩的产生过程
服务雪崩效应是一种因服务提供者的不可用导致服务调用者的不可用,并将不可用 逐渐放大的过程,如果所示:
正常情况下,服务调用者发送请求,经过多个服务的传递到达服务F,服务F处理完毕后然后逐级返回,最终服务调用者拿到结果。
当服务E因为某种原因导致不可用时,整个服务调用调用过程会进行阻塞状态,但是服务调用者不知道该问题,会不断重试服务调用。
因为服务调用者的请求量多大,导致服务E压力过大,进而导致各级服务都会崩溃,最后服务调用者也将变为不可用。
服务雪崩的原因
- 流量激增:比如异常流量、用户重试导致系统负载升高;
- 缓存击穿:假设A为client端,B为Server端,假设A系统请求都流向B系统,请求超出了B系统的承载能力,就会造成B系统崩溃;
- 程序Bug:代码循环调用的逻辑问题,资源未释放引起的内存泄漏等问题;
- 硬件故障:比如宕机,机房断电,光纤被挖断等。
- 线程同步等待:系统间经常采用同步服务调用模式,核心服务和非核心服务共用一个线程池和消息队列。如果一个核心业务线程调用非核心线程,这个非核心线程交由第三方系统完成,当第三方系统本身出现问题,导致核心线程阻塞,一直处于等待状态,而进程间的调用是有超时限制的。大量的等待线程会占用系统资源,一旦系统资源被耗尽,服务将会转为不可用状态,从而引发服务雪崩。
通过实践发现,线程同步等待是最常见引发的雪崩效应的场景。
服务雪崩的应对策略
针对上述雪崩产生的场景,有很多应对方案,但没有一个万能的模式能够应对所有场景。
- 流量控制
- 网关限流。因为 Nginx 的高性能, 目前一线互联网公司大量采用 Nginx+Lua 的网关进行流量控制, 由此而来的 OpenResty 也越来越热门.
- 用户交互限流。用户交互限流的具体措施有:1、 采用加载动画,提高用户的忍耐等待时间.;2、提交按钮添加强制等待时间机制。
- 关闭重试
- 改进缓存模式
- 缓存预加载
- 同步改为异步刷新
- 服务自动扩容
- AWS的auto scaling
- 针对硬件故障,多机房容灾,跨机房路由,异地多活等。
- 针对同步等待,使用 Hystrix 做故障隔离,熔断器机制等可以解决依赖服务不可用的问题。
通过实践发现,线程同步等待是最常见引发的雪崩效应的场景,本文将重点介绍使用 Hystrix 技术解决服务的雪崩问题。
简介
什么是Hystrix?
在分布式环境中,不可避免地会有许多服务依赖项中的某些失败。Hystrix是一个库,可通过添加延迟容限和容错逻辑来帮助您控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点,停止服务之间的级联故障并提供后备选项来实现此目的,所有这些都可以提高系统的整体弹性。
Github 地址: https://github.com/Netflix/Hystrix/wiki
Hystrix的工作原理
- 防止任何单个依赖项耗尽所有容器(例如Tomcat)用户线程。
- 减少负载并快速失败,而不是排队。
- 在可行的情况下提供备用,以保护用户免受故障的影响。
- 使用隔离技术(例如隔板,泳道和断路器模式)来限制任何一种依赖关系的影响。
- 通过近实时指标,监视和警报优化发现时间
- 通过在Hystrix的大多数方面中以低延迟传播配置更改来优化恢复时间,并支持动态属性更改,这使您可以通过低延迟反馈环路进行实时操作修改。
- 防止整个依赖项客户端执行失败,而不仅仅是网络流量失败。
如何通过 Hystrix 解决服务雪崩?
降级
超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。
实现一个 fallback 方法,当请求后端服务出现异常的时候,可以使用 fallback 方法返回值。
隔离(线程池隔离和信号量隔离)
限制调用分布式服务的资源使用,某一个调用的服务出现问题不会影响其他服务调用。
熔断
当失败率(网络故障、超时造成的失败率)达到阈值自动触发降级,熔断器触发的快速失败会进行快速恢复 。
请求合并
对于高并发情况下的多次请求合并为一个请求。
测试
基于Ribbon实现的负载均衡
降级
降级是针对客户端的操作,即我们对 springcloud-consumer-dept-80 项目进行修改。
1、导入 hystrix 依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> <version>1.4.6.RELEASE</version> </dependency> 复制代码
2、修改 DeptConsumerController
@RestController public class DeptConsumerController { @Autowired RestTemplate restTemplate; // private static final String REST_URL_PREFIX = "http://localhost:8001"; //通过服务名进行调用 private static final String REST_URL_PREFIX = "http://SPRINGCLOUD-PROVIDER-DEPT"; @HystrixCommand(fallbackMethod = "getDeptHystrix") @RequestMapping("/consumer/dept/get/{id}") public Dept getDept(@PathVariable("id") long id) { Dept dept = restTemplate.getForObject(REST_URL_PREFIX+"/dept/get/"+id, Dept.class); return dept; } public Dept getDeptHystrix(long id){ return new Dept().setDeptId(id). setDpName("id="+id+"=>没有对应的信息,null"). setDbSource("no database in MySQL"); } @RequestMapping("/consumer/dept/list") public List<Dept> queryAll(){ return restTemplate.getForObject(REST_URL_PREFIX+"/dept/list",List.class); } @RequestMapping(name = "/consumer/dept/add") public boolean addDept(Dept dept){ return restTemplate.postForObject(REST_URL_PREFIX+"/dept/add",dept,Boolean.class); } } 复制代码
3、改回轮询策略
@Configuration public class RuleConfig { @Bean public IRule getRule(){ //仅当测试自定义负载均衡策略时使用 // return new MyRandomRule(); return new RoundRobinRule(); } } 复制代码
4、入口类增加注解
@SpringBootApplication @EnableEurekaClient //在微服务启动时去加载我们自定义的负载均衡策略 @RibbonClient(name = "SPRINGCLOUD-PROVIDER-DEPT",configuration = RuleConfig.class) @EnableCircuitBreaker public class DeptConsumer_80 { public static void main(String[] args) { SpringApplication.run(DeptConsumer_80.class,args); } } 复制代码
5、启动一个注册中心,3个服务提供者,以及本项目服务消费者,然后访问 http://localhost/consumer/dept/get/1,不断刷新可以看到,页面会挨个访问3个服务提供者的数据,假设此时我们关闭 8001 端口的服务提供者,然后继续刷新访问该网址,页面内容效果如下:
从结果可知,当轮询到第一个服务提供者,即 8001 端口代表的服务时,由于该端口被我们停止了,导致服务不可访问,所以才会返回我们在客户端代码中定义的服务降级后的结果。
隔离(线程池隔离)
我们可以用蓄水池做比喻来解释什么是资源隔离。生活中一个大的蓄水池由一个一个小的池子隔离开来,这样如果某一个水池的水被污染,也不会波及到其它蓄水池,如果只有一个蓄水池,水池被污染,整池水都不可用了。软件资源隔离如出一辙,如果采用资源隔离模式,将对远程服务的调用隔离到一个单独的线程池后,若服务提供者不可用,那么受到影响的只会是这个独立的线程池。
隔离是针对服务提供者 Provider 做的修改,我们根据 springcloud-provider-dept-8001 项目复制新增一个名为 springcloud-provider-dept-threadpool-8001 的项目,然后修改新项目。
1、导入 hystrix 依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> <version>1.4.6.RELEASE</version> </dependency> 复制代码
2、修改 DeptServiceImpl
@Service public class DeptServiceImpl implements DeptService { @Autowired DeptMapper deptMapper; @Override public boolean addDept(Dept dept) { return deptMapper.addDept(dept); } @HystrixCommand(groupKey = "ego-dept-provider", commandKey = "queryDept", threadPoolKey = "ego-dept-provider", //给线程名添加前缀 threadPoolProperties = { @HystrixProperty(name = "coreSize", value = "30"),//线程池大小 @HystrixProperty(name = "maxQueueSize", value = "100"),//最大队列长度 @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),//线程存活时间 @HystrixProperty(name = "queueSizeRejectionThreshold", value = "15")//拒绝请求 }, fallbackMethod = "getDeptHystrix") @Override public Dept queryDept(long id) { return deptMapper.queryDept(id); } public Dept getDeptHystrix(long id){ return new Dept().setDeptId(id). setDpName("id="+id+"=>没有对应的信息,null"). setDbSource("no database in MySQL"); } @Override public List<Dept> queryAll() { return deptMapper.queryAll(); } } 复制代码
线程池隔离参数详解:
img
3、DeptController
@RestController public class DeptController { @Autowired DeptService deptService; @GetMapping("/dept/get/{id}") public Dept getDept(@PathVariable("id") long id) { Dept dept = deptService.queryDept(id); return dept; } @GetMapping("/dept/list") public List<Dept> queryAll(){ List<Dept> list = deptService.queryAll(); return list; } @PostMapping("/dept/add") public boolean addDept(@RequestBody Dept dept){ System.out.println(dept); return deptService.addDept(dept); } } 复制代码
4、入口类增加@EnableCircuitBreaker 注解
@SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker public class DeptProviderThreadPool_8001 { public static void main(String[] args) { SpringApplication.run(DeptProviderThreadPool_8001.class,args); } } 复制代码
5、模拟测试
我在本地使用 Jmeter 模拟高并发,虽然瞬间高并发会造成数据的错误返回,但是没有导致服务不可用,所以实验场景可能还有待考量。如果有大神知道该如何模拟测试,望不吝赐教。
隔离(信号隔离)
使用一个原子计数器来记录当前有多少个线程在运行,当请求来临时,先判断计数器的数值,若超过设置的最大线程个数则丢弃该类型的新请求,若不超过则执行计数操作,请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务),参考Java的信号量的用法。
Hystrix 默认采用线程池隔离机制,当然用户也可以配置 HystrixCommandProperties 为隔离策略为ExecutionIsolationStrategy.SEMAPHORE。
信号隔离的特点:
- 信号隔离与线程隔离最大不同在于执行依赖代码的线程依然是请求线程,该线程需要通过信号申请;
- 如果客户端是可信的且可以快速返回,可以使用信号隔离替换线程隔离,降低开销。
img
关于信号隔离的代码书写与线程池隔离的写法很相似,由于不知道怎么测试,所以就不做代码的展示。