我们知道大量请求会阻塞在Tomcat服务器上,影响其它整个服务.在复杂的分布式架构的应用程序有很多的依赖,都会不可避免地在某些时候失败.高并发的依赖失败时如果没有隔离措施,当前应用服务就有被拖垮的风险.
Spring Cloud Netflix Hystrix就是隔离措施的一种实现,可以设置在某种超时或者失败情形下断开依赖调用或者返回指定逻辑,从而提高分布式系统的稳定性.
生活中举个例子,如电力过载保护器,当电流过大的的时候,出问题,过载器会自动断开,从而保护电器不受烧坏。因此Hystrix请求熔断的机制跟电力过载保护器的原理很类似。
比如:订单系统请求库存系统,结果一个请求过去,因为各种原因,网络超时,在规定几秒内没反应,或者服务本身就挂了,这时候更多的请求来了,不断的请求库存服务,不断的创建线程,因为没有返回,也就资源没有释放,
这也导致了系统资源被耗尽,你的服务奔溃了,这订单系统好好的,你访问了一个可能有问题的库存系统,结果导致你的订单系统也奔溃了,你再继续调用更多的依赖服务,可会会导致更多的系统奔溃,这时候Hystrix可以实现快速失败,
如果它在一段时间内侦测到许多类似的错误,会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作进而导致资源耗尽。这时候Hystrix进行FallBack操作来服务降级,
Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存.通知后面的请求告知这服务暂时不可用了。
使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。Hystrix熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。
如下图所示:
Hystrix设计原则
-
防止单个服务的故障,耗尽整个系统服务的容器(比如tomcat)的线程资源,避免分布式环境里大量级联失败。通过第三方客户端访问(通常是通过网络)依赖服务出现失败、拒绝、超时或短路时执行回退逻辑
-
用快速失败代替排队(每个依赖服务维护一个小的线程池或信号量,当线程池满或信号量满,会立即拒绝服务而不会排队等待)和优雅的服务降级;当依赖服务失效后又恢复正常,快速恢复
-
提供接近实时的监控和警报,从而能够快速发现故障和修复。监控信息包括请求成功,失败(客户端抛出的异常),超时和线程拒绝。如果访问依赖服务的错误百分比超过阈值,断路器会跳闸,此时服务会在一段时间内停止对特定服务的所有请求
-
将所有请求外部系统(或请求依赖服务)封装到HystrixCommand或HystrixObservableCommand对象中,然后这些请求在一个独立的线程中执行。使用隔离技术来限制任何一个依赖的失败对系统的影响。每个依赖服务维护一个小的线程池(或信号量),当线程池满或信号量满,会立即拒绝服务而不会排队等待
Hystrix特性
-
请求熔断: 当Hystrix Command请求后端服务失败数量超过一定比例(默认50%), 断路器会切换到开路状态(Open). 这时所有请求会直接失败而不会发送到后端服务. 断路器保持在开路状态一段时间后(默认5秒), 自动切换到半开路状态(HALF-OPEN).
这时会判断下一次请求的返回情况, 如果请求成功, 断路器切回闭路状态(CLOSED), 否则重新切换到开路状态(OPEN). Hystrix的断路器就像我们家庭电路中的保险丝, 一旦后端服务不可用, 断路器会直接切断请求链, 避免发送大量无效请求影响系统吞吐量, 并且断路器有自我检测并恢复的能力. -
服务降级:Fallback相当于是降级操作. 对于查询操作, 我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值. fallback方法的返回值一般是设置的默认值或者来自缓存.告知后面的请求服务不可用了,不要再来了。
-
依赖隔离(采用舱壁模式,Docker就是舱壁模式的一种):在Hystrix中, 主要通过线程池来实现资源隔离. 通常在使用的时候我们会根据调用的远程服务划分出多个线程池.比如说,一个服务调用两外两个服务,你如果调用两个服务都用一个线程池,那么如果一个服务卡在哪里,资源没被释放
后面的请求又来了,导致后面的请求都卡在哪里等待,导致你依赖的A服务把你卡在哪里,耗尽了资源,也导致了你另外一个B服务也不可用了。这时如果依赖隔离,某一个服务调用A B两个服务,如果这时我有100个线程可用,我给A服务分配50个,给B服务分配50个,这样就算A服务挂了,我的B服务依然可以用。 -
请求缓存:比如一个请求过来请求我userId=1的数据,你后面的请求也过来请求同样的数据,这时我不会继续走原来的那条请求链路了,而是把第一次请求缓存过了,把第一次的请求结果返回给后面的请求。
-
请求合并:我依赖于某一个服务,我要调用N次,比如说查数据库的时候,我发了N条请求发了N条SQL然后拿到一堆结果,这时候我们可以把多个请求合并成一个请求,发送一个查询多条数据的SQL的请求,这样我们只需查询一次数据库,提升了效率。
下面开始进行实战:
1.引入Hystrix相关的依赖如下依赖所示:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2.在启动类中加入@EnableCircuitBreaker注解,表示允许断路器。如下代码所示:
@SpringBootApplication
@EnableDiscoveryClient
//允许断路器
@EnableCircuitBreaker
public class RibbonApplication {
public static void main(String[] args) {
SpringApplication.run(RibbonApplication.class, args);
}
@Bean
public IRule ribbonRule(){
return new RandomRule();
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
2.现在为了代码比较清晰一点,我们需要在先前的Ribbon模块进行新建一个service
@Service
public class HelloService {
@Autowired
private RestTemplate restTemplate;
//请求熔断注解,当服务出现问题时候会执行fallbackMetho属性的名为helloFallBack的方法
@HystrixCommand(fallbackMethod = "helloFallBack")
public String helloService() throws ExecutionException, InterruptedException {
return restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
}
public String helloFallBack(){
return "error";
}
}
Controller端代码修改为:
@RestController
public class ConsumerController {
@Autowired
private HelloService helloService;
@RequestMapping("/consumer")
public String helloConsumer() throws ExecutionException, InterruptedException {
return helloService.helloService();
}
}
先把前面的两个Eureka注册中心,和前面的provider1,和provider2模块启动起来。
接着再把Ribbon模块启动起来
不管敲几遍,还是出现hello1,hello2,因为有前面的轮询算法。
现在如果我们突然将provider2模块断开,即停止下来,再来在浏览器上输入localhost:8082/consumer,运行结果如下:
hello 1
再进行一次localhost:8082/consumer,运行结果,就变成如下:
error
我们看到了当轮询到第二个服务提供者的时候,即provider2,由于provider2被我们停止了,导致服务不可访问了,返回我们原先在代码中定义的服务降级后的结果error回来,当后面还有请求再也不会轮询到provider2了,
网页上永远出现hello1。
到这里简单演示了用Hystrix的注解@HystrixCommand(fallbackMethod = “helloFallBack”),来实现熔断和服务降级。这只是表面的东西而已,根本不清楚他背后的原理,
因此这里进入注解@HystrixCommand(fallbackMethod = “helloFallBack”)的背后原理来实现熔断和服务降级。用我们自己手写的代码去实现熔断和服务降级。那么Hystrix给我们留下了什么样的接口呢?可以让我们自己手动更灵活的去实现熔断和服务降级。
Hystrix给我们提供了HystrixCommand类,让我们去继承它,去实现灵活的熔断和服务降级。
如下代码:
public class HelloServiceCommand extends HystrixCommand<String> {
private RestTemplate restTemplate;
protected HelloServiceCommand(HystrixCommandGroupKey group) {
super(group);
}
//服务调用
@Override
protected String run() throws Exception {
System.out.println(Thread.currentThread().getName());
return restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
}
//服务降级时所调用的Fallback()
@Override
protected String getFallback() {
return "error";
}
}
看到上面的代码,问题又来了,我们知道我们继承HystrixCommand类的HelloServiceCommand 是没有交由Spring进行管理的,那么也就没法进行RestTemplate注入了。
那么我们怎么做的呢?这时候读者要转过弯来了,我们为什么不通过Controller先注入,然后调用Service层的时候,通过HelloServiceCommand的构造方法注入呢?因此问题就迎刃而解了。
修改后的代码如下:
public class HelloServiceCommand extends HystrixCommand<String> {
private RestTemplate restTemplate;
protected HelloServiceCommand(String commandGroupKey,RestTemplate restTemplate) {
super(HystrixCommandGroupKey.Factory.asKey(commandGroupKey));
this.restTemplate = restTemplate;
}
@Override
protected String run() throws Exception {
System.out.println(Thread.currentThread().getName());
return restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
}
@Override
protected String getFallback() {
return "error";
}
}
Controller层的代码如下:
@RestController
public class ConsumerController {
@Autowired
private HelloService helloService;
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/consumer")
public String helloConsumer() throws ExecutionException, InterruptedException {
HelloServiceCommand command = new HelloServiceCommand("hello",restTemplate);
String result = command.execute();
return result;
}
}
我们看到了当轮询到第二个服务提供者的时候,即provider2,由于provider2被我们停止了,导致服务不可访问了,返回我们原先在代码中定义的服务降级后的结果error回来,当后面还有请求再也不会轮询到provider2了,
网页上永远出现hello1。
那么问题又来了,restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
这是阻塞式的,因为这是阻塞式的,如果后面还有代码,必须等到网络请求restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
返回结果后,你后面的代码才会执行。
如果此刻,有一个请求过来,通过Ribbon客户端进来了,Ribbon客户端调用了三个服务,每一服务执行的时间都是2秒钟,那么这三个服务都是用阻塞IO来执行的话,那么耗时是2+2+2=6,一共就花了6秒钟。那么如果我们使用异步来执行的话,花费的时间就是这三个服务中
哪一个耗时长就是总耗时时间,比如,此时耗时最多的一个服务是3秒钟,那么总共耗时就花了3秒钟。那么异步IO是什么意思呢?就是请求发出去以后,主线程不会在原地等着,会继续往下执行我的主线程,什么时候返回结果,我就什么时候过去取出来。等着三个服务执行完了我就一次性把结果取
出来。
非阻塞式IO有两个分别是:Future将来式,Callable回调式
1.Future将来式:就是说你用Future将来式去请求一个网络IO之类的任务,它会一多线程的形式去实现,主线程不必卡死在哪里等待,等什么时候需要结果就通过Future的get()方法去取,不用阻塞。
2.Callable回调式:预定义一个回调任务,Callable发出去的请求,主线程继续往下执行,等你请求返回结果执行完了,会自动调用你哪个回调任务。
好了,那么代码如何修改呢?其实HelloServiceCommand类几面不用变,只需要改变一下在Controller层的command的调用方式即可,command的叫用方式如下:
Future<String> queue = command.queue();
return queue.get();
然后重启Ribbon模块,结果跟上面一样。
那么Future的注解方式如何调用呢?代码如下所示:
@Service
public class HelloService {
@Autowired
private RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "helloFallBack")
public String helloService() throws ExecutionException, InterruptedException {
Future<String> future = new AsyncResult<String>() {
@Override
public String invoke() {
return restTemplate.getForEntity("http://HELLO-SERVICE/hello",String.class).getBody();
}
};
return future.get();
}
public String helloFallBack(){
return "error";
}
}
对于服务降级里面还有网络请求,请求又失败可以再次降级,在一级降级方法上继续打上 @HystrixCommand注解进行级联,然后进行二次服务降级,一般不会这样干,因为这样下去没完没了。
如果想在服务降级拿到异常,给业务一些提示,那怎么办呢?很简单,你在方法里面加入Throwable即可,代码如下:
@HystrixCommand(fallbackMethod = "XX降级方法",observableExecutionMode = ObservableExecutionMode.LAZY)
public String helloFallBack(Throwable throwable){
//网络请求
........
return "error";
}
服务监控
如果我们还要进行服务的监控的话,那么我们需要在Ribbon模块,和两个服务提供者模块提供如下依赖:
Ribbon模块依赖如下:
<!--仪表盘-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
<version>1.4.0.RELEASE</version>
</dependency>
<!--监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
两个provider模块依赖如下:
<!--监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-actuator</artifactId>
</dependency>
接着在Ribbon启动类打上@EnableHystrixDashboard注解,然后启动,localhost:8082/hystrix,如下图:
每次访问都有记录:如下:
接下来我们看一下常用的Hystrix属性:
hystrix.command.default和hystrix.threadpool.default中的default为默认CommandKey
Command Properties:
- Execution相关的属性的配置:
-
hystrix.command.default.execution.isolation.strategy 隔离策略,默认是Thread, 可选Thread|Semaphore
-
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 命令执行超时时间,默认1000ms
-
hystrix.command.default.execution.timeout.enabled 执行是否启用超时,默认启用true
-
hystrix.command.default.execution.isolation.thread.interruptOnTimeout 发生超时是是否中断,默认true
-
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests 最大并发请求数,默认10,该参数当使用ExecutionIsolationStrategy.SEMAPHORE策略时才有效。如果达到最大并发请求数,请求会被拒绝。理论上选择semaphore size的原则和选择thread size一致,但选用semaphore时每次执行的单元要比较小且执行速度快(ms级别),否则的话应该用thread。
semaphore应该占整个容器(tomcat)的线程池的一小部分。
- Fallback相关的属性
这些参数可以应用于Hystrix的THREAD和SEMAPHORE策略
- hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests 如果并发数达到该设置值,请求会被拒绝和抛出异常并且fallback不会被调用。默认10
- hystrix.command.default.fallback.enabled 当执行失败或者请求被拒绝,是否会尝试调用hystrixCommand.getFallback() 。默认true
- Circuit Breaker相关的属性
- hystrix.command.default.circuitBreaker.enabled 用来跟踪circuit的健康性,如果未达标则让request短路。默认true
- hystrix.command.default.circuitBreaker.requestVolumeThreshold 一个rolling window内最小的请求数。如果设为20,那么当一个rolling window的时间内(比如说1个rolling window是10秒)收到19个请求,即使19个请求都失败,也不会触发circuit break。默认20
- hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds 触发短路的时间值,当该值设为5000时,则当触发circuit break后的5000毫秒内都会拒绝request,也就是5000毫秒后才会关闭circuit。默认5000
- hystrix.command.default.circuitBreaker.errorThresholdPercentage错误比率阀值,如果错误率>=该值,circuit会被打开,并短路所有请求触发fallback。默认50
- hystrix.command.default.circuitBreaker.forceOpen 强制打开熔断器,如果打开这个开关,那么拒绝所有request,默认false
- hystrix.command.default.circuitBreaker.forceClosed 强制关闭熔断器 如果这个开关打开,circuit将一直关闭且忽略circuitBreaker.errorThresholdPercentage
- Metrics相关参数
- hystrix.command.default.metrics.rollingStats.timeInMilliseconds 设置统计的时间窗口值的,毫秒值,circuit break 的打开会根据1个rolling window的统计来计算。若rolling window被设为10000毫秒,则rolling window会被分成n个buckets,每个bucket包含success,failure,timeout,rejection的次数的统计信息。默认10000
- hystrix.command.default.metrics.rollingStats.numBuckets 设置一个rolling window被划分的数量,若numBuckets=10,rolling window=10000,那么一个bucket的时间即1秒。必须符合rolling window % numberBuckets == 0。默认10
- hystrix.command.default.metrics.rollingPercentile.enabled 执行时是否enable指标的计算和跟踪,默认true
- hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds 设置rolling percentile window的时间,默认60000
- hystrix.command.default.metrics.rollingPercentile.numBuckets 设置rolling percentile window的numberBuckets。逻辑同上。默认6
- hystrix.command.default.metrics.rollingPercentile.bucketSize 如果bucket size=100,window=10s,若这10s里有500次执行,只有最后100次执行会被统计到bucket里去。增加该值会增加内存开销以及排序的开销。默认100
- hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds 记录health 快照(用来统计成功和错误绿)的间隔,默认500ms
- ThreadPool 相关参数
线程数默认值10适用于大部分情况(有时可以设置得更小),如果需要设置得更大,那有个基本得公式可以follow:
requests per second at peak when healthy × 99th percentile latency in seconds + some breathing room
每秒最大支撑的请求数 (99%平均响应时间 + 缓存值)
比如:每秒能处理1000个请求,99%的请求响应时间是60ms,那么公式是:
1000 (0.060+0.012)
基本得原则时保持线程池尽可能小,他主要是为了释放压力,防止资源被阻塞。
当一切都是正常的时候,线程池一般仅会有1到2个线程激活来提供服务
- hystrix.threadpool.default.coreSize 并发执行的最大线程数,默认10
- hystrix.threadpool.default.maxQueueSize BlockingQueue的最大队列数,当设为-1,会使用SynchronousQueue,值为正时使用LinkedBlcokingQueue。该设置只会在初始化时有效,之后不能修改threadpool的queue size,除非reinitialising thread executor。默认-1。
- hystrix.threadpool.default.queueSizeRejectionThreshold 即使maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝。因为maxQueueSize不能被动态修改,这个参数将允许我们动态设置该值。if maxQueueSize == -1,该字段将不起作用
- hystrix.threadpool.default.keepAliveTimeMinutes 如果corePoolSize和maxPoolSize设成一样(默认实现)该设置无效。如果通过plugin(https://github.com/Netflix/Hystrix/wiki/Plugins)使用自定义实现,该设置才有用,默认1.
- hystrix.threadpool.default.metrics.rollingStats.timeInMilliseconds 线程池统计指标的时间,默认10000
- hystrix.threadpool.default.metrics.rollingStats.numBuckets 将rolling window划分为n个buckets,默认10