一、Hystrix简介
在分布式环境中,许多服务依赖项中的一些不可避免地会失败。Hystrix是一个库,通过添加延迟容忍和容错逻辑,可以控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点( isolating points of access between the services)、停止级联失败(stopping cascading failures across them)和提供回退选项(providing fallback options),提高了系统的整体弹性。
二、Hystrix的目标
- 对通过第三方客户端库访问的依赖项(通常是通过网络)的延迟和故障进行保护和控制。
- 在复杂的分布式系统中阻止级联故障。
- 快速失败,快速恢复。
- 回退,尽可能优雅地降级。
- 启用近实时监控、警报和操作控制。
三、Hystrix解决的问题
复杂分布式体系结构中的应用程序有许多依赖项,每个依赖项在某些时候都不可避免地会失败。
(一) 当一切正常时
(二) 当其中一个系统有延迟时,可能阻塞全部用户请求
(三) 在高流量的情况下,一个后端依赖项的延迟可能导致所有服务器上的所有资源在数秒内饱和。也就意味着后续所有的请求都无法立即得到服务。
四、Hystrix的设计原则
- 防止任何单个依赖项耗尽所有容器(如Tomcat)用户线程。
- 甩掉包袱,快速失败而不是排队。
- 在任何可行的地方提供回退,以保护用户不受失败的影响。
- 使用隔离技术(如隔离板、泳道和断路器模式)来限制任何一个依赖项的影响。
- 通过近实时的度量、监视和警报来优化发现时间。
- 通过配置的低延迟传播来优化恢复时间。
- 支持对Hystrix的大多数方面的动态属性更改,允许使用低延迟反馈循环进行实时操作修改。
- 避免在整个依赖客户端执行中出现故障,而不仅仅是在网络流量中。
五、Hystrix实现方式
- 用一个HystrixCommand 或者 HystrixObservableCommand 包装所有的对外部系统(或者依赖)的调用,让它们在一个单独的线程中执行
- 调用超时时间比自定义的阈值要长。超时时间有一个默认值,对于大多数的依赖项,可以自定义超时时间的。
- 为每个依赖项维护一个小的线程池(或信号量);如果线程池满了,那么该依赖性将会立即拒绝请求,而不是排队。
- 调用的结果有这么几种:成功、失败(客户端抛出异常)、超时、拒绝。
- 在一段时间内,如果服务的错误百分比超过了一个阈值,就会触发一个断路器来停止对特定服务的所有请求,无论是手动的还是自动的。
- 当请求失败、被拒绝、超时或短路时,执行回退逻辑。
- 近实时地监控指标和配置的变化。
当使用Hystrix来包裹每个依赖项时,架构如下图所示:
每个依赖项相互隔离,当延迟发生时,它会被限制在资源中,并包含回退逻辑。该逻辑决定了,在依赖项中发生故障,时应作出何种响应。
六、Hystrix的重要概念
1. 服务降级
不让客户端等待并立刻返回一个友好提示。
程序运行异常、超时、服务熔断触发服务降级、线程池或信号量打满也会导致服务降级。
2. 服务熔断
类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示
服务的降级->进而熔断->恢复调用链路
3. 服务限流
秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
七、测试服务超时
(一)模拟provider服务
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>cloud</artifactId>
<groupId>com.example</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-provider-hystrix-payment</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
application.yml
server:
port: 8001
spring:
application:
name: cloud-provider-hystrix-payment
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。
fetchRegistry: true
service-url:
#单机版
#defaultZone: http://localhost:7001/eureka
# 集群版
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
PaymentServiceImpl.java
/**
* 模拟正常业务和超时业务
*/
@Service
public class PaymentServiceImpl implements PaymentService {
@Override
public String paymentInfoOk(Integer id) {
return "线程池:" + Thread.currentThread().getName() + " Payment Info OK, id: " + id;
}
@Override
public String paymentInfoTimeout(Integer id) {
int timeMillis = 3000;
try {
Thread.sleep(timeMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + " Payment Info Timeout, id: " + id + "\t" + "O(∩_∩)O哈哈~,耗时:" + timeMillis;
}
}
PaymentController.java
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfoOk(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfoOk(id);
log.info("=================" + result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfoTimeout(@PathVariable("id") Integer id) {
String result = paymentService.paymentInfoTimeout(id);
log.info("=================" + result);
return result;
}
}
分别访问测试地址:
(二)Jmeter模拟高并发
使用Jmeter模拟高并发访问payment服务
此时,浏览器访问 http://localhost:8001/payment/hystrix/ok/1 出现明显延迟。
(三)模拟consumer服务
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>cloud</artifactId>
<groupId>com.example</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-consumer-hystrix-order</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
application.yml
server:
port: 80
spring:
application:
name: cloud-consumer-hystrix-order
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#单机版
#defaultZone: http://localhost:7001/eureka
# 集群版
defaultZone: http://localhost:7001/eureka,http://localhost:7002/eureka
feign:
client:
config:
default:
#建立连接所用的时间,适用于网络状况正常的情况下,两端连接所需要的时间
ConnectTimeOut: 10000
#指建立连接后从服务端读取到可用资源所用的时间
ReadTimeOut: 10000
PaymentHystrixService.java
@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
/**
* 模拟正常
*
* @param id
* @return
*/
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfoOk(@PathVariable("id") Integer id);
/**
* 模拟超时
*
* @param id
* @return
*/
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfoTimeout(@PathVariable("id") Integer id);
}
OrderHystrixController.java
@RestController
@Slf4j
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfoOk(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfoOk(id);
log.info("=========" + result);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
public String paymentInfoTimeout(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfoTimeout(id);
log.info("=========" + result);
return result;
}
}
对consumer服务进行高并发测试,与payment测试结果一致
(四)小结
如果同一层次的其他接口服务被困死,导致tomcat线程里面的工作线程已经被挤占完毕,导致访问其他服务时也会变卡。
八、服务降级
有两种场景:
- 当下游的服务因为某种原因响应过慢,下游服务主动停掉一些不太重要的业务,释放出服务器资源,增加响应速度!
- 当下游的服务因为某种原因不可用,上游主动调用本地的一些降级逻辑,避免卡顿,迅速返回给用户!
(一)下游服务(provider)的降级
pom.xml增加hystrix依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
主启动类增加@EnableHystrix注解
@SpringBootApplication
@EnableEurekaClient
@EnableHystrix
public class PaymentHystrixApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixApplication.class, args);
}
}
开启@HystrixCommand注解,新增paymentInfoTimeoutHandler作为fallbackMethod
@Override
@HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
})
public String paymentInfoTimeout(Integer id) {
int timeMillis = 3000;
//int time = 10 / 0;
try {
Thread.sleep(timeMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + " Payment Info Timeout, id: " + id + "\t" + "O(∩_∩)O哈哈~,耗时:" + timeMillis;
}
@Override
public String paymentInfoTimeoutHandler(Integer id) {
return "线程池:" + Thread.currentThread().getName() + " Payment Info Timeout Handler, 系统繁忙或运行报错,o(╥﹏╥)o呜呜~";
}
@HystrixProperty中设置了超时时间为2秒,方法中线程休眠时间3秒,超时后,跳转至paymentInfoTimeoutHandler,如下图所示:
同理,若方法中出现错误,如10/0,则立即返回上述降级后的错误提示。
(二)上游服务(consumer)的主动降级
在pom.xml中添加spring-cloud-starter-netflix-hystrix依赖,在主启动类上添加@EnableHystrix注解。
在application.yml中增加hystrix超时配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000
该配置项默认为1秒。
修改controller层,测试服务超时后降级:
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
})
public String paymentInfoTimeout(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfoTimeout(id);
log.info("=========" + result);
return result;
}
/**
* hystrix降级方法
*
* @param id
* @return
*/
public String paymentInfoTimeoutHandler(@PathVariable("id") Integer id) {
return "我是消费者,支付系统繁忙或异常,请10秒后再次尝试,o(╥﹏╥)o";
}
测试超时,上游服务主动降级。
(三) fallback代码解耦
目前@HystrixCommond注解中的fallbackMethod与方法高度耦合,需要进行优化。
1. 全局默认降级
在controller方法增加@DefaultProperties注解
@RestController
@Slf4j
@DefaultProperties(defaultFallback = "paymentGlobalHandler")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/ok/{id}")
@HystrixCommand
public String paymentInfoOk(@PathVariable("id") Integer id) {
int age = 10 / 0;
String result = paymentHystrixService.paymentInfoOk(id);
log.info("=========" + result);
return result;
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentInfoTimeoutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
})
public String paymentInfoTimeout(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfoTimeout(id);
log.info("=========" + result);
return result;
}
/**
* hystrix降级方法
*
* @param id
* @return
*/
public String paymentInfoTimeoutHandler(@PathVariable("id") Integer id) {
return "我是消费者,支付系统繁忙或异常,请10秒后再次尝试,o(╥﹏╥)o";
}
/**
* hystrix全局降级方法
*
* @param
* @return
*/
public String paymentGlobalHandler() {
return "我是消费者Global异常处理,支付系统繁忙或异常,请10秒后再次尝试,o(╥﹏╥)o";
}
}
无自定义fallbackMethod的方法,服务降级,返回全局降级方法,有自定义fallbackMethod的方法,返回自定义的方法。
2. feign-hystrix
在application.yml中配置:
feign:
hystrix:
enabled: true
在FeignClient注解中增加fallback值:
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentFallbackServiceImpl.class)
PaymentFallbackServiceImpl.java如下:
@Component
public class PaymentFallbackServiceImpl implements PaymentHystrixService {
@Override
public String paymentInfoOk(Integer id) {
return "来自Payment Info Ok的异常处理";
}
@Override
public String paymentInfoTimeout(Integer id) {
return "来自Payment Info Timeout的异常处理";
}
}
将payment下游服务关闭,分别访问测试链接:
若同时有feign注解和HystrixCommond注解,其结果取决于生效的降级方法。
九、服务熔断
服务熔断的功能类似于保险丝,是应对雪崩效应的一种微服务链路保护机制。当扇出链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
(一) Hystrix服务熔断
- 当失败的调用达到阈值(HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())
- 当失败的频率超过阈值临界点(HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())
- circuit-breaker由关闭状态(Closed)变为打开(Open)
- 当服务熔断打开时,它会使所有的请求短路(即服务降级)。
- 在一定时间后(HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()), 下一个请求被放行(变为:半开状态HALF-OPEN)。如果请求失败了,熔断器继续变为打开状态;如果请求成功了,熔断器变为关闭状态。再回到逻辑步骤1,从头开始。
(二)代码测试
在主启动类增加@EnableCircuitBreaker注解
在service层增加模拟服务熔断方法:
@Override
@HystrixCommand(fallbackMethod = "paymentCircuitBreakerFallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60")
})
public String paymentCircuitBreaker(Integer id) {
if (id < 0) {
throw new RuntimeException("============id不能为负数");
}
String serialNumber = IdUtil.simpleUUID();
return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
}
@Override
public String paymentCircuitBreakerFallback(Integer id) {
return "id不能为负数,请检查后重试。。o(╥﹏╥)o id:" + id;
}
在controller层增加调用:
@GetMapping("/payment/hystrix/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
String result = paymentService.paymentCircuitBreaker(id);
log.info("===============" + result);
return result;
}
测试:先使用负数id,使熔断器打开:
使用正数id,依然是降级状态:
等待熔断器半开——关闭状态,服务正常:
十、服务监控——Hystrix Dashboard
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>cloud</artifactId>
<groupId>com.example</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-hystrix-dashboard</artifactId>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
2. application.yml
server:
port: 9001
hystrix:
dashboard:
proxy-stream-allow-list: "*"
3. 主启动类
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
4. 启动项目
访问:http://localhost:9001/hystrix
5. 在cloud-provider-hystrix-payment服务中添加配置
在application.yml中增加:
management:
endpoints:
web:
exposure:
include: '*'
6. 开始监控服务
注意,监控链接为:http://localhost:8001/actuator/hystrix.stream
仪表盘含义:
十一、 Hystrix的工作流程
- 创建一个HystrixCommand or HystrixObservableCommand 对象;
- 执行命令
- 是否有缓存
- 熔断器是否打开
- 线程池、队列、信号量是否满了
- HystrixObservableCommand.construct() 或 HystrixCommand.run()
- 计算回路健康
- 获得Fallback
- 返回成功响应