敖胤

绳锯木断,水滴石穿;聚沙成塔,集腋成裘。

导航

Spring Cloud Netflix 学习笔记(四)—服务容错(Hystrix)

在实际中,通常会将业务拆分成一个个微服务,微服务之间通过网络进行互相调用,从而形成了微服务之间的依赖关系。

由于网络原因或者自身的原因,微服务并不能保证服务百分之百可用。如果单个服务出现问题,则调用该服务时会出现延迟甚至调用失败的情况;若调用失败,用户则会重新刷新页面并尝试再次调用,再加上其他服务的调用,从而增加了服务器的负载,导致某个服务瘫痪,甚至整个服务崩溃。

调用相互依赖的微服务而出现问题是在所难免的,开发者很难避免因某些因素而导致的服务之间依赖的调用失败,但是可以尽可能地在调用失败时减少或避免对调用方带来的影响,提前做好应急措施。当遇到问题时,可以及时地启动应急预案,让系统进行自我调节和保护。如果不能及时有效地隔离有问题的微服务,则所有的服务请求有可能会因为这个单点故障而阻塞,从而产生“雪崩效应”,导致整个服务都不能正常对外提供服务。此时就需要我们对微服务进行容错保护,及时发现微服务故障并进行处理。

Hystrix是Netflix针对微服务分布式系统采用的熔断保护中间件,相当于电路中的保险丝。Hystrix通过HystrixCommand对调用进行隔离,这样可以阻止故障的连锁效应,能够让接口调用快速失败并迅速恢复正常,或者回退并优雅降级。而不是长时间地等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间不必要地占用,从而避免了故障在分布式系统中的蔓延乃至崩溃。

1、服务降级

Spring Cloud官网关于服务降级的介绍:Spring Cloud CircuitBreaker supports the notion of a fallback: a default code path that is executed when they circuit is open or there is an error.

当我们的应用在使用Feign调用其他远程服务出现异常或错误时,fallback给我们提供了一种服务降级的备案,通过执行预定义的代码,返回托底数据而不是抛出异常或错误。

1.1、导入依赖

在emp模块中添加Hystrix依赖包:

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

1.2、添加注解

在emp的启动类上添加@EnableHystrix或者@EnableCircuitBreaker注解。

注意,@EnableHystrix中包含了@EnableCircuitBreaker。

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = "org.silence.emp.remote")
@EnableCircuitBreaker
public class EmpApplication {
    public static void main(String[] args) {
        SpringApplication.run(EmpApplication.class, args);
    }
}

1.3、指定降级方法

为映射接口调用远程服务的方法编写对应的降级方法,并通过@HystrixCommand注解的fallbackMethod属性指定。

@RestController
@RequestMapping("/emp")
public class EmpController {

    @Autowired
    private DeptClient deptClient;

    @GetMapping("/all")
    public String getAll() {
        String result = deptClient.getAll();
        return "emp: " + "\t" + result;
    }

    @GetMapping(value = "/byDeptNo/{deptNo}/{dName}")
    @HystrixCommand(fallbackMethod = "getByDeptNoFail")//指定降级方法
    public Dept getByDeptNo(@PathVariable Integer deptNo, @PathVariable String dName) {
        System.out.println(deptNo + "\t" + dName);
        int i = 1 / 0;//手动制造一个异常
        Dept result = deptClient.getByDeptNo(deptNo, dName);
        return result;
    }

    //getByDeptNo的降级方法,注意方法的参数必须一致
    public Dept getByDeptNoFail(Integer deptNo, String dName) {
        //返回托底数据
        return new Dept(-1, "default", "local");
    }
}

1.4、测试

在浏览器输入http://localhost:8080/emp/byDeptNo/12/dName访问。

2、Feign服务降级

当我们使用Feign简化远程服务调用时,可以为远程服务调用构建一个回退实现,并在Feign的注解中进行声明。这样当远程服务不可用时,Hystrix则会对服务进行降级处理。

OpenFeign自带了Hystrix,支持服务降级,但默认没有打开,需要在application.yml中添加以下配置开启Hystrix:

feign:
  hystrix:
    enabled: true

Feign提供了两种fallback降级方式:

  • 映射接口实现
  • 工厂实现类

2.1、Feign映射接口实现方式

2.1.1、编写映射接口实现类

