StatisticSlot次要统计两类数据:
- 线程数
- 申请数,也就是QPS
对于线程数统计比较简单,是通过外部保护LongAdder进行的以后线程数的统计,每进入一个线程加1,线程执行完减1,从而失去线程数。
对于QPS的统计则要简单点,其中用到了滑动窗口的原理。上面就来重点剖析下实现的详情。
Bucket
Sentinel应用Bucket统计一个窗口工夫内的各项指标数据,这些指标数据包含申请总数、胜利总数、异样总数、总耗时、最小耗时、最大耗时等,而一个Bucket能够是记录1s内的数据,也能够是10ms内的数据,这个工夫长度称为窗口工夫。
public class MetricBucket { /** * 存储各事件的计数,比方异样总数、申请总数等 */ private final LongAdder[] counters; /** * 这段事件内的最小耗时 */ private volatile long minRt;}
Bucket记录一段时间内的各项指标数据用的是一个LongAdder数组,LongAdder保障了数据批改的原子性,并且性能较AtomicInteger体现更好。数组的每个元素别离记录一个工夫窗口的申请总数、异样数、部耗时。
Sentinel应用枚举类型MetricEvent的ordinal属性作为下标,当须要获取Bucket记录总的胜利申请数或异样总数、总的申请解决耗时,可依据事件类型(MetricEvent)从Bucket的LongAdder数组中获取对应的LongAdder,并调用sum办法获取:
// 假如事件为 MetricEvent.SUCCESSpublic long get(MetricEvent event) { // MetricEvent.SUCCESS.ordinal()为 1 return counters[event.ordinal()].sum();}
须要记录申请数时操作如下:
// 假如事件为 MetricEvent.RTpublic void add(MetricEvent event, long n) { // MetricEvent.RT.ordinal()为 2 counters[event.ordinal()].add(n);}
滑动窗口
咱们心愿晓得某个接口的每秒解决胜利申请数(胜利QPS)、申请均匀耗时(avg rt),咱们只须要管制Bucket统计一秒钏的指标数据即可。Sentinel是如何实现的呢?它定义了一个Bucket数组,依据工夫戳来定位到数组的下标。假如咱们须要统计每1秒解决的申请数,且只须要保留最近一分钟的数据,那么Bucket数组的大小就能够设置为60,每个Bucket的windowLengthInMs(窗口工夫)大小就是1000ms。
咱们不可能当然也不须要有限存储Bucket,如果说只须要保留一分钟的数据,那咱们就能够将Bucket的大小设置为60并循环应用,防止频繁创立Bucket。这种状况下如何定位Bucket呢?做法是将以后工夫戳去掉毫秒局部等到以后秒数,再将失去的秒数与数组长度取余,就能失去以后工夫窗口的Bucket在数组中的地位。
例如给定工夫戳计算数组索引:
private int calculateTimeIdx(long timeMillis) { /** * 假如以后工夫戳为 1577017699235 * windowLengthInMs 为 1000 毫秒(1 秒) * 则 * 将毫秒转为秒 => 1577017699 * 而后对数组长度取余=>映射到数组的索引 * 取余是为了循环利用数组 */ long timeId = timeMillis / windowLengthInMs; return (int) (timeId % array.length()); }
因为数组是循环应用的,以后工夫戳与一分钟之前的工夫戳与后一分钟的工夫戳都会映射到数组中的同一个Bucket,因而,必须要可能判断获得的Bucket是否是统计以后工夫窗口内的指标数据,这便要数组每个元素都存储Bucet工夫窗口的开始工夫戳。那开始工夫如果计算呢?
protected long calculateWindowStart(long timeMillis) { /** * 假如窗口大小为 1000 毫秒,即数组每个元素存储 1 秒钟的统计数据 * timeMillis % windowLengthInMs 就是获得毫秒局部 * timeMillis - 毫秒数 = 秒局部 * 这就失去每秒的开始工夫戳 */ return timeMillis - timeMillis % windowLengthInMs; }
WindowWrap
因为Bucket本身并不保留工夫窗口信息,所以Sentinel给Bucket加了一个包装类WindowWrap,用于记录Bcuket的工夫窗口。
public class WindowWrap<T> { /** * 窗口工夫长度(毫秒) */ private final long windowLengthInMs; /** * 开始工夫戳(毫秒) */ private long windowStart; /** * 工夫窗口的内容,在 WindowWrap 中是用泛型示意这个值的, * 但实际上就是 MetricBucket 类 */ private T value; public WindowWrap(long windowLengthInMs, long windowStart, T value) { this.windowLengthInMs = windowLengthInMs; this.windowStart = windowStart; this.value = value; }}
咱们只有晓得窗口的开始工夫和窗口工夫大小,给定一个工夫戳,就能晓得该工夫戳是否在Bucket的窗口工夫内。
/** * 查看给定的工夫戳是否在以后 bucket 中。 * * @param timeMillis 工夫戳,毫秒 * @return */ public boolean isTimeInWindow(long timeMillis) { return windowStart <= timeMillis && timeMillis < windowStart + windowLengthInMs; }
通过工夫戳定位Bucket
Bucket用于统计各项指标数据,WindowWrap用于记录Bucket的工夫窗口信息,记录窗口的开始工夫和窗口的大小,WindowWrap数组就是一个滑动窗口。
当接管到一个申请时,可依据申请的工夫戳计算出一个数组索引,从滑动窗口(WindowWrap)中获取一个WindowWrap,从而获取WindowWrap包装的Bucket,调用Bucket的add办法记录相应的事件。
/** * 依据工夫戳获取 bucket * * @param timeMillis 工夫戳(毫秒) * @return 如果工夫无效,则在提供的工夫戳处显示以后存储桶项;如果工夫有效,则为空 */ public WindowWrap<T> currentWindow(long timeMillis) { if (timeMillis < 0) { return null; } // 获取工夫戳映射到的数组索引 int idx = calculateTimeIdx(timeMillis); // 计算 bucket 工夫窗口的开始工夫 long windowStart = calculateWindowStart(timeMillis); // 从数组中获取 bucket while (true) { WindowWrap<T> old = array.get(idx); // 个别是我的项目启动时,工夫未达到一个周期,数组还没有存储满,没有到复用阶段,所以数组元素可能为空 if (old == null) { // 创立新的 bucket,并创立一个 bucket 包装器 WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); // cas 写入,确保线程平安,冀望数组下标的元素是空的,否则就不写入,而是复用 if (array.compareAndSet(idx, null, window)) { return window; } else { Thread.yield(); } } // 如果 WindowWrap 的 windowStart 正好是以后工夫戳计算出的工夫窗口的开始工夫,则就是咱们想要的 bucket else if (windowStart == old.windowStart()) { return old; } // 复用旧的 bucket else if (windowStart > old.windowStart()) { if (updateLock.tryLock()) { try { // 重置 bucket,并指定 bucket 的新工夫窗口的开始工夫 return resetWindowTo(old, windowStart); } finally { updateLock.unlock(); } } else { Thread.yield(); } } // 计算出来的以后 bucket 工夫窗口的开始工夫比数组以后存储的 bucket 的工夫窗口开始工夫还小, // 间接返回一个空的 bucket 就行 else if (windowStart < old.windowStart()) { return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); } } }protected WindowWrap<MetricBucket> resetWindowTo(WindowWrap<MetricBucket> w, long time) { // Update the start time and reset value. // 重置windowStart w.resetTo(time); MetricBucket borrowBucket = borrowArray.getWindowValue(time); if (borrowBucket != null) { w.value().reset(); w.value().addPass((int)borrowBucket.pass()); } else { w.value().reset(); } return w; }
下面代码通过以后工夫戳计算以后是的工夫窗口的Bucket(new Bucket)在数组中的索引,以驻Bucket工夫窗口的开始工夫,通过索引从数组中获得Bucket(old bucket)。
- 当索引处不存在Bucket时,创立一个新的Bucket,并线程平安的写入到索引处,而后将Bucket返回
- 当old Bucket不为空时,且old Bucket工夫窗口的开始工夫与以后计算失去的new Bucket的工夫窗口开始工夫相等,则该Bucket就是以后要找的Bucket,间接返回
- 当计算的new Bucket工夫窗口的开始工夫大于以后数组存储的old Bucket工夫窗口的开始工夫时,能够复用这个old Bucket,以线程平安重置
- 当计算出new Bucket工夫窗口的开始工夫小于以后数组存储的old Bucket工夫窗口的开始工夫时,间接返回一个空的Bucket。
如何获取以后工夫戳的前一个Bucket呢,答案是依据以后工夫戳计算以后Bucket的工夫窗口开始工夫,用以后Bucket的工夫窗口开始工夫减去一个窗口大小就能够定位出前一个Bucket了。
须要留神的时,数组是循环应用的,所以以后Bucket与计算出的Bucket可能相差一个滑动窗口也可能相差一个以上,所以须要依据Bucket的工夫窗口开始工夫与以后工夫戳进行比拟,如果跨了一个周期就是有效的。
总结
- WindowWrap用于包装Bucket,随着Bucket一起创立
- WindowWrap数组实现滑动窗口,Bucket只负责统计各项指标数据,WindowWrap用于记录Bucket的工夫窗口信息
- 定位Bucket实际上是定位WindowWrap,拿到WindowWrap就能够拿到Bucket