1. 概述
1.1. 分布式系统需要解决的问题
复杂分布式体系结构中的应用程序有数十个依赖服务,每个依赖服务在某时将不可避免发生异常或失败。多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他微服务,这就叫扇出,如果扇出的链路上某个微服务的调用响应时间过长或者不可使用,对微服务A的调用就会占用越来越多的系统资源,进而引发整个系统的崩溃,称为雪崩效应,因此就需要一个组件来解决这个问题
1.2. 什么是Hystrix
Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统中,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整个系统服务失败,避免级联故障发生,以提高分布式系统的弹性。当某个服务发生故障后,通过断路器的故障监控向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩
1.3. 重要概念
服务降级:当程序运行异常、超时、服务熔断触发、线程池/信号量打满等情况下会让客户端不再等待并返回一个友好提示
服务熔断:请求量达到最大服务访问后,直接拒绝访问,然后调用服务降级的方法并返回友好提示
服务限流:高并发操作时,严禁一瞬间过来海量请求,要求一秒钟多少个请求有序进行
2. 服务提供端集成Hystrix
2.1. 引入核心依赖
在pom.xml文件中引入Hystrix依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
2.2. 业务方法配置服务降级
@HystrixCommand(fallbackMethod = "timeOutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
})
@GetMapping(value = "/getProviderInfoForHystrix/{message}")
public String getProviderInfoForHystrix(@PathVariable("message") String message) {
try {
TimeUnit.SECONDS.sleep(6);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Hello" + message + ", This is provider hystrix, The current time is " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
public String timeOutHandler(String message) {
return "Hello " + message + "调用服务接口超时或异常!";
}
2.3. 主启动类引入Hystrix配置
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
3. 服务消费端集成Hystrix
3.1. 引入核心依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<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-openfeign</artifactId>
</dependency>
3.2. 配置application.yml文件
server:
port: 8800
spring:
application:
name: cloud-hystrix
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
3.3. 编写主启动类
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableHystrix
public class HystrixApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixApplication.class, args);
}
}
3.4. 编写业务接口
@RestController
@RequestMapping("/hystrix")
public class HystrixController {
@Autowired
private ProviderClient providerClient;
@GetMapping(value = "/getHystrixInfoTimeOut/{message}")
@HystrixCommand(fallbackMethod = "timeOutFallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
})
public String getHystrixInfoTimeOut(@PathVariable("message") String message) {
return providerClient.getProviderInfoForHystrix(message);
}
public String timeOutFallback(String message) {
return "This is Hystrix consumer, The request Provider time out, The message is " + message;
}
}
3.5. 业务类配置默认返回类
由于每个方法都配置一个异常处理类,会导致代码膨胀,可以在类上配置默认的异常处理类,在需要特殊处理的方法上再配置单独的异常处理方法
@RestController
@RequestMapping("/hystrix")
@DefaultProperties(defaultFallback = "providerDefaultFallback")
public class HystrixController {
@Autowired
private ProviderClient providerClient;
@HystrixCommand
@GetMapping(value = "/getHystrixInfo/{message}")
public String getHystrixInfo(@PathVariable("message") String message) {
return providerClient.getProviderInfoForHystrix(message);
}
public String providerDefaultFallback() {
return "This is Hystrix consumer The global error fallback";
}
}
3.6. 编写异常请求类
上面两种方法都是在业务代码中处理异常业务,这样容易导致异常代码和业务代码混淆在一起,可以单独将异常处理拧出来,在Feign接口上引用该异常类即可
@Component
public class ProviderClientFallback implements ProviderClient {
@Override
public String getProviderInfoForHystrix(String message) {
return "failBack provider info rest service call failed!";
}
}
在Feign接口上引用该异常类
@Component
@FeignClient(value = "CLOUD-PROVIDER", configuration = ProviderClientFallback.class)
public interface ProviderClient {
@GetMapping(value = "/provider/getProviderInfoForHystrix/{message}")
String getProviderInfoForHystrix(@PathVariable("message") String message);
}
在yml配置中开启Feign的hystrix支持
feign:
hystrix:
enabled: true
3.7. 编写异常回退工厂类
定义和使用一个Fallback回退处理工厂类
@Component
public class ProviderClientFallbackFactory implements FallbackFactory<ProviderClient> {
@Override
public ProviderClient create(Throwable throwable) {
return new ProviderClient() {
@Override
public String getProviderInfoForHystrix(String message) {
return "FallbackFactory fallback: provider rest service call failed!";
}
};
}
}
在Feign接口引入该工厂类
@Component
@FeignClient(value = "CLOUD-PROVIDER", configuration = FeignConfiguration.class, fallbackFactory = ProviderClientFallbackFactory.class)
public interface ProviderClient {
@GetMapping(value = "/provider/getProviderInfoForHystrix/{message}")
String getProviderInfoForHystrix(@PathVariable("message") String message);
}
回退类和回退工厂类区别
- 使用回退类时,远程调用RPC过程中所引发的异常已经被回退逻辑彻底屏蔽掉了,应用程序不方便干预,也看不到RPC过程中的具体异常
- 使用回退工厂类时,应用程序可以通过Java代码对RPC异常进行拦截和处理
4. 服务熔断
当失败的调用到一定阈值,缺省是5秒内20次调用失败,就会启动熔断机制
4.1. 注解配置
在服务端新增测试熔断方法
@GetMapping(value = "/getProviderInfoForCircuitBreaker/{id}")
@HystrixCommand(fallbackMethod = "circuitBreakerFallback", 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 getProviderInfoForCircuitBreaker(@PathVariable("id") Integer id) {
if (id < 0) {
throw new RuntimeException("id不能为负数!");
}
return "The id is " + id;
}
public String circuitBreakerFallback(@PathVariable("id") Integer id) {
return "id 不能为负数,请稍后再试! id is " + id;
}
4.2. 三个重要参数
快照时间窗:断路器确定是否打开需要统计一些请求和错误数据,而统计的时间范围就是快照时间窗,默认为最近10秒
请求总数阈值:在快照时间窗内,必须满足请求总数阈值才有资格熔断,默认20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或其他原因失败,断路器都不会打开
错误百分比阈值:当请求总数在快照时间窗内超过了阈值,比如发生了30次调用,如果在这30次调用中有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阈值情况下,这时就会将断路器打开
断路器开启或关闭的条件
当满足一定的阈值的时候(默认10秒内超过20个请求次数)
当失败率达到一定的时候(默认10秒内超过50%的请求失败)
到达以上阈值,断路器将会开启
当开启的时候,所有请求都不会进行转发
一段时间后(默认5秒),此时断路器是半开状态,会让其中一个请求进行转发,如果成功,断路器会关闭,若失败,继续开启
4.3. yml文件配置
在yml文件中也可以进行服务熔断相关配置
hystrix:
command:
default:
circuitBreaker:
enabled: true #是否开启熔断器
requestVolumeThreshold: 20 #窗口时间内最小请求数
sleepWindowInMilliseconds: 5000 #打开后允许一次尝试的睡眠时间,默认5秒
errorThresholdPercentage: 50 #窗口时间内熔断器开启的错误比例,默认50
metrics:
rollingStats:
timeInMilliseconds: 1000 #滑动窗口时间
numBuckets: 10 #滑动窗口的时间桶数
5. RPC保护之舱壁模式
对于不同的服务提供者可以设置不同的RPC调用线程池,让不同RPC通过专门的线程池请求到各自的Provider服务提供者,像舱壁一样对Provider进行隔离,对于不同的服务提供者设置不同的RPC调用线程池,这种模式被称为舱壁模式
优点:
避免对单个Provider的RPC的消耗掉所有资源,从而防止由于某一个服务性能低而引起的级联故障和雪崩效应
Hystrix提供两种RPC隔离方式:线程池隔离和信号量隔离
5.1. 线程池隔离
每一个线程池都有一个Key,名为Thread Pool Key(线程池名)。如果没有为HystrixCommand指定线程池,Hystrix就会为HystrixCommand创建一个与Group Key(命令组Key)同名的线程池,当然,如果与Group Key同名的线程池已经存在,就直接进行关联。
线程池隔离配置如下:
hystrix:
threadpool:
default:
coreSize: 10 #线程池核心线程数
maximumSize: 20 线程池最大线程数
allowMaximumSizeToDivergeFromCoreSize: true #线程池maximumSize最大线程数是否生效
keepAliveTimeMinutes: 10 #可空闲时间,分钟
command:
default:
execution:
isolation:
strategy: THREAD #配置请求隔离方式为线程池
thread:
timeoutInMilliseconds: 100000 #RPC执行超时时间,默认1000毫秒
interruptOnTimeout: true #超时后是否中断方法的执行,默认true
5.2. 信号量隔离
信号量所起到的作用就像一个开关,而信号量的值就是每个命令的并发执行数量,当并发数高于信号量的值时就不再执行命令。信号量可以细分为run执行信号量和fallback回退信号量
IO线程在执行HystrixCommand命令之前需要抢到run执行信号量,成功之后才允许执行HystrixCommand.run()方法。如果争抢失败,就准备回退,但是在执行HystrixCommand.getFallback()回退方法之前,还需要争抢fallback回退信号量,成功之后才允许执行HystrixCommand.getFallback()回退方法。如果都获取失败,操作就会直接终止。
信号量隔离配置如下
hystrix:
command:
fallback:
isolation:
semaphore:
maxConcurrentRequests: 10 #回退信号量大小,默认为10,信号量大小不能大于容器线程池大小
5.3. 线程池隔离与信号量隔离区别
线程池隔离 | 信号量隔离 | |
---|---|---|
调用线程 | RPC线程与Web容器IO线程相互隔离 | RPC线程与Web容器IO线程相同 |
开销 | 存在请求排队、线程调度、线程上下文切换 | 无线程切换,开销低 |
异步 | 支持 | 不支持 |
并发量 | 最大线程池大小 | 最大信号量上限,且最大信号量需要小于IO线程数 |
6. RPC保护之熔断器模式
统计最近RPC调用发生错误的次数,然后根据统计值中的失败比例等信息决定是否允许后面的RPC调用继续,或者快速地失败回退
熔断器的3种状态如下:
关闭(closed):熔断器初始状态,RPC调用正常放行
开启(open):失败比例到一定的阈值后,熔断器进入开启状态,RPC将会快速失败,然后执行失败回退逻辑
半开启(half-open):打开一定时间后(睡眠窗口结束),熔断器进入半开启状态,小流量尝试进行RPC调用放行,如果尝试成功,熔断器就变为关闭状态,RPC调用正常,如果尝试失败,熔断器就变为开启状态,RPC调用快速失败
6.1. 熔断器配置
包含滑动窗口配置和熔断器自身配置
hystrix:
command:
default:
circuitBreaker:
enabled: true #是否开启熔断器
requestVolumeThreshold: 20 #窗口时间内最小请求数
sleepWindowInMilliseconds: 5000 #打开后允许一次尝试的睡眠时间,默认5秒
errorThresholdPercentage: 50 #窗口时间内熔断器开启的错误比例,默认50
metrics:
rollingStats:
timeInMilliseconds: 1000 #滑动窗口时间
numBuckets: 10 #滑动窗口的时间桶数
6.2. HystrixCommand工作流程
- 判断是否使用缓存响应请求,若启用了缓存,且缓存可用,则直接使用缓存响应请求,Hystrix支持请求缓存,但需要用户自定义启动
- 判断熔断器是否开启,如果熔断器处于open状态,则跳至第5步
- 若使用线程池进行请求隔离,则判断线程池是否已占满,若已满则跳至第5步;若使用信号量进行请求隔离,则判断信号量是否耗尽,若耗尽则跳至第5步
- 使用HystrixCommand.run()方法执行具体业务逻辑,如果执行失败或者超时,就跳至第5步,否则跳至第6步
- 执行HystrixCommand.getFallback()服务降级处理逻辑
- 返回请求响应
7. 服务监控
Hystrix提供了准实时的调用监控(Hystrix Dashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等
7.1. 引入核心依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
7.2. 编写application.yml文件
server:
port: 8801
spring:
application:
name: cloud-hystrix-dashboard
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
hystrix:
dashboard:
proxy-stream-allow-list: '*'
7.3. 编写主启动类
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}
主要新增注解@EnableHystrixDashboard
7.4. 验证
依次启动Eureka Server、Provider和HystrixDashboard consumer微服务
浏览器地址输入http://localhost:8801/hystrix
Delay:用来控制服务器上轮询监控信息的延迟时间,默认2000毫秒,可以通过配置该属性来降低客户端的网络和CPU消耗
Title:对应头部标题Hystrix Stream之后的内容,默认会使用具体监控实例的URL,可以通过配置该信息来展示更合适的标题
填写监控地址http://localhost:8770/actuator/hystrix.stream、延迟时间和应用名称
点击Monitor Stream
浏览器输入地址http://localhost:8801/hystrix/dashboard/getProviderInfoForCircuitBreaker/100,多次访问
再次在浏览器地址输入http://localhost:8801/hystrix/dashboard/getProviderInfoForCircuitBreaker/-100,多次访问
实心圆:通过颜色的变化代表了实例的健康程度,健康度从绿色<黄色<橙色<红色递减。其大小会根据实例的请求流量发生变化,流量越大该实心圆就越大
曲线:用来记录2分钟内流量的相对变化,通过其来观察到流量的上升和下降趋势
8. 聚合监控
dashboard用于监控单个微服务,如果需要监控多个微服务,就需要用到turbine
8.1. 引入核心依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
</dependency>
8.2. 编写application.yml文件
server:
port: 8802
spring:
application:
name: cloud-turbine
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka
hystrix:
dashboard:
proxy-stream-allow-list: '*'
turbine:
aggregator:
cluster-config: default
app-config: cloud-provider8771,cloud-provider8772 #服务列表
cluster-name-expression: new String('default')
8.3. 编写主启动类
@EnableTurbine
@SpringBootApplication
@EnableHystrixDashboard
public class TurbineApplication {
public static void main(String[] args) {
SpringApplication.run(TurbineApplication.class, args);
}
}
8.4. 验证
启动两个微服务,在浏览器输入地址http://localhost:8802/hystrix
多次访问两个微服务接口,在面板地址栏输入http://localhost:8802/turbine.stream,点击Monitor Stream查看监控界面信息