Hystrix

1 Hystrix

1.1 简介

hystrix,即熔断器英文意思是豪猪,全身是刺,看起来就不好惹,是一种保护机制。

Hystrix也是Netflix公司的一款组件。

主页:https://github.com/Netflix/Hystrix/

1525658740266

那么Hystrix的作用是什么呢?具体要保护什么呢?

Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库、防止出现级联失败。

​ 熔断器Hystrix是容错管理工具,作用是通过隔离、控制服务从而对延迟和故障提供更强大的容错能力,避免整个系统被拖垮。
​ 复杂分布式架构通常都具有很多依赖,当一个应用高度耦合其他服务时非常危险且容易导致失败,这种失败很容易伤害服务的调用者,最后导致一个接一个的连续错误,应用本身就处在被拖垮的风险中,最后失去控制,就像在一个高流量的网站中,某个单一的后端一旦发生延迟,将会在数秒内导致所有应用资源被耗尽。如何处理这些问题是有关系统性能和效率的关键性问题。
​ 当在系统高峰时期,大量对微服务的调用可能会堵塞远程服务器的线程池,如果这个线程池没有和主应用服务器的线程池隔离,就可能导致整个服务器挂机。
​ Hystrix使用自己的线程池,这样和主应用服务器线程池隔离,如果调用花费很长时间,会停止调用,不同的命令或命令组能够被配置使用它们各自的线程池,可以隔离不同的服务。

1.2 雪崩问题

微服务中,服务间调用关系错综复杂,一个请求,可能需要调用多个微服务接口才能实现,会形成非常复杂的调用链路:

1547575632050

如图,一次业务请求,需要调用A、P、H、l四个服务,这四个服务又可能调用其它服务。

如果此时,某个服务出现异常:

1547576017270

比如,H 服务出现了异常,因为用户请求使用的是连接池中的线程,每有一个用户请求就会占用一条线程,如果访问H服务,那么因为H服务异常,所以线程处于堵塞状态得不到释放,越来越多的用户访问H服务都因为服务异常而造成堵塞,从而使线程池中无线程可用,那么后来的访问者拿不到线程就访问不了任何一个服务了。这样子就造成了雪崩效应。

例如微服务I发生异常,请求阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞:

1547576552547

服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,形成雪崩效应。

这就好比,一个汽车生产线,生产不同的汽车,需要使用不同的零件,如果某个零件因为种种原因无法使用,那么就会造成整台车无法装配,陷入等待零件的状态,直到零件到位,才能继续组装。此时如果有很多个车型都需要这个零件,那么整个工厂都将陷入等待的状态,导致所有生产都陷入瘫痪。一个零件的波及范围不断扩大。

Hystrix解决雪崩问题的手段有两个:

  • 线程隔离
  • 服务熔断

1.3 线程隔离,服务降级

1.3.1 原理

线程隔离示意图:

1547577225635

解读:
Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队,加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,什么是服务降级?

服务降级:优先保证核心服务,而非核心服务不可用或弱可用。

用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息),。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。

服务降级的简单理解:降低服务的级别:如果该服务的线程池中没有可用的线程,即不为该请求提供服务,而是立即返回一个友好的提示信息,比如,服务器繁忙,请稍后再试 。比如双11,为了提供某个重要的服务,不重要的服务进行降级,不提供服务了。

当服务繁忙时,如果服务出现异常,不是粗暴的直接报错,而是返回一个友好的提示,虽然拒绝了用户的访问,但是会返回一个结果。

这就好比去买鱼,平常超市买鱼会额外赠送杀鱼的服务。等到逢年过节,超时繁忙时,可能就不提供杀鱼服务了,这就是服务的降级。

系统特别繁忙时,一些次要服务暂时中断,优先保证主要服务的畅通,一切资源优先让给主要服务来使用,在双十一、618时,京东天猫都会采用这样的策略。

触发Hystix服务降级的情况:

  • 线程池已满
  • 请求超时

1.3.2 动手实践

引入依赖

首先我们在 user-consumer 这个工程中引入 hystrix 依赖

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

开启熔断

在主程序类上加上 @EnableCircuitBreaker 依赖

