一个系统,所有请求共用同一个APP容器(Tomcat/jetty/等),共用一个用户线程池,依赖多个不同的远程服务。
当系统健康时,处理请求的延时较低,服务正常运行;当某个后端依赖项变得延迟,会导致处理该请求的用户线程长时间阻塞。在流量较低时,只会影响请求本身;在高流量的情况下,单个后端依赖项的延迟可能会导致服务的所有用户线程都在阻塞等待。
这个问题不仅导致与该后端依赖项有关的请求没办法被正常处理,还会导致其他请求拿不到所需资源而出现异常,从而导致整个系统都没办法正常运行。
对于上述问题,需要通过手段对故障、延迟进行隔离和管理,以便单个失败的依赖项不会导致整个应用程序或系统崩溃。
Hystrix是Netflix开源的容错库,旨在处理分布式系统中的故障和延迟。它通过实现断路器模式、请求缓存、请求合并等功能,提供了弹性和可靠的解决方案。Hystrix能够在服务之间进行隔离,当一个服务出现故障时,它可以防止故障的扩散,提高系统的可用性和稳定性。在微服务架构中,一个请求需要调用多个服务是非常常见的,较底层的服务如果出现故障,会导致连锁故障。当对特定的服务的调用的不可用达到一个阈值(Hystrix是5秒20次)断路器将会被打开。断路打开后,可以避免连锁故障,通过fallback方法快速返回一个值。
Hystrix旨在实现以下目标:
为了实现上述目标,Hystrix提供了以下功能:
HystrixCommand
或HystrixObservableCommand
对象包装对所有外部系统(或“依赖项”)的调用,通常在单独的线程中执行下图展示了通过Hystrix请求远程服务的流程
HystrixCommand
/HystrixObservableCommand
对象第1步,创建一个HystrixCommand
或HystrixObservableCommand
包装远程调用依赖的过程。HystrixCommand
和HystrixObservableCommand
的区别后续介绍。
Command
第2步,执行第一步创建的命令Command
,可以使用以下四种方式:execute()
、queue()
、observe()
、toObservable()
,区别后续介绍
第3步,判断是否有请求缓存,若存在请求缓存则直接将缓存的值返回,请求缓存的用法后续介绍。
第4步,判断断路器是否开启,开启则不能执行远程调用的操作,直接到第8步;若未开启,则到第5步。第4步的断路器开启与否,是通过第7步的断路器健康值判断的。
第5步,如果与命令关联的线程池和队列(或信号量)已满,那么Hystrix将不会执行该命令,而是立即将流量路由到(8)获取Fallback。第5步的结果会反馈到第7步的断路器健康值中。
第6步,执行远程调用的方法。在第1步构造的HystrixCommand
或HystrixObservableCommand
中,会重写HystrixCommand.run()
和HystrixObservableCommand.construct()
,里面执行远程调用过程。
如果执行失败/超时,则会路由到第8步执行fallback方法。
值得注意的是,执行超时时,线程隔离策略下定时器会抛出超时异常,并且通知执行任务的线程中断;信号量隔离策略下,定时器会抛出超时异常,但是执行任务的线程会执行到任务结束。
第7步,Hystrix向断路器报告成功、失败、拒绝和超时,断路器维护一组滚动计数器来计算统计数据。
它使用这些统计数据来确定何时应该断路,此时它会拒绝任何后续请求,直到恢复期过去,然后它会让会进入半开状态。在此状态下,下一个请求将尝试调用后端服务。如果该请求成功,断路器会认为服务已经恢复并转回 关闭 状态;如果请求仍然失败,断路器会再次回到 打开 状态,继续进行短路操作。
第8步,当第4步发现断路器打开、第5步线程池/信号量已满、第6步执行异常/超时,Hystrix就会尝试获取Command中自定义的备用方法getFallback()。建议该方法不依赖任何网络连接,而是从内存缓存或其他静态逻辑中获取。
如果fallback执行异常或者没有重写getFallback()方法,则会抛出异常。
如果执行成功,则会将远程调用的结果返回给调用者。
下图展示了HystrixCommand和断路器的交互流程以及断路器的工作流程:
Hystrix 使用舱壁模式来隔离依赖关系,并限制对任何单一依赖关系的并发访问。
在此策略中,每个服务调用都在独立的线程中执行。线程隔离可以防止服务调用时间过长,导致其他服务调用受到影响。当一个服务调用超时或发生故障时,它不会影响到其他服务调用。线程隔离可以确保服务之间的独立性,提高系统的稳定性。
信号量隔离是一种基于计数器的轻量级隔离方法,它不创建新的线程。相反,Hystrix 使用一个指定大小的信号量来控制并发访问的数量。一旦达到最大值,任何额外的请求都将被拒绝并触发降级策略。
本章节将介绍hystrix提供的原始API、Spring Cloud集成Hystrix的使用
<dependency>
<groupId>com.netflix.hystrixgroupId>
<artifactId>hystrix-coreartifactId>
<version>1.5.18version>
dependency>
下面是创建一个HystrixCommand
的方式,继承HystrixCommand
,泛型为远程调用响应的类型。
构造方法中指定了该命令所属的分组key,Hystrix会基于分组key用于报告、告警、仪表盘、权限控制等,并且在不指定线程池的情况下,会根据这个key命名线程池。(也就是说,相同key共用同一个线程池)
run()
方法中,是真正执行远程调用的位置。
public class CommandHelloWorld extends HystrixCommand<String> {
private final String name;
public CommandHelloWorld(String name) {
// 指定命令分组为ExampleGroup
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
@Override
protected String run() {
// 真正执行远程调用的位置
return "Hello " + name + "!";
}
}
执行HystrixCommand
,同步等待结果
String s = new CommandHelloWorld("World").execute();
执行HystrixCommand
,异步等待结果。
Future<String> fs = new CommandHelloWorld("World").queue();
String s = fs.get();
响应式执行HystrixCommand
,observe()
将HystrixCommand
转换为Observable
,并通过subscribe()
设置订阅者,设置当处理结果、处理异常、出现新元素时的行为。
Observable<String> ho = new CommandHelloWorld("World").observe();
ho.subscribe(new Subscriber<String>() {
@Override
public void onCompleted() {
LOGGER.info("Completed.");
}
@Override
public void onError(Throwable e) {
LOGGER.error("Error ", e);
}
@Override
public void onNext(String s) {
// 获取结果
LOGGER.info("Next {}", s);
}
});
除HystrixCommand
以外,还可以创建一个 HystrixObservableCommand
。这是一种专门用于包装 Observables 的 HystrixCommand
版本。HystrixObservableCommand
可以包装发射(emit)多个元素的 Observable
,而普通的 HystrixCommands
(即使转换为 Observable
)也永远不会发射(emit)超过一个元素。
可以通过实现Fallback方法,当执行任务出现超时/异常时优雅地降级,返回一个默认值或用于表示错误的值。
对于HystrixCommand
,需要实现getFallback()
方法, 当出现失败、超时、线程池/信号量拒绝、断路器开启时,会执行getFallback()
方法。对于调用方,最终会收到getFallback()
的出参。
public class CommandHelloFailure extends HystrixCommand<String> {
private final String name;
public CommandHelloFailure(String name) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.name = name;
}
@Override
protected String run() {
throw new RuntimeException("this command always fails");
}
@Override
protected String getFallback() {
return "Hello Failure " + name + "!";
}
}
在没有实现getFallback()
时,HystrixCommand
会抛出HystrixRuntimeException
给调用方。
无论实现getFallback()
与否,只要失败都会在断路器中计入失败。
值得注意的是,如果想在出现错误之后不执行getFallback()
,可以抛出HystrixBadRequestException
通过HystrixCommandKey
标识HystrixCommand
。当不设置是默认为类名getClass().getSimpleName();
public CommandHelloWorld(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld")));
this.name = name;
}
可以通过HystrixThreadPoolKey
指定线程池的名称,否则默认使用HystrixCommandGroupKey
使用HystrixThreadPoolKey
而不是单纯使用HystrixCommandGroupKey
的原因可能是有一些HystrixCommand
确实是在权限或者逻辑功能上是同一个组的,但是需要互相隔离。
public CommandHelloWorld(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool")));// 指定线程池的名称为HelloWorldPool
this.name = name;
}
通过以下的方式可以指定隔离策略
protected CommandIsolationStrategy(String name) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD))
//.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE))
);
this.name = name;
}
其他参数配置与上述方式基本一致,在此不再赘述
每个配置参数,有四个优先级,越往后的优先级越高。
Hystrix自带的配置参数的默认值
可修改Java系统参数修改默认值。通过代码设置或者在启动服务时指定
System.setProperty("hystrix.command.default.execution.isolation.strategy", "SEMAPHORE");
java -Dhystrix.command.default.execution.isolation.strategy=SEMAPHORE ...
其中,default
说明是全局默认值
即上述几个小节在创建HystrixCommand
的设置方式
与3.1.7.2一样,通过修改系统参数实现。
System.setProperty("hystrix.command.HystrixCommandKey.execution.isolation.strategy", "THREAD");
java -Dhystrix.command.HystrixCommandKey.execution.isolation.strategy=THREAD ...
其中,HystrixCommandKey
修改为HystrixCommand
的对应值
更多参数有关的内容,见第4章
本小节将介绍Hystrix在Spring Cloud中如何使用。主要介绍两种方式,Spring Cloud OpenFeign 集成Hystrix、Spring Cloud Netflix Hystrix
本小节仅介绍简单的使用,若想了解OpenFeign集成Hystrix的原理,可参考源码feign.hystrix.HystrixInvocationHandler
(从源码中可以看到,其实就是通过动态代理的方式,用HystrixCommand将FeignClient远程调用的方法包装起来)
只需引入spring-cloud-starter-openfeign
即可。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
我们点击进spring-cloud-starter-openfeign
的pom文件中可以看到它已引入feign-hystrix
,它将Hystrix集成到Feign中。
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-hystrixartifactId>
<version>10.10.1version>
<scope>compilescope>
dependency>
(这个依赖不需要单独引)
首先,划重点,配置必须开启feign-hystrix,否则hystrix默认是不开启的。详见OpenFeign源码org.springframework.cloud.openfeign.FeignClientsConfiguration.HystrixFeignConfiguration
feign:
hystrix:
enabled: true
其次,如果需要修改Hystrix的配置参数,可通过修改配置实现。比如,修改默认隔离策略:
hystrix:
command:
default:
execution:
isolation:
strategy: THREAD
key和value与Hystrix原生的保持一致,详见第4章。
需要说明的是,feignClient的每一个方法都是一个HystrixCommand,HystrixCommandKey为简单类名#方法名(字段类型...)
。
比如说下面的FeignClient
的runsWell
的HystrixCommandKey为HystrixService#runsWell(Integer)
、runs
的HystrixCommandKey为HystrixService#runs()
因此,根据第4章介绍的配置方法,如果想细粒度地修改指定的HystrixCommand的配置,需要指定对应的HystrixCommandKey。
hystrix:
command:
HystrixService#runsWell(Integer):
execution:
isolation:
strategy: SEMAPHORE
HystrixService#runs():
execution:
isolation:
strategy: THREAD
@FeignClient(value = "HystrixCommandGroupKey")
public interface ExampleApi {
@GetMapping("/hystrix/ok/{id}")
String runsWell(@PathVariable("id") Integer id);
@GetMapping("/hystrix/ok")
String runs();
}
另外,还有一个点值得提一下,@FeignClient上可以通过name
/value
指定远程RPC的服务,同时会将它作为HystrixCommandGroupKey
,也就是说在不特殊配置的情况下,无论创建多少个相同服务的FeignClient
,它们都会在同一个分组下,并且会使用相同的线程池。
Fallback比较简单,在@FeignClient
上指定fallbackFactory
,并且实现一个FallbackFactory
即可。
@FeignClient(name = "base-cloud-for-biz", url = "${base-cloud.url}",
fallbackFactory = BaseCloudClientForBizFallbackFactory.class)
public interface BaseCloudClientForBiz {
@PostMapping(path="/api/v2/internal/sendSMSWithoutRegister",
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE)
ApiResult sendSmsWithoutRegister(SendSmsWithoutRegisterDto dto);
}
@Slf4j
@Component
public class BaseCloudClientForBizFallbackFactory implements FallbackFactory<BaseCloudClientForBiz> {
private static final String ERROR_LOG = "internal service api is unavailable, api = {}, request data : {}.";
@Override
public BaseCloudClientForBiz create(Throwable throwable) {
return new BaseCloudClientForBiz() {
@Override
public ApiResult sendSmsWithoutRegister(SendSmsWithoutRegisterDto dto) {
log.warn(ERROR_LOG, "sendSmsWithoutRegister", dto);
return ApiResult.SYSTEM_INTERNAL_ERROR;
}
};
}
}
除了OpenFeign之外,可能还会有其他调用远程依赖的方法,我们可以通过spring-cloud-starter-netflix-hystrix
引入Hystrix
。它支持我们通过注解的方式实现HystrixCommand
的创建、配置。实现原理可参考com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect
(简单来说就是通过AOP包装远程调用的方法)
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
需要在配置类上加上注解EnableHystrix
@EnableHystrix
public class Application {
}
使用比较简单,在方法上添加注解@HystrixCommand
即可
@HystrixCommand(fallbackMethod = "exampleFallback",
commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")})
@GetMapping("/example")
public String example() {
return "";
}
public String exampleFallback() {
return "";
}
可以看出,字段与3.1的用法基本相同
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HystrixCommand {
String groupKey() default "";
String commandKey() default "";
String threadPoolKey() default "";
String fallbackMethod() default "";
HystrixProperty[] commandProperties() default {};
HystrixProperty[] threadPoolProperties() default {};
Class<? extends Throwable>[] ignoreExceptions() default {};
ObservableExecutionMode observableExecutionMode() default ObservableExecutionMode.EAGER;
HystrixException[] raiseHystrixExceptions() default {};
String defaultFallback() default "";
}
配置 | 参数名 | 默认值 | 备注 |
---|---|---|---|
隔离策略 | hystrix.command.default.execution.isolation.strategy hystrix.command.HystrixCommandKey.execution.isolation.strategy | THREAD | 可选:THREAD, SEMAPHORE |
命令执行超时时间(毫秒) | hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds hystrix.command.HystrixCommandKey.execution.isolation.thread.timeoutInMilliseconds | 1000 | execution.timeout.enabled=true生效 |
执行超时开关 | hystrix.command.default.execution.timeout.enabled hystrix.command.HystrixCommandKey.execution.timeout.enabled | true | |
超时中断开关 | hystrix.command.default.execution.isolation.thread.interruptOnTimeout hystrix.command.HystrixCommandKey.execution.isolation.thread.interruptOnTimeout | true | |
信号量最高并发请求数 | hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests hystrix.command.HystrixCommandKey.execution.isolation.semaphore.maxConcurrentRequests | 10 |
配置 | 参数名 | 默认值 | 备注 |
---|---|---|---|
fallback开关 | hystrix.command.default.fallback.enabled hystrix.command.HystrixCommandKey.fallback.enabled | true |
配置 | 参数名 | 默认值 | 备注 |
---|---|---|---|
断路器开关 | hystrix.command.default.circuitBreaker.enabled hystrix.command.HystrixCommandKey.circuitBreaker.enabled | true | |
断路最小阈值 | hystrix.command.default.circuitBreaker.requestVolumeThreshold hystrix.command.HystrixCommandKey.circuitBreaker.requestVolumeThreshold | 20 | 只有滑动时间窗口内的请求数超过阈值,才会有机会触发断路 |
断路睡眠时间(毫秒) | hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds hystrix.command.HystrixCommandKey.circuitBreaker.sleepWindowInMilliseconds | 5000 | 断路之后的睡眠时长,在此时间内拒绝任何请求 |
断路错误率阈值 | hystrix.command.default.circuitBreaker.errorThresholdPercentage hystrix.command.HystrixCommandKey.circuitBreaker.errorThresholdPercentage | 50 | 错误率超过阈值触发断路 |
配置 | 参数名 | 默认值 | 备注 |
---|---|---|---|
滑动统计时间长度(毫秒) | hystrix.command.default.metrics.rollingStats.timeInMilliseconds hystrix.command.HystrixCommandKey.metrics.rollingStats.timeInMilliseconds | 10000 | |
滑动统计桶数量 | hystrix.command.default.metrics.rollingStats.numBuckets hystrix.command.HystrixCommandKey.metrics.rollingStats.numBuckets | 10 | 注意:timeInMilliseconds%numBuckets必须等于0 |
统计延迟开关 | hystrix.command.default.metrics.rollingPercentile.enabled hystrix.command.HystrixCommandKey.metrics.rollingPercentile.enabled | true | 若设置为false,则延迟的情况进行统计 |
配置 | 参数名 | 默认值 | 备注 |
---|---|---|---|
线程池核心线程数 | hystrix.threadpool.default.coreSize hystrix.threadpool.HystrixThreadPoolKey.coreSize | 10 | |
线程池最大线程数 | hystrix.threadpool.default.maximumSize hystrix.threadpool.HystrixThreadPoolKey.maximumSize | 10 | |
线程池队列最大长度 | hystrix.threadpool.default.maxQueueSize hystrix.threadpool.HystrixThreadPoolKey.maxQueueSize | -1 | 如果设置-1,则使用SynchronousQueue队列 如果设置正数,则使用LinkedBlockingQueue队列 |
空闲线程存活时长(分钟) | hystrix.threadpool.default.keepAliveTimeMinutes hystrix.threadpool.HystrixThreadPoolKey.keepAliveTimeMinutes | 1 |
线程池计算公式:
线程数=最高峰时每秒的请求数量 × 99%命令执行时间 + 喘息空间
public class CommandThatFailsFast extends HystrixCommand<String> {
private final boolean throwException;
public CommandThatFailsFast(boolean throwException) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.throwException = throwException;
}
@Override
protected String run() {
if (throwException) {
throw new RuntimeException("failure from CommandThatFailsFast");
} else {
return "success";
}
}
public class CommandThatFailsSilently extends HystrixCommand<String> {
private final boolean throwException;
public CommandThatFailsSilently(boolean throwException) {
super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
this.throwException = throwException;
}
@Override
protected String run() {
if (throwException) {
throw new RuntimeException("failure from CommandThatFailsFast");
} else {
return "success";
}
}
@Override
protected String getFallback() {
return null;
}
}
@Override
protected Boolean getFallback() {
return true;
}
public class CommandWithFallbackViaNetwork extends HystrixCommand<String> {
private final int id;
protected CommandWithFallbackViaNetwork(int id) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceX"))
.andCommandKey(HystrixCommandKey.Factory.asKey("GetValueCommand")));
this.id = id;
}
@Override
protected String run() {
// RemoteServiceXClient.getValue(id);
throw new RuntimeException("force failure for example");
}
@Override
protected String getFallback() {
return new FallbackViaNetwork(id).execute();
}
private static class FallbackViaNetwork extends HystrixCommand<String> {
private final int id;
public FallbackViaNetwork(int id) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceX"))
.andCommandKey(HystrixCommandKey.Factory.asKey("GetValueFallbackCommand"))
// use a different threadpool for the fallback command
// so saturating the RemoteServiceX pool won't prevent
// fallbacks from executing
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("RemoteServiceXFallback")));
this.id = id;
}
@Override
protected String run() {
MemCacheClient.getValue(id);
}
@Override
protected String getFallback() {
// the fallback also failed
// so this fallback-of-a-fallback will
// fail silently and return null
return null;
}
}
}
hystrix-core
使用原生的Hystrix,也可以通过Spring Cloud OpenFeign
或者Spring Cloud Netflix Hystrix
使用。