自定义fallback实现类,实现服务映射接口,重写映射接口中的方法,指定调用远程服务出现异常情况时的操作。并将此实现类注入到Spring容器。

@Component
public class DeptClientFallBack implements DeptClient {
    @Override
    public String getAll() {
        return "There is a Error!";
    }

    ......
}
2.1.2、修改映射接口

给映射接口的@FeignClien添加fallback属性值,指定为刚编写的实现类。

注意:若映射接口被@RequestMapping注解修饰,需要去掉此注解,否则程序将因SpringMVC映射错误而无法启动

@FeignClient(value = "DEPT", fallback = DeptClientFallBack.class, name = "DEPT")
//@RequestMapping("/dept")
public interface DeptClient {

    @RequestMapping(method = RequestMethod.GET, value = "/dept/all")
    String getAll();

    ......

}
2.1.3、开启Hystrix

在配置文件中添加添加开启服务降级的配置

feign:
  hystrix:
    enabled: true
2.1.4、测试
  • 在dept的controller的方法中手动制造一个异常
@RestController
@RequestMapping("/dept")
public class DeptController {
    @Value("${server.port}")
    private String port;

    @GetMapping("/all")
    public String getAll() {
        int i = 1 / 0; //手动异常
        return "dept====>" + port;
    }
    
    ......
}
  • 访问,可以获取降级方法返回的托底数据

2.2、Feign工厂接口实现方式

工厂实现方式是基于映射接口实现的一种,可解决调用方无法获取错误信息的问题。

2.2.1、编写FallbackFactory实现类
  • 在泛型中指定服务映射接口
  • 注入映射映射接口的实现
  • 在create()方法中返回映射接口的实现对象
@Component
public class DeptClientFallBackFactory implements FallbackFactory<DeptClient> {//将映射接口作为泛型传入
    @Autowired
    private DeptClientFallBack fallBack;//注入映射映射接口的实现类

    @Override
    public DeptClient create(Throwable throwable) {
        throwable.printStackTrace();
        return fallBack;//返回映射接口的实现类
    }
}
2.2.2、修改映射接口

指定@FeignClient注解的fallbackFactory属性值为刚刚编写的FallBack工厂实现

@FeignClient(value = "DEPT",
//        fallback = DeptClientFallBack.class,
        fallbackFactory = DeptClientFallBackFactory.class,
        name = "DEPT")
public interface DeptClient {
    ......
}

3、服务隔离

Hystrix的核心是提供服务容错保护,其设计原则中有一条:防止任何单一依赖使用掉整个容器(如Tomcat)的全部用户线程。

Hystrix通过舱壁隔离模式(Bulkhead Isolation Pattern)。对资源或失败单元进行隔离,避免一个服务的失效导致整个系统垮掉(雪崩效应)。

Hystrix实现服务隔离的思路如下:

  • 使用命令模式(HystrixCommand/HystrixObservableCommand)对服务调用进行封装,使每个命令在单独线程中/信号授权下执行。
  • 为每一个命令的执行提供一个小的线程池/信号量,当线程池/信号已满时,立即拒绝执行该命令,直接转入服务降级处理。
  • 为每一个命令的执行提供超时处理,当调用超时时,直接转入服务降级处理。
  • 提供熔断器组件,通过设置相关配置及实时的命令执行数据统计,完成服务健康数据分析,使得在命令执行过程中可以快速判断是否可以执行,还是执行服务降级处理。

Hystrix提供了线程池隔离(Thread Pools)信号量隔离(Semaphores)两种服务隔离策略。

  • 线程池隔离:不同服务的执行使用不同的线程池,同时将用户请求的线程(如Tomcat)与具体业务执行的线程分开,业务执行的线程池可以控制在指定的大小范围内,从而使业务之间不受影响,达到隔离的效果。
  • 信号量隔离:用户请求线程和业务执行线程是同一线程,通过设置信号量的大小限制用户请求对业务的并发访问量,从而达到限流的保护效果。

Hystrix默认使用了线程池隔离,在EmpController中,分别在getAll和getByDeptNo方法中输出当前线程名:

@GetMapping("/all")
public String getAll() {
    System.out.println("非Hystrix:" + Thread.currentThread().getName());
    String result = deptClient.getAll();
    return "emp: " + "\t" + result;
}

