关于熔断,一个常见的说法是,在时间窗口内,请求数达到多少,且错误率达到多少,就会打开断路器,熔断请求。
如何计数?
假设我要统计时间窗口
一个简单的想法,一个 int
变量 value
,请求到了 value
+ 1;一个int
变量errCt
,捕获异常了,errCt
加一。
但是多线程环境下不安全啊,Sentinel为了尽可能提升性能,没有使用synchronized
重量级锁,选用了LongAddr(比AtomicInteger性能更高)进行计数。
所以一个时间窗口的计数数据结构如下:
public class MetricBucket { /** * 存储各事件的计数,比如异常总数、请求总数等; 比如counters[0]表示异常数,counters[1]表示请求总数 */ private final LongAdder[] counters; /** * 这段事件内的最小耗时 */ private volatile long minRt; }
再进一步,我要记录前一秒,前两秒的异常数、请求数;还要统计当前秒的异常数,紧紧一个Bucket是不够的。
循环数组
Sentinel使用一个Bucket数组,每个元素记录了一秒时间窗口的总请求数,异常数。如下图:
Bucket数组的下标定位计算方式如下:
private int calculateTimeIdx(long timeMillis) { /** * 假设当前时间戳为 1722503263235 * windowLengthInMs 为 1000 毫秒(1 秒) * 则将毫秒转为秒 => 1722503263 * 映射到数组的索引为 => 3 */ long timeId = timeMillis / windowLengthInMs; return (int) (timeId % array.length()); }
不是说好的循环数组吗,其实取余的过程就是循环的过程。
解释一下循环的意义:因为数组长度有限,而时间每秒是无限的。当时间戳太多的时候,数组不够写了,就要从数组开头进行覆盖写入。达到循环利用空间的效果。
MySQL的 redolog,Redis主从同步的buffer都是采用 循环覆盖写的思想。
继续刨根问底:如何确定时间窗口是否要覆盖?
由于循环使用的问题,当前时间戳与一分钟之前的时间戳和一分钟之后的时间戳都会映射到数组中的同一个 Bucket,因此,必须要能够判断取得的 Bucket 是否是统计当前时间窗口内的指标数据,这便要数组每个元素都存储 Bucket 时间窗口的开始时间戳。
Sentinel直接在每个窗口都计算一个开始时间,startTime
比如当前时间戳是 1577017699235,Bucket 统计一秒的数据,将时间戳的毫秒部分全部替换为 0,就能得到 Bucket 时间窗口的开始时间戳为 1577017699000。
计算 Bucket 时间窗口的开始时间戳代码实现如下:
protected long calculateWindowStart(long timeMillis) { /** * 假设窗口大小为 1000 毫秒,即数组每个元素存储 1 秒钟的统计数据 * timeMillis % windowLengthInMs 就是取得毫秒部分 * timeMillis - 毫秒数 = 秒部分 * 这就得到每秒的开始时间戳 */ return timeMillis - timeMillis % windowLengthInMs; }
通过startTime
就能区分是否要覆盖了,还是当前时间就属于这个窗口。