每日一句
用微笑告诉别人,今天的我比昨天强,今后也一样
目录
前言
指标收集对于熔断器的重要作用不言而喻。熔断器的各个状态的切换都依赖于指标的收集。而各种熔断降级组件收集的思路大体是一致的 (大部分的指标都可以通过滑动窗口来收集) 只不过实现不同而已。熟悉hystrix的朋友都知道它是通过Rxjava来实现的。而本篇文章来将探讨Resilience4j的收集指标机制。
Metrics
指标接口,有两种实现 基于计数的滑动窗口 FixedSizeSlidingWindowMetrics 和基于时间滑动窗口 SlidingTimeWindowMetrics。
/**
* 记录一个请求 返回一个快照.
*/
Snapshot record(long duration, TimeUnit durationUnit, Outcome outcome);
/**
* 获取一个指标的快照
*/
Snapshot getSnapshot();
Snapshot(指标快照)
/**
*返回总调用时长
*/
Duration getTotalDuration();
/**
*平均调用时间
*/
Duration getAverageDuration();
/**
* 返回低于特定阈值的调用
*/
int getTotalNumberOfSlowCalls();
/**
* 返回当前慢请求的数量
*/
int getNumberOfSlowSuccessfulCalls();
/**
*返回当前失败调用的数量,该数量低于某个特定阈值。
*/
int getNumberOfSlowFailedCalls();
/**
* 返回低于特定阈值的(配置文件中配置的)请求的百分比
*/
float getSlowCallRate();
/**返回当前成功调用的数量。
*/
int getNumberOfSuccessfulCalls();
/**返回当前失败的调用数。
*/
int getNumberOfFailedCalls();
/**返回所有调用的当前总数。
*/
int getTotalNumberOfCalls();
/**返回当前失败率(百分比)。
*/
float getFailureRate();
指标快照接口 有且仅有一个实现SnapshotImpl。该实现类根据以下的5个值来计算上述接口所需要的值。
- 调用总耗时 totalDurationInMillis
- 慢调用数量 totalNumberOfSlowCalls
- 慢调用失败的数量 totalNumberOfSlowFailedCalls
- 失败调用数量 totalNumberOfFailedCalls
- 总调用数量 totalNumberOfCalls
我们来解释一下上面几个变量的。
totalNumberOfSlowFailedCalls 这个很容器理解 就是代表在某一段时间内的总的调用数量。
totalNumberOfSlowCalls 慢调用数量。还记得上一篇我们说配置的时候有一个配置 slowCallDurationThreshold (慢执行的阈值,大于该值的调用都会被记入慢调用)。
totalNumberOfSlowFailedCalls 慢执行的失败的数量,也就是说这些执行时间都大于我们配置的slowCallDurationThreshold 阈值。并且执行还失败了。
totalNumberOfFailedCalls 失败的执行数量
那么有了以上的这些值 就能得到Snapshot这个接口定义的方法的返回值。所以SnapshotImpl实现很简单 我们就不贴源码占用位置了。
FixedSizeSlidingWindowMetrics
基于计数的滑动窗口。该实现是基于固定大小的数组 Measurement[]存放每次调用的结果 totalAggregation是结果的聚合。 但是totalAggregation最多只能聚合windowSize 个调用结果。作为Resilience4j 断路器指标的默认实现。
在聊FixedSizeSlidingWindowMetrics之前 我们先来对涉及到的基础类进行清扫。
AbstractAggregation
从名字看出事一个聚合操作的类。
//下面是上面 我们聊的五大基础变量
long totalDurationInMillis = 0;
//慢执行的数量
int numberOfSlowCalls = 0;
//慢执行失败的数量
int numberOfSlowFailedCalls = 0;
//执行失败的数量
int numberOfFailedCalls = 0;
//总数量
int numberOfCalls = 0;
//下面的方法就是对每次请求的记录。值得注意的是 这个操作是 非线程安全的哦
void record(long duration, TimeUnit durationUnit, Metrics.Outcome outcome) {
this.numberOfCalls++;
this.totalDurationInMillis += durationUnit.toMillis(duration);
//主要是下面的这个switch 我们看到执行失败被划分为 慢执行失败和 普通的失败
switch (outcome) {
case SLOW_SUCCESS:
numberOfSlowCalls++;
break;
case SLOW_ERROR:
numberOfSlowCalls++;
numberOfFailedCalls++;
numberOfSlowFailedCalls++;
break;
case ERROR:
numberOfFailedCalls++;
break;
}
}
TotalAggregation
该聚合只是增加了一个移除桶的方法。 很容易理解 从总聚合总移除一个桶
void removeBucket(AbstractAggregation bucket) {
this.totalDurationInMillis -= bucket.totalDurationInMillis;
this.numberOfSlowCalls -= bucket.numberOfSlowCalls;
this.numberOfSlowFailedCalls -= bucket.numberOfSlowFailedCalls;
this.numberOfFailedCalls -= bucket.numberOfFailedCalls;
this.numberOfCalls -= bucket.numberOfCalls;
}
Measurement
该类在聚合的基础上提供了一个重置的方法。
void reset() {
this.totalDurationInMillis = 0;
this.numberOfSlowCalls = 0;
this.numberOfFailedCalls = 0;
this.numberOfCalls = 0;
}
值得注意的是 这个重置方法 并没有对numberOfSlowFailedCalls进行重置。
PartialAggregation
该类是public 修饰的,是唯一的一个开放的一个聚合类。
PartialAggregation: 部分聚集
private long epochSecond;
//构造方法需要一个时间 意义就是这段时间的聚合
PartialAggregation(long epochSecond) {
this.epochSecond = epochSecond;
}
void reset(long epochSecond) {
this.epochSecond = epochSecond;
this.totalDurationInMillis = 0;
this.numberOfSlowCalls = 0;
this.numberOfFailedCalls = 0;
this.numberOfCalls = 0;
}
FixedSizeSlidingWindowMetrics
有了上面的聚合类的支持 我们再来看FixedSizeSlidingWindowMetrics
private final int windowSize;
private final TotalAggregation totalAggregation;
private final Measurement[] measurements;
int headIndex;
属性说明
- windowSize: 窗口的大小 可以通过配置项 slidingWindowSize来设定。
- totalAggregation: 总聚合对每次请求的结果进行聚合
- measurements: 统计固定窗口的指标。你可以理解为它是对一个窗口的所有指标的统计。例如一个窗口大小为30 那么它就是对这30次的执行结果进行统计。等到第31次的时候就会移除第一次 加上第31的结果。会一直统计30次的结果,滑动统计。
- headIndex: 一个标志位,
构造方法
public FixedSizeSlidingWindowMetrics(int windowSize) {
this. = windowSize;
this.measurements = new Measurement[this.windowSize];
this.headIndex = 0;
for (int i = 0; i < this.windowSize; i++) {
measurements[i] = new Measurement();
}
this.totalAggregation = new TotalAggregation();
}
该类只提供了一个构造方法,用于对measurements、windowSize、totalAggregation等属性做初始化。
滑动窗口实现
//这个方法是滑动窗口算法的实现 的核心逻辑
private Measurement moveWindowByOne() {
//将下标向下移动一位 如果下标已经是数组的最大值 向下一位就会移动到数组的第一位
moveHeadIndexByOne();
//获取最新的桶
Measurement latestMeasurement = getLatestMeasurement();
//如果最新的桶有值就从总聚合中删除
totalAggregation.removeBucket(latestMeasurement);
//重置最新的桶
latestMeasurement.reset();
return latestMeasurement;
}
// 向下移动一个桶 这个是循环数组的下标
void moveHeadIndexByOne() {
this.headIndex = (headIndex + 1) % windowSize;
}
//获取最新的桶
private Measurement getLatestMeasurement() {
return measurements[headIndex];
}
通过上面的介绍 你可能对 moveWindowByOne 这个方法的操作有疑问。
假如我们圆形数组(measurements)有10个元素,也就是滑动窗口大小为10。那么当headIndex值为9的时候,这个时候来了一个请求 需要记录一个执行结果 通过 moveHeadIndexByOne 的计算 headIndex为0
getLatestMeasurement 获取最新的桶 就是数组下标为 0的元素。但是此时 下标为0的元素是有值的。需要我们从总聚合中移除该值 并且重置此处的值 才能保证 滑动窗口的值是最近10次执行的结果的聚合。
getSnapshot& record
//记录一次请求结果的执行值
@Override
public synchronized Snapshot record(long duration, TimeUnit durationUnit, Outcome outcome) {
//总聚合记录
totalAggregation.record(duration, durationUnit, outcome);
//滑动记录
moveWindowByOne().record(duration, durationUnit, outcome);
return new SnapshotImpl(totalAggregation);
}
//获取一个总聚合的快照
@Override
public synchronized Snapshot getSnapshot() {
//直接获取总聚合的快照信息
return new SnapshotImpl(totalAggregation);
}
上面两个方法是实现Metrics接口的方法!
SlidingTimeWindowMetrics
了解完基于计数的滑动窗口 再来看基于时间的 那就很easy了。维度不同而已。
public SlidingTimeWindowMetrics(int timeWindowSizeInSeconds, Clock clock) {
this.clock = clock;
/**初始化窗口大小 1s一个存储桶 如果timeWindowSizeInSeconds为10 就会分成10个存储桶 并且总的窗口大小为10s*/
this.timeWindowSizeInSeconds = timeWindowSizeInSeconds;
/**下面初始化存储桶 初始化的时候会初始化存储桶的时间*/
this.partialAggregations = new PartialAggregation[timeWindowSizeInSeconds];
this.headIndex = 0;
//初始化 partialAggregations 注意这里是epochSecond++ 所以是一秒一个存储桶 这个是写死的。如果窗口大小是 10s 那么久会初始化10个存储桶
long epochSecond = clock.instant().getEpochSecond();
for (int i = 0; i < timeWindowSizeInSeconds; i++) {
partialAggregations[i] = new PartialAggregation(epochSecond);
epochSecond++;
}
this.totalAggregation = new TotalAggregation();
}
上面有一个地方需要注意,在初始化滑动窗口的时候 每秒一个存储桶 这个是写死的。了解Hystrix的朋友应该知道 Hystrix这个桶时长是可配置的
record方法
@Override
public synchronized Snapshot record(long duration, TimeUnit durationUnit, Outcome outcome) {
//总聚合增加一个记录
totalAggregation.record(duration, durationUnit, outcome);
//滑动记录
moveWindowToCurrentEpochSecond(getLatestPartialAggregation())
.record(duration, durationUnit, outcome);
return new SnapshotImpl(totalAggregation);
}
重点就是moveWindowToCurrentEpochSecond这个方法。继续跟进这个方法
private PartialAggregation moveWindowToCurrentEpochSecond(
PartialAggregation latestPartialAggregation) {
//这里使用clock实例获取当前对象
long currentEpochSecond = clock.instant().getEpochSecond();
/**当前时间 减去最新的一个桶的时间 计算出要滑动的窗口*/
long differenceInSeconds = currentEpochSecond - latestPartialAggregation.getEpochSecond();
/**如果还在当前的统计窗口内 直接返回最新的桶*/
if (differenceInSeconds == 0) {
return latestPartialAggregation;
}
/**如果要移动的窗口个数 这里做了一个判断如果需要移动的个数大于窗口的总大小 就将移动的窗口个数改变成窗口的总大小*/
long secondsToMoveTheWindow = Math.min(differenceInSeconds, timeWindowSizeInSeconds);
PartialAggregation currentPartialAggregation;
/**下面就是滑动窗口的逻辑了 滑动的过程*/
do {
secondsToMoveTheWindow--;
//移动headIndex到下一个窗口(存储桶)
moveHeadIndexByOne();
//获取下一个存储桶
currentPartialAggregation = getLatestPartialAggregation();
//从总的统计中移除当前存储桶
totalAggregation.removeBucket(currentPartialAggregation);
//重置当前存储桶 并对epochSecond赋值
// 例如我们当前向前移动了3个窗口 那么移动的第一个窗口对应的时间就是当前时间减去2s 以此类推
currentPartialAggregation.reset(currentEpochSecond - secondsToMoveTheWindow);
} while (secondsToMoveTheWindow > 0);
return currentPartialAggregation;
}
主要的逻辑就在上面这个方法中。
demo
Metrics metrics = new FixedSizeSlidingWindowMetrics(5);
//记录一次成功的执行
Snapshot snapshot = metrics.record(100, TimeUnit.MILLISECONDS, Metrics.Outcome.SUCCESS);
assertThat(snapshot.getTotalNumberOfCalls()).isEqualTo(1);
assertThat(snapshot.getNumberOfSuccessfulCalls()).isEqualTo(1);
assertThat(snapshot.getNumberOfFailedCalls()).isEqualTo(0);
assertThat(snapshot.getTotalNumberOfSlowCalls()).isEqualTo(0);
assertThat(snapshot.getNumberOfSlowSuccessfulCalls()).isEqualTo(0);
assertThat(snapshot.getNumberOfSlowFailedCalls()).isEqualTo(0);
assertThat(snapshot.getTotalDuration().toMillis()).isEqualTo(100);
assertThat(snapshot.getAverageDuration().toMillis()).isEqualTo(100);
assertThat(snapshot.getFailureRate()).isEqualTo(0);
总结
Resilience4j的指标统计功能到这就完了。它相对于Hystrix的还是比较好理解的。把基于计数的和基于时间的滑动窗口两个核心的类搞明白就好了。其中默认是基于计数的滑动窗口。