@GetMapping(value = "/byDeptNo/{deptNo}/{dName}")
@HystrixCommand(fallbackMethod = "getByDeptNoFail")
public Dept getByDeptNo(@PathVariable Integer deptNo, @PathVariable String dName) {
    System.out.println("Hystrix:" + Thread.currentThread().getName());
    System.out.println(deptNo + "\t" + dName);
    int i = 1 / 0;
    Dept result = deptClient.getByDeptNo(deptNo, dName);
    return result;
}

在浏览器中分别访问两个方法,可在控制台看到:

当没有使用Hystrix时,业务执行使用的还是Tomcat的线程池,使用Hystrix后,业务执行使用的是Hystrix的线程池。

3.1、配置方式

通过@HystrixCommand注解的commandProperties属性配置,配置方式如下:

@GetMapping(value = "/byDeptNo/{deptNo}/{dName}")
@HystrixCommand(fallbackMethod = "getByDeptNoFail",
     commandProperties = {
         @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
         @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
     }
)
public Dept getByDeptNo(@PathVariable Integer deptNo, @PathVariable String dName) {
    System.out.println("Hystrix:" + Thread.currentThread().getName());
    System.out.println(deptNo + "\t" + dName);
    int i = 1 / 0;
    Dept result = deptClient.getByDeptNo(deptNo, dName);
    return result;
}

commandProperties属性是一个数组,在它内部使用@HystrixProperty来定义一个个的属性,@HystrixProperty使用name-value的K-V对指定具体的属性名和值。

@HystrixProperty的name定义在com.netflix.hystrix.HystrixCommandProperties类中

3.2、常用属性

hystrix.command.default.circuitBreaker.errorThresholdPercentage:该配置用于设置错误率阈值,当错误率超过此值时,所有请求都会触发fallback,默认值为50。

hystrix.command.default.execution.isolation.strategy:指定隔离策略,具体策略有下面2种:

  • THREAD:线程池隔离,在单独的线程上执行,并发请求受线程池大小的控制。

  • SEMAPHORE:信号量隔离,在调用线程上执行,并发请求受信号量计数器的限制。

针对线程池隔离
  1. hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:HystrixCommand执行的超时时间设置,当HystrixCommand执行的时间超过了该配置所设置的数值后就会进入服务降级处理,单位是毫秒,默认值为1000。
  2. hystrix.command.default.execution.timeout.enabled:是否启用execution.isolation.thread.timeoutInMilliseconds设置的超时时间,默认值为true。设置为false后execution.isolation.thread.timeoutInMilliseconds配置也将失效。
  3. hystrix.command.default.execution.isolation.thread.interruptOnTimeout:确定HystrixCommand执行超时后是否需要中断它,默认值为true。
针对信号量隔离
  1. hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests:确定Hystrix使用信号量策略时最大的并发请求数,默认值为10(5000 rps只需要2)。
  2. hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests:如果并发数达到该设置值,请求会被拒绝和抛出异常并且fallback不会被调用,默认值为10。
示例

设置getByDeptNo方法的隔离策略为THREAD,超时时间为2000 ms,并在方法内部使线程休眠3000 ms

@GetMapping(value = "/byDeptNo/{deptNo}/{dName}")
@HystrixCommand(fallbackMethod = "getByDeptNoFail",
        commandProperties = {
            @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
            }
        )
public Dept getByDeptNo(@PathVariable Integer deptNo, @PathVariable String dName) throws InterruptedException {
    System.out.println("Hystrix:" + Thread.currentThread().getName());
    System.out.println(deptNo + "\t" + dName);
    //        int i = 1 / 0;
    Thread.sleep(3000);
    Dept result = deptClient.getByDeptNo(deptNo, dName);
    return result;
}

public Dept getByDeptNoFail(Integer deptNo, String dName) {
    return new Dept(-1, "default", "local");
}

在浏览器中访问此接口,获取到返回的托底数据

4、服务熔断

熔断器(CircuitBreaker)一词来源于物理学中的电路知识,它的作用是当电路中出现故障时迅速切断电路,从而保护电路,熔断器机制如下图所示:

  • 当一个服务的处理用户请求的失败次数在一定时间内小于设定的阀值时,熔断器处于关闭状态,服务正常;
  • 当服务处理用户请求的失败次数大于设定的阀值时,说明服务出现了故障,打开熔断器,这时所有的请求会执行快速失败,不执行业务逻辑。
  • 当处于打开状态的熔断器时,一段时间后会处于半打开状态,并执行一定数量的请求,剩余的请求会执行快速失败,若执行的请求失败了,则继续打开熔断器;若成功了,则将熔断器关闭。

