SpringCloud(五)SpringCloud的限流、熔断和降级——Hystrix
在分布式系统中,远程系统或服务不可避免的调用失败(超时或者异常)。假设客户端依赖多个服务,在一次请求中,某一个服务出现异常,则整个请求会处理失败;当某一服务等待时间过长,则所有的请求都会阻塞在这个服务的请求上。这样因为一个服务就导致了整个系统的可用性。Netflix的组件Hystrix可以将这些请求隔离,针对服务限流,当服务不可用时能够熔断并降级,防止级联故障。
认识Hystrix
在理想状态下,一个应用依赖的服务都是健康可用的,我们可以正常的处理所有的请求。
当某一个服务出现延迟时,所有的请求都阻塞在依赖的服务Dependency I
当我们使用了Hystrix时,Hystrix将所有的外部调用都封装成一个HystrixCommand
或者HystrixObservableCommand
对象,这些外部调用将会在一个独立的线程中运行。我们可以将出现问题的服务通过熔断、降级等手段隔离开来,这样不影响整个系统的主业务。
新建依赖的服务
为了演示一个异常的服务,我们使用了一个超时接口和一个异常的接口。
@RestController
@RequestMapping("/user")
public class UserController {
private static Logger LOGGER = LoggerFactory.getLogger(UserController.class);
@RequestMapping("/timeout")
public String timeout() throws InterruptedException {
LOGGER.info("invoking timeout endpoint");
Thread.sleep(10000L);
return "success";
}
@RequestMapping("/exception")
public String exception() {
LOGGER.info("invoking exception endpoint");
if (System.currentTimeMillis() % 2 == 0) {
throw new RuntimeException("random exception");
}
return "success";
}
}
这个服务的端口为8002
,服务名称为user-service
客户端引入依赖
在服务调用方的pom文件中引入hystrix的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
客户端配置Hystrix
Hystrix的配置都在客户端web-app-service
进行。
配置文件和API配置Hystrix
作为Netflix的组件之一,Hystrix也使用了Archaius的默认配置文件。在classppath下我们新建config.properties
文件(archaius-core
包中com.netflix.config.WebApplicationProperties#initApplicationProperties
默认加载文件)。
# Hystrix 默认加载的配置文件 - 限流、 熔断示例
# 线程池大小
hystrix.threadpool.default.coreSize=1
# 缓冲区大小, 如果为-1,则不缓冲,直接进行降级 fallback
hystrix.threadpool.default.maxQueueSize=200
# 缓冲区大小超限的阈值,超限就直接降级
hystrix.threadpool.default.queueSizeRejectionThreshold=2
# 执行策略
# 资源隔离模式,默认thread。 还有一种叫信号量
hystrix.command.default.execution.isolation.strategy=THREAD
# 是否打开超时
hystrix.command.default.execution.timeout.enabled=true
# 超时时间,默认1000毫秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=15000
# 超时时中断线程
hystrix.command.default.execution.isolation.thread.interruptOnTimeout=true
# 取消时候中断线程
hystrix.command.default.execution.isolation.thread.interruptOnFutureCancel=false
# 信号量模式下,最大并发量
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests=2
# 降级策略
# 是否开启服务降级
hystrix.command.default.fallback.enabled=true
# fallback执行并发量
hystrix.command.default.fallback.isolation.semaphore.maxConcurrentRequests=100
# 熔断策略
# 启用/禁用熔断机制
hystrix.command.default.circuitBreaker.enabled=true
# 强制开启熔断
hystrix.command.default.circuitBreaker.forceOpen=false
# 强制关闭熔断
hystrix.command.default.circuitBreaker.forceClosed=false
# 前提条件,一定时间内发起一定数量的请求。 也就是5秒钟内(这个5秒对应下面的滚动窗口长度)至少请求4次,熔断器才发挥起作用。 默认20
hystrix.command.default.circuitBreaker.requestVolumeThreshold=4
# 错误百分比。达到或超过这个百分比,熔断器打开。 比如:5秒内有4个请求,2个请求超时或者失败,就会自动开启熔断
hystrix.command.default.circuitBreaker.errorThresholdPercentage=50
# 10秒后,进入半打开状态(熔断开启,间隔一段时间后,会让一部分的命令去请求服务提供者,如果结果依旧是失败,则又会进入熔断状态,如果成功,就关闭熔断)。 默认5秒
hystrix.command.default.circuitBreaker.sleepWindowInMilliseconds=10000
# 度量策略
# 5秒为一次统计周期,术语描述:滚动窗口的长度为5秒
hystrix.command.default.metrics.rollingStats.timeInMilliseconds=5000
# 统计周期内 度量桶的数量,必须被timeInMilliseconds整除。作用:
hystrix.command.default.metrics.rollingStats.numBuckets=10
# 是否收集执行时间,并计算各个时间段的百分比
hystrix.command.default.metrics.rollingPercentile.enabled=true
# 设置执行时间统计周期为多久,用来计算百分比
hystrix.command.default.metrics.rollingPercentile.timeInMilliseconds=60000
# 执行时间统计周期内,度量桶的数量
hystrix.command.default.metrics.rollingPercentile.numBuckets=6
# 执行时间统计周期内,每个度量桶最多统计多少条记录。设置为50,有100次请求,则只会统计最近的10次
hystrix.command.default.metrics.rollingPercentile.bucketSize=100
# 数据取样时间间隔
hystrix.command.default.metrics.healthSnapshot.intervalInMilliseconds=500
# 设置是否缓存请求,request-scope内缓存
hystrix.command.default.requestCache.enabled=false
# 设置HystrixCommand执行和事件是否打印到HystrixRequestLog中
hystrix.command.default.requestLog.enabled=false
# 限流策略
#如果没有定义HystrixThreadPoolKey,HystrixThreadPoolKey会默认定义为HystrixCommandGroupKey的值
hystrix.threadpool.userGroup.coreSize=1
hystrix.threadpool.userGroup.maxQueueSize=-1
hystrix.threadpool.userGroup.queueSizeRejectionThreshold=800
hystrix.threadpool.userThreadPool.coreSize=1
hystrix.threadpool.userThreadPool.maxQueueSize=-1
hystrix.threadpool.userThreadPool.queueSizeRejectionThreshold=800
hystrix.command.userCommandKey.execution.isolation.thread.timeoutInMilliseconds=5000
创建两个HystrixCommand,分别用于请求/user/timeout
接口和/user/exception
接口。
public class UserTimeOutCommand extends HystrixCommand<String> {
public static final Logger LOGGER = LoggerFactory.getLogger(UserTimeOutCommand.class);
public UserTimeOutCommand() {
super(Setter
// 分组名称用于统计
.withGroupKey(HystrixCommandGroupKey.Factory.asKey("userGroup"))
// 用于监控、熔断、度量发布、缓存的Key值
.andCommandKey(HystrixCommandKey.Factory.asKey("userCommandKey"))
// 线程池命名,默认是HystrixCommandGroupKey的名称。
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("userThreadPool"))
// command 熔断相关参数配置
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
// 隔离方式:线程池和信号量。默认使用线程池
// .withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.SEMAPHORE)
// 超时时间500毫秒
// .withExecutionTimeoutInMilliseconds(500)
// 信号量隔离的模式下,最大的请求数。和线程池大小的意义一样
// .withExecutionIsolationSemaphoreMaxConcurrentRequests(2)
// 熔断时间(熔断开启后,各5秒后进入半开启状态,试探是否恢复正常)
// .withCircuitBreakerSleepWindowInMilliseconds(5000)
)
// 设置线程池参数
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
// 线程池大小
.withCoreSize(1)));
}
@Override
protected String run() throws Exception {
LOGGER.info("start query timeout endpoint");
URL url = new URL("http://localhost:8002/user/timeout");
byte[] result = new byte[1024];
url.openStream().read(result);
return new String(result);
}
@Override
protected String getFallback() {
// 执行超时、出错或者开启熔断之后,使用这个方法返回
// 这种策略称为服务降级
// 这里可以是一个固定返回值,查询缓存等
return "服务降级,暂时不可用";
}
}
public class UserExceptionCommand extends HystrixCommand<String> {
public static final Logger LOGGER = LoggerFactory.getLogger(UserTimeOutCommand.class);
public UserExceptionCommand() {
super(HystrixCommandGroupKey.Factory.asKey("userGroup"));
}
@Override
protected String run() throws Exception {
LOGGER.info("start query exception endpoint");
URL url = new URL("http://localhost:8002/user/exception");
byte[] result = new byte[1024];
url.openStream().read(result);
return new String(result);
}
@Override
protected String getFallback() {
return "服务降级,暂时不可用";
}
}
在Java配置中我们定义了HystrixCommandGroupKey
,HystrixCommandKey
和HystrixThreadPoolKey
,这三个key值是和config.properties
文件中配置项对应的关键因素。如果没定义HystrixThreadPoolKey
默认地会使用HystrixCommandGroupKey
的值。所以hystrix.threadpool.userGroup.coreSize
会在UserExceptionCommand
中生效,而hystrix.threadpool.userThreadPool.coreSize
会在UserTimeOutCommand
中生效。当然所有的属性都是可以通过Java API进行配置的。
getFallback
是我们在执行run方法请求超时、异常或者服务不可用时执行的方法。通常可以返回固定值,查询缓存或者备用服务。
配置完成后我们在web-app-service
新增一个请求接口
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/command/timeout")
public String commandTimeout() {
return new UserTimeOutCommand().execute();
}
@RequestMapping("/command/exception")
public String commandException() {
return new UserExceptionCommand().execute();
}
}
这里HystrixCommand的每次都必须使用新的对象(保证每次请求都是一个新的线程)调用其execute
方法,而不能使用单例。否则会提示
This instance can only be executed once. Please instantiate a new instance.
HystrixCommand除了上面的同步执行,还有异步执行和响应式执行的方式。
/**同步**/
new UserExceptionCommand().execute();
/**异步**/
//UserExceptionCommand().execute()和new UserExceptionCommand().queue().get()是等效的。
Future<String> fs = new UserExceptionCommand().queue();
fs.get();
/**响应式**/
//立即执行
Observable<String> ho = new UserExceptionCommand().observe();
//只有当被订阅的时候才会开始执行
Observable<String> co = new UserExceptionCommand().toObservable();
//阻塞的
ho.toBlockingObservable().single();
//非阻塞的
ho.subscribe(new Observer<String>() {
@Override
public void onCompleted() {
// nothing needed here
}
@Override
public void onError(Throwable e) {
e.printStackTrace();
}
@Override
public void onNext(String v) {
System.out.println("onNext: " + v);
}
});
// 非阻塞的
ho.subscribe(new Action1<String>() {
@Override
public void call(String v) {
System.out.println("onNext: " + v);
}
});
降级
我们请求web-app-service
的/user/command/exception
接口,因为user-service
的/user/exception
会随机抛出运行时异常。当服务异常时,将返回服务降级,暂时不可用
,而服务正常时将返回success
。这个示例说明我们的服务因为异常降级。
请求/user/command/timeout
接口时,返回服务降级,暂时不可用
。如果修改hystrix.command.userCommandKey.execution.isolation.thread.timeoutInMilliseconds=15000
保证Hstrix限制的超时时间大于接口的返回时长,就能成功返回success
。这个示例说明我们的服务因为请求超时,所以降级了。
限流
在微服务架构中我们服务端的请求承载量是有限的,我们希望在客户端请求时,限制请求的并发数,防止请求数量过大对服务端产生压力。我们可以通过semaphore.maxConcurrentRequests
,coreSize
,maxQueueSize
和queueSizeRejectionThreshold
设置信号量模式下的最大并发量、线程池大小、缓冲区大小和缓冲区降级阈值。在示例中我们作如下设置
#不设置缓冲区,当请求数超过coreSize时直接降级
hystrix.threadpool.userThreadPool.maxQueueSize=-1
#超时时间大于我们的timeout接口返回时间
hystrix.command.userCommandKey.execution.isolation.thread.timeoutInMilliseconds=15000
这个时候我们连续多次请求/user/command/timeout
接口,在第一个请求还没有成功返回时,查看输出日志可以发现只有第一个请求正常的进入到user-service
的接口中,其它请求会直接返回降级信息。这样我们就实现了对服务请求的限流。
熔断
在配置文件中我们开启了熔断,并且以5秒为度量周期,当5秒内请求超过4个错误超过50%时,就会开启熔断器,所有的请求都会直接降级,如果5秒内的请求不够4个,就算有三个请求且全部失败也不会开启熔断器。10秒后熔断器进入半打开状态会让一部分请求向服务端发起调用,如果成功关闭熔断器,否则再次进入熔断状态。
我们对web-app-serivce
的/user/command/exception
连续发起请求(5秒内至少4次),当我们的请求异常超过50%时,服务会直接返回降级信息。
实际上当服务异常、超时、宕机并满足熔断条件时,都会开启熔断。
注解配置Hystrix
Hystrix也支持以注解的形式配置。通过@HystrixCommand
注解的fallbackMethod
属性指定降级方法。groupKey
和commandKey
默认为方法名(当然threadPoolKey
不指定时,默认和groupKey
一致,所以也是方法名),也可以指定这三个key值,配置文件通过groupKey
,commandKey
,threadPoolKey
使用恰当的配置。commandProperties
和threadPoolProperties
是通过@HystrixProperty
的name
value
键值对进行配置。
@Component
public class UserAnnotationCommand {
@Autowired
@Qualifier("lbRestTemplate")
RestTemplate lbRestTemplate;
@HystrixCommand(fallbackMethod = "timeoutFallback", threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "20"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "20")
}, commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "8000")
})
public String timeout() {
return lbRestTemplate.getForObject("http://user-service/user/timeout", String.class);
}
public String timeoutFallback() {
return "timeout 降级";
}
@HystrixCommand(fallbackMethod = "exceptionFallback", threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "20"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "20")
}, commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "2000")
})
public String exception() {
return lbRestTemplate.getForObject("http://user-service/user/exception", String.class);
}
public String exceptionFallback() {
return "exception 降级";
}
}
查看com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect
源码,我们可以看到@HystrixCommand
注解配置的方式是使用了AOP动态的生成了一个HystrixInvokable
对象,通过调用HystrixInvokable
的方法实现了HystrixCommand的功能。
// 使用AOP让注解的@HystrixCommand生效
@Aspect
public class HystrixCommandAspect {
@Pointcut("@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand)")
public void hystrixCommandAnnotationPointcut() {
}
@Pointcut("@annotation(com.netflix.hystrix.contrib.javanica.annotation.HystrixCollapser)")
public void hystrixCollapserAnnotationPointcut() {
}
@Around("hystrixCommandAnnotationPointcut() || hystrixCollapserAnnotationPointcut()")
public Object methodsAnnotatedWithHystrixCommand(final ProceedingJoinPoint joinPoint) throws Throwable {
Method method = getMethodFromTarget(joinPoint);
/***省略***/
MetaHolderFactory metaHolderFactory = META_HOLDER_FACTORY_MAP.get(HystrixPointcutType.of(method));
MetaHolder metaHolder = metaHolderFactory.create(joinPoint);
HystrixInvokable invokable = HystrixCommandFactory.getInstance().create(metaHolder);
ExecutionType executionType = metaHolder.isCollapserAnnotationPresent() ?
metaHolder.getCollapserExecutionType() : metaHolder.getExecutionType();
Object result;
try {
if (!metaHolder.isObservable()) {
result = CommandExecutor.execute(invokable, executionType, metaHolder);
} else {
result = executeObservable(invokable, executionType, metaHolder);
}
} catch (HystrixBadRequestException e) {
throw e.getCause() != null ? e.getCause() : e;
} catch (HystrixRuntimeException e) {
throw hystrixRuntimeExceptionToThrowable(metaHolder, e);
}
return result;
}
}
同样的为了测试这个配置是否生效我们新增接口
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserAnnotationCommand userAnnotationCommand;
@RequestMapping("/command/annotation/timeout")
public String commandAnnotationTimeout() {
return userAnnotationCommand.timeout();
}
@RequestMapping("/command/annotation/exception")
public String commandAnnotationException() {
return userAnnotationCommand.exception();
}
}
我们可以通过测试超时和随机异常接口查看Hystrix设置是否成功。
http://localhost:8101/user/command/annotation/exception
http://localhost:8101/user/command/annotation/timeout
和FeignClient集成
在SpringCloud中Hystrix和Feign的集成十分方便。在客户端我们需要使用@EnableCircuitBreaker
启用熔断机制。
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(defaultConfiguration = FeignClientsConfiguration.class)
@EnableCircuitBreaker
public class WebApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(WebApplicationStarter.class, args);
}
}
同时application.yml
配置文件中开启Hystrix,并进行Hystrix配置。
# 开启熔断机制
feign:
hystrix:
enabled: true
ribbon:
# 开启eureka与ribbon的集成
eureka:
enabled: true
# 暂不开启熔断机制
hystrix:
enabled: false
# 配置ribbon默认的超时时间
ConnectTimeout: 20000
ReadTimeout: 20000
# 是否开启重试
OkToRetryOnAllOperations: true
# 重试的时候实例切换次数
MaxAutoRetriesNextServer: 3
# 每个实例重试次数
MaxAutoRetries: 2
## hystrix相关配置
## hystrix默认会读取classpath下的config.properties文件,application会覆盖config.properties中的属性
hystrix:
threadpool:
# 指定服务的配置
user-service:
coreSize: 20
maxQueueSize: 200
queueSizeRejectionThreshold: 3
# userThreadPool是UserTimeOutCommand中配置的threadPoolKey
userThreadPool:
coreSize: 20
maxQueueSize: 20
queueSizeRejectionThreshold: 3
# 这是默认的配置
default:
coreSize: 10
maxQueueSize: 200
queueSizeRejectionThreshold: 2
command:
# 指定feign客户端中具体的方法
UserService#timeout():
execution:
isolation:
thread:
timeoutInMilliseconds: 20000
userCommandKey:
execution:
isolation:
thread:
timeoutInMilliseconds: 15000
# 这是默认的配置
default:
execution:
timeout:
enabled: true
isolation:
strategy: THREAD
thread:
timeoutInMilliseconds: 15000
interruptOnTimeout: true
interruptOnFutureCancel: false
semaphore:
maxConcurrentRequests: 2
fallback:
enabled: true
isolation:
semaphore:
maxConcurrentRequests: 10
circuitBreaker:
enabled: true
forceOpen: false
forceClosed: false
requestVolumeThreshold: 4
errorThresholdPercentage: 50
sleepWindowInMilliseconds: 10000
metrics:
rollingStats:
timeInMilliseconds: 5000
numBuckets: 10
rollingPercentile:
enabled: true
timeInMilliseconds: 60000
numBuckets: 6
bucketSize: 100
healthSnapshot:
intervalInMilliseconds: 500
这里为了测试超时接口,所以Ribbon配置的超时时间(20s)大于接口的返回时间(10s),否则在Ribbon超时后将自动重试,直到超过Hystrix的超时时间时返回降级信息。在实际中,Hystrix的超时时间应该大于Ribbon的超时时间*Ribbon的重试次数。
如果项目中
config.properties
和application.yml
文件中都有Hystrix的配置,对于同一配置项,application.yml
中的属性会覆盖config.properties
中的属性值。当然使用Java API和注解方式也是优先读取application.yml
中的配置。
在这个配置中我们甚至可以指定FeignClient中的某一方法的Hystrix配置。比如上面的配置文件中我们定义了UserService
的timeout()
方法的超时时间为20s。
新增Feign客户端和降级类,降级类必须实现对应的Feign客户端。
@FeignClient(name="user-service", fallback = UserServiceFallback.class)
public interface UserService {
@RequestMapping(value = "/user/timeout", method = RequestMethod.GET)
public String timeout();
@RequestMapping(value = "/user/exception", method = RequestMethod.GET)
public String exception();
}
@Component
public class UserServiceFallback implements UserService {
@Override
public String timeout() {
return "timeout 降级";
}
@Override
public String exception() {
return "exception 降级";
}
}
同样的,我们新增接口测试Hystrix设置是否成功。
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@RequestMapping("/feign/timeout")
public String feignTimeout() {
return userService.timeout();
}
@RequestMapping("/feign/exception")
public String feignException() {
return userService.exception();
}
}
http://localhost:8101/user/feign/annotation/exception
http://localhost:8101/user/feign/annotation/timeout
Hystrix属性配置参考
Hystrix的wiki上详细地罗列了所有属性的含义、默认配置、实例配置以及Java API的配置方式供我们参考。
Hystrix wiki——属性配置
相关代码