一、 什么是灾难性的雪崩效应
在微服务架构的项目中,尤其是中大型项目,肯定会出现一个服务调用其他的服务,其他服务又调用别的服务,服务和服务之间形成了一种链式的调用关系。
当少量请求时,对于整个服务链条是没有过多的影响的。
虽然每个服务的请求都是少量的,但是最终都访问服务T。所以对于服务T来说请求量就是比较大的。所在的服务器CPU压力比较高。
当其中某一个服务突然遇到大量请求时。整个链条上所有服务负载骤增。
导致服务U和服务T的负载过高。运行性能下降。会导致其他调用服务U和服务T的链条出现问题。从而所有的项目可能都出现的问题。
这种情况就称为灾难性的雪崩效应。
造成灾难性雪崩效应的原因,可以简单归结为下述三种:
服务提供者(Application Service)不可用。如:硬件故障、程序BUG、缓存击穿、并发请求量过大等。
重试加大流量。如:用户重试、代码重试逻辑等。
服务调用者(Application Client)不可用。如:同步请求阻塞造成的资源耗尽等。
雪崩效应最终的结果就是:服务链条中的某一个服务不可用,导致一系列的服务不可用,最终造成服务逻辑崩溃。这种问题造成的后果,往往是无法预料的。
二、如何防止灾难性雪崩效应
降级
超时降级、资源不足时(线程或信号量)降级,降级后可以配合降级接口返回托底数据。实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值.
保证:服务出现问题整个项目还可以继续运行。
熔断
当失败率(如因网络故障/超时造成的失败率高)达到阀值自动触发降级,熔断器触发的快速失败会进行快速恢复。
通俗理解:熔断就是具有特定条件的降级,当出现熔断时在设定的时间内容就不在请求Application Service了。所以在代码上熔断和降级都是一个注解
保证:服务出现问题整个项目还可以继续运行。
请求缓存
提供了请求缓存。服务A调用服务B,如果在A中添加请求缓存,第一次请求后走缓存了,就不在访问服务B了,即使出现大量请求时,也不会对B产生高负载。
请求缓存可以使用Spring Cache实现。
保证:减少对Application Service的调用。
请求合并
提供请求合并。当服务A调用服务B时,设定在5毫秒内所有请求合并到一起,对于服务B的负载就会大大减少,解决了对于服务B负载激增的问题。
保证:减少对Application Service的调用。
隔离
隔离分为线程池隔离和信号量隔离。通过判断线程池或信号量是否已满,超出容量的请求直接降级,从而达到限流的作用。
三、Hystrix简介
- 在Spring Cloud中解决灾难性雪崩效应就是通过Spring Cloud Netflix Hystrix实现的。
- Hystrix [hɪst'rɪks],中文含义是豪猪,因其背上长满棘刺,从而拥有了自我保护的能力。本文所说的Hystrix(中文:断路器)是Netflix开源的一款容错框架,同样具有自我保护能力。
- 通俗解释:Hystrix就是保证在高并发下即使出现问题也可以保证程序继续运行的一系列方案。作用包含两点:容错和限流。
- 在Spring cloud中处理服务雪崩效应,都需要依赖hystrix组件。在Application Client应用的pom文件中都需要引入下述依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
四、降级
- 降级是指,当请求超时、资源不足等情况发生时进行服务降级处理,不调用真实服务逻辑,而是使用快速失败(fallback)方式直接返回一个托底数据,保证服务链条的完整,避免服务雪崩。
- 解决服务雪崩效应,都是避免application client请求application service时,出现服务调用错误或网络问题。处理手法都是在application client中实现。我们需要在application client相关工程中导入hystrix依赖信息。并在对应的启动类上增加新的注解@EnableCircuitBreaker,这个注解是用于开启hystrix熔断器的,简言之,就是让代码中的hystrix相关注解生效。
Dome
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
spring:
application:
name: fallback-demo
eureka:
client:
service-url:
defaultZone: http://eurekaserver1:8761/eureka/
server:
port: 8081
@Configuration
public class RibbonConfig {
//此处使用@LoadBalancer方式快捷配置负载均衡。
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
接下来就是service、controller、application启动
public interface DemoService {
String test();
}
@Service
public class DemoServiceImpl implements DemoService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "myFallback")
@Override
public String test() {
String result = restTemplate.postForObject("http://application-service-demo/demo", null, String.class);
System.out.println(result);
return result;
}
public String myFallback(){
return "托底数据";
}
}
@Controller
public class FallbackController {
@Autowired
private DemoService demoService;
@RequestMapping("/demo")
@ResponseBody
public String demo(){
return demoService.test();
}
}
@SpringBootApplication
@EnableCircuitBreaker
public class ApplicationClientApplication {
public static void main(String[] args) {
SpringApplication.run(ApplicationclientdemoApplication.class, args);
}
}
五、熔断
- 当一定时间内,异常请求比例(请求超时、网络故障、服务异常等)达到阀值时,启动熔断器,熔断器一旦启动,则会停止调用具体服务逻辑,通过fallback快速返回托底数据,保证服务链的完整。
- 熔断有自动恢复机制,如:当熔断器启动后,每隔5秒,尝试将新的请求发送给Application Service,如果服务可正常执行并返回结果,则关闭熔断器,服务恢复。如果仍旧调用失败,则继续返回托底数据,熔断器持续开启状态。
-
降级是出错了返回托底数据,而熔断是出错后如果开启了熔断将会一定时间不在访问application service
1、属性
HystrixCommand里面的属性
熔断的实现是在调用远程服务的方法上增加@HystrixCommand注解。当注解配置满足则开启或关闭熔断器。
@HystrixProperty的name属性取值可以使用HystrixPropertiesManager常量,也可以直接使用字符串进行操作。
CIRCUIT_BREAKER_ENABLED
"circuitBreaker.enabled";
是否开启熔断策略。默认值为true。
CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD
"circuitBreaker.requestVolumeThreshold";
单位时间内(默认10s内),请求超时数超出则触发熔断策略。默认值为20次请求数。通俗说明:单位时间内容要判断多少次请求。
EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS
"execution.isolation.thread.timeoutInMilliseconds"
设置单位时间,判断circuitBreaker.requestVolumeThreshold的时间单位,默认10秒。单位毫秒。
CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS
"circuitBreaker.sleepWindowInMilliseconds";
当熔断策略开启后,延迟多久尝试再次请求远程服务。默认为5秒。单位毫秒。这5秒直接执行fallback方法,不在请求远程application service。
CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE
"circuitBreaker.errorThresholdPercentage";
单位时间内,出现错误的请求百分比达到限制,则触发熔断策略。默认为50%。
CIRCUIT_BREAKER_FORCE_OPEN
"circuitBreaker.forceOpen";
是否强制开启熔断策略。即所有请求都返回fallback托底数据。默认为false。
CIRCUIT_BREAKER_FORCE_CLOSED
"circuitBreaker.forceClosed";
是否强制关闭熔断策略。即所有请求一定调用远程服务。默认为false。
@HystrixCommand(fallbackMethod = "myFallback",commandProperties = {
// 条件一: 请求数量到达3个
@HystrixProperty(name= "circuitBreaker.requestVolumeThreshold",value="3"),
//可以这样写@HystrixProperty(name=HystrixPropertiesManager.CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD ,value="3")
// 判断时间,没10秒作为一个判断单位
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value="10000"),
// 条件二: 失败了到达50%
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value="50"),
// 结果: 开启熔断后,30秒不在请求远程服务
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value = "30000")
})
六、请求缓存
Hystrix为了降低访问服务的频率,支持将一个请求与返回结果做缓存处理。如果再次请求的URL没有变化,那么Hystrix不会请求服务,而是直接从缓存中将结果返回。这样可以大大降低访问服务的压力。
Hystrix自带缓存。有两个缺点:
- 是一个本地缓存。在集群情况下缓存是不能同步的。
- 不支持第三方缓存容器。Redis,memcached不支持的。
所以可以利用spring cache。实现请求缓存。
在降级处理的代码基础上完成下面变化。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
修改配置文件
添加redis的配置,此处使用的是redis单机版。如果是redis集群使用spring.redis.cluster.nodes进行配置。
spring:
redis:
host: ipaddress #你的redis的IP地址
注意添加东西
- 在启动类上添加@EnableCaching注解
- 在service实现类方法上面额外在添加一个注解。
@Cacheable(key = "'key值'",cacheNames = "缓存名字")
七、请求合并
没有请求合并
Application Service 负载是Application Client发送请求的总数量
请求合并
把一段时间范围内的所有请求合并为一个请求。大大的降低了Application Service 负载。
什么情况下使用请求合并
在微服务架构中,我们将一个项目拆分成很多个独立的项目,这些独立的项目通过远程调用来互相配合工作,但是,在高并发情况下,通信次数的增加会导致总的通信时间增加,同时,线程池的资源也是有限的,高并发环境会导致有大量的线程处于等待状态,进而导致响应延迟,为了解决这些问题,我们需要来了解Hystrix的请求合并。
请求合并的缺点
设置请求合并之后,本来一个请求可能5ms就搞定了,但是现在必须再等10ms看看还有没有其他的请求一起的,这样一个请求的耗时就从5ms增加到15ms了,不过,如果我们要发起的命令本身就是一个高延迟的命令,那么这个时候就可以使用请求合并了,因为这个时候时间窗的时间消耗就显得微不足道了,另外高并发也是请求合并的一个非常重要的场景。
请求合并参数介绍
- @HystrixCollapser 进行请求合并
- batchMethod:处理请求合并的方法
- scope - 合并请求的请求作用域。可选值有global和request。
global代表所有的请求线程都可以等待可合并。 常用
request代表一个请求线程中的多次远程服务调用可合 - timerDelayInMilliseconds:等待时长,默认10毫秒。
- maxRequestInBatch:最大请求合并数量。
- @HystrixCommand 处理请求合并的方法必须有此注解。
- 实现类中client(String)方法一旦被@HystrixCollapser标记,方法就不会被执行,方法体中为空即可。直接执行batchMethod对应的方法。
- batchMethod方法返回值顺序和传递进来的参数顺序有关系的。
注意:
在实际测试中scope使用默认值REQUEST会出现空指针异常,请换成GLOBAL
Future<String> client(String name);
//上面是接口
@Override
@HystrixCollapser(batchMethod = "myBatchMethod",scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL
,collapserProperties = {@HystrixProperty(name="timerDelayInMilliseconds",value="10"),@HystrixProperty(name="maxRequestsInBatch",value = "200")})
public Future<String> client(String name) {
System.out.println("client方法,有请求合并时将不支持这个方法");
return null;
}
@HystrixCommand
public List<String> myBatchMethod(List<String> name){
System.out.println("传递过去的参数:"+name);
List<String> list = restTemplate.postForObject("http://APPLICATION-SERVICE/service2", name, List.class);
return list;
}
八、隔离
1、线程隔离
1.1 为什么使用线程池隔离
没有线程池隔离的时候可能因为某个接口的高并发导致其他接口也出现问题。
当使用线程池隔离。不同接口有着自己独立的线程池
即使某个线程池都被占用,也不影响其他线程。
1.2 Hystrix的线程池隔离
Hystrix采用Bulkhead Partition舱壁隔离技术。
舱壁隔离指的的是讲船体内部分为多个隔舱,一旦其中某几个隔舱发生破损进水,水流不会在其他舱壁中流动,从而保证船舱依然具有足够的浮力和稳定性,降低沉船危险。
1.3线程池隔离的优缺点
- 优点:
- 任何一个服务都会被隔离在自己的线程池内,即使自己的线程池资源填满也不会影响其他服务。
- 当依赖的服务重新恢复时,可通过清理线程池,瞬间恢复服务的调用。但是如果是tomcat线程池被填满,再恢复就会很麻烦。
- 每个都是独立线程池。一定程度上解决了高并发问题。
- 由于线程池中线程个数是有限制,所以也解决了限流问题。
- 缺点:
- 增加了CPU开销。因为不仅仅有Tomcat的线程池,还需要有Hystrix线程池。
- 每个操作都是独立的线程,就有排队、调度和上下文切换等问题。
代码:
//代码只是把这个注解加到实现类的方法上面就好了
//注解里面的意思是一个服务名为groupKey = "jqk",服务接口是commandKey = "abc",虽然自定义但是一般为方法名,线程名称是threadPoolKey = "jqk"
@HystrixCommand(groupKey = "jqk",commandKey = "abc",threadPoolKey = "jqk",threadPoolProperties = {
//threadPoolProperties线程池里面的配置,最大并发也是最大数量name="coreSize",value="8"
@HystrixProperty(name="coreSize",value="8"),
//设置了最大队列长度name="maxQueueSize",value="5"
@HystrixProperty(name="maxQueueSize",value="5"),
//设置了线程最大存活时间name="keepAliveTimeMinutes",value="2"单位分钟
@HystrixProperty(name="keepAliveTimeMinutes",value="2"),
//设置了拒绝请求临界值为name="queueSizeRejectionThreshold",value="5",在maxQueueSize的基础之上再加这个值设置数量超过就拒绝请求响应了
@HystrixProperty(name="queueSizeRejectionThreshold",value="5")
})
@Override
public String thread() {
System.out.println(Thread.currentThread().getName());
return "thread1";
}
@Override
public String thread2() {
System.out.println(Thread.currentThread().getName());
return "thread2";
}
参数说明
2、信号量隔离
2.1信号量是什么
- java.util.concurrent.Semaphore用来控制可同时并发的线程数。通过构造方法指定内部虚拟许可的数量。每次线程执行操作时先通过acquire方法获得许可,执行完毕再通过release方法释放许可。如果无可用许可,那么acquire方法将一直阻塞,直到其它线程释放许可。
- 如果采用信号量隔离技术,每接收一个请求,都是服务自身线程去直接调用依赖服务,信号量就相当于一道关卡,每个线程通过关卡后,信号量数量减1,当为0时不再允许线程通过,而是直接执行fallback逻辑并返回,说白了仅仅做了一个限流。
代码演示
@HystrixCommand(commandProperties = {
//.EXECUTION_ISOLATION_STRATEGY默认就是线程池隔离,现在改为SEMAPHORE信号隔离
@HystrixProperty(name=HystrixPropertiesManager.EXECUTION_ISOLATION_STRATEGY,value="SEMAPHORE"),
//信号最大并发度
@HystrixProperty(name=HystrixPropertiesManager.EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS,value="10")
},fallbackMethod = "jqk")
@Override
public String semaphore() {
System.out.println("执行了,访问service");
参数说明
线程池隔离和信号量隔离
限流说明
- 在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击。
- 通过Hystrix的线程池隔离和信号量隔离控制了线程数量也就实现了限流效果。