Hystrix不仅有熔断器的功能,还有熔断器的状态监测,并提供界面友好的UI,开发人员或者运维人员通过UI界面能够直观地看到熔断器的状态和各种性能指标。

4.1、Hystrix监听

在服务调用时,Hystrix会实时累积关于HystrixCommand的执行信息,比如每秒的请求数、成功数等。可以借助hystrix-dashboard对监控进行图形化展示。

4.1.1、导入依赖

在emp的pom.xml中添加dashboard的依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
4.1.2、添加注解

启动类,在启动类上添加@EnableHystrixDashboard注解

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = "org.silence.emp.remote")
@EnableCircuitBreaker
@EnableHystrixDashboard //dashboard注解
public class EmpApplication {
    public static void main(String[] args) {
        SpringApplication.run(EmpApplication.class, args);
    }
}
4.1.3、配置Servlet

由于Spring Boot的版本是2.x,需要手动配置一个Servlet,简单做法是在emp的启动类中注册一个Servlet,路径映射为“/hystrix.stream”

@Bean
public ServletRegistrationBean getServlet() {
    HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
    ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
    registrationBean.setLoadOnStartup(1);
    registrationBean.addUrlMappings("/hystrix.stream");
    registrationBean.setName("HystrixMetricsStreamServlet");
    return registrationBean;
}

另一种做法是,手动编写一个类,继承HystrixMetricsStreamServlet,然后在启动类上添加@ServletComponentScan("包路径")注解,将其纳入spring容器。

@WebServlet("/hystrix.stream")
public class HystrixServlet extends HystrixMetricsStreamServlet {
}
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = "org.silence.emp.remote")
@EnableCircuitBreaker
@EnableHystrixDashboard
@ServletComponentScan("org.silence.emp.servlet")//扫描Servlet所在包
public class EmpApplication {
    public static void main(String[] args) {
        SpringApplication.run(EmpApplication.class, args);
    }
}
4.1.4、添加注解

在启动类上添加@EnableHystrixDashboard注解

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = "org.silence.emp.remote")
@EnableCircuitBreaker
@EnableHystrixDashboard //dashboard注解
public class EmpApplication {
    public static void main(String[] args) {
        SpringApplication.run(EmpApplication.class, args);
    }


    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}
4.1.5、添加配置

在emp的application.xml文件中配置允许dashboard代理的服务

hystrix:
  dashboard:
    proxy-stream-allow-list: localhost
4.1.6、访问监听页面

访问 http://localhost:8080/hystrix 可看到如下页面

在监听地址栏输入: http://localhost:8080/hystrix.stream 进入监听页面

出现这种情况是因为还没有数据,等到HystrixCommand执行(我们调用访问@HystrixCommand注解修饰的方法)之后就可以看到具体数据了:

4.2、属性配置

  • hystrix.command.default.circuitBreaker.enabled:是否开启熔断器,默认值为true。
  • hystrix.command.default.circuitBreaker.requestVolumeThreshold:设置一个rolling window时间内最小的请求数。如果设为20,那么如果一个rolling window时间内(比如说1个rolling window时间是10秒)只收到19个请求,即使19个请求都失败,也不会触发circuit break,默认值为20。
  • hystrix.command.default.circuitBreaker.errorThresholdPercentage:设置错误率阈值,当错误率超过此值时,所有请求都会触发fallback,默认值为50,即一半的请求失败时。
  • hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds:设置一个断路时间,当该值设为5000时,则当触发熔断后的5000毫秒内都会拒绝请求,5000毫秒后熔断器变为半开。默认值为5000。
  • hystrix.command.default.circuitBreaker.forceOpen:如果配置为true,将强制打开熔断器,在这个状态下将拒绝所有请求,默认值为false。与hystrix.command.default.circuitBreaker.forceClosed互斥。
  • hystrix.command.default.circuitBreaker.forceClosed:如果配置为true,则将强制关闭熔断器,在这个状态下,不管错误率有多高,都允许请求,默认值为false。与hystrix.command.default.circuitBreaker.forceOpen互斥。

posted on 2021-03-17 18:21  敖胤  阅读(141)  评论(0编辑  收藏  举报