降级熔断框架 Hystrix 源码解析:滑动窗口统计
概述
Hystrix 是一个开源的降级熔断框架,用于提高服务可靠性,适用于依赖大量外部服务的业务系统。什么是降级熔断呢?
降级
业务降级,是指牺牲非核心的业务功能,保证核心功能的稳定运行。简单来说,要实现优雅的业务降级,需要将功能实现拆分到相对独立的不同代码单元,分优先级进行隔离。在后台通过开关控制,降级部分非主流程的业务功能,减轻系统依赖和性能损耗,从而提升集群的整体吞吐率。
降级的重点是:业务之间有优先级之分。降级的典型应用是:电商活动期间关闭非核心服务,保证核心买买买业务的正常运行。
熔断
老式电闸都安装了保险丝,一旦有人使用超大功率的设备,保险丝就会烧断以保护各个电器不被强电流给烧坏。同理我们的接口也需要安装上“保险丝”,以防止非预期的请求对系统压力过大而引起的系统瘫痪,当流量过大时,可以采取拒绝或者引流等机制。
同样在分布式系统中,当被调用的远程服务无法使用时,如果没有过载保护,就会导致请求的资源阻塞在远程服务器上耗尽资源。很多时候,刚开始可能只是出现了局部小规模的故障,然而由于种种原因,故障影响范围越来越大,最终导致全局性的后果。这种过载保护,就是熔断器。
在 hystrix 中,熔断相关的配置有以下几个:滑动窗口长度,单位毫秒
hystrix.command.HystrixCommandKey.circuitBreaker.sleepWindowInMilliseconds
滑动窗口滚动桶的长度,单位毫秒
hystrix.command.HystrixCommandKey.metrics.rollingPercentile.bucketSize
触发熔断的失败率阈值
hystrix.command.HystrixCommandKey.circuitBreaker.errorThresholdPercentage
触发熔断的请求量阈值
hystrix.command.HystrixCommandKey.circuitBreaker.requestVolumeThreshold
从配置信息里可以看出来,熔断逻辑判断里使用了滑动窗口来统计服务调用的成功、失败量。那么这里的滑动窗口是如何实现的呢?下面我们深入源码来研究一下。
注:使用的源码版本是 2017-09-13 GitHub 上 master 分支最新代码。
滑动窗口
在 hystrix 里,大量使用了 RxJava 这个响应式函数编程框架,滑动窗口的实现也是使用了 RxJava 框架。
源码分析
一个滑动窗口有两个关键要素组成:窗口时长、窗口滚动时间间隔。通常一个窗口会划分为若干个桶 bucket,每个桶的大小等于窗口滚动时间间隔。也就是说,滑动窗口统计数据时,分两步:统计一个 bucket 内的数据;
统计一个窗口,即若干个 bucket 的数据。
bucket 统计的代码位于 BucketedCounterStream 类中,其关键的代码如下所示:// 这里的代码并非全部,只展示了和 bucket 统计相关的关键代码public abstract class BucketedCounterStream { protected final int numBuckets; protected final Observable bucketedStream; protected final AtomicReference subscription = new AtomicReference(null); private final Func1, Observable> reduceBucketToSummary; protected BucketedCounterStream(final HystrixEventStream inputEventStream, final int numBuckets, final int bucketSizeInMs, final Func2 appendRawEventToBucket) { this.numBuckets = numBuckets; this.reduceBucketToSummary = new Func1, Observable>() { @Override
public Observable call(Observable eventBucket) { return eventBucket.reduce(getEmptyBucketSummary(), appendRawEventToBucket);
}
}; final List emptyEventCountsToStart = new ArrayList(); for (int i = 0; i
emptyEventCountsToStart.add(getEmptyBucketSummary());
} this.bucketedStream = Observable.defer(new Func0>() { @Override
public Observable call() { return inputEventStream
.observe()
.window(bucketSizeInMs, TimeUnit.MILLISECONDS) //bucket it by the counter window so we can emit to the next operator in time chunks, not on every OnNext
.flatMap(reduceBucketToSummary) //for a given bucket, turn it into a long array containing counts of event types
.startWith(emptyEventCount