SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例

1 雪崩问题与Hystrix原理

在微服务架构中,服务间的调用关系比较复杂,一个请求到达了微服务A,然后A可能会调用微服务B,C,D等,然后B可能又会调用F,G等。
SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例

上图中,请求达到后应用会调用微服务A,H,I,P。
问题提出:如果微服务I阻塞了,那么请求一直得不到响应,但是连接(线程)会一直被占用,都知道Tomcat的最大连接数是有限的,200左右。如何很多请求最后都调用了服务I,那么请求就会一直被阻塞,连接(线程)会一直被占用,直到消耗完Tomcat的最大连接数。此时就不能对外提供任何服务了,注意:是任何服务,这就形成了雪崩效应。

问题解决:使用hystrix,hystrix提供了两种解决手段

  1. 线程隔离,服务降级
  2. 服务熔断

那么hytrix是怎么做的呢?看下面的图

SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例
原理解读:从图中看到,服务A-T里面都有有一定的线程数。实际上Hystrix为每个依赖服务调用分配了一个小的线程池(数量较少的线程数),如果线程池满了,或者请求超时了,调用就会立即被拒绝,而不是阻塞等待响应了,这也叫做服务降级(优先保证核心服务,而非核心服务不可用或者弱可用)。通常拒绝后,会返回给浏览器一个友好的提示信息。

2 线程隔离、服务降级案例

案例是这样的,
服务提供者user-service :提供了根据id查询用户信息的服务。访问地址:http://localhost:8083/user/1 1表示访问的用户id。

服务调用者user-consumer:访问地址http://localhost:8082/consumer/1 ,1仍然表示访问的用户id,只是,调用者直接带着这个id去请求服务提供者,然后响应浏览器。

组成中心eureka-server:访问地址http://localhost:10001,服务提供者向该注册中心进行地址注册,调用者从注册中心拉取服务提供者地址列表,使用负载均衡进行访问。

完整的代码参考eureka。为了看着方便,下面给出重要部分截图。

2.1 服务提供者user-service相关

SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例
SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例
SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例

2.1 服务消费者user-consumer相关

SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例
SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例

SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例

2.3 注册中心eureka-server

SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例
SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例

2.4 页面访问

SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例
SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例

这都是这个小案例的准备工作,现在要使用Hystrix

2.5.1服务提供者user-service进行改造

在服务层进行调用查询的使用休眠2秒。

package com.scu.service;

import com.scu.mapper.UserMapper;
import com.scu.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

	@Autowired
	private UserMapper userMapper;
	public User queryUser(Long id){
		try {
			Thread.sleep(2000);//休眠两秒
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return userMapper.selectByPrimaryKey(id);
	}
}

SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例

2.5.1服务调用者user-consumer进行改造

1)添加hystrix依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

2)修改启动类,添加@EnableCircuitBreaker注解

@EnableDiscoveryClient
@EnableCircuitBreaker
@SpringBootApplication
public class ConsumerApplicationStarter {
    @Bean
    @LoadBalanced //负载均衡  拦截
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplicationStarter.class);
    }
}

3)修改Controller

@RestController
@RequestMapping("consumer")
public class ConsumerController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("{id}")
    //声明一个失败回滚处理函数queryUserByIdFallback,当queryUserById执行超时(默认是1000毫秒),就会执行fallback函数,返回错误提示。
    @HystrixCommand(fallbackMethod = "fallbackQueryById")
    public String queryById(@PathVariable("id") Long id) {
        String url = "http://user-service/user/"+id;
        String user = restTemplate.getForObject(url, String.class);
        return user;
    }
    //返回类型和参数要与queryById一致
    public String fallbackQueryById(Long id) {
        return "服务拥挤,稍候再试!";
    }
 }

注意:默认的超时时间是1秒,在服务层设置成了2秒肯定是足够测试的。

重启两个服务,页面进行访问,用了1.04秒返回了错误提示。
SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例

2.6 Hystrix的补充

1 全局超时回滚设置:上述的超时回滚写在了方法上,如果每个方法都写会显得比较麻烦,那么如何写个所有方法共用的呢?
使用 @[email protected]即可

