diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java index 0d661094..fa559868 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/node/StatisticNode.java @@ -26,11 +26,72 @@ import com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric; import com.alibaba.csp.sentinel.slots.statistic.metric.Metric; /** + *

The statistic node keep three kinds of real-time statistics metrics:

+ *
    + *
  1. metrics in second level ({@code rollingCounterInSecond})
  2. + *
  3. metrics in minute level ({@code rollingCounterInMinute})
  4. + *
  5. thread count
  6. + *
+ * + *

+ * Sentinel use sliding window to record and count the resource statistics in real-time. + * The sliding window infrastructure behind the {@link ArrayMetric} is {@code LeapArray}. + *

+ * + *

+ * case 1: When the first request comes in, Sentinel will create a new window bucket of + * a specified time-span to store running statics, such as total response time(rt), + * incoming request(QPS), block request(bq), etc. And the time-span is defined by sample count. + *

+ *
+ * 	0 	   100ms
+ *  +-------+--→ Sliding Windows
+ * 		^
+ * 	   	|
+ * 	  request
+ * 
+ *

+ * Sentinel use the statics of the valid buckets to decide whether this request can be passed. + * For example, if a rule defines that only 100 requests can be passed, + * it will sum all qps in valid buckets, and compare it to the threshold defined in rule. + *

+ * + *

case 2: continuous requests

+ *
+ *  0 	 100ms 	  200ms	  300ms
+ *  +-------+-------+-------+-----→ Sliding Windows
+ * 						^
+ * 						|
+ * 					  request
+ * 
+ * + *

case 3: requests keeps coming, and previous buckets become invalid

+ *
+ *  0 	 100ms 	  200ms	  800ms	   900ms  1000ms	1300ms
+ *  +-------+-------+ ...... +-------+-------+ ...... +-------+-----→ Sliding Windows
+ *  													^
+ *  													|
+ * 													  request
+ * 
+ * + *

The sliding window should become:

+ *
+ * 300ms	  	800ms  900ms  1000ms	1300ms
+ *  + ...... +-------+ ...... +-------+-----→ Sliding Windows
+ *  													^
+ *  													|
+ * 													  request
+ * 
+ * * @author qinan.qn * @author jialiang.linjl */ public class StatisticNode implements Node { + /** + * Holds statistics of the recent {@code INTERVAL} seconds. The {@code INTERVAL} is divided into time spans + * by given {@code sampleCount}. + */ private transient volatile Metric rollingCounterInSecond = new ArrayMetric(1000 / SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL); @@ -40,6 +101,9 @@ public class StatisticNode implements Node { */ private transient Metric rollingCounterInMinute = new ArrayMetric(1000, 60); + /** + * The counter for thread count. + */ private AtomicInteger curThreadNum = new AtomicInteger(0); private long lastFetchTime = -1; diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LeapArray.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LeapArray.java index 30761e2f..39ee3c04 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LeapArray.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/LeapArray.java @@ -32,10 +32,10 @@ import com.alibaba.csp.sentinel.util.TimeUtil; * {@link #sampleCount} = intervalInMs / windowLengthInMs. *

* - * @param type of data bucket. + * @param type of statistic data * @author jialiang.linjl * @author Eric Zhao - * @author CarpenterLee + * @author Carpenter Lee */ public abstract class LeapArray { @@ -48,7 +48,8 @@ public abstract class LeapArray { private final ReentrantLock updateLock = new ReentrantLock(); /** - * The total bucket count is: {@link #sampleCount} = intervalInSec * 1000 / windowLengthInMs. + * The total bucket count is: {@code sampleCount = intervalInMs / windowLengthInMs}. + * * @param windowLengthInMs a single window bucket's time length in milliseconds. * @param intervalInSec the total time span of this {@link LeapArray} in seconds. */ @@ -61,74 +62,129 @@ public abstract class LeapArray { } /** - * Get the window at current timestamp. + * Get the bucket at current timestamp. * - * @return the window at current timestamp + * @return the bucket at current timestamp */ public WindowWrap currentWindow() { return currentWindow(TimeUtil.currentTimeMillis()); } /** - * Create a new bucket. + * Create a new statistic value for bucket. * * @return the new empty bucket */ public abstract T newEmptyBucket(); /** - * Reset current window to provided start time and reset all counters. + * Reset given bucket to provided start time and reset the value. * - * @param startTime the start time of the window - * @param windowWrap current window - * @return new clean window wrap + * @param startTime the start time of the bucket + * @param windowWrap current bucket + * @return new clean bucket at given start time */ protected abstract WindowWrap resetWindowTo(WindowWrap windowWrap, long startTime); /** - * Get window at provided timestamp. + * Get bucket item at provided timestamp. * * @param time a valid timestamp - * @return the window at provided timestamp + * @return current bucket item at provided timestamp */ public WindowWrap currentWindow(long time) { long timeId = time / windowLengthInMs; - // Calculate current index. + // Calculate current index so we can map the timestamp to the leap array. int idx = (int)(timeId % array.length()); - // Cut the time to current window start. - time = time - time % windowLengthInMs; + // Calculate current bucket start time. + long windowStart = time - time % windowLengthInMs; + /* + * Get bucket item at given time from the array. + * + * (1) Bucket is absent, then just create a new bucket and CAS update to circular array. + * (2) Bucket is up-to-date, then just return the bucket. + * (3) Bucket is deprecated, then reset current bucket and clean all deprecated buckets. + */ while (true) { WindowWrap old = array.get(idx); if (old == null) { - WindowWrap window = new WindowWrap(windowLengthInMs, time, newEmptyBucket()); + /* + * B0 B1 B2 NULL B4 + * ||_______|_______|_______|_______|_______||___ + * 200 400 600 800 1000 1200 timestamp + * ^ + * time=888 + * bucket is empty, so create new and update + * + * If the old bucket is absent, then we create a new bucket at {@code windowStart}, + * then try to update circular array via a CAS operation. Only one thread can + * succeed to update, while other threads yield its time slice. + */ + WindowWrap window = new WindowWrap(windowLengthInMs, windowStart, newEmptyBucket()); if (array.compareAndSet(idx, null, window)) { + // Successfully updated, return the created bucket. return window; } else { + // Contention failed, the thread will yield its time slice to wait for bucket available. Thread.yield(); } - } else if (time == old.windowStart()) { + } else if (windowStart == old.windowStart()) { + /* + * B0 B1 B2 B3 B4 + * ||_______|_______|_______|_______|_______||___ + * 200 400 600 800 1000 1200 timestamp + * ^ + * time=888 + * startTime of Bucket 3: 800, so it's up-to-date + * + * If current {@code windowStart} is equal to the start timestamp of old bucket, + * that means the time is within the bucket, so directly return the bucket. + */ return old; - } else if (time > old.windowStart()) { + } else if (windowStart > old.windowStart()) { + /* + * (old) + * B0 B1 B2 NULL B4 + * |_______||_______|_______|_______|_______|_______||___ + * ... 1200 1400 1600 1800 2000 2200 timestamp + * ^ + * time=1676 + * startTime of Bucket 2: 400, deprecated, should be reset + * + * If the start timestamp of old bucket is behind provided time, that means + * the bucket is deprecated. We have to reset the bucket to current {@code windowStart}. + * Note that the reset and clean-up operations are hard to be atomic, + * so we need a update lock to guarantee the correctness of bucket update. + * + * The update lock is fine-grained and will take effect only when + * bucket is deprecated, so in most cases it won't lead to performance loss. + */ if (updateLock.tryLock()) { try { - // if (old is deprecated) then [LOCK] resetTo currentTime. - return resetWindowTo(old, time); + // Successfully get the update lock, now we reset the bucket. + return resetWindowTo(old, windowStart); } finally { updateLock.unlock(); } } else { + // Contention failed, the thread will yield its time slice to wait for bucket available. Thread.yield(); } - - } else if (time < old.windowStart()) { - // Cannot go through here. - return new WindowWrap(windowLengthInMs, time, newEmptyBucket()); + } else if (windowStart < old.windowStart()) { + // Should not go through here, as the provided time is already behind. + return new WindowWrap(windowLengthInMs, windowStart, newEmptyBucket()); } } } + /** + * Get the previous bucket item before provided timestamp. + * + * @param time a valid timestamp + * @return the previous bucket item before provided timestamp + */ public WindowWrap getPreviousWindow(long time) { long timeId = (time - windowLengthInMs) / windowLengthInMs; int idx = (int)(timeId % array.length()); @@ -146,10 +202,21 @@ public abstract class LeapArray { return wrap; } + /** + * Get the previous bucket item for current timestamp. + * + * @return the previous bucket item for current timestamp + */ public WindowWrap getPreviousWindow() { - return getPreviousWindow(System.currentTimeMillis()); + return getPreviousWindow(TimeUtil.currentTimeMillis()); } + /** + * Get statistic value from bucket for provided timestamp. + * + * @param time a valid timestamp + * @return the statistic value if bucket for provided timestamp is up-to-date; otherwise null + */ public T getWindowValue(long time) { long timeId = time / windowLengthInMs; int idx = (int)(timeId % array.length()); @@ -162,10 +229,23 @@ public abstract class LeapArray { return old.value(); } - private boolean isWindowDeprecated(WindowWrap windowWrap) { + /** + * Check if a bucket is deprecated, which means that the bucket + * has been behind for at least an entire window time span. + * + * @param windowWrap a non-null bucket + * @return true if the bucket is deprecated; otherwise false + */ + private boolean isWindowDeprecated(/*@NonNull*/ WindowWrap windowWrap) { return TimeUtil.currentTimeMillis() - windowWrap.windowStart() >= intervalInMs; } + /** + * Get valid bucket list for entire sliding window. + * The list will only contain "valid" buckets. + * + * @return valid bucket list for entire sliding window. + */ public List> list() { List> result = new ArrayList>(); @@ -180,6 +260,12 @@ public abstract class LeapArray { return result; } + /** + * Get aggregated value list for entire sliding window. + * The list will only contain value from "valid" buckets. + * + * @return aggregated value list for entire sliding window + */ public List values() { List result = new ArrayList(); @@ -192,4 +278,22 @@ public abstract class LeapArray { } return result; } + + /** + * Get sample count (total amount of buckets). + * + * @return sample count + */ + public int getSampleCount() { + return sampleCount; + } + + /** + * Get total interval length of the sliding window. + * + * @return interval in second + */ + public int getIntervalInSecond() { + return intervalInMs / 1000; + } } diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/MetricBucket.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/MetricBucket.java index eb5c75ce..d59ca984 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/MetricBucket.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/MetricBucket.java @@ -18,7 +18,7 @@ package com.alibaba.csp.sentinel.slots.statistic.base; import com.alibaba.csp.sentinel.Constants; /** - * Represents metrics data in a period of time window. + * Represents metrics data in a period of time span. * * @author jialiang.linjl * @author Eric Zhao @@ -42,9 +42,9 @@ public class MetricBucket { } /** - * Clean the adders and reset window to provided start time. + * Reset the adders. * - * @return new clean window + * @return new metric bucket in initial state */ public MetricBucket reset() { pass.reset(); diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/WindowWrap.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/WindowWrap.java index 5818e4ba..8edb2277 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/WindowWrap.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/base/WindowWrap.java @@ -25,24 +25,24 @@ package com.alibaba.csp.sentinel.slots.statistic.base; public class WindowWrap { /** - * a single window bucket's time length in milliseconds. + * Time length of a single window bucket in milliseconds. */ private final long windowLengthInMs; /** - * Start time of the window in milliseconds. + * Start timestamp of the window in milliseconds. */ private long windowStart; /** - * Statistic value. + * Statistic data. */ private T value; /** * @param windowLengthInMs a single window bucket's time length in milliseconds. - * @param windowStart the start timestamp of the window - * @param value window data + * @param windowStart the start timestamp of the window + * @param value statistic data */ public WindowWrap(long windowLengthInMs, long windowStart, T value) { this.windowLengthInMs = windowLengthInMs; @@ -66,6 +66,12 @@ public class WindowWrap { this.value = value; } + /** + * Reset start timestamp of current bucket to provided time. + * + * @param startTime valid start timestamp + * @return bucket after reset + */ public WindowWrap resetTo(long startTime) { this.windowStart = startTime; return this; diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java index f6a9277f..7a141a54 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/ArrayMetric.java @@ -34,8 +34,6 @@ public class ArrayMetric implements Metric { private final MetricsLeapArray data; /** - * Constructor - * * @param windowLengthInMs a single window bucket's time length in milliseconds. * @param intervalInSec the total time span of this {@link ArrayMetric} in seconds. */ diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java index 3a41e4e6..1b62057a 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/Metric.java @@ -35,6 +35,11 @@ public interface Metric { */ long success(); + /** + * Get max success count. + * + * @return max success count + */ long maxSuccess(); /** @@ -59,7 +64,7 @@ public interface Metric { long pass(); /** - * Get total RT. + * Get total response time. * * @return total RT */ @@ -72,6 +77,11 @@ public interface Metric { */ long minRt(); + /** + * Get aggregated metric nodes of all resources. + * + * @return metric node list of all resources + */ List details(); /** diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/MetricsLeapArray.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/MetricsLeapArray.java index d5972b6d..fcf452e1 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/MetricsLeapArray.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/MetricsLeapArray.java @@ -20,7 +20,7 @@ import com.alibaba.csp.sentinel.slots.statistic.base.MetricBucket; import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; /** - * The fundamental data structure for metric statistics in a time window. + * The fundamental data structure for metric statistics in a time span. * * @see LeapArray * @author jialiang.linjl @@ -29,8 +29,6 @@ import com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap; public class MetricsLeapArray extends LeapArray { /** - * Constructor - * * @param windowLengthInMs a single window bucket's time length in milliseconds. * @param intervalInSec the total time span of this {@link MetricsLeapArray} in seconds. */ @@ -45,6 +43,7 @@ public class MetricsLeapArray extends LeapArray { @Override protected WindowWrap resetWindowTo(WindowWrap w, long startTime) { + // Update the start time and reset value. w.resetTo(startTime); w.value().reset(); return w; diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/CacheMap.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/CacheMap.java index f014f1e5..4039632b 100644 --- a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/CacheMap.java +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/CacheMap.java @@ -41,5 +41,5 @@ public interface CacheMap { void clear(); - Set ascendingKeySet(); + Set keySet(boolean ascending); } diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/ConcurrentLinkedHashMapWrapper.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/ConcurrentLinkedHashMapWrapper.java index cda53766..8b25e0c0 100644 --- a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/ConcurrentLinkedHashMapWrapper.java +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/cache/ConcurrentLinkedHashMapWrapper.java @@ -86,7 +86,11 @@ public class ConcurrentLinkedHashMapWrapper implements CacheMap { } @Override - public Set ascendingKeySet() { - return map.ascendingKeySet(); + public Set keySet(boolean ascending) { + if (ascending) { + return map.ascendingKeySet(); + } else { + return map.descendingKeySet(); + } } } diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/data/ParamMapBucket.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/data/ParamMapBucket.java index dba6fd5e..05ef13c8 100644 --- a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/data/ParamMapBucket.java +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/data/ParamMapBucket.java @@ -66,7 +66,11 @@ public class ParamMapBucket { } public Set ascendingKeySet(RollingParamEvent type) { - return data[type.ordinal()].ascendingKeySet(); + return data[type.ordinal()].keySet(true); + } + + public Set descendingKeySet(RollingParamEvent type) { + return data[type.ordinal()].keySet(false); } public static final int DEFAULT_MAX_CAPACITY = 200; diff --git a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/HotParameterLeapArray.java b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/HotParameterLeapArray.java index e3262d7b..1eab4402 100644 --- a/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/HotParameterLeapArray.java +++ b/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/statistic/metric/HotParameterLeapArray.java @@ -60,10 +60,24 @@ public class HotParameterLeapArray extends LeapArray { return w; } + /** + * Add event count for specific parameter value. + * + * @param event target event + * @param count count to add + * @param value parameter value + */ public void addValue(RollingParamEvent event, int count, Object value) { currentWindow().value().add(event, count, value); } + /** + * Get "top-N" value-QPS map of provided event. + * + * @param event target event + * @param number max number of values + * @return "top-N" value map + */ public Map getTopValues(RollingParamEvent event, int number) { currentWindow(); List buckets = this.values();