@EnableCircuitBreaker //开启熔断
@SpringBootApplication
@EnableDiscoveryClient
public class UserConsumerApplication {
	....
}

可以看到,我们类上的注解越来越多,在微服务中,经常会引入上面的三个注解,于是Spring就提供了一个组合注解

@SpringCloudApplication

1547579660927

因此,我们可以使用这个组合注解来代替之前的3个注解

@SpringCloudApplication
public class UserConsumerApplication {
	....
}

编写降级逻辑

当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用 Hystixcommond来指定降级逻辑方法。

package cn.clocktt.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;

@Controller
public class WebController {

    @Autowired
    private RestTemplate restTemplate;

    @ResponseBody
    @RequestMapping("/consumer/user")
    @HystrixCommand(fallbackMethod = "queryUserByIdFallBack")
    public String queryUserById(long id){
        //格式
        //http://服务id/RequestMapping
        String url = "http://user-service/user/"+id;
        String userJson = restTemplate.getForObject(url, String.class);
        return userJson;
    }

    public String queryUserByIdFallBack(long id){
        return "不好意思,服务器太拥挤了";
    }
}

要注意,因为熔断的降级逻辑方法必须跟正常的逻辑方法要保证:相同的参数列表和返回值声明。 因为queryUserById的返回值是User,降级的逻辑方法的返回值也必须是User。但是降级逻辑返回一个User对象没有太大意义,因为我们要返回的是一个友好提示。因此我们把 queryUserById 方法的返回值改为String,因为该方法获取的数据也是一个json数据,即String类型的数据,这样降级逻辑中返回一个错误的说明会比较方便。

修改 user-service 服务,让其线程睡眠2秒。而user-consumer中的hystrix默认的超时时长是1秒,因此获取不到数据,从而会触发降级方法。

package cn.clocktt.service;

import cn.clocktt.mapper.UserMapper;
import cn.clocktt.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User queryUserById(Long id){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        User user = userMapper.selectByPrimaryKey(id);
        return user;
    }
}

运行测试

启动 eureka 注册中心,启动user-service ,启动user-consumer。

启动完毕之后,在监控中心我们可以看到 user-service 和 user-consumer 已经注册成功了。因为eureka-server不是注册自己,所以在这里看不到eureka的注册信息。

1547581863748

访问:http://localhost:8082/consumer/user?id=1

运行结果为:

1547581948131

默认的fallback

如果一个 Controller 层有多个方法进行服务,如果为每一个方法都自定义一个 FallBack ,显然是一件费时繁琐的事情。所以我们可以为一个Controller中的所有方法定义一个默认的 FallBack 方法。

@DefaultProperties(defaultFallback = "fallBack")

其中 fallBack() 方法中不能有参数,因为一个controller中有多个方法,而它们的形参也各不相同。所以fallBack()形参中做不到和每个方法的形参都对应,因此官方指定 默认的fallBack 中的形参为空。

package cn.clocktt.controller;

import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;

@Controller
@DefaultProperties(defaultFallback = "fallBack")
public class WebController {

    @Autowired
    private RestTemplate restTemplate;

    @ResponseBody
    @RequestMapping("/consumer/user")
    @HystrixCommand
    public String queryUserById(long id){
        //格式
        //http://服务id/RequestMapping
        String url = "http://user-service/user/"+id;
        String userJson = restTemplate.getForObject(url, String.class);
        return userJson;
    }

    public String fallBack(){
        return "不好意思,服务器太拥挤了,请稍后再试!";
    }
}

运行结果为:

1547582848036

超时设置

Hystrix默认的超时时长是1秒。我们设置服务的线程休眠是2秒,因此获取不到数据,因此hystrix会降级服务,从而会调用FallBack方法返回一个友好的提示。

但是不同的服务的超时时长应该是不一样的,所以我们要根据业务的不同来设置不同的超时时长。

@HystrixCommand(commandProperties = {
        @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
})
package cn.clocktt.controller;

import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;

@Controller
@DefaultProperties(defaultFallback = "fallBack")
public class WebController {

    @Autowired
    private RestTemplate restTemplate;