@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "fallbackQueryById")
public class ConsumerController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("{id}")
    @HystrixCommand//会找DefaultProperties
    public String queryById(@PathVariable("id") Long id) {
        String url = "http://user-service/user/"+id;
        String user = restTemplate.getForObject(url, String.class);
        return user;
    }
    //通用的不要写参数
    public String fallbackQueryById() {
        return "服务拥挤,稍候再试!";
    }

2 超时时间设置:既然默认的超时时间是1秒,有时候网络延迟比较严重可能觉得一秒太短,如果想设置任意时间该如何设置呢?

使用@HystrixProperty注解,里面填写name和value,name的指定可以查看类HystrixCommandProperties,当然该注解也可以使用在@DefaultProperties注解里面,此时就成了全局的(仅针对该类)。

@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "fallbackQueryById")
public class ConsumerController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("{id}")
    @HystrixCommand(commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")//指定超时时间为3秒
    })
    public String queryById(@PathVariable("id") Long id) {
        String url = "http://user-service/user/"+id;
        String user = restTemplate.getForObject(url, String.class);
        return user;
    }
    //通用的不要写参数
    public String fallbackQueryById() {
        return "服务拥挤,稍候再试!";
    }
}

SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例
此时页面访问就不会出现错误提示了,因为睡眠时间2秒 < 超时时间3秒
SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例

3 全局超时时间设置

全局也就是针对所有类的所有方法了,在配置文件application.yml里面进行配置

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 3000

此时将2中的commandProperties 属性删掉,变回1中那样。重启后页面仍然能访问而不会出现错误提示。

3 服务熔断

服务熔断可以想象成保险丝,当宿舍使用吹风机等大功率电器时,保险丝会熔断,但是之后又会恢复。服务熔断也是如此,当出现异常的时候将不再对外提供服务。

3.1 熔断原理

SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例

解读一下,首先熔断器关闭状态,所有请求正常访问;但是熔断器会统计最近的一定数量的请求数,比如10,计算请求失败的比例,当比例达到阈值时,比如50%,即失败的比例达到超过50的时候,熔断器打开,所有请求都不能访问(包括原来不出错的请求),通常均会返回友好错误提示;但是这种状态也不会一直持续下去,一定时间后,比如默认是5秒,此时会进入半开状态,此时会释放部分请求过去,如果这些请求都是健康的,那么熔断器会关闭,所有请求都能正常访问了,否则又会进去打开状态,所有请求都不能正常访问。

3.2 熔断案例

还是接着前面的案例,现在请求到达服务调用者user-consumer的时候,判断id是否为偶数,偶数的话则正常访问,否则抛出异常。
为了测试方便,修改三个参数,统计数量设置为10,即统计最近的10次请求(默认20);设置失败比例为50%,即失败比例达超过50%(默认就这么多)那么就开启熔断,所有请求都将失败;设置休眠时长为10秒(默认5秒),即10秒后进入半开状态。修改下ConsumerController

@RestController
@RequestMapping("consumer")
@DefaultProperties(defaultFallback = "fallbackQueryById")
public class ConsumerController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("{id}")
    @HystrixCommand(commandProperties = {
            @HystrixProperty(name="circuitBreaker.requestVolumeThreshold",value = "10"),
            @HystrixProperty(name="circuitBreaker.errorThresholdPercentage",value = "50"),
            @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds",value = "10000")
    })
    public String queryById(@PathVariable("id") Long id) {
        if(id % 2 == 0)
            throw new RuntimeException("服务出现异常,待会再来吧!");
        String url = "http://user-service/user/"+id;
        String user = restTemplate.getForObject(url, String.class);
        return user;
    }
    //通用的不要写参数
    public String fallbackQueryById() {
        return "服务拥挤,稍候再试!";//熔断会走到这
    }
 }

重启服务,页面访问
SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例
id是奇数,正常访问,id是偶数结果就是下面的样子。
SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例
注意到提示信息并不是抛出的异常中的提示信息,是因为最后会走fallbackQueryById方法,不信可以debug测试下。抛出异常只是告诉熔断器,请求出现问题,返回错误提示信息还是之前定义的。

那么我们快速刷新下页面,刷个十几次,然后id改为奇数,再去访问

SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例
过段时间再去访问又正常了
SpringCloud(5):Hystrix的线程隔离、服务降级与服务熔断介绍与案例