Hystrix服务容错
目录
Hystrix是由Netfix团队于2011年研发,主要就是解决分布式项目中,由于个别服务故障引起的蝴蝶效应,导致的雪崩效应,通过hystrix是一个库,对其添加等待时间和容错措施来解决雪崩效应。
✧ 通过JMeter来模拟高并发测试
首先搭建测试环境,这里创建一个聚合项目,四个子项目,一个服务生产者,两个注册中心,一个消费者
一、环境搭建
父工程依赖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.yjxxt</groupId>
<artifactId>hystrix-demo</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>hystrix-server</module>
<module>hystrix-server02</module>
<module>product-server</module>
<module>product-consumer</module>
</modules>
<!-- 继承 spring-boot-starter-parent 依赖 -->
<!-- 使用继承方式,实现复用,符合继承的都可以被使用 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
</parent>
<!--
集中定义依赖组件版本号,但不引入,
在子工程中用到声明的依赖时,可以不加依赖的版本号,
这样可以统一管理工程中用到的依赖版本
-->
<properties>
<!-- Spring Cloud Hoxton.SR1 依赖 -->
<spring-cloud.version>Hoxton.SR1</spring-cloud.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>
</dependencies>
</dependencyManagement>
</project>
两个注册中心配置与依赖,基本一致
-----------依赖
<?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.yjxxt</groupId>
<artifactId>hystrix-server02</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 继承父依赖 -->
<parent>
<groupId>com.yjxxt</groupId>
<artifactId>hystrix-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<name>hystrix-server02</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- netflix eureka server 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- spring boot web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring boot test 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
</build>
</project>
启动类
@SpringBootApplication
// 开启 EurekaServer 注解
@EnableEurekaServer
public class Server02 {
public static void main(String[] args) {
SpringApplication.run(Server02.class, args);
}
}
application.yml配置(两个端口号别一样就行了,并且两个服务注册8762、8761
spring:
application:
name: eureka-server # 应用名称(集群下相同)
# 端口
server:
port: 8762
# 配置 Eureka Server 注册中心
eureka:
instance:
hostname: eureka02 # 主机名,不配置的时候将根据操作系统的主机名来获取
prefer-ip-address: true # 是否使用 ip 地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
# 设置服务注册中心地址,指向另一个注册中心
service-url: # 注册中心对外暴露的注册地址
defaultZone: http://localhost:8761/eureka/
服务生产者
依赖
<?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.yjxxt</groupId>
<artifactId>product-server</artifactId>
<version>1.0-SNAPSHOT</version>
<name>product-server</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<!-- 继承父依赖 -->
<parent>
<groupId>com.yjxxt</groupId>
<artifactId>hystrix-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- netflix eureka client 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring boot web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- spring boot test 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
</build>
</project>
JavaBean对象pojo/Product.java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {
private Integer id;
private String productName;
private Integer productNum;
private Double productPrice;
}
service层接口ProductService.java和实现类ProductServiceImp.java
public interface ProductService {
/**
* 查询商品列表
*
* @return
*/
List<Product> selectProductList();
/**
* 根据多个主键查询商品
*
* @param ids
* @return
*/
List<Product> selectProductListByIds(List<Integer> ids);
/**
* 根据主键查询商品
*
* @param id
* @return
*/
Product selectProductById(Integer id);
}
//--------------------------------------------------------
@Service
public class ProductServiceImp implements ProductService {
/**
* 查询商品列表
*
* @return
*/
@Override
public List<Product> selectProductList() {
return Arrays.asList(
new Product(1, "华为手机", 1, 5800D),
new Product(2, "联想笔记本", 1, 6888D),
new Product(3, "小米平板", 5, 2020D)
);
}
/**
* 根据多个主键查询商品
*
* @param ids
* @return
*/
@Override
public List<Product> selectProductListByIds(List<Integer> ids) {
List<Product> products = new ArrayList<>();
ids.forEach(id -> {
products.add(new Product(id, "电视机" + id, 1, 5800D));
});
return products;
}
/**
* 根据主键查询商品
*
* @param id
* @return
*/
@Override
public Product selectProductById(Integer id) {
return new Product(id, "冰箱", 1, 2666D);
}
}
controller层ProductController.java
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
/**
* 查询商品列表
*
* @return
*/
@GetMapping("/list")
public List<Product> selectProductList() {
// 服务提供者接口添加 `Thread.sleep(2000)`,模拟服务处理时长。
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return productService.selectProductList();
}
/**
* 根据多个主键查询商品
*
* @param ids
* @return
*/
@GetMapping("/listByIds")
public List<Product> selectProductListByIds(@RequestParam("id") List<Integer> ids) {
return productService.selectProductListByIds(ids);
}
/**
* 根据主键查询商品
*
* @param id
* @return
*/
@GetMapping("/{id}")
public Product selectProductById(@PathVariable("id") Integer id) {
return productService.selectProductById(id);
}
}
application.yml配置
server:
port: 7070 # 端口
spring:
application:
name: product-service # 应用名称
# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true # 是否使用 ip 地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
service-url: # 设置服务注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
消费者
依赖
<?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.yjxxt</groupId>
<artifactId>product-consumer</artifactId>
<version>1.0-SNAPSHOT</version>
<name>product-consumer</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<!-- 继承父依赖 -->
<parent>
<groupId>com.yjxxt</groupId>
<artifactId>hystrix-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- netflix eureka client 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring boot web 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- spring boot test 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<build>
</build>
</project>
JavaBean对象pojo/Order和Product
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order implements Serializable {
private Integer id;
private String orderNo;
private String orderAddress;
private Double totalPrice;
private List<Product> productList;
}
//--------------------------------------------------
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product implements Serializable {
private Integer id;
private String productName;
private Integer productNum;
private Double productPrice;
}
service层接口类OrderService和ProductService
public interface OrderService {
Order selectOrderById(Integer id);
Order queryOrderById(Integer id);
Order searchOrderById(Integer id);
}
//--------------------------------------------
/**
* 商品管理
*/
public interface ProductService {
List<Product> selectProductList();
List<Product> selectProductListByIds(List<Integer> ids);
Product selectProductById(Integer id);
}
实现类:
/**
* 商品管理
*/
@Service
public class ProductServiceImp implements ProductService {
@Autowired
private RestTemplate restTemplate;
/**
* 查询商品列表
*
* @return
*/
@Override
public List<Product> selectProductList() {
// ResponseEntity: 封装了返回数据
return restTemplate.exchange(
"http://product-service/product/list",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Product>>() {
}).getBody();
}
/**
* 根据多个主键查询商品
* @param ids
* @return
*/
@Override
public List<Product> selectProductListByIds(List<Integer> ids) {
StringBuffer sb = new StringBuffer();
ids.forEach(id -> sb.append("id=" + id + "&"));
return restTemplate.getForObject("http://product-service/product/listByIds?" + sb.toString(), List.class);
}
/**
* 根据主键查询商品
*
* @param id
* @return
*/
@Override
public Product selectProductById(Integer id) {
return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
}
}
controller层
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 根据主键查询订单-调用商品服务 /product/list
*/
@GetMapping("/{id}/product/list")
public Order selectOrderById(@PathVariable("id") Integer id) {
return orderService.selectOrderById(id);
}
/**
* 根据主键查询订单-调用商品服务 /product/listByIds
*/
@GetMapping("/{id}/product/listByIds")
public Order queryOrderById(@PathVariable("id") Integer id) {
return orderService.queryOrderById(id);
}
/**
* 根据主键查询订单-调用商品服务 /product/{id}
*/
@GetMapping("/{id}/product")
public Order searchOrderById(@PathVariable("id") Integer id) {
return orderService.searchOrderById(id);
}
}
application.yml配置
server:
port: 9090 # 端口
tomcat:
max-threads: 10 # 降低最大线程数方便模拟高并发
spring:
application:
name: order-service-rest # 应用名称
# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true # 是否使用 ip 地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
service-url: # 设置服务注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
基本环境搭建完毕,下一步测试
二、JMeter并发测试
建线程组,在建HTTP请求设置请求数量和时间,再创建监听树查看结果
启动jmeter和服务我们通过浏览器发一个请求:
由于高频率的访问2000个导致服务请求异常慢
三、利用Redis缓存解决
通过加入redis解决高并发的问题,修改消费者代码如下:
添加依赖
<!-- spring boot data redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2 对象池依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
application.yml
spring:
# redis 缓存
redis:
host: 192.168.168.129 # Redis服务器地址
password: 123456
port: 6399
RedisConfig.java
@Configuration
public class RedisConfig {
// 重写 RedisTemplate 序列化
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
// 为 String 类型 key 设置序列化器
template.setKeySerializer(new StringRedisSerializer());
// 为 String 类型 value 设置序列化器
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 为 Hash 类型 key 设置序列化器
template.setHashKeySerializer(new StringRedisSerializer());
// 为 Hash 类型 value 设置序列化器
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
// 重写 Cache 序列化
@Bean
public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory());
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
// 设置默认过期时间 30 min
.entryTtl(Duration.ofMinutes(30))
// 设置 key 和 value 的序列化
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getKeySerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}
}
在启动类上添加注解:// 开启缓存注解 @EnableCaching
ProductServiceImp.java
/**
* 查询商品列表
*
* @return
*/
@Cacheable(cacheNames = "orderService:product:list")
@Override
public List<Product> selectProductList() {
// ResponseEntity: 封装了返回数据
return restTemplate.exchange(
"http://product-service/product/list",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<Product>>() {
}).getBody();
}
/**
* 根据主键查询商品
*
* @param id
* @return
*/
@Cacheable(cacheNames = "orderService:product:single", key = "#id")
@Override
public Product selectProductById(Integer id) {
return restTemplate.getForObject("http://product-service/product/" + id, Product.class);
}
测试结果:效率极高,即便再高并发的情况下也不会影响我们的请求
四、请求合并
将大量的同样的请求,化为一个请求,不在一次次的请求,直接变成一个请求去提高效率
请求合并的缺点
设置请求合并之后,本来一个请求可能 5ms 就搞定了,但是现在必须再等 10ms 看看还有没有其他的请求一起,这样一个请求的耗时就从 5ms 增加到 15ms 了。
如果我们要发起的命令本身就是一个高延迟的命令,那么这个时候就可以使用请求合并了,因为这个时候时间消耗就显得微不足道了,另外高并发也是请求合并的一个非常重要的场景。
修改消费者模块代码,添加依赖
<!-- spring-cloud netflix hystrix 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
业务层:ProductServiceImp
// 声明需要服务容错的方法
@HystrixCommand
@Override
public List<Product> selectProductListByIds(List<Integer> ids) {
System.out.println("-----orderService-----selectProductListByIds-----");
StringBuffer sb = new StringBuffer();
ids.forEach(id -> sb.append("id=" + id + "&"));
return restTemplate.getForObject("http://product-service/product/listByIds?" + sb.toString(), List.class);
}
/**
* 根据主键查询商品
*
* @param id
* @return
*/
// 处理请求合并的方法一定要支持异步,返回值必须是 Future<T>
// 合并请求
@HystrixCollapser(batchMethod = "selectProductListByIds", // 合并请求方法
scope = com.netflix.hystrix.HystrixCollapser.Scope.GLOBAL, // 请求方式
collapserProperties = {
// 间隔多久的请求会进行合并,默认 10ms
@HystrixProperty(name = "timerDelayInMilliseconds", value = "20"),
// 批处理之前,批处理中允许的最大请求数
@HystrixProperty(name = "maxRequestsInBatch", value = "200")
})
@Override
public Future<Product> selectProductById(Integer id) {
System.out.println("-----orderService-----selectProductById-----");
return null;
}
//import java.util.concurrent.Future;
//修改接口返回类型
OrderServiceImp.java模拟多用户请求
@Override
public Order searchOrderById(Integer id) {
// 模拟同一时间用户发起多个请求。
Future<Product> p1 = productService.selectProductById(1);
Future<Product> p2 = productService.selectProductById(2);
Future<Product> p3 = productService.selectProductById(3);
Future<Product> p4 = productService.selectProductById(4);
Future<Product> p5 = productService.selectProductById(5);
try {
System.out.println(p1.get());
System.out.println(p2.get());
System.out.println(p3.get());
System.out.println(p4.get());
System.out.println(p5.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
return new Order(id, "order-003", "中国", 29000D, null);
}
启动类开启熔断器注解@EnableCircuitBreaker,启动测试
五、服务隔离
1、线程池隔离
没有隔离的项目所有接口都在一个线程池中,一旦一个接口出现问题就会占完全部资源导致瘫痪,所以使用隔离线程池,每一个接口都是一个单独的线程池,即便一个故障,也不会影响其他服务接口,这里的隔离粒度便是线程池。
优点:通过隔离提高了并发效率,并减少了服务故障的影响,以及服务故障后恢复,可以立即恢复工作
缺点:多个线程池中执行,导致CPU调度大,消耗高,涉及到跨线程,数据传递问题
2、信息量隔离
信息量隔离,就是当请求的信息量大于最大信息量限制后,会让请求失败的请求直接执行
fallback
接口快速返回,其余请求则是进行同步,直到结果返回,因此不能设置超时
3、线程池隔离 vs 信号量隔离
线程池隔离
不同线程,支持超时,当达到线程池最大线程进行熔断执行fallback进行熔断,支持同步或异步,线程调度资源消耗大
信号量隔离
同线程,不支持超时,当请求信息达到执行fallback熔断,支持同步不支持异步,资源消耗小
六、服务熔断
是软件系统的保护机制,类似与现实的跳闸,就是当大量数据请求时,服务器过载时则会启动熔断,又叫过载防护,一旦开启熔断则后去请求都直接进入fallback(可能是预先准备好的页面或者托底数据返回)
七、服务降级
服务降级就是当系统资源被大量占用,不够时,此时只能舍弃一些不重要的服务来保证整个服务的运作,比如吃鸡游戏开始一级包太小我们只能捡一些必需品,保证生存即可。
触发条件
- 方法抛出非
HystrixBadRequestException
异常; - 方法调用超时;
- 熔断器开启拦截调用;
- 线程池/队列/信号量跑满。