文章目录
前言
这里记录我学习熔断器(Hystrix)的内容。
代码地址
提示:以下是本篇文章正文内容,下面案例可供参考
一、相关介绍
1.1 什么是Hystrix
Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务,防止出现级联失败;
1.2 雪崩效应
微服务中,服务之间的调用关系错综复杂,一个请求,可能需要多个微服务接口才能实现,会形成非常复杂的调用链路:
如上图,一次业务请求,会调用A、H、I、P四个服务,这四个服务又可能需要调用其他服务,如果此时,某个服务出现异常,那会怎么样呢?
如上图,就加入服务I发生异常,请求阻塞,当前用户的请求就得不到响应,我们的tomcat(服务器)就不会释放这个线程,当更多用户的请求到来后,越来越多的线程就会在此发生阻塞,如下图:
而服务器支持的线程和并发数是有限的,所以,当线程阻塞到一定程度后,会导致服务器的资源耗尽,从而导致其他所有的服务都不可用,形成雪崩效应。
1.3 应对方法
应对雪崩效应的方法:
- 线程隔离
- 服务降级
1.3.1 线程隔离
线程隔离,示意图:
- Hystrix为每个依赖服务分配到一个小的线程池,如果线程池已满,那么随后而来的调用服务将被立即拒绝(默认不排队),加速请求失败的判定;
- 用户的请求不在直接访问服务,而是直接通过线程池中的空闲线程来访问服务,如果线程池已满,或请求超时,则会进行服务降级处理;
1.3.2 服务降级
- 服务降级可以优先保证核心服务的正常工作;
- 用户的请求故障时,不会被阻塞,更不会无休止的等待或者直接看到系统崩溃,至少是可以看到一个执行结果的(如失败的提示信息);
- 服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。 触发Hystrix服务降级的情况:
- 线程池已满
- 请求超时
二、入门案例
服务降级:及时返回服务调用失败的结果,让线程不因为等待服务而阻塞
2.1 依赖
使用Hystrix时,引入的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2.2 开启熔断
在启动类上增加 @EnableCircuitBreaker 注解
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class ConsumerApplication {
// ...
}
2.2.1 @SpringCloudApplication注解
可以看到我们的启动类上的注解越来越多,在微服务中,我们经常会引入@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker这三个注解,于是Spring就提供了一个新的注解@SpringCloudApplication;源码如下:
可以看到这一个注解包含了我们刚才所说的三个注解,所以若是有必要,可以使用次注解代替;如下,和2.1 中的代码片段是等效的:
@SpringCloudApplication
public class ConsumerApplication {
// ...
}
2.3 父工程pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.kaikeba</groupId>
<artifactId>springcloud-hystrix</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>user-service</module>
<module>consumer-service</module>
<module>eureka-server</module>
</modules>
<!-- springboot -->
<parent>
<artifactId>spring-boot-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.5.RELEASE</version>
<relativePath/>
</parent>
<!-- 版本管理 -->
<properties>
<java.version>1.8</java.version>
<spring-cloud-version>Greenwich.SR1</spring-cloud-version>
<tk-mybatis.version>2.1.5</tk-mybatis.version>
<mysql-connection-version>8.0.25</mysql-connection-version>
</properties>
<dependencyManagement>
<dependencies>
<!-- spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- tk mybatis -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connection-version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.4 eureka-server注册中心
2.4.1 pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-hystrix</artifactId>
<groupId>com.kaikeba</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-server</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
2.4.2 配置文件application.yml
server:
port: 8080
spring:
application:
name: eureka-server
eureka:
client:
service-url:
defaultZone: http://localhost:8080/eureka
register-with-eureka: false
server:
eviction-interval-timer-in-ms: 60000 #扫描失效服务的时间间隔
enable-self-preservation: false # 关闭自我保护
2.4.2 启动类EureaServerApplication.java
package com.kaikeba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class);
}
}
2.5 user-service服务提供者
2.5.1 pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-hystrix</artifactId>
<groupId>com.kaikeba</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>
2.5.2 application.yml
server:
port: 10000
spring:
application:
name: user-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springcloud
username: root
password: root
eureka:
client:
service-url:
defaultZone: http://localhost:8080/eureka
instance:
ip-address: 127.0.0.1
prefer-ip-address: true
lease-renewal-interval-in-seconds: 30
lease-expiration-duration-in-seconds: 90
2.5.3 实体类User.java
package com.kaikeba.entity;
import lombok.Data;
import javax.persistence.*;
import java.util.Date;
@Data
@Table(name = "tb_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "user_name")
private String username;
private String password;
private String name;
private Integer age;
private Integer sex;
private Date birthday;
private Date created;
private Date updated;
private String note;
}
2.5.4 UserMapper.java
package com.kaikeba.mapper;
import com.kaikeba.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends tk.mybatis.mapper.common.Mapper<User> {
}
2.5.5 业务层接口和实现类
UserService.java
package com.kaikeba.service;
import com.kaikeba.entity.User;
public interface UserService {
/**
* 根据id查询
*/
User findById(Integer userId);
}
UserServiceImpl.java
package com.kaikeba.service.impl;
import com.kaikeba.entity.User;
import com.kaikeba.mapper.UserMapper;
import com.kaikeba.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
public User findById(Integer userId) {
return userMapper.selectByPrimaryKey(userId);
}
}
2.5.6 UserController.java
package com.kaikeba.controller;
import com.kaikeba.entity.User;
import com.kaikeba.service.UserService;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@RequestMapping("/{userId}")
public User searchById(@PathVariable("userId")Integer userId) {
return userService.findById(userId);
}
}
2.5.7 启动类
package com.kaikeba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@EnableEurekaClient
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class);
}
}
2.6 consumer-service服务消费者
2.6.1 pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-hystrix</artifactId>
<groupId>com.kaikeba</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>consumer-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka客户端 -->
<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>
</dependencies>
</project>
2.6.2 application.yml
server:
port: 20000
spring:
application:
name: consumer-service
eureka:
client:
service-url:
defaultZone: http://localhost:8080/eureka
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000 #hystrix超时时间 默认是1000ms
circuitBreaker:
errorThresholdPercentage: 50 # 触发熔断错误比例阈值,默认值50%
sleepWindowInMilliseconds: 10000 # 熔断后休眠时长,默认值5秒
requestVolumeThreshold: 10 # 熔断触发最小请求次数,默认值是20
2.6.3 实体类User.java
package com.kaikeba.entity;
import lombok.Data;
import java.util.Date;
@Data
public class User {
private Integer id;
private String username;
private String password;
private String name;
private Integer age;
private Integer sex;
private Date birthday;
private Date created;
private Date updated;
private String note;
}
2.6.4 控制层UserController.java
package com.kaikeba.controller;
import com.kaikeba.entity.User;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/consumer")
@Slf4j
public class ConsumerController {
@Resource
private RestTemplate restTemplate;
@Resource
private DiscoveryClient discoveryClient;
@RequestMapping("/{userId}")
@HystrixCommand(fallbackMethod = "searchByIdFallback")
public String searchById(@PathVariable("userId")Integer userId) {
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
ServiceInstance serviceInstance = instances.get(0);
String url = "http://user-service/user/" + userId;
User user = restTemplate.getForObject(url, User.class);
return user.toString();
}
/**
* 失败逻辑:方法级
*/
public String searchByIdFallback(Integer userId) {
log.error("查询用户信息失败,id: {}", userId);
return "对不起,查询失败!!!";
}
}
2.6.5 启动类
package com.kaikeba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
//@EnableCircuitBreaker // 熔断器
//@SpringBootApplication
//@EnableEurekaClient
@SpringCloudApplication
public class ConsumerServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerServiceApplication.class);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
2.7 配置项
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000 #hystrix超时时间 默认是1000ms
circuitBreaker:
errorThresholdPercentage: 50 # 触发熔断错误比例阈值,默认值50%
sleepWindowInMilliseconds: 10000 # 熔断后休眠时长,默认值5秒
requestVolumeThreshold: 20 # 熔断触发最小请求次数,默认值是20
2.8 熔断原理
2.8.1 熔断原理
在服务熔断中,使用的熔断器,也叫断路器,其英文单词为:Circuit Breaker 熔断机制与家里使用的电路熔断原理类似;
- 当如果电路发生短路的时候能立刻熔断电路,避免发生灾难。
- 在分布式系统中应用服务熔断后;服务调用方可以自己进行判断哪些服务反应慢或存在大量超时,可以针对这些服务进行主动熔断,防止整个系统被拖垮。
- Hystrix的服务熔断机制,可以实现弹性容错;当服务请求情况好转之后,可以自动重连。
- 通过断路的方式,将后续请求直接拒绝,一段时间(默认5秒)之后允许部分请求通过,如果调用成功则回到断路器关闭状态,否则继续打开,拒绝请求的服务。
Hystrix的熔断状态机模型:
状态机有3个状态:
- Closed:关闭状态(断路器关闭),所有请求都正常访问。
- Open:打开状态(断路器打开),所有请求都会被降级。Hystrix会对请求情况计数,当一定时间内失败请求
百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20
次。 - Half Open:半开状态,不是永久的,断路器打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半
开状态。此时会释放部分请求通过,若这些请求都是健康的,则会关闭断路器,否则继续保持打开,再次进
行休眠计时
2.8.2 分析图
2.9 测试结果
2.9.1 正常访问
2.9.2 访问失败
2.9.3 熔断器启动
当访问失败达到阈值时(访问20次以上,失败率50%以上),熔断器开启,此时,原先能成功访问的请求,也将失败:
2.9.4 熔断恢复
在熔断器启动后,10秒内(默认是5秒)会进入半启动状态,此时,过来的请求,若是成功访问,则熔断器关闭,若是访问失败,则熔断器继续开启动,10秒(默认是5秒)后重新半开放,如此循环
2.10 熔断第二种写法
这样的方式也可以达到熔断的效果,适用于,整个类中所有的方法都需要服务降级时使用:
package com.kaikeba.controller;
import com.kaikeba.entity.User;
import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/consumer")
@Slf4j
// 当所有的方法都要降级时,只要在类上加@DefaultProperties注解就可以了
@DefaultProperties(defaultFallback = "defaultFallback")
public class ConsumerController {
@Resource
private RestTemplate restTemplate;
@Resource
private DiscoveryClient discoveryClient;
@RequestMapping("/{userId}")
@HystrixCommand
public String searchById(@PathVariable("userId")Integer userId) {
List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
ServiceInstance serviceInstance = instances.get(0);
String url = "http://user-service/user/" + userId;
User user = restTemplate.getForObject(url, User.class);
return user.toString();
}
/**
* 失败逻辑:方法级
*/
public String searchByIdFallback(Integer userId) {
log.error("查询用户信息失败,id: {}", userId);
return "对不起,查询失败!!!";
}
/**
* 失败逻辑:类级
*/
public String defaultFallback() {
log.error("查询用户信息失败Class");
return "对不起,查询失败Class!!!";
}
}
2.11 总结
- Hystrix熔断,实现线程隔离的方法是为每依赖服务增加线程池;
- 服务降级,能及时的返回失败信息,防止资源一直被占用;
- Hystrix是解决线程长久持有资源,导致系统奔溃的问题;解决方法是,是请求失败或超时,释放资源。