Hystrix使用:
使用方式一:自定义Hystrix请求命令的方式
com.netflix.hystrix.contrib.javanica.aop.aspectj.HystrixCommandAspect
com.netflix.hystrix.HystrixCommand
public class BookCommand extends HystrixCommand<Book> {
private RestTemplate restTemplate;
private Long id;
@Override
protected Book getFallback() {
Throwable executionException = getExecutionException();
System.out.println(executionException.getMessage());
return new Book("宋诗选注", 88, "钱钟书", "三联书店");
}
@Override
protected Book run() throws Exception {
return restTemplate.getForObject("http://HELLO-SERVICE/getbook5/{1}", Book.class,id);
}
public BookCommand(Setter setter, RestTemplate restTemplate,Long id) {
super(setter);
this.restTemplate = restTemplate;
this.id = id;
}
@Override
protected String getCacheKey() {
return String.valueOf(id);
}
}
调用方式:
BookCommand bc1 = new BookCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("HELLO-SERVICE")).andCommandKey(commandKey), restTemplate, 1l);
Book e1 = bc1.execute();
方式二:Spring boot使用
@EnableCircuitBreaker
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplication {
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
@Service
public class UserService {
@Autowired
RestTemplate restTemplate;
@HystrixCommand(fallbackMethod = "userFallback", commandKey = "userKey")
public User user(String name) {
User user = restTemplate.getForEntity("http://HELLO-SERVICE/hello1?name={1}", User.class, name).getBody();
return user;
}
public String userFallback() {
return "error";
}
}
Spring Cloud中Hystrix的请求缓存 - 个人文章 - SegmentFault 思否
hystrix缓存功能的使用 - CSDN博客
通过方法重载开启缓存
Hystrix原理
策略配置:Hystrix有两种降级模型,即信号量(同步)模型和线程池(异步)模型,这两种模型所有可定制的部分都体现在了HystrixCommandProperties和HystrixThreadPoolProperties两个类中。Hystrix提供了配置修改的入口,没有将配置界面化。
数据统计:为了计算断路器的健康度,使用滑动窗口对数据进行“平滑”统计
断路器:断路器可以说是Hystrix内部最重要的状态机,是它决定着每个Command的执行过程。
断路器
断路器HystrixCircuitBreaker有三个状态,
CLOSED关闭状态:允许流量通过。
OPEN打开状态:不允许流量通过,即处于降级状态,走降级逻辑。
HALF_OPEN半开状态:允许某些流量通过,并关注这些流量的结果,如果出现超时、异常等情况,将进入OPEN状态,如果成功,那么将进入CLOSED状态。
- CLOSED -> OPEN :
时间窗口内(默认10秒)请求量大于请求量阈值(即circuitBreakerRequestVolumeThreshold,默认值是20),并且该时间窗口内错误率大于错误率阈值(即circuitBreakerErrorThresholdPercentage,默认值为50,表示50%),那么断路器的状态将由默认的CLOSED状态变为OPEN状态
// 检查是否超过了我们设置的断路器请求量阈值
if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) {
// 如果没有超过统计窗口的请求量阈值,则不改变断路器状态,
// 如果它是CLOSED状态,那么仍然是CLOSED.
// 如果它是HALF-OPEN状态,我们需要等待请求被成功执行,
// 如果它是OPEN状态, 我们需要等待睡眠窗口过去。
} else {
if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) {
//如果没有超过统计窗口的错误率阈值,则不改变断路器状态,,
// 如果它是CLOSED状态,那么仍然是CLOSED.
// 如果它是HALF-OPEN状态,我们需要等待请求被成功执行,
// 如果它是OPEN状态, 我们需要等待【睡眠窗口】过去。
} else {
// 如果错误率太高,那么将变为OPEN状态
if (status.compareAndSet(Status.CLOSED, Status.OPEN)) {
// 因为断路器处于打开状态会有一个时间范围,所以这里记录了变成OPEN的时间
circuitOpened.set(System.currentTimeMillis());
}
}
这里的错误率是个整数,即errorPercentage= (int) ((double) errorCount / totalCount * 100);
- OPEN ->HALF_OPEN:
前面说过,当进入OPEN状态后,会进入一段睡眠窗口,即只会OPEN一段时间,所以这个睡眠窗口过去,就会“自动”从OPEN状态变成HALF_OPEN状态,这种设计是为了能做到弹性恢复,这种状态的变更,并不是由调度线程来做,而是由请求来触发,每次请求都会进行如下检查:
@Override
public boolean attemptExecution() {
if (properties.circuitBreakerForceOpen().get()) {
return false;
}
if (properties.circuitBreakerForceClosed().get()) {
return true;
}
// circuitOpened值等于1说明断路器状态为CLOSED
if (circuitOpened.get() == -1) {
return true;
} else {
if (isAfterSleepWindow()) {
// 睡眠窗口过去后只有第一个请求能被执行
// 如果执行成功,那么状态将会变成CLOSED
// 如果执行失败,状态仍变成OPEN
if (status.compareAndSet(Status.OPEN, Status.HALF_OPEN)) {
return true;
} else {
return false;
}
} else {
return false;
}
}
}
// 睡眠窗口是否过去
private boolean isAfterSleepWindow() {
// 还记得上面CLOSED->OPEN时记录的时间吗?
final long circuitOpenTime = circuitOpened.get();
final long currentTime = System.currentTimeMillis();
final long sleepWindowTime = properties.circuitBreakerSleepWindowInMilliseconds().get();
return currentTime > circuitOpenTime + sleepWindowTime;
}
- HALF_OPEN ->CLOSED :
变为半开状态后,会放第一笔请求去执行,并跟踪它的执行结果,如果是成功,那么将由HALF_OPEN状态变成CLOSED状态:
@Override
public void markSuccess() {
if (status.compareAndSet(Status.HALF_OPEN, Status.CLOSED)) {
//This thread wins the race to close the circuit - it resets the stream to start it over from 0
metrics.resetStream();
Subscription previousSubscription = activeSubscription.get();
if (previousSubscription != null) {
previousSubscription.unsubscribe();
}
Subscription newSubscription = subscribeToStream();
activeSubscription.set(newSubscription);
// 已经进入了CLOSED阶段,所以将OPEN的修改时间设置成-1
circuitOpened.set(-1L);
}
}
- HALF_OPEN ->OPEN :
变为半开状态时,如果第一笔被放去执行的请求执行失败(资源获取失败、异常、超时等),就会由HALP_OPEN状态再变为OPEN状态:
@Override
public void markNonSuccess() {
if (status.compareAndSet(Status.HALF_OPEN, Status.OPEN)) {
// This thread wins the race to re-open the circuit - it resets the start time for the sleep window
circuitOpened.set(System.currentTimeMillis());
}
}
滑动窗口
上面提到的断路器需要的时间窗口请求量和错误率这两个统计数据,都是指固定时间长度内的统计数据,断路器的目标,就是根据这些统计数据来预判并决定系统下一步的行为,Hystrix通过滑动窗口来对数据进行“平滑”统计,默认情况下,一个滑动窗口包含10个桶(Bucket),每个桶时间宽度是1秒,负责1秒的数据统计。滑动窗口包含的总时间以及其中的桶数量都是可以配置的,来张官方的截图认识下滑动窗口:
上图的每个小矩形代表一个桶,可以看到,每个桶都记录着1秒内的四个指标数据:成功量、失败量、超时量和拒绝量,这里的拒绝量指的就是上面流程图中【信号量/线程池资源检查】中被拒绝的流量。10个桶合起来是一个完整的滑动窗口,所以计算一个滑动窗口的总数据需要将10个桶的数据加起来。
线程池/信号量模式
hystrix提供两种模式:线程池和信号量模式。
线程池隔离模式:使用一个线程池来存储当前的请求,线程池对请求作处理,设置任务返回处理超时时间,堆积的请求堆积入线程池队列。这种方式需要为每个依赖的服务申请线程池,有一定的资源消耗,好处是可以应对突发流量(流量洪峰来临时,处理不完可将数据存储到线程池队里慢慢处理),缺点主要是就是增加了 CPU 的开销。
信号量隔离模式:使用一个原子计数器(或信号量)来记录当前有多少个线程在运行,请求来先判断计数器的数值,若超过设置的最大线程个数则丢弃改类型的新请求,若不超过则执行计数操作请求来计数器+1,请求返回计数器-1。这种方式是严格的控制线程且立即返回模式,无法应对突发流量(流量洪峰来临时,处理的线程超过数量,其他的请求会直接返回,不继续去请求依赖的服务)。
Hystrix 采用了 Bulkhead Partition 舱壁隔离技术,来将外部依赖进行资源隔离,进而避免任何外部依赖的故障导致本服务崩溃。
舱壁隔离,是说将船体内部空间区隔划分成若干个隔舱,一旦某几个隔舱发生破损进水,水流不会在其间相互流动,如此一来船舶在受损时,依然能具有足够的浮力和稳定性,进而减低立即沉船的危险。
Hystrix 对每个外部依赖用一个单独的线程池,这样的话,如果对那个外部依赖调用延迟很严重,最多就是耗尽那个依赖自己的线程池而已,不会影响其他的依赖调用。
可以用 Hystrix semaphore 技术来实现对某个依赖服务的并发访问量的限制,而不是通过线程池/队列的大小来限制流量。Semaphore 技术可以用来限流和削峰,但是不能用来对调研延迟的服务进行 timeout 和隔离。Execution.isolation.strategy 设置为 SEMAPHORE,那么 Hystrix 就会用 semaphore 机制来替代线程池机制,来对依赖服务的访问进行限流。如果通过 semaphore 调用的时候,底层的网络调用延迟很严重,那么是无法 timeout 的,只能一直 block 住。一旦请求数量超过了 semaphore 限定的数量之后,就会立即开启限流。
Semaphore 不能设置超时和实现异步访问,所以只有在依赖的服务是足够可靠的情况下才使用信号量,所以使用hystrix时一般使用线程池隔离。
Hystrix中的信号量使用TryableSemaphore实现
com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy#SEMAPHORE/THREAD
com.netflix.hystrix.AbstractCommand.TryableSemaphore
com.netflix.hystrix.AbstractCommand#applyHystrixSemantics
com.netflix.hystrix.AbstractCommand#executeCommandWithSpecifiedIsolation
TryableSemaphore:
Semaphore that only supports tryAcquire and never blocks and that supports a dynamic permit count.
Using AtomicInteger increment/decrement instead of java.util.concurrent.Semaphore since we don't need blocking and need a custom implementation to get the dynamic permit count and since AtomicInteger achieves the same behavior and performance without the more complex implementation of the actual Semaphore class using AbstractQueueSynchronizer.
Hystrix中的信号量没有使用java中的Semaphore实现,由于不需要阻塞,只是使用AtomicInteger代替相对复杂的AbstractQueueSynchronizer。
总结
- 线程池隔离/信号量 用于限流;
- hystrix使用滑动窗口用于统计信息用于降级;
- 由于Semaphore 不能设置超时和实现异步访问,hystrix时一般使用线程池隔离,线程池隔离有一定的性能损耗;
- Hystrix只提供了配置修改的入口,没有将配置界面化,如果想在页面上动态调整配置,还需要自己实现。