    @ResponseBody
    @RequestMapping("/consumer/user")
    @HystrixCommand(commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")
    })
    public String queryUserById(long id){
        //格式
        //http://服务id/RequestMapping
        String url = "http://user-service/user/"+id;
        String userJson = restTemplate.getForObject(url, String.class);
        return userJson;
    }

    public String fallBack(){
        return "不好意思,服务器太拥挤了,请稍后再试!";
    }
}

运行结果:因为服务睡眠时长是2秒,hystrix超时时长是3秒,所以能正常获取到数据。

1547583387569

全局配置hystrix超时时长:

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

配置了全局之后,就把controller中的方法单个超时时长注释掉

1547583708618

运行结果:

1547583387569

1.4 服务熔断

1.4.1 熔断原理

熔断器,也叫断路器,其英文单词为 Circuit Breaker

​ 熔断机制的原理很简单,像家里的电路熔断器,如果电路发生短路能立刻熔断电路,避免发生灾难。在分布式系统中应用这一模式之后,服务调用方可以自己进行判断某些服务反应慢或者存在大量超时的情况时,能够主动熔断,防止整个系统被拖垮。
​ 不同于电路熔断只能断不能自动重连,Hystrix可以实现弹性容错,当情况好转之后,可以自动重连。这就好比魔术师把鸽子变没了容易,但是真正考验技术的是如何把消失的鸽子再变回来。
通过断路的方式,可以将后续请求直接拒绝掉,一段时间之后允许部分请求通过,如果调用成功则回到电路闭合状态,否则继续断开。

Hystrix 的熔断状态机模型

1547584898479

状态机有3个状态:

  • Closed:关闭状态(断路器关闭),所有请求都正常访问。
  • Open:打开状态(断路器打开),所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到闽值,则触发熔断,断路器会完全关闭。默认失败比例的闽值是50%,请求次数最少不低于20次。
  • Half Open:半开状态,Closed状态不是永久的,关闭后会进入休眠时间(默认是5S。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,则会完全打开断路器,否则继续保持关闭,再次进行休眠计时

1.4.2 动手实践

为了能够精确控制请求的成功或失败,我们在user-consumer的调用业务中加入一段逻辑:

当传递过来的参数是偶数时,抛出异常。则会触发降级逻辑方法

public String queryUserById(long id){
    if(id % 2 == 0){
        throw new RuntimeException();
    }
}

为了提高测试的速度,我们要删除掉之前我们为了测试服务异常时添加的睡眠方法

    public User queryUserById(Long id){
//        try {
//            Thread.sleep(2000);
//        } catch (InterruptedException e) {
//            e.printStackTrace();
//        }
        User user = userMapper.selectByPrimaryKey(id);
        return user;
    }

不过,默认的熔断触发要求较高,休眠时间窗较短,为了测试方便,我们可以通过配置修改熔断策略:

    @ResponseBody
    @RequestMapping("/consumer/user")
    @HystrixCommand(commandProperties = {
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold" , value = "10"),
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds" , value = "10000"),
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage" , value = "60")
    })
    public String queryUserById(long id){
        if(id % 2 == 0){
            throw new RuntimeException();
        }
        //格式
        //http://服务id/RequestMapping
        String url = "http://user-service/user/"+id;
        String userJson = restTemplate.getForObject(url, String.class);
        return userJson;
    }

解读:

  • requestVolumeThreshold:触发熔断的最小请求次数,默认20
  • sleepwindowInMilliseconds:休眠时长,默认是5000毫秒
  • errorThresholdPercentage:触发熔的失败请求最小占比,默认50%

运行测试:

  1. 先访问 http://localhost:8082/consumer/user?id=2 显示 服务繁忙信息

  2. 再访问 http://localhost:8082/consumer/user?id=1 显示 获取到的用户信息

  3. 连续访问 http://localhost:8082/consumer/user?id=2 10次 显示 服务繁忙信息

  4. 再访问 http://localhost:8082/consumer/user?id=1 显示 服务繁忙信息

  5. 等待10秒睡眠时间过去之后

  6. 再访问 http://localhost:8082/consumer/user?id=1 显示 获取到的用户信息

posted @ 2019-01-16 07:47  竹子の云  阅读(1125)  评论(0)    收